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

+104
View File
@@ -0,0 +1,104 @@
// pvfsemcmp 逐路径比较两个 PVF「解码后的文本」,而不是原始字节。
//
// 动机:字符串池被重排(只追加 vs 全量重建)会让每个字符串 token 的 magic offset 变化,
// 于是几乎所有文件的原始字节都不同——但解码后的文本可能完全一致(说明重排是自洽的、无害的),
// 也可能变成乱码(说明池和 token 不再对应,客户端必然解析失败)。
//
// 用法:
//
// go run ./cmd/scratch/pvfsemcmp -a <A.pvf> -b <B.pvf> [-max-examples 20] [-prefix stackable/]
package main
import (
"flag"
"fmt"
"os"
"strings"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func main() {
aPath := flag.String("a", "", "基线 PVF")
bPath := flag.String("b", "", "改后 PVF")
maxExamples := flag.Int("max-examples", 20, "每类最多打印多少条示例")
prefix := flag.String("prefix", "", "只比较该前缀(可选)")
limit := flag.Int("limit", 0, "最多比较多少个文件(0=全部)")
flag.Parse()
if *aPath == "" || *bPath == "" {
fmt.Fprintln(os.Stderr, "-a and -b are required")
os.Exit(2)
}
a, err := platformpvf.Open(*aPath)
if err != nil {
fmt.Fprintln(os.Stderr, "open A:", err)
os.Exit(2)
}
b, err := platformpvf.Open(*bPath)
if err != nil {
fmt.Fprintln(os.Stderr, "open B:", err)
os.Exit(2)
}
prefixKey := strings.ToLower(*prefix)
bIdx := map[string]string{}
for _, f := range b.Files() {
bIdx[strings.ToLower(f.ArchivePath)] = f.ArchivePath
}
var compared, identical, different, onlyA, readErr int
var examples []string
for _, fa := range a.Files() {
key := strings.ToLower(fa.ArchivePath)
if prefixKey != "" && !strings.HasPrefix(key, prefixKey) {
continue
}
bp, ok := bIdx[key]
if !ok {
onlyA++
continue
}
if *limit > 0 && compared >= *limit {
break
}
ta, errA := a.ReadText(fa.ArchivePath)
tb, errB := b.ReadText(bp)
if errA != nil || errB != nil {
readErr++
continue
}
compared++
if ta == tb {
identical++
continue
}
different++
if len(examples) < *maxExamples {
examples = append(examples, fmt.Sprintf(" - %s\n A: %s\n B: %s",
fa.ArchivePath, clip(ta), clip(tb)))
}
}
fmt.Printf("比较文件数: %d\n", compared)
fmt.Printf("解码后文本完全相同: %d\n", identical)
fmt.Printf("解码后文本不同: %d\n", different)
fmt.Printf("只在 A 存在: %d\n", onlyA)
fmt.Printf("读取失败: %d\n", readErr)
if different > 0 {
fmt.Printf("\n文本不同的示例:\n%s\n", strings.Join(examples, "\n"))
}
if different == 0 && readErr == 0 {
fmt.Println("\n结论: 两个库语义一致 —— 字节差异只来自字符串池重排/重压缩,解码结果未变。")
}
}
func clip(s string) string {
s = strings.ReplaceAll(s, "\r", "")
s = strings.Join(strings.Fields(s), " ")
if len(s) > 200 {
return s[:200] + "…"
}
return s
}