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,296 @@
|
||||
// pvflstaudit 审计 PVF 内 `.lst` 索引文件的结构完整性(只读,不修改任何库)。
|
||||
//
|
||||
// 回答四个问题:
|
||||
// 1. id / 路径是否成对(token 总数是否为偶数、每对是否真的是 int + 引用);
|
||||
// 2. id 有无重复(重复会把哪一条路径先命中暴露出来);
|
||||
// 3. 每一条路径在归档里是否真实存在(同时报告「按 .lst 所在目录相对解析」的结果);
|
||||
// 4. 指定 id 列表的落点(是否存在、解析到什么文件、该文件是否存在)。
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// go run ./cmd/scratch/pvflstaudit -pvf <Script.pvf> -list stackable/stackable.lst -ids 202609180,202690000
|
||||
// go run ./cmd/scratch/pvflstaudit -pvf <Script.pvf> -all
|
||||
// go run ./cmd/scratch/pvflstaudit -pvf <Script.pvf> -list stackable/stackable.lst -dump out.txt
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
id int64
|
||||
ref string
|
||||
index int // 第几对(0 起)
|
||||
}
|
||||
|
||||
func main() {
|
||||
pvfPath := flag.String("pvf", `../runtime/data/dnf/Script.pvf`, "Script.pvf path")
|
||||
listPath := flag.String("list", "stackable/stackable.lst", "list path")
|
||||
all := flag.Bool("all", false, "审计归档内全部 .lst")
|
||||
ids := flag.String("ids", "", "逗号分隔的 id,逐个报告落点")
|
||||
dump := flag.String("dump", "", "把解析出的 id/路径对照表写到本地文件")
|
||||
limit := flag.Int("limit", 30, "每类问题最多打印多少条")
|
||||
flag.Parse()
|
||||
|
||||
archive, err := platformpvf.Open(*pvfPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "open:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 归档路径索引:一次遍历建好,供路径存在性检查使用。
|
||||
existing := map[string]string{} // 小写路径 -> 真实路径
|
||||
count := 0
|
||||
for _, f := range archive.Files() {
|
||||
// File.Path 是所在目录,File.ArchivePath 才是完整路径。
|
||||
full := f.ArchivePath
|
||||
if strings.TrimSpace(full) == "" {
|
||||
full = f.Path
|
||||
}
|
||||
key := strings.ToLower(strings.ReplaceAll(full, "\\", "/"))
|
||||
if _, ok := existing[key]; !ok {
|
||||
existing[key] = full
|
||||
}
|
||||
count++
|
||||
}
|
||||
fmt.Printf("归档: %s\n", *pvfPath)
|
||||
fmt.Printf("文件总数: %d\n", count)
|
||||
|
||||
targets := []string{}
|
||||
if *all {
|
||||
for _, f := range archive.Files() {
|
||||
full := f.ArchivePath
|
||||
if strings.TrimSpace(full) == "" {
|
||||
full = f.Path
|
||||
}
|
||||
if strings.HasSuffix(strings.ToLower(full), ".lst") {
|
||||
targets = append(targets, full)
|
||||
}
|
||||
}
|
||||
sort.Strings(targets)
|
||||
fmt.Printf("发现 .lst: %d 个\n", len(targets))
|
||||
} else {
|
||||
targets = []string{*listPath}
|
||||
}
|
||||
|
||||
wantIDs := map[int64]bool{}
|
||||
var wantList []int64
|
||||
if strings.TrimSpace(*ids) != "" {
|
||||
for _, tok := range strings.Split(*ids, ",") {
|
||||
tok = strings.TrimSpace(tok)
|
||||
if tok == "" {
|
||||
continue
|
||||
}
|
||||
v, convErr := strconv.ParseInt(tok, 10, 64)
|
||||
if convErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "bad id %q: %v\n", tok, convErr)
|
||||
os.Exit(2)
|
||||
}
|
||||
wantIDs[v] = true
|
||||
wantList = append(wantList, v)
|
||||
}
|
||||
}
|
||||
|
||||
var dumpLines []string
|
||||
totalIssues := 0
|
||||
|
||||
for _, lp := range targets {
|
||||
text, readErr := archive.ReadText(lp)
|
||||
if readErr != nil {
|
||||
fmt.Printf("\n### %s\n读不到: %v\n", lp, readErr)
|
||||
totalIssues++
|
||||
continue
|
||||
}
|
||||
fmt.Printf("\n### %s (%d 字节)\n", lp, len(text))
|
||||
|
||||
tokens := tokenize(text)
|
||||
fmt.Printf("token 总数: %d -> %s\n", len(tokens),
|
||||
map[bool]string{true: "偶数,可完整配对 ✅", false: "奇数,末尾有孤立 token ❌"}[len(tokens)%2 == 0])
|
||||
|
||||
entries := make([]entry, 0, len(tokens)/2)
|
||||
var badPairs int
|
||||
for i := 0; i+1 < len(tokens); i += 2 {
|
||||
rawID := tokens[i]
|
||||
rawRef := tokens[i+1]
|
||||
id, convErr := strconv.ParseInt(rawID, 10, 64)
|
||||
if convErr != nil {
|
||||
badPairs++
|
||||
if badPairs <= *limit {
|
||||
fmt.Printf(" 非数字 id 位置 pair#%d: id=%q ref=%q\n", i/2, rawID, rawRef)
|
||||
}
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entry{id: id, ref: rawRef, index: i / 2})
|
||||
}
|
||||
if badPairs > 0 {
|
||||
fmt.Printf("非数字 id 的对数: %d ❌\n", badPairs)
|
||||
totalIssues += badPairs
|
||||
}
|
||||
fmt.Printf("解析出 id/路径 对: %d\n", len(entries))
|
||||
|
||||
// ---- 重复 id ----
|
||||
byID := map[int64][]entry{}
|
||||
for _, e := range entries {
|
||||
byID[e.id] = append(byID[e.id], e)
|
||||
}
|
||||
dupIDs := 0
|
||||
var dupKeys []int64
|
||||
for id, list := range byID {
|
||||
if len(list) > 1 {
|
||||
dupIDs++
|
||||
dupKeys = append(dupKeys, id)
|
||||
}
|
||||
}
|
||||
sort.Slice(dupKeys, func(i, j int) bool { return dupKeys[i] < dupKeys[j] })
|
||||
fmt.Printf("重复 id 个数: %d\n", dupIDs)
|
||||
if dupIDs > 0 {
|
||||
totalIssues += dupIDs
|
||||
for i, id := range dupKeys {
|
||||
if i >= *limit {
|
||||
fmt.Printf(" ... 其余 %d 个重复 id 省略\n", dupIDs-*limit)
|
||||
break
|
||||
}
|
||||
var refs []string
|
||||
for _, e := range byID[id] {
|
||||
refs = append(refs, fmt.Sprintf("pair#%d:%s", e.index, e.ref))
|
||||
}
|
||||
fmt.Printf(" DUPLICATE id=%d -> %s\n", id, strings.Join(refs, " "))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 路径存在性 ----
|
||||
missing := 0
|
||||
emptyRef := 0
|
||||
dir := path.Dir(strings.ReplaceAll(lp, "\\", "/"))
|
||||
if dir == "." {
|
||||
dir = ""
|
||||
}
|
||||
var missingSamples []string
|
||||
var resolvedRel int
|
||||
for _, e := range entries {
|
||||
ref := strings.TrimSpace(e.ref)
|
||||
if ref == "" {
|
||||
emptyRef++
|
||||
continue
|
||||
}
|
||||
// .lst 里的引用可能用反斜杠,先统一成正斜杠再解析。
|
||||
norm := strings.ReplaceAll(ref, "\\", "/")
|
||||
key := strings.ToLower(norm)
|
||||
if _, ok := existing[key]; ok {
|
||||
continue
|
||||
}
|
||||
// 再按「相对 .lst 所在目录」解析一次
|
||||
if dir != "" {
|
||||
rel := path.Clean(path.Join(dir, norm))
|
||||
if _, ok := existing[strings.ToLower(rel)]; ok {
|
||||
resolvedRel++
|
||||
continue
|
||||
}
|
||||
}
|
||||
missing++
|
||||
if len(missingSamples) < *limit {
|
||||
missingSamples = append(missingSamples, fmt.Sprintf("id=%d ref=%s", e.id, ref))
|
||||
}
|
||||
}
|
||||
fmt.Printf("空引用: %d\n", emptyRef)
|
||||
fmt.Printf("路径缺失(两种解析都不存在): %d\n", missing)
|
||||
if resolvedRel > 0 {
|
||||
fmt.Printf("需按 .lst 所在目录相对解析才存在的路径: %d(正常,说明是相对引用)\n", resolvedRel)
|
||||
}
|
||||
for _, s := range missingSamples {
|
||||
fmt.Printf(" MISSING %s\n", s)
|
||||
}
|
||||
totalIssues += missing + emptyRef
|
||||
|
||||
// ---- 指定 id ----
|
||||
if len(wantList) > 0 {
|
||||
fmt.Printf("-- 指定 id 落点 --\n")
|
||||
for _, id := range wantList {
|
||||
list := byID[id]
|
||||
if len(list) == 0 {
|
||||
fmt.Printf(" id=%d : 未在此 .lst 注册 ❌\n", id)
|
||||
continue
|
||||
}
|
||||
for _, e := range list {
|
||||
ref := strings.TrimSpace(e.ref)
|
||||
norm := strings.ReplaceAll(ref, "\\", "/")
|
||||
status := "文件存在 ✅"
|
||||
actual := ""
|
||||
if real, ok := existing[strings.ToLower(norm)]; ok {
|
||||
actual = real
|
||||
} else if dir != "" {
|
||||
rel := path.Clean(path.Join(dir, norm))
|
||||
if real, ok := existing[strings.ToLower(rel)]; ok {
|
||||
actual = real
|
||||
status = "文件存在(相对解析)✅"
|
||||
}
|
||||
}
|
||||
if actual == "" {
|
||||
status = "文件不存在 ❌"
|
||||
}
|
||||
fmt.Printf(" id=%d : pair#%d ref=%q -> %s (%s)\n", id, e.index, ref, actual, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *dump != "" {
|
||||
dumpLines = append(dumpLines, fmt.Sprintf("# %s entries=%d", lp, len(entries)))
|
||||
for _, e := range entries {
|
||||
dumpLines = append(dumpLines, fmt.Sprintf("%d\t%s", e.id, e.ref))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *dump != "" {
|
||||
if werr := os.WriteFile(*dump, []byte(strings.Join(dumpLines, "\n")+"\n"), 0o644); werr != nil {
|
||||
fmt.Fprintln(os.Stderr, "dump:", werr)
|
||||
} else {
|
||||
fmt.Printf("\n已写出对照表: %s (%d 行)\n", *dump, len(dumpLines))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n===== 结构问题合计: %d =====\n", totalIssues)
|
||||
if totalIssues == 0 {
|
||||
fmt.Println("结论: 所查 .lst 结构完整(成对、无重复 id、路径全部存在)")
|
||||
}
|
||||
}
|
||||
|
||||
// tokenize 按空白切分,并把 `path` 的反引号剥掉。
|
||||
// 反引号内的内容不切分(路径理论上不含空白,这里只为稳健)。
|
||||
func tokenize(text string) []string {
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
inTick := false
|
||||
flush := func() {
|
||||
if cur.Len() > 0 {
|
||||
out = append(out, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
}
|
||||
for _, r := range text {
|
||||
switch {
|
||||
case r == '`':
|
||||
if inTick {
|
||||
inTick = false
|
||||
flush()
|
||||
} else {
|
||||
flush()
|
||||
inTick = true
|
||||
}
|
||||
case !inTick && (r == ' ' || r == '\t' || r == '\r' || r == '\n'):
|
||||
flush()
|
||||
default:
|
||||
cur.WriteRune(r)
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user