DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -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完成。")
|
||||
}
|
||||
Reference in new issue
Block a user