服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
348 lines
12 KiB
Go
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))
|
|
}
|
|
}
|