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

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

211 lines
6.2 KiB
Go

// Scratch tool: scan every PVF text document for integer tokens equal to one of
// the account-shared materials (cubes 3033..3037 / 3262, souls 10100115 /
// 10100116 / 10099773..10099775 / 10158124), and report where they are
// referenced (directory prefix + section name + document count).
//
// go run ./cmd/scratch/idscan -pvf <Script.pvf> [-top 40] [-maxdoc 2000000]
//
// Why: answering "which acquisition systems can actually hand out a cube/soul"
// from code alone is guesswork. This reads the data instead -- each hit names a
// system (npc shop list, quest reward, package data, drop table, event config),
// which then maps to exactly one server-side grant path.
package main
import (
"flag"
"fmt"
"os"
"path"
"sort"
"strings"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
var accountSharedIDs = map[int64]string{
3033: "3033", 3034: "3034", 3035: "3035", 3036: "3036",
3037: "3037无色", 3262: "3262金",
10100115: "10100115普通灵魂", 10100116: "10100116高级灵魂",
10099773: "10099773稀有灵魂", 10099774: "10099774独有灵魂",
10099775: "10099775传说灵魂", 10158124: "10158124史诗灵魂",
}
type hit struct {
prefix string
section string
itemID int64
docs int
}
func main() {
pvfPath := flag.String("pvf", "../runtime/data/dnf/Script.pvf", "Script.pvf path")
top := flag.Int("top", 40, "rows to print per report")
maxDoc := flag.Int("maxdoc", 2000000, "skip documents larger than this")
prefixFilter := flag.String("prefix", "", "only documents under this directory prefix (e.g. etc)")
sectionFilter := flag.String("section", "", "only this section name (comma separated)")
flag.Parse()
archive, err := platformpvf.Open(*pvfPath)
if err != nil {
fmt.Fprintln(os.Stderr, "open:", err)
os.Exit(1)
}
type key struct {
prefix string
section string
itemID int64
}
wantedSections := map[string]struct{}{}
for _, name := range strings.Split(*sectionFilter, ",") {
name = strings.ToLower(strings.TrimSpace(name))
if name != "" {
wantedSections[name] = struct{}{}
}
}
filtered := *prefixFilter != "" || len(wantedSections) != 0
paths := map[key][]string{}
docs := map[key]map[string]struct{}{}
perItem := map[int64]map[string]struct{}{}
scanned, parsed, skipped := 0, 0, 0
for _, file := range archive.Files() {
full := strings.ReplaceAll(file.ArchivePath, "\\", "/")
if strings.TrimSpace(full) == "" {
continue
}
if !isTextDocument(full) {
continue
}
if *prefixFilter != "" && !strings.HasPrefix(full, *prefixFilter) {
continue
}
text, readErr := archive.ReadText(full)
if readErr != nil {
skipped++
continue
}
if len(text) > *maxDoc {
skipped++
continue
}
scanned++
document, parseErr := dnfpvf.Parse(full, text)
if parseErr != nil {
continue
}
parsed++
// 每个段名 -> 该段出现的 int token 集合,命中即记一次(按文档去重)。
for _, section := range document.Sections {
name := strings.ToLower(strings.TrimSpace(section.Name))
if len(wantedSections) != 0 {
if _, ok := wantedSections[name]; !ok {
continue
}
}
if section.Start < 0 || section.End > len(document.Tokens) || section.Start > section.End {
continue
}
found := map[int64]struct{}{}
for _, token := range document.Tokens[section.Start:section.End] {
if token.Kind != dnfpvf.TokenInt {
continue
}
if _, ok := accountSharedIDs[token.Int]; !ok {
continue
}
found[token.Int] = struct{}{}
}
for itemID := range found {
slot := key{prefix: firstSegment(full), section: name, itemID: itemID}
if docs[slot] == nil {
docs[slot] = map[string]struct{}{}
}
docs[slot][full] = struct{}{}
if filtered && len(paths[slot]) < 6 {
paths[slot] = append(paths[slot], full)
}
if perItem[itemID] == nil {
perItem[itemID] = map[string]struct{}{}
}
perItem[itemID][name] = struct{}{}
}
}
}
fmt.Printf("files=%d documents=%d parsed=%d skipped=%d\n", len(archive.Files()), scanned, parsed, skipped)
rows := make([]hit, 0, len(docs))
for slot, set := range docs {
rows = append(rows, hit{prefix: slot.prefix, section: slot.section, itemID: slot.itemID, docs: len(set)})
}
sort.Slice(rows, func(a, b int) bool {
if rows[a].docs != rows[b].docs {
return rows[a].docs > rows[b].docs
}
if rows[a].prefix != rows[b].prefix {
return rows[a].prefix < rows[b].prefix
}
if rows[a].section != rows[b].section {
return rows[a].section < rows[b].section
}
return rows[a].itemID < rows[b].itemID
})
fmt.Printf("--- top %d (directory prefix / section / item / documents) ---\n", *top)
for index, entry := range rows {
if index >= *top {
break
}
fmt.Printf("%-22s [%s] item=%s docs=%d\n",
entry.prefix, entry.section, accountSharedIDs[entry.itemID], entry.docs)
if !filtered {
continue
}
for _, documentPath := range paths[key{prefix: entry.prefix, section: entry.section, itemID: entry.itemID}] {
fmt.Printf(" %s\n", documentPath)
}
}
fmt.Println("--- per item: sections that mention it ---")
ids := make([]int64, 0, len(perItem))
for itemID := range perItem {
ids = append(ids, itemID)
}
sort.Slice(ids, func(a, b int) bool { return ids[a] < ids[b] })
for _, itemID := range ids {
names := make([]string, 0, len(perItem[itemID]))
for name := range perItem[itemID] {
names = append(names, name)
}
sort.Strings(names)
fmt.Printf("%s: %s\n", accountSharedIDs[itemID], strings.Join(names, ", "))
}
}
func firstSegment(full string) string {
dir := path.Dir(full)
if dir == "." {
return "(root)"
}
if index := strings.Index(dir, "/"); index >= 0 {
return dir[:index]
}
return dir
}
func isTextDocument(full string) bool {
lower := strings.ToLower(full)
for _, suffix := range []string{
".lst", ".stk", ".equ", ".qst", ".dgn", ".key", ".act", ".ani", ".map",
".cre", ".npc", ".set", ".etc", ".ui", ".txt", ".chr",
// 商店(483)、脚本/事件/任务表等:NPC 商店的道具表就在这里(.shp),
// 第一版扫描漏了它们,会得出"商店不卖晶块"的错误结论。
".shp", ".tbl", ".evt", ".rep", ".msn", ".wrd", ".apd", ".cmt",
".mob", ".als", ".obj", ".skl", ".ai", ".aic", ".ptl",
} {
if strings.HasSuffix(lower, suffix) {
return true
}
}
return false
}