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,177 @@
|
||||
// pvftokendump 把 protected PVF 里某个脚本的令牌流逐条打印出来:
|
||||
// 序号、令牌类型、4 字节负载、以及字符串负载解出来的文本。
|
||||
//
|
||||
// 用途:看清「某个段的值是空的」在令牌流里到底是【没有 token】还是【一个指向空串/别处的 token】。
|
||||
// 这正是「工具往空字段里塞看不见的字符串引用」那类问题的判据。
|
||||
//
|
||||
// go run ./cmd/scratch/pvftokendump -pvf <Script.pvf> -path stackable/newstk/2026091800.stk
|
||||
// go run ./cmd/scratch/pvftokendump -pvf <Script.pvf> -path <文件> -around "[booster info]"
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pvfPath := flag.String("pvf", "", "Script.pvf 路径")
|
||||
path := flag.String("path", "", "要看的文件归档路径")
|
||||
around := flag.String("around", "", "只打印包含该字符串的 token 前后各若干条")
|
||||
resolveList := flag.String("resolve", "", "逗号分隔的 magic offset,直接解析成字符串(不需要 -path)")
|
||||
ctx := flag.Int("ctx", 4, "around 的前后条数")
|
||||
flag.Parse()
|
||||
if *pvfPath == "" {
|
||||
fmt.Fprintln(os.Stderr, "-pvf is required")
|
||||
os.Exit(2)
|
||||
}
|
||||
if *path == "" && *resolveList == "" {
|
||||
fmt.Fprintln(os.Stderr, "-path(或 -resolve)is required")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// -resolve 模式:只把给定的 magic offset 解出来,用来回答「偏移 0 指向哪个字符串」。
|
||||
if *resolveList != "" {
|
||||
archive, err := platformpvf.Open(*pvfPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "open:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("归档: %s\n\n", *pvfPath)
|
||||
for _, tok := range strings.Split(*resolveList, ",") {
|
||||
tok = strings.TrimSpace(tok)
|
||||
if tok == "" {
|
||||
continue
|
||||
}
|
||||
var v int
|
||||
if _, e := fmt.Sscanf(tok, "%d", &v); e != nil {
|
||||
fmt.Fprintf(os.Stderr, " bad offset %q\n", tok)
|
||||
continue
|
||||
}
|
||||
pool := "ANSI池"
|
||||
if v&1 != 0 {
|
||||
pool = "UTF16池"
|
||||
}
|
||||
fmt.Printf(" magic offset %-12d -> %-8s @%-12d %q\n", v, pool, v>>1, archive.ResolveString(v))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
archive, err := platformpvf.Open(*pvfPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "open:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
raw, err := archive.ReadRaw(*path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "读不到 %s: %v\n", *path, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
n := len(raw) / 5
|
||||
fmt.Printf("文件: %s\n原始字节: %d 令牌数: %d (%d 字节不是 5 的整数倍则有余数)\n\n",
|
||||
*path, len(raw), n, len(raw)%5)
|
||||
if len(raw)%5 != 0 {
|
||||
fmt.Printf("【注意】余 %d 字节:令牌流长度不是 5 的倍数,这本身可疑。\n\n", len(raw)%5)
|
||||
}
|
||||
|
||||
type tok struct {
|
||||
i int
|
||||
kind byte
|
||||
value int32
|
||||
text string
|
||||
}
|
||||
toks := make([]tok, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
off := i * 5
|
||||
kind := raw[off]
|
||||
v := int32(uint32(raw[off+1]) | uint32(raw[off+2])<<8 | uint32(raw[off+3])<<16 | uint32(raw[off+4])<<24)
|
||||
t := tok{i: i, kind: kind, value: v}
|
||||
switch kind {
|
||||
case 0, 2:
|
||||
case 3, 5, 6, 7:
|
||||
t.text = archive.ResolveString(int(v))
|
||||
}
|
||||
toks = append(toks, t)
|
||||
}
|
||||
|
||||
kindName := func(k byte) string {
|
||||
switch k {
|
||||
case 0:
|
||||
return "整数"
|
||||
case 2:
|
||||
return "浮点"
|
||||
case 3:
|
||||
return "行字符串"
|
||||
case 5:
|
||||
return "{5=``}"
|
||||
case 6:
|
||||
return "反引号串"
|
||||
case 7:
|
||||
return "{7=``}"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
poolOf := func(v int32) string {
|
||||
if v < 0 {
|
||||
return "-"
|
||||
}
|
||||
if v&1 != 0 {
|
||||
return "UTF16池"
|
||||
}
|
||||
return "ANSI池"
|
||||
}
|
||||
|
||||
show := func(i int) {
|
||||
t := toks[i]
|
||||
extra := ""
|
||||
switch t.kind {
|
||||
case 0:
|
||||
extra = fmt.Sprintf("%d", t.value)
|
||||
case 2:
|
||||
extra = fmt.Sprintf("bits=0x%08X", uint32(t.value))
|
||||
default:
|
||||
// 空串要显式标出来 —— 这正是我们关心的
|
||||
if t.text == "" {
|
||||
extra = fmt.Sprintf("%s@%d \"\" ←【空串】", poolOf(t.value), t.value>>1)
|
||||
} else {
|
||||
extra = fmt.Sprintf("%s@%d %q", poolOf(t.value), t.value>>1, t.text)
|
||||
}
|
||||
}
|
||||
fmt.Printf(" #%-4d type=%-2d %-8s raw=0x%08X %s\n", t.i, t.kind, kindName(t.kind), uint32(t.value), extra)
|
||||
}
|
||||
|
||||
if *around != "" {
|
||||
hit := false
|
||||
for i, t := range toks {
|
||||
if strings.Contains(t.text, *around) {
|
||||
hit = true
|
||||
lo, hi := i-*ctx, i+*ctx
|
||||
if lo < 0 {
|
||||
lo = 0
|
||||
}
|
||||
if hi >= len(toks) {
|
||||
hi = len(toks) - 1
|
||||
}
|
||||
fmt.Printf("=== 命中 %q(token #%d),显示 #%d..#%d ===\n", *around, i, lo, hi)
|
||||
for j := lo; j <= hi; j++ {
|
||||
show(j)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
if !hit {
|
||||
fmt.Printf("没有 token 的解码文本包含 %q\n", *around)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := range toks {
|
||||
show(i)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user