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