Files
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

280 lines
7.8 KiB
Go

package pvf
import (
"context"
"errors"
"testing"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestParseSectionsAndListEntries(t *testing.T) {
doc, err := Parse("equipment/equipment.lst", `
[equipment]
1001 `+"`equipment/weapon/sword.equ`"+`
1002 `+"`equipment/armor/coat.equ`"+`
[metadata]
rarity 3 1.5
`)
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(doc.Sections) != 2 {
t.Fatalf("expected 2 sections, got %+v", doc.Sections)
}
tokens, ok := doc.Section("equipment")
if !ok || len(tokens) != 4 {
t.Fatalf("expected equipment section tokens, got ok=%v tokens=%+v", ok, tokens)
}
entries := ParseList(doc)
if len(entries) != 2 {
t.Fatalf("expected list entries, got %+v", entries)
}
if entries[0].ID != 1001 || entries[0].Path != "equipment/weapon/sword.equ" {
t.Fatalf("unexpected first entry: %+v", entries[0])
}
}
func TestParseRejectsBadInput(t *testing.T) {
if _, err := Parse("", ""); !errors.Is(err, ErrPathRequired) {
t.Fatalf("expected ErrPathRequired, got %v", err)
}
_, err := Parse("bad.lst", "`missing")
if err == nil {
t.Fatalf("expected unclosed string error")
}
}
func TestDocumentValueHelpers(t *testing.T) {
doc, err := Parse("dungeon/test.dgn", `
[monsters]
1001 1002 `+"`monster/boss.mob`"+`
[maps]
`+"`map/a.map` `map/b.map`"+`
`)
if err != nil {
t.Fatalf("parse: %v", err)
}
if got := doc.Ints("monsters"); len(got) != 2 || got[0] != 1001 || got[1] != 1002 {
t.Fatalf("unexpected ints: %+v", got)
}
if got := doc.Texts("maps"); len(got) != 2 || got[0] != "map/a.map" || got[1] != "map/b.map" {
t.Fatalf("unexpected texts: %+v", got)
}
}
func TestParseMultilineBacktickString(t *testing.T) {
doc, err := Parse("skill/test.skl", "[explain]\n`first line\nsecond line`\n[required level]\n5\n")
if err != nil {
t.Fatalf("parse multiline string: %v", err)
}
value, ok := doc.Text("explain")
if !ok || value != "first line\nsecond line" {
t.Fatalf("multiline value = %q ok=%t", value, ok)
}
level, ok := doc.Int("required level")
if !ok || level != 5 {
t.Fatalf("required level = %d ok=%t", level, ok)
}
}
func TestBuildIndexFromMemorySource(t *testing.T) {
source := &memSource{
texts: map[string]string{
"equipment/equipment.lst": "1001 `equipment/weapon/sword.equ`\n",
"equipment/weapon/sword.equ": `
[name]
` + "`short sword`" + `
[attack]
12
`,
"skill/skill.lst": "2001 `skill/slash.skl`\n",
},
files: []platformpvf.File{
{ArchivePath: "equipment/equipment.lst"},
{ArchivePath: "equipment/weapon/sword.equ"},
{ArchivePath: "skill/skill.lst"},
},
reads: make(map[string]int),
}
index, err := Build(context.Background(), source, BuildOptions{
Lists: []string{"equipment/equipment.lst"},
})
if err != nil {
t.Fatalf("build index: %v", err)
}
if snapshot := index.Snapshot(); snapshot.Documents != 2 || snapshot.Lists != 1 || snapshot.Refs != 1 {
t.Fatalf("unexpected snapshot: %+v", snapshot)
}
path, ok := index.Resolve("equipment/equipment.lst", 1001)
if !ok || path != "equipment/weapon/sword.equ" {
t.Fatalf("unexpected resolved path: %q ok=%v", path, ok)
}
doc, ok := index.Document("./EQUIPMENT/weapon/sword.equ")
if !ok {
t.Fatalf("expected indexed sword document")
}
if _, ok := doc.Section("attack"); !ok {
t.Fatalf("expected attack section")
}
before := source.reads["equipment/weapon/sword.equ"]
if _, ok := index.Document("equipment/weapon/sword.equ"); !ok {
t.Fatalf("expected cached document")
}
if source.reads["equipment/weapon/sword.equ"] != before {
t.Fatalf("document lookup should not read source again")
}
}
func TestBuildIndexResolvesListRelativePaths(t *testing.T) {
source := &memSource{
texts: map[string]string{
"equipment/equipment.lst": "1001 `weapon/sword.equ`\n",
"equipment/weapon/sword.equ": `
[name]
` + "`relative sword`" + `
`,
},
files: []platformpvf.File{
{ArchivePath: "equipment/equipment.lst"},
{ArchivePath: "equipment/weapon/sword.equ"},
},
reads: make(map[string]int),
}
index, err := Build(context.Background(), source, BuildOptions{
Lists: []string{"equipment/equipment.lst"},
})
if err != nil {
t.Fatalf("build index: %v", err)
}
resolved, ok := index.Resolve("equipment/equipment.lst", 1001)
if !ok || resolved != "equipment/weapon/sword.equ" {
t.Fatalf("resolved = %q ok=%v", resolved, ok)
}
if _, ok := index.Document("equipment/weapon/sword.equ"); !ok {
t.Fatalf("expected relative document to be indexed")
}
}
func TestBuildIndexLoadsNestedLists(t *testing.T) {
source := &memSource{
texts: map[string]string{
"skill/skilllist.lst": "0 `SwordmanSkill.lst`\n",
"skill/SwordmanSkill.lst": "46 `Swordman/UpperSlash.skl`\n",
"skill/Swordman/UpperSlash.skl": `
[name]
` + "`upper slash`" + `
`,
},
files: []platformpvf.File{
{ArchivePath: "skill/skilllist.lst"},
{ArchivePath: "skill/SwordmanSkill.lst"},
{ArchivePath: "skill/Swordman/UpperSlash.skl"},
},
reads: make(map[string]int),
}
index, err := Build(context.Background(), source, BuildOptions{Lists: []string{"skill/skilllist.lst"}})
if err != nil {
t.Fatalf("build nested index: %v", err)
}
if snapshot := index.Snapshot(); snapshot.Documents != 3 || snapshot.Lists != 2 || snapshot.Refs != 2 {
t.Fatalf("unexpected nested snapshot: %+v", snapshot)
}
jobList, ok := index.Resolve("skill/skilllist.lst", 0)
if !ok || jobList != "skill/SwordmanSkill.lst" {
t.Fatalf("job list = %q ok=%t", jobList, ok)
}
skillPath, ok := index.Resolve(jobList, 46)
if !ok || skillPath != "skill/Swordman/UpperSlash.skl" {
t.Fatalf("skill path = %q ok=%t", skillPath, ok)
}
if _, ok := index.Document(skillPath); !ok {
t.Fatal("nested skill document was not loaded")
}
}
func TestBuildIndexRequiresSource(t *testing.T) {
_, err := Build(context.Background(), nil, BuildOptions{})
if !errors.Is(err, ErrSourceRequired) {
t.Fatalf("expected ErrSourceRequired, got %v", err)
}
}
func TestBuildIndexHonorsCanceledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := Build(ctx, &memSource{
texts: map[string]string{"a.lst": "1 `a/a.equ`"},
reads: make(map[string]int),
}, BuildOptions{Paths: []string{"a.lst"}})
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected context canceled, got %v", err)
}
}
func TestBuildIndexUsesDirectLookupWithoutListingArchive(t *testing.T) {
source := &finderMemSource{memSource: memSource{
texts: map[string]string{
"skill/skill.lst": "1 `relative.skl`\n",
"skill/relative.skl": "[name]\n`direct lookup`\n",
},
files: []platformpvf.File{
{ArchivePath: "skill/skill.lst"},
{ArchivePath: "skill/relative.skl"},
},
reads: make(map[string]int),
}}
index, err := Build(context.Background(), source, BuildOptions{Lists: []string{"skill/skill.lst"}})
if err != nil {
t.Fatalf("build index: %v", err)
}
if source.listCalls != 0 {
t.Fatalf("Files calls=%d want=0", source.listCalls)
}
if _, ok := index.Document("skill/relative.skl"); !ok {
t.Fatal("relative document was not loaded through direct lookup")
}
}
type memSource struct {
texts map[string]string
files []platformpvf.File
reads map[string]int
}
type finderMemSource struct {
memSource
listCalls int
}
func (s *finderMemSource) Files() []platformpvf.File {
s.listCalls++
return s.memSource.Files()
}
func (s *finderMemSource) FindFile(relativePath string) (platformpvf.File, bool) {
key := pathKey(relativePath)
for _, file := range s.files {
if pathKey(file.ArchivePath) == key {
return file, true
}
}
return platformpvf.File{}, false
}
func (s *memSource) ReadText(relativePath string) (string, error) {
key := pathKey(relativePath)
for path, text := range s.texts {
if pathKey(path) == key {
s.reads[cleanPath(path)]++
return text, nil
}
}
return "", platformpvf.ErrFileNotFound
}
func (s *memSource) Files() []platformpvf.File {
return append([]platformpvf.File(nil), s.files...)
}