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,347 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user