Files
90CNServer/server/cmd/scratch/pvfsearch/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

348 lines
12 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path"
"sort"
"strconv"
"strings"
"sync"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
platformpvf "longheng.io/server/internal/platform/pvf"
)
type result struct {
Kind string `json:"kind"`
ItemID int64 `json:"item_id"`
Name string `json:"name,omitempty"`
Name2 string `json:"name2,omitempty"`
Path string `json:"path"`
Rarity int64 `json:"rarity,omitempty"`
Grade int64 `json:"grade,omitempty"`
MinimumLevel int64 `json:"minimum_level,omitempty"`
Durability int64 `json:"durability,omitempty"`
EquipmentType string `json:"equipment_type,omitempty"`
StackableType string `json:"stackable_type,omitempty"`
StackLimit int64 `json:"stack_limit,omitempty"`
StackLimitFound bool `json:"stack_limit_found,omitempty"`
ActionType string `json:"action_type,omitempty"`
ActionInts []int64 `json:"action_ints,omitempty"`
DirectExpType string `json:"direct_experience_type,omitempty"`
DirectExpValues []int64 `json:"direct_experience_values,omitempty"`
ExpBonusRate float64 `json:"exp_bonus_rate,omitempty"`
EffectDuration int64 `json:"effect_duration_ms,omitempty"`
RelatedSections []string `json:"experience_sections,omitempty"`
}
type sourceEntry struct {
kind string
itemID int64
path string
}
func main() {
pvfPath := flag.String("pvf", `D:/DNF/runtime/data/dnf/Script.pvf`, "Script.pvf path")
readPath := flag.String("read", "", "print one exact archive text path and exit")
allMedals := flag.Bool("all-medals", false, "include every [flag] equipment")
allGuardianGems := flag.Bool("all-guardian-gems", false, "include every [flag gem] stackable")
equipmentOnly := flag.Bool("equipment-only", false, "search names only in equipment")
npcOnly := flag.Bool("npc-only", false, "search names only in npc/npc.lst definitions")
pathOnly := flag.Bool("path-only", false, "search every archive path and print matches without reading documents")
scanPrefix := flag.String("scan-prefix", "", "search decoded text under one archive path prefix and print matching paths")
experienceEffects := flag.Bool("experience-effects", false, "list stackables with experience-related PVF sections")
allItems := flag.Bool("all-items", false, "inspect every listed equipment and stackable item")
auditItemExtensions := flag.Bool("audit-item-extensions", false, "summarize required item extension fields without dumping every item")
flag.Parse()
archive, err := platformpvf.Open(*pvfPath)
if err != nil {
panic(err)
}
if strings.TrimSpace(*readPath) != "" {
value, readErr := archive.ReadText(*readPath)
if readErr != nil {
panic(readErr)
}
fmt.Print(value)
return
}
queries := make([]string, 0, flag.NArg())
for _, query := range flag.Args() {
if query = strings.ToLower(strings.TrimSpace(query)); query != "" {
queries = append(queries, query)
}
}
if *pathOnly {
for _, file := range archive.Files() {
lowerPath := strings.ToLower(file.ArchivePath)
for _, query := range queries {
if strings.Contains(lowerPath, query) {
fmt.Println(file.ArchivePath)
break
}
}
}
return
}
if prefix := strings.ToLower(strings.TrimSpace(*scanPrefix)); prefix != "" {
for _, file := range archive.Files() {
lowerPath := strings.ToLower(file.ArchivePath)
if !strings.HasPrefix(lowerPath, prefix) {
continue
}
text, readErr := archive.ReadText(file.ArchivePath)
if readErr != nil {
continue
}
haystack := strings.ToLower(text)
for _, query := range queries {
if strings.Contains(haystack, query) {
fmt.Println(file.ArchivePath)
break
}
}
}
return
}
sources := []struct {
kind string
listPath string
}{
{kind: "equipment", listPath: "equipment/equipment.lst"},
{kind: "stackable", listPath: "stackable/stackable.lst"},
{kind: "npc", listPath: "npc/npc.lst"},
}
entries := make([]sourceEntry, 0, 65536)
for _, source := range sources {
if *npcOnly && source.kind != "npc" {
continue
}
if !*npcOnly && source.kind == "npc" {
continue
}
if source.kind == "stackable" && *equipmentOnly && !*allGuardianGems {
continue
}
text, readErr := archive.ReadText(source.listPath)
if readErr != nil {
panic(readErr)
}
doc, parseErr := dnfpvf.Parse(source.listPath, text)
if parseErr != nil {
panic(parseErr)
}
for _, entry := range dnfpvf.ParseList(doc) {
docPath := resolvePath(archive, source.listPath, entry.Path)
if docPath == "" {
continue
}
lowerPath := strings.ToLower(docPath)
if source.kind == "stackable" && *allGuardianGems && len(queries) == 0 && !strings.Contains(lowerPath, "flaggem/") {
continue
}
if source.kind == "equipment" && *allMedals && len(queries) == 0 && !strings.Contains(lowerPath, "flag/") {
continue
}
entries = append(entries, sourceEntry{kind: source.kind, itemID: entry.ID, path: docPath})
}
}
jobs := make(chan sourceEntry)
results := make(chan result, 256)
var workers sync.WaitGroup
for worker := 0; worker < 16; worker++ {
workers.Add(1)
go func() {
defer workers.Done()
for entry := range jobs {
if row, ok := inspect(archive, entry, queries, *allMedals, *allGuardianGems, *experienceEffects, *allItems || *auditItemExtensions); ok {
results <- row
}
}
}()
}
go func() {
for _, entry := range entries {
jobs <- entry
}
close(jobs)
workers.Wait()
close(results)
}()
rows := make([]result, 0, 64)
for row := range results {
rows = append(rows, row)
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].Kind != rows[j].Kind {
return rows[i].Kind < rows[j].Kind
}
return rows[i].ItemID < rows[j].ItemID
})
if *auditItemExtensions {
printItemExtensionAudit(rows)
return
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(rows); err != nil {
panic(fmt.Errorf("encode results: %w", err))
}
}
func resolvePath(archive *platformpvf.Archive, listPath, reference string) string {
reference = strings.TrimSpace(strings.Trim(reference, "`\"'"))
for _, candidate := range []string{reference, path.Join(path.Dir(listPath), reference)} {
if file, ok := archive.FindFile(candidate); ok {
return file.ArchivePath
}
}
return ""
}
func inspect(archive *platformpvf.Archive, entry sourceEntry, queries []string, allMedals, allGuardianGems, experienceEffects, allItems bool) (result, bool) {
text, err := archive.ReadText(entry.path)
if err != nil {
return result{}, false
}
doc, err := dnfpvf.Parse(entry.path, text)
if err != nil {
return result{}, false
}
name, _ := doc.Text("name")
name2, _ := doc.Text("name2")
equipmentType, _ := doc.Text("equipment type")
stackableType, _ := doc.Text("stackable type")
stackLimit, stackLimitFound := doc.Int("stack limit")
actionType, _ := doc.Text("action type")
actionInts := doc.Ints("action type")
directExpType, _ := doc.Text("increase status type")
directExpValues := doc.Ints("increase status type")
expBonusRate, _ := doc.Number("exp bonus rate")
effectDuration, _ := doc.Int("stat change duration")
relatedSections := make([]string, 0, 4)
for _, section := range doc.Sections {
sectionName := strings.ToLower(strings.TrimSpace(section.Name))
if strings.Contains(sectionName, "experience") || strings.HasPrefix(sectionName, "exp ") ||
strings.HasSuffix(sectionName, " exp") || strings.Contains(sectionName, " exp ") {
relatedSections = append(relatedSections, section.Name)
}
}
medal := entry.kind == "equipment" && strings.EqualFold(strings.TrimSpace(equipmentType), "[flag]")
guardianGem := entry.kind == "stackable" && strings.EqualFold(strings.TrimSpace(stackableType), "[flag gem]")
matched := allItems || (allMedals && medal) || (allGuardianGems && guardianGem)
if experienceEffects && entry.kind == "stackable" && (strings.TrimSpace(directExpType) != "" || len(relatedSections) > 0 ||
strings.Contains(strings.ToLower(actionType), " exp ") || strings.Contains(name, "经验书")) {
matched = true
}
haystack := strings.ToLower(strings.Join([]string{name, name2, entry.path}, "\n"))
for _, query := range queries {
if strings.Contains(haystack, query) {
matched = true
break
}
}
if !matched {
return result{}, false
}
rarity, _ := doc.Int("rarity")
grade, _ := doc.Int("grade")
minimumLevel, _ := doc.Int("minimum level")
durability, _ := doc.Int("durability")
return result{
Kind: entry.kind,
ItemID: entry.itemID,
Name: name,
Name2: name2,
Path: entry.path,
Rarity: rarity,
Grade: grade,
MinimumLevel: minimumLevel,
Durability: durability,
EquipmentType: equipmentType,
StackableType: stackableType,
StackLimit: stackLimit,
StackLimitFound: stackLimitFound,
ActionType: actionType,
ActionInts: actionInts,
DirectExpType: directExpType,
DirectExpValues: directExpValues,
ExpBonusRate: expBonusRate,
EffectDuration: effectDuration,
RelatedSections: relatedSections,
}, true
}
type itemExtensionAudit struct {
EquipmentItems int `json:"equipment_items"`
StackableItems int `json:"stackable_items"`
EquipmentTypes int `json:"equipment_types"`
StackableTypes int `json:"stackable_types"`
MissingEquipmentType []result `json:"missing_equipment_type,omitempty"`
MissingStackableType []result `json:"missing_stackable_type,omitempty"`
InferredGoldKaleidoType []result `json:"inferred_gold_kaleido_type,omitempty"`
StackableWithoutStackLimit int `json:"stackable_without_stack_limit"`
MissingStackLimitByType map[string]int `json:"missing_stack_limit_by_type,omitempty"`
PresentStackLimitByType map[string]map[string]int `json:"present_stack_limit_by_type,omitempty"`
InvalidPositiveStackLimit []result `json:"invalid_positive_stack_limit,omitempty"`
}
func printItemExtensionAudit(rows []result) {
audit := itemExtensionAudit{
MissingStackLimitByType: make(map[string]int),
PresentStackLimitByType: make(map[string]map[string]int),
}
equipmentTypes := make(map[string]struct{})
stackableTypes := make(map[string]struct{})
for _, row := range rows {
switch row.Kind {
case "equipment":
audit.EquipmentItems++
typeName := strings.TrimSpace(row.EquipmentType)
if typeName == "" {
audit.MissingEquipmentType = append(audit.MissingEquipmentType, row)
} else {
equipmentTypes[strings.ToLower(typeName)] = struct{}{}
}
case "stackable":
audit.StackableItems++
typeName := strings.TrimSpace(row.StackableType)
if typeName == "" && strings.EqualFold(strings.TrimSpace(row.ActionType), "[gold kaleido box]") {
audit.InferredGoldKaleidoType = append(audit.InferredGoldKaleidoType, row)
typeName = "[gold kaleido]"
}
if typeName == "" {
audit.MissingStackableType = append(audit.MissingStackableType, row)
} else {
stackableTypes[strings.ToLower(typeName)] = struct{}{}
}
if !row.StackLimitFound {
audit.StackableWithoutStackLimit++
audit.MissingStackLimitByType[typeName]++
} else if row.StackLimit <= 0 {
audit.InvalidPositiveStackLimit = append(audit.InvalidPositiveStackLimit, row)
} else {
limits := audit.PresentStackLimitByType[typeName]
if limits == nil {
limits = make(map[string]int)
audit.PresentStackLimitByType[typeName] = limits
}
limits[strconv.FormatInt(row.StackLimit, 10)]++
}
}
}
audit.EquipmentTypes = len(equipmentTypes)
audit.StackableTypes = len(stackableTypes)
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(audit); err != nil {
panic(fmt.Errorf("encode item extension audit: %w", err))
}
}