Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

295 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// pvfantonpatch 一次性把「安徒恩团本奖励改造」写进 Script.pvf,并自动回读校验。
//
// 设计要点:
// - 只做**整数令牌**手术(修改/插入/删除 type-0 令牌),字符串池一个字节都不碰。
// - 定位 [int data] 段用 internal/modules/dnf/pvf 的 Parse,它给出的 token 下标
// 与 5 字节原始令牌一一对应;工具会硬断言 len(Tokens) == len(raw)/5,
// 断言失败就直接拒绝执行(防止下标错位把文件写坏)。
// - 全部改动在一次 RepackRaw 里完成,最后回读新归档、比对解码文本。
//
// 用法:pvfantonpatch -pvf <输入> -out <输出>
package main
import (
"flag"
"fmt"
"os"
"strings"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
type tok struct {
kind byte
i32 int32
}
func decode(raw []byte) []tok {
out := make([]tok, 0, len(raw)/5)
for i := 0; i+5 <= len(raw); i += 5 {
v := int32(uint32(raw[i+1]) | uint32(raw[i+2])<<8 | uint32(raw[i+3])<<16 | uint32(raw[i+4])<<24)
out = append(out, tok{kind: raw[i], i32: v})
}
return out
}
func encode(tokens []tok) []byte {
out := make([]byte, 0, len(tokens)*5)
for _, t := range tokens {
v := uint32(t.i32)
out = append(out, t.kind, byte(v), byte(v>>8), byte(v>>16), byte(v>>24))
}
return out
}
// rowIndices 返回所有「排名 权重 定义ID 标志」整行的**排名令牌**下标,顺序即出现顺序。
func rowIndices(tokens []tok) []int {
var out []int
for i := 2; i+3 < len(tokens); i++ {
if tokens[i].kind != 0 || tokens[i+1].kind != 0 || tokens[i+2].kind != 0 || tokens[i+3].kind != 0 {
continue
}
if tokens[i-2].kind != 3 || tokens[i-1].kind != 6 {
continue
}
out = append(out, i)
}
return out
}
// patchIntData 把某个 .stk 的 [int data] 内容整体换成 newValues。
func patchIntData(path string, raw []byte, text string, newValues []int32) ([]byte, error) {
doc, err := dnfpvf.Parse(path, text)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
tokens := decode(raw)
if len(doc.Tokens) != len(tokens) {
return nil, fmt.Errorf("%s: token 下标对不上(Parse=%d, raw=%d),拒绝写入", path, len(doc.Tokens), len(tokens))
}
// parse.go 的 addToken/closeSection:Section.Start = 段标记之后第一个内容令牌的下标,
// Section.End = 下一个段标记令牌的下标。所以 [int data] 的内容区间就是 [Start, End)。
start, contentEnd := -1, -1
for _, s := range doc.Sections {
if strings.ToLower(strings.TrimSpace(s.Name)) == "int data" {
start, contentEnd = s.Start, s.End
}
}
if start < 0 || contentEnd <= start {
return nil, fmt.Errorf("%s: 找不到 [int data] 段(start=%d end=%d)", path, start, contentEnd)
}
for i := start; i < contentEnd; i++ {
if tokens[i].kind != 0 {
return nil, fmt.Errorf("%s: [int data] 内第 %d 个令牌不是整数(kind=%d)", path, i-start, tokens[i].kind)
}
}
replacement := make([]tok, 0, len(newValues))
for _, v := range newValues {
replacement = append(replacement, tok{kind: 0, i32: v})
}
rest := append([]tok(nil), tokens[contentEnd:]...)
tokens = append(tokens[:start], replacement...)
tokens = append(tokens, rest...)
fmt.Printf(" %-46s [int data] %d 个令牌 -> %d 个\n", path, contentEnd-start, len(newValues))
return encode(tokens), nil
}
func main() {
pvfPath := flag.String("pvf", "", "输入 PVF")
outPath := flag.String("out", "", "输出 PVF")
flag.Parse()
if *pvfPath == "" || *outPath == "" {
fmt.Fprintln(os.Stderr, "-pvf 与 -out 必填")
os.Exit(2)
}
archive, err := platformpvf.Open(*pvfPath)
if err != nil {
fmt.Fprintln(os.Stderr, "open:", err)
os.Exit(1)
}
repl := map[string][]byte{}
expect := map[string]string{}
// ---- A. 6 个奖励定义的 [int data] 改造 ----
fmt.Println("A. 奖励定义内容改造:")
intDataTargets := []struct {
path string
values []int32
note string
}{
{"stackable/10157001/10157773.stk", []int32{490002019, 100, 1}, "深渊票 -> 3000礼盒(阶段1)"},
{"stackable/10157001/10157775.stk", []int32{490002019, 100, 1}, "传说装备 -> 3000礼盒(阶段1,连带单人)"},
{"stackable/10157001/10157776.stk", []int32{490002019, 100, 1}, "传说卡片 -> 3000礼盒(阶段1,连带单人)"},
{"stackable/10157001/10157777.stk", []int32{490002019, 100, 1}, "史诗装备 -> 3000礼盒(阶段1)"},
{"stackable/10157001/10157778.stk", []int32{490005021, 100, 1}, "深渊票 -> 8888礼盒(阶段2)"},
{"stackable/10157001/10157786.stk", []int32{490712331, 100, 1}, "心脏卡片 -> 花篮(阶段2,连带单人)"},
}
for _, target := range intDataTargets {
raw, err := archive.ReadRaw(target.path)
if err != nil {
fmt.Fprintln(os.Stderr, "read:", target.path, err)
os.Exit(1)
}
text, err := archive.ReadText(target.path)
if err != nil {
fmt.Fprintln(os.Stderr, "text:", target.path, err)
os.Exit(1)
}
patched, err := patchIntData(target.path, raw, text, target.values)
if err != nil {
fmt.Fprintln(os.Stderr, "FAIL:", err)
os.Exit(1)
}
repl[target.path] = patched
expect[target.path] = target.note
}
// ---- B. etc/raid/anton.etc 权重与定义 ID ----
fmt.Println("\nB. etc/raid/anton.etc 奖励行:")
const antonPath = "etc/raid/anton.etc"
raw, err := archive.ReadRaw(antonPath)
if err != nil {
fmt.Fprintln(os.Stderr, "read anton:", err)
os.Exit(1)
}
tokens := decode(raw)
// 行序(桌面 PVF 现状,21 行):
// 0 10094735 1 10094733 2 10157775 3 10157776 4 10157777
// 5 3330 6 10094733 7 10161936 8 10157783 9 10157784
// 10 10157785 11 10157786 12..20 单人段
rows := rowIndices(tokens)
if len(rows) != 21 {
fmt.Fprintf(os.Stderr, "预期 21 行奖励,实际 %d 行,拒绝写入\n", len(rows))
os.Exit(1)
}
wantDefs := []int32{10094735, 10094733, 10157775, 10157776, 10157777, 3330, 10094733,
10161936, 10157783, 10157784, 10157785, 10157786,
10094735, 10164076, 10164077, 10157783, 10157784, 10157785, 10157775, 10157776, 10157786}
for i, at := range rows {
if tokens[at+2].i32 != wantDefs[i] {
fmt.Fprintf(os.Stderr, "第 %d 行定义 ID 预期 %d 实际 %d,拒绝写入(防止按行号改错)\n",
i, wantDefs[i], tokens[at+2].i32)
os.Exit(1)
}
}
// 1) 先补回被删掉的 `squad_item` -1 50 10157773 0(用第 2 行做模板复制 [段名][名字] 令牌)
tpl := rows[2]
insert := []tok{tokens[tpl-2], tokens[tpl-1],
{kind: 0, i32: -1}, {kind: 0, i32: 50}, {kind: 0, i32: 10157773}, {kind: 0, i32: 0}}
at := tpl + 4
restTokens := append([]tok(nil), tokens[at:]...)
tokens = append(tokens[:at], insert...)
tokens = append(tokens, restTokens...)
fmt.Println(" + 补回阶段1 squad_item 10157773 权重 50")
rows = rowIndices(tokens)
if len(rows) != 22 {
fmt.Fprintf(os.Stderr, "插入后应为 22 行,实际 %d 行,拒绝写入\n", len(rows))
os.Exit(1)
}
// 2) 按行号改权重 / 定义 ID(插入后行序:
// 0 10094735 1 10094733 2 10157775 3 10157773 4 10157776 5 10157777
// 6 3330 7 10094733 8 10161936 9 10157783 10 10157784 11 10157785 12 10157786
// 13..21 单人段)
type rowEdit struct {
row int
def int32 // <0 表示不改
weight int32
label string
}
edits := []rowEdit{
{row: 2, def: -1, weight: 1, label: "阶段1 10157775 权重 0 -> 1"},
{row: 3, def: -1, weight: 50, label: "阶段1 10157773 权重 50(新补)"},
{row: 4, def: -1, weight: 1, label: "阶段1 10157776 权重 0 -> 1"},
{row: 5, def: -1, weight: 48, label: "阶段1 10157777 权重 5000 -> 48"},
{row: 6, def: 10157778, weight: 100, label: "阶段2 party_card 定义 3330 -> 10157778,权重 10 -> 100"},
{row: 8, def: -1, weight: 701, label: "阶段2 10161936 权重 40 -> 701"},
{row: 9, def: -1, weight: 131, label: "阶段2 10157783 权重 10 -> 131"},
{row: 10, def: -1, weight: 45, label: "阶段2 10157784 权重 20 -> 45"},
{row: 11, def: -1, weight: 23, label: "阶段2 10157785 权重 20 -> 23"},
{row: 12, def: -1, weight: 100, label: "阶段2 10157786 权重 20 -> 100(花篮 10%)"},
}
for _, edit := range edits {
at := rows[edit.row]
if edit.def >= 0 {
tokens[at+2].i32 = edit.def
}
tokens[at+1].i32 = edit.weight
fmt.Printf(" ~ %s\n", edit.label)
}
// 校验阶段2 squad 权重和 = 1000
sum := tokens[rows[8]+1].i32 + tokens[rows[9]+1].i32 + tokens[rows[10]+1].i32 +
tokens[rows[11]+1].i32 + tokens[rows[12]+1].i32
if sum != 1000 {
fmt.Fprintf(os.Stderr, "阶段2 squad_item 权重和 = %d,应为 1000,拒绝写入\n", sum)
os.Exit(1)
}
fmt.Printf(" 阶段2 squad_item 权重和 = %d(花篮 %d/1000 = %.2f%%)\n",
sum, tokens[rows[12]+1].i32, float64(tokens[rows[12]+1].i32)/float64(sum)*100)
repl[antonPath] = encode(tokens)
// ---- C. 一次重打包 ----
fmt.Printf("\nC. 重打包 %d 个文件 -> %s\n", len(repl), *outPath)
replacements := make([]platformpvf.RawReplacement, 0, len(repl))
for path, data := range repl {
replacements = append(replacements, platformpvf.RawReplacement{Path: path, Data: data})
}
packed, err := archive.RepackRaw(replacements)
if err != nil {
fmt.Fprintln(os.Stderr, "repack:", err)
os.Exit(1)
}
if err := os.WriteFile(*outPath, packed, 0o644); err != nil {
fmt.Fprintln(os.Stderr, "write:", err)
os.Exit(1)
}
fmt.Printf(" 写出 %d 字节\n", len(packed))
// ---- D. 回读校验 ----
fmt.Println("\nD. 回读校验(新归档 vs 原归档的解码文本):")
verify, err := platformpvf.Open(*outPath)
if err != nil {
fmt.Fprintln(os.Stderr, "reopen:", err)
os.Exit(1)
}
fail := false
for path := range repl {
before, _ := archive.ReadText(path)
after, _ := verify.ReadText(path)
if before == after {
fmt.Printf(" !! %s 文本没变化,改动可能没生效\n", path)
fail = true
continue
}
if path != antonPath {
if !strings.Contains(after, "[int data]") {
fmt.Printf(" !! %s 解码后缺少 [int data]\n", path)
fail = true
continue
}
}
fmt.Printf(" OK %-46s %d -> %d 字符\n", path, len(before), len(after))
}
// 汇总新归档的奖励行
rawAfter, err := verify.ReadRaw(antonPath)
if err != nil {
fmt.Fprintln(os.Stderr, "read after:", err)
os.Exit(1)
}
after := decode(rawAfter)
rowsAfter := rowIndices(after)
fmt.Printf("\n改后奖励行(共 %d 行):\n", len(rowsAfter))
for i, at := range rowsAfter {
fmt.Printf(" #%-3d 权重=%-6d 定义ID=%-10d 标志=%d\n", i, after[at+1].i32, after[at+2].i32, after[at+3].i32)
}
if fail {
os.Exit(1)
}
fmt.Println("\n完成。")
}