DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

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

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

No files matched your search

+294
View File
@@ -0,0 +1,294 @@
// 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完成。")
}