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,872 @@
|
||||
package worldmap
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseDungeonMetadataRepeatedMazesAndExtensions(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/test.dgn", `
|
||||
[name]
|
||||
`+"`Test Dungeon`"+`
|
||||
[explain]
|
||||
`+"`Structured from local PVF`"+`
|
||||
[cutscene image]
|
||||
`+"`Map/Test/loading.img`"+` 3
|
||||
[minimum required level]
|
||||
10
|
||||
[basis level]
|
||||
20
|
||||
[experience increasing point]
|
||||
1.25
|
||||
[dungeon type]
|
||||
`+"`[normal]`"+`
|
||||
[tutorial dungeon]
|
||||
1
|
||||
[recommended level]
|
||||
10 30
|
||||
[fatigue]
|
||||
8
|
||||
[raid dungeon]
|
||||
1
|
||||
[maze info]
|
||||
[size]
|
||||
3 2
|
||||
[greed]
|
||||
`+"`BBFFEE`"+`
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100 `+"`map`"+` 1 0 101 102 `+"`boss`"+` 2 1 103
|
||||
[start map]
|
||||
0 0 7
|
||||
[boss map]
|
||||
2 1
|
||||
[quest connection]
|
||||
0 900 -1
|
||||
[boss map specification]
|
||||
2 1 103
|
||||
[layered map specification]
|
||||
1 1 104 105
|
||||
[maze minimap ani icon]
|
||||
0 1 0 `+"`ui/maze.ani`"+` 1
|
||||
[randomized object creation]
|
||||
[select]
|
||||
1
|
||||
[regenerate]
|
||||
0
|
||||
[minimap icon]
|
||||
2
|
||||
[object]
|
||||
[map]
|
||||
1 0
|
||||
[index]
|
||||
64024
|
||||
[team]
|
||||
[monster]
|
||||
[pos]
|
||||
563 197
|
||||
[/object]
|
||||
[/randomized object creation]
|
||||
[clear condition]
|
||||
[destroy object]
|
||||
80458 1
|
||||
[/clear condition]
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 200
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[fatigue result]
|
||||
4
|
||||
`)
|
||||
|
||||
dungeon := ParseDungeon(77, "dungeon/test.dgn", doc)
|
||||
if dungeon.Metadata.Name != "Test Dungeon" || !dungeon.Metadata.MinimumRequiredLevel.Set || dungeon.Metadata.MinimumRequiredLevel.Value != 10 {
|
||||
t.Fatalf("unexpected metadata: %+v", dungeon.Metadata)
|
||||
}
|
||||
if !dungeon.Metadata.ExperienceIncreasingPoint.Set || dungeon.Metadata.ExperienceIncreasingPoint.Value != 1.25 {
|
||||
t.Fatalf("unexpected numeric metadata: %+v", dungeon.Metadata.ExperienceIncreasingPoint)
|
||||
}
|
||||
if !dungeon.Metadata.TutorialDungeon.Set || dungeon.Metadata.TutorialDungeon.Value != 1 {
|
||||
t.Fatalf("tutorial dungeon metadata missing: %+v", dungeon.Metadata.TutorialDungeon)
|
||||
}
|
||||
if dungeon.Metadata.IntegerValues["fatigue"] != 8 || dungeon.Metadata.IntegerValues["fatigue result"] != 4 {
|
||||
t.Fatalf("stable integer metadata missing: %+v", dungeon.Metadata.IntegerValues)
|
||||
}
|
||||
if len(dungeon.Mazes) != 2 || dungeon.Mazes[0].Index != 0 || dungeon.Mazes[1].Index != 1 {
|
||||
t.Fatalf("repeated mazes = %+v", dungeon.Mazes)
|
||||
}
|
||||
first := dungeon.Mazes[0]
|
||||
if first.Width.Value != 3 || first.Height.Value != 2 || first.Greed != "BBFFEE" || len(first.MapSpecifications) != 3 {
|
||||
t.Fatalf("unexpected first maze: %+v", first)
|
||||
}
|
||||
if got := first.MapSpecifications[1].MapIDs; len(got) != 2 || got[0] != 101 || got[1] != 102 {
|
||||
t.Fatalf("map candidates = %+v", got)
|
||||
}
|
||||
if first.Start == nil || first.Start.X != 0 || len(first.Start.Params) != 1 || first.Boss == nil || first.Boss.X != 2 {
|
||||
t.Fatalf("start/boss points = start=%+v boss=%+v", first.Start, first.Boss)
|
||||
}
|
||||
if len(dungeon.Extensions) != 1 || dungeon.Extensions[0].Name != "raid dungeon" || dungeon.Extensions[0].Kind != SectionIntegers || dungeon.Extensions[0].Integers[0] != 1 {
|
||||
t.Fatalf("local metadata extension = %+v", dungeon.Extensions)
|
||||
}
|
||||
if len(first.Extensions) != 1 || first.Extensions[0].Name != "maze minimap ani icon" || first.Extensions[0].Kind != SectionMixed || first.Extensions[0].Texts[0] != "ui/maze.ani" {
|
||||
t.Fatalf("local maze extension = %+v", first.Extensions)
|
||||
}
|
||||
if len(first.OpaqueSections) < 2 || len(first.SourceSections) == 0 || len(dungeon.SourceSections) != len(doc.Sections) {
|
||||
t.Fatalf("raw ownership missing: maze opaque=%+v source=%d dungeon_source=%d", first.OpaqueSections, len(first.SourceSections), len(dungeon.SourceSections))
|
||||
}
|
||||
if len(first.BossSpecifications) != 1 || first.BossSpecifications[0].MapIDs[0] != 103 || len(first.LayeredSpecifications) != 1 || len(first.LayeredSpecifications[0].MapIDs) != 2 {
|
||||
t.Fatalf("boss/layered specifications = boss=%+v layered=%+v", first.BossSpecifications, first.LayeredSpecifications)
|
||||
}
|
||||
if len(first.RandomizedObjects) != 1 || first.RandomizedObjects[0].MinimapIcon.Value != 2 || len(first.RandomizedObjects[0].Objects) != 1 {
|
||||
t.Fatalf("randomized object script = %+v", first.RandomizedObjects)
|
||||
}
|
||||
object := first.RandomizedObjects[0].Objects[0]
|
||||
if object.ObjectIndex.Value != 64024 || object.Team != "monster" || object.Position.X != 563 || object.MazeCoordinate.X != 1 {
|
||||
t.Fatalf("randomized object = %+v", object)
|
||||
}
|
||||
if len(first.ClearConditions) != 1 || first.ClearConditions[0].Type != "destroy object" || first.ClearConditions[0].TargetID != 80458 {
|
||||
t.Fatalf("clear conditions = %+v", first.ClearConditions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonEscapeHellBelongsToSelectedMaze(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/escape_hell.dgn", `
|
||||
[hell dungeon]
|
||||
1
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[maze info]
|
||||
[escape hell]
|
||||
1
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 200
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
`)
|
||||
|
||||
dungeon := ParseDungeon(7542, "dungeon/escape_hell.dgn", doc)
|
||||
if got := dungeon.Metadata.IntegerValues["hell dungeon"]; got != 1 {
|
||||
t.Fatalf("hell dungeon=%d want 1", got)
|
||||
}
|
||||
if len(dungeon.Mazes) != 2 {
|
||||
t.Fatalf("mazes=%d want 2", len(dungeon.Mazes))
|
||||
}
|
||||
if dungeon.Mazes[0].EscapeHell.Set {
|
||||
t.Fatalf("unmarked maze inherited escape-hell state: %+v", dungeon.Mazes[0].EscapeHell)
|
||||
}
|
||||
if got := dungeon.Mazes[1].EscapeHell; !got.Set || got.Value != 1 {
|
||||
t.Fatalf("selected maze escape-hell=%+v want {Value:1 Set:true}", got)
|
||||
}
|
||||
if _, leaked := dungeon.Metadata.IntegerValues["escape hell"]; leaked {
|
||||
t.Fatalf("maze escape-hell leaked into dungeon metadata: %+v", dungeon.Metadata.IntegerValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonBossRoomEntranceConditionBelongsToCurrentMaze(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/boss_gate.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[boss room entrance condition]
|
||||
`+"`[clear action]` `[time condition]` 1 0 `[hunt monster]` 2 69201 0 2 69210 0 2"+`
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 200
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[boss room entrance condition]
|
||||
`+"`[clear action]` `[time condition]` 7 9 `[hunt monster]` 1 70001 4 3"+`
|
||||
`)
|
||||
|
||||
dungeon := ParseDungeon(2005, "dungeon/boss_gate.dgn", doc)
|
||||
if len(dungeon.Mazes) != 2 {
|
||||
t.Fatalf("mazes=%+v", dungeon.Mazes)
|
||||
}
|
||||
first := dungeon.Mazes[0].BossRoomEntranceCondition
|
||||
if first == nil || first.Action != "clear action" || len(first.TimeCondition) != 2 ||
|
||||
first.TimeCondition[0] != 1 || first.TimeCondition[1] != 0 || len(first.HuntMonsters) != 2 {
|
||||
t.Fatalf("first boss-room entrance condition=%+v diagnostics=%+v", first, dungeon.Mazes[0].Diagnostics)
|
||||
}
|
||||
if got := first.HuntMonsters[0]; got.TargetID != 69201 || got.Opaque != 0 || got.Count != 2 {
|
||||
t.Fatalf("first hunt target=%+v", got)
|
||||
}
|
||||
if got := first.HuntMonsters[1]; got.TargetID != 69210 || got.Opaque != 0 || got.Count != 2 {
|
||||
t.Fatalf("second hunt target=%+v", got)
|
||||
}
|
||||
second := dungeon.Mazes[1].BossRoomEntranceCondition
|
||||
if second == nil || second.TimeCondition[0] != 7 || second.TimeCondition[1] != 9 ||
|
||||
len(second.HuntMonsters) != 1 || second.HuntMonsters[0] != (BossRoomEntranceHuntMonster{TargetID: 70001, Opaque: 4, Count: 3}) {
|
||||
t.Fatalf("second boss-room entrance condition=%+v diagnostics=%+v", second, dungeon.Mazes[1].Diagnostics)
|
||||
}
|
||||
for _, section := range dungeon.OpaqueSections {
|
||||
if sectionKey(section.Name) == "boss room entrance condition" {
|
||||
t.Fatalf("per-maze boss-room gate leaked into dungeon opaque sections: %+v", section)
|
||||
}
|
||||
}
|
||||
|
||||
cloned := cloneDungeon(dungeon)
|
||||
cloned.Mazes[0].BossRoomEntranceCondition.TimeCondition[0] = 99
|
||||
cloned.Mazes[0].BossRoomEntranceCondition.HuntMonsters[0].Count = 99
|
||||
if first.TimeCondition[0] != 1 || first.HuntMonsters[0].Count != 2 {
|
||||
t.Fatalf("clone aliases source condition: source=%+v clone=%+v", first, cloned.Mazes[0].BossRoomEntranceCondition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonEventMonsterMetadataRemainsDungeonWide(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/event_monster.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 58708
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[event monster random map]
|
||||
0
|
||||
[event monster]
|
||||
1 1000 21100 1 1000 1 1 5
|
||||
[event monster2]
|
||||
2 2000 31100
|
||||
`)
|
||||
|
||||
dungeon := ParseDungeon(26, "dungeon/event_monster.dgn", doc)
|
||||
if !dungeon.Metadata.EventMonsterRandomMap.Set || dungeon.Metadata.EventMonsterRandomMap.Value != 0 {
|
||||
t.Fatalf("event-monster random-map selector=%+v", dungeon.Metadata.EventMonsterRandomMap)
|
||||
}
|
||||
if len(dungeon.Metadata.EventMonsters) != 2 ||
|
||||
dungeon.Metadata.EventMonsters[0].Kind != "event monster" ||
|
||||
dungeon.Metadata.EventMonsters[1].Kind != "event monster2" {
|
||||
t.Fatalf("event-monster definitions=%+v", dungeon.Metadata.EventMonsters)
|
||||
}
|
||||
want := []int64{1, 1000, 21100, 1, 1000, 1, 1, 5}
|
||||
if !slices.Equal(dungeon.Metadata.EventMonsters[0].Values, want) {
|
||||
t.Fatalf("event-monster values=%v want=%v", dungeon.Metadata.EventMonsters[0].Values, want)
|
||||
}
|
||||
|
||||
cloned := cloneDungeon(dungeon)
|
||||
cloned.Metadata.EventMonsters[0].Values[0] = 99
|
||||
if dungeon.Metadata.EventMonsters[0].Values[0] != 1 {
|
||||
t.Fatalf("clone aliases source event-monster row: source=%v clone=%v",
|
||||
dungeon.Metadata.EventMonsters[0].Values, cloned.Metadata.EventMonsters[0].Values)
|
||||
}
|
||||
compactDungeonForRuntime(&cloned)
|
||||
if len(cloned.Metadata.EventMonsters) != 2 || !cloned.Metadata.EventMonsterRandomMap.Set {
|
||||
t.Fatalf("runtime compaction removed typed event-monster metadata: %+v", cloned.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonKeepsEventAndAddedAdmissionItemsSeparate(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/event_mixed.dgn", `
|
||||
[required item]
|
||||
1001 2 0
|
||||
[/required item]
|
||||
[event required item]
|
||||
2001 1 0
|
||||
[/event required item]
|
||||
[added required item]
|
||||
3001 3 0
|
||||
[/added required item]
|
||||
`)
|
||||
dungeon := ParseDungeon(9000, "dungeon/event_mixed.dgn", doc)
|
||||
if len(dungeon.Metadata.RequiredItems) != 1 || dungeon.Metadata.RequiredItems[0] != (TicketItem{ItemID: 1001, Count: 2}) {
|
||||
t.Fatalf("ordinary required items=%+v", dungeon.Metadata.RequiredItems)
|
||||
}
|
||||
if len(dungeon.Metadata.EventRequiredItems) != 1 || dungeon.Metadata.EventRequiredItems[0] != (TicketItem{ItemID: 2001, Count: 1}) {
|
||||
t.Fatalf("event required items=%+v", dungeon.Metadata.EventRequiredItems)
|
||||
}
|
||||
if len(dungeon.Metadata.AddedRequiredItems) != 1 || dungeon.Metadata.AddedRequiredItems[0] != (TicketItem{ItemID: 3001, Count: 3}) {
|
||||
t.Fatalf("added required items=%+v", dungeon.Metadata.AddedRequiredItems)
|
||||
}
|
||||
|
||||
cloned := cloneDungeon(dungeon)
|
||||
cloned.Metadata.EventRequiredItems[0].Count = 99
|
||||
cloned.Metadata.AddedRequiredItems[0].Count = 99
|
||||
if dungeon.Metadata.EventRequiredItems[0].Count != 1 || dungeon.Metadata.AddedRequiredItems[0].Count != 3 {
|
||||
t.Fatalf("clone aliases admission evidence: source=%+v clone=%+v", dungeon.Metadata, cloned.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonBossRoomEntranceConditionRejectsMalformedValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
}{
|
||||
{name: "wrong action", payload: "`[open action]` `[time condition]` 1 0 `[hunt monster]` 1 69201 0 2"},
|
||||
{name: "missing time value", payload: "`[clear action]` `[time condition]` 1 `[hunt monster]` 1 69201 0 2"},
|
||||
{name: "wrong hunt label", payload: "`[clear action]` `[time condition]` 1 0 `[hunt apc]` 1 69201 0 2"},
|
||||
{name: "truncated row", payload: "`[clear action]` `[time condition]` 1 0 `[hunt monster]` 2 69201 0 2 69210 0"},
|
||||
{name: "trailing row", payload: "`[clear action]` `[time condition]` 1 0 `[hunt monster]` 1 69201 0 2 7"},
|
||||
{name: "non-positive target", payload: "`[clear action]` `[time condition]` 1 0 `[hunt monster]` 1 0 0 2"},
|
||||
{name: "non-positive count", payload: "`[clear action]` `[time condition]` 1 0 `[hunt monster]` 1 69201 0 0"},
|
||||
{name: "non-integer row", payload: "`[clear action]` `[time condition]` 1 0 `[hunt monster]` 1 69201 `opaque` 2"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/boss_gate_bad.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[boss room entrance condition]
|
||||
`+tt.payload+`
|
||||
`)
|
||||
dungeon := ParseDungeon(2005, "dungeon/boss_gate_bad.dgn", doc)
|
||||
if len(dungeon.Mazes) != 1 || dungeon.Mazes[0].BossRoomEntranceCondition != nil || len(dungeon.Mazes[0].Diagnostics) == 0 {
|
||||
t.Fatalf("malformed condition accepted: maze=%+v", dungeon.Mazes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonBossRoomEntranceConditionRejectsDuplicates(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/boss_gate_duplicate.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[boss room entrance condition]
|
||||
`+"`[clear action]` `[time condition]` 1 0 `[hunt monster]` 1 69201 0 2"+`
|
||||
[boss room entrance condition]
|
||||
`+"`[clear action]` `[time condition]` 1 0 `[hunt monster]` 1 69210 0 2"+`
|
||||
`)
|
||||
dungeon := ParseDungeon(2005, "dungeon/boss_gate_duplicate.dgn", doc)
|
||||
if len(dungeon.Mazes) != 1 || dungeon.Mazes[0].BossRoomEntranceCondition != nil || len(dungeon.Mazes[0].Diagnostics) == 0 {
|
||||
t.Fatalf("duplicate condition accepted: maze=%+v", dungeon.Mazes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonBloodAltarMetadata(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantKind BloodAltarDungeonKind
|
||||
wantRound int64
|
||||
valid bool
|
||||
}{
|
||||
{name: "endless", body: "[blood dungeon]\n1\n[blood max round]\n15", wantKind: BloodAltarDungeonKindEndless, wantRound: 15, valid: true},
|
||||
{name: "ultimate", body: "[blood dungeon]\n2\n[blood max round]\n7", wantKind: BloodAltarDungeonKindUltimate, wantRound: 7, valid: true},
|
||||
{name: "empty kind rejected", body: "[blood dungeon]\n[blood max round]\n15", valid: false},
|
||||
{name: "invalid kind", body: "[blood dungeon]\n3\n[blood max round]\n15"},
|
||||
{name: "invalid round", body: "[blood dungeon]\n1\n[blood max round]\n256"},
|
||||
{name: "extra round field", body: "[blood dungeon]\n1\n[blood max round]\n7 8"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dungeon := ParseDungeon(11006, "dungeon/altar.dgn", parseTestDocument(t, "dungeon/altar.dgn", tt.body))
|
||||
if !dungeon.Metadata.Flags["blood dungeon"] {
|
||||
t.Fatalf("blood dungeon compatibility flag missing: %+v", dungeon.Metadata.Flags)
|
||||
}
|
||||
if tt.valid {
|
||||
if dungeon.BloodAltarKind != tt.wantKind || dungeon.BloodMaxRounds != tt.wantRound ||
|
||||
dungeon.Metadata.IntegerValues["blood max round"] != tt.wantRound {
|
||||
t.Fatalf("blood altar metadata = kind=%d rounds=%d integers=%+v diagnostics=%+v",
|
||||
dungeon.BloodAltarKind, dungeon.BloodMaxRounds, dungeon.Metadata.IntegerValues, dungeon.Diagnostics)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(dungeon.Diagnostics) == 0 || (dungeon.BloodAltarKind != 0 && dungeon.BloodMaxRounds != 0) {
|
||||
t.Fatalf("invalid blood altar metadata accepted: %+v", dungeon)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonInfernoMetadataAfterMazeUsesClosedQuestList(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/inferno.dgn", `
|
||||
[inferno dungeon]
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 80117
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[inferno quest]
|
||||
10104
|
||||
[/inferno quest]
|
||||
`)
|
||||
dungeon := ParseDungeon(7527, "dungeon/inferno.dgn", doc)
|
||||
if !dungeon.Metadata.InfernoDungeon || len(dungeon.Metadata.InfernoQuestIDs) != 1 || dungeon.Metadata.InfernoQuestIDs[0] != 10104 {
|
||||
t.Fatalf("inferno metadata=%+v", dungeon.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonSpecialPassiveObjectItemGroups(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/passive_items.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 76512
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[special passive object item]
|
||||
0 -1 3 1069 1500 1071 500 1072 3000 1 -1 0 2 85 1 10099777 9400
|
||||
[/special passive object item]
|
||||
`)
|
||||
dungeon := ParseDungeon(102, "dungeon/passive_items.dgn", doc)
|
||||
if len(dungeon.Mazes) != 1 {
|
||||
t.Fatalf("mazes=%+v", dungeon.Mazes)
|
||||
}
|
||||
// Record 1 is declared empty (`1 -1 0`), so the following record sits at
|
||||
// vector position 2 and carries its own level override 85. The table is
|
||||
// dungeon-wide even when the file places it under a maze.
|
||||
if len(dungeon.Mazes[0].SpecialPassiveObjectItems) != 0 {
|
||||
t.Fatalf("maze retained route-local rows=%+v", dungeon.Mazes[0].SpecialPassiveObjectItems)
|
||||
}
|
||||
rows := dungeon.Metadata.SpecialPassiveObjectItems
|
||||
if len(rows) != 4 || rows[0].Index != 0 || rows[1].Index != 0 || rows[2].Index != 0 || rows[3].Index != 2 ||
|
||||
rows[0].ItemID != 1069 || rows[0].DropRate != 1500 || rows[0].LevelOverride != -1 ||
|
||||
rows[3].LevelOverride != 85 || rows[3].ItemID != 10099777 || rows[3].DropRate != 9400 {
|
||||
t.Fatalf("rows=%+v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParticularHellPromotesTrailingPassiveItemsToEveryMaze(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/particular_hell.dgn", `
|
||||
[dungeon type]
|
||||
`+"`particular hell`"+`
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 101
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[special passive object item]
|
||||
0 -1 1 3276 5000
|
||||
[/special passive object item]
|
||||
`)
|
||||
dungeon := ParseDungeon(316, "dungeon/particular_hell.dgn", doc)
|
||||
rows := dungeon.Metadata.SpecialPassiveObjectItems
|
||||
if len(rows) != 1 || rows[0].ItemID != 3276 || rows[0].DropRate != 5000 {
|
||||
t.Fatalf("particular-hell rows=%+v", rows)
|
||||
}
|
||||
for index, maze := range dungeon.Mazes {
|
||||
if len(maze.SpecialPassiveObjectItems) != 0 {
|
||||
t.Fatalf("maze=%d retained promoted rows=%+v", index, maze.SpecialPassiveObjectItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSpecialDungeonPromotesTrailingPassiveItemsToEveryMaze(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/special.dgn", `
|
||||
[special dungeon]
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 101
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[special passive object item]
|
||||
0 -1 1 10088439 10000
|
||||
[/special passive object item]
|
||||
`)
|
||||
dungeon := ParseDungeon(2007, "dungeon/special.dgn", doc)
|
||||
rows := dungeon.Metadata.SpecialPassiveObjectItems
|
||||
if len(rows) != 1 || rows[0].Index != 0 || rows[0].ItemID != 10088439 || rows[0].DropRate != 10000 {
|
||||
t.Fatalf("special-dungeon rows=%+v", rows)
|
||||
}
|
||||
for index, maze := range dungeon.Mazes {
|
||||
if len(maze.SpecialPassiveObjectItems) != 0 {
|
||||
t.Fatalf("maze=%d retained promoted rows=%+v", index, maze.SpecialPassiveObjectItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonInfernoRouteMetadata(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/infernomode/time_rift.dgn", `
|
||||
[hell dungeon]
|
||||
1
|
||||
[hell party required item count]
|
||||
24
|
||||
[inferno dungeon]
|
||||
[select dungeon normal disable]
|
||||
[maze info]
|
||||
[size]
|
||||
5 2
|
||||
[maze chance rate]
|
||||
11875
|
||||
[greed]
|
||||
`+"`AAAAAAAAII`"+`
|
||||
[map specification]
|
||||
`+"`map`"+` 0 1 80117 `+"`boss`"+` 4 0 80297 80298
|
||||
[start map]
|
||||
0 1
|
||||
[boss map]
|
||||
4 0
|
||||
[maze info]
|
||||
[size]
|
||||
5 2
|
||||
[maze chance rate]
|
||||
2500
|
||||
[inferno special]
|
||||
[greed]
|
||||
`+"`JJFFFFFFEE`"+`
|
||||
[map specification]
|
||||
`+"`map`"+` 0 1 80165 `+"`boss`"+` 4 0 80170
|
||||
[start map]
|
||||
0 1
|
||||
[boss map]
|
||||
4 0
|
||||
[inferno quest]
|
||||
10104
|
||||
`)
|
||||
dungeon := ParseDungeon(7527, "dungeon/infernomode/time_rift.dgn", doc)
|
||||
if !dungeon.Metadata.InfernoDungeon || !dungeon.Metadata.SelectDungeonNormalDisabled ||
|
||||
!dungeon.Metadata.HellPartyRequiredItemCount.Set || dungeon.Metadata.HellPartyRequiredItemCount.Value != 24 {
|
||||
t.Fatalf("inferno metadata=%+v", dungeon.Metadata)
|
||||
}
|
||||
if got := dungeon.Metadata.InfernoQuestIDs; len(got) != 1 || got[0] != 10104 {
|
||||
t.Fatalf("inferno quests=%v", got)
|
||||
}
|
||||
if len(dungeon.Mazes) != 2 || !dungeon.Mazes[0].ChanceRate.Set || dungeon.Mazes[0].ChanceRate.Value != 11875 ||
|
||||
!dungeon.Mazes[1].InfernoSpecial || dungeon.Mazes[1].ChanceRate.Value != 2500 {
|
||||
t.Fatalf("inferno mazes=%+v", dungeon.Mazes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonWarpMapConditionsPreserveOrderedPairsAndAmbiguousSources(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/warp.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
3 3
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100 `+"`map`"+` 2 2 101
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
2 2
|
||||
[warp map condition]
|
||||
[source grid pos]
|
||||
1 1
|
||||
[dest grid pos]
|
||||
2 2
|
||||
[source grid pos]
|
||||
1 1
|
||||
[dest grid pos]
|
||||
0 0
|
||||
[source grid pos]
|
||||
-1 -1
|
||||
[dest grid pos]
|
||||
0 0
|
||||
[/warp map condition]
|
||||
`)
|
||||
dungeon := ParseDungeon(4000, "dungeon/warp.dgn", doc)
|
||||
want := []WarpMapCondition{
|
||||
{Source: Point{X: 1, Y: 1}, Destination: Point{X: 2, Y: 2}},
|
||||
{Source: Point{X: 1, Y: 1}, Destination: Point{X: 0, Y: 0}},
|
||||
{Source: Point{X: -1, Y: -1}, Destination: Point{X: 0, Y: 0}},
|
||||
}
|
||||
if len(dungeon.WarpMapConditions) != len(want) {
|
||||
t.Fatalf("warp rules=%+v diagnostics=%+v", dungeon.WarpMapConditions, dungeon.Diagnostics)
|
||||
}
|
||||
for index := range want {
|
||||
if dungeon.WarpMapConditions[index] != want[index] {
|
||||
t.Fatalf("warp rule %d=%+v want=%+v", index, dungeon.WarpMapConditions[index], want[index])
|
||||
}
|
||||
}
|
||||
if len(dungeon.Mazes) != 1 || len(dungeon.Mazes[0].WarpMapConditions) != len(want) {
|
||||
t.Fatalf("per-maze warp rules=%+v", dungeon.Mazes)
|
||||
}
|
||||
for index := range want {
|
||||
if dungeon.Mazes[0].WarpMapConditions[index] != want[index] {
|
||||
t.Fatalf("maze warp rule %d=%+v want=%+v", index, dungeon.Mazes[0].WarpMapConditions[index], want[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonWarpMapConditionsBelongToPhysicalMaze(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/per_maze_warp.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
3 3
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100 `+"`map`"+` 2 2 101
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
2 2
|
||||
[warp map condition]
|
||||
[source grid pos]
|
||||
2 1
|
||||
[dest grid pos]
|
||||
0 0
|
||||
[/warp map condition]
|
||||
[maze info]
|
||||
[size]
|
||||
3 3
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 200 `+"`map`"+` 2 2 201
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
2 2
|
||||
[warp map condition]
|
||||
[source grid pos]
|
||||
1 2
|
||||
[dest grid pos]
|
||||
2 2
|
||||
[/warp map condition]
|
||||
`)
|
||||
dungeon := ParseDungeon(4000, "dungeon/per_maze_warp.dgn", doc)
|
||||
if len(dungeon.WarpMapConditions) != 2 || len(dungeon.Mazes) != 2 {
|
||||
t.Fatalf("dungeon=%+v", dungeon)
|
||||
}
|
||||
want := []WarpMapCondition{
|
||||
{Source: Point{X: 2, Y: 1}, Destination: Point{X: 0, Y: 0}},
|
||||
{Source: Point{X: 1, Y: 2}, Destination: Point{X: 2, Y: 2}},
|
||||
}
|
||||
for mazeIndex := range dungeon.Mazes {
|
||||
got := dungeon.Mazes[mazeIndex].WarpMapConditions
|
||||
if len(got) != 1 || got[0] != want[mazeIndex] {
|
||||
t.Fatalf("maze %d warp rules=%+v want=%+v", mazeIndex, got, want[mazeIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonLostLandDurations(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/lost_land.dgn", `
|
||||
[dungeon type]
|
||||
`+"`lost land`"+`
|
||||
[lost land parameters]
|
||||
120000 120000
|
||||
[limit time]
|
||||
600 1200
|
||||
`)
|
||||
dungeon := ParseDungeon(4000, "dungeon/lost_land.dgn", doc)
|
||||
if got := dungeon.Metadata.LostLandParameters; !got.Set || got.DayMilliseconds != 120000 || got.NightMilliseconds != 120000 {
|
||||
t.Fatalf("lost land parameters=%+v", got)
|
||||
}
|
||||
if got := dungeon.Metadata.LimitTime; !got.Set || got.PrimarySeconds != 600 || got.SecondarySeconds != 1200 {
|
||||
t.Fatalf("limit time=%+v", got)
|
||||
}
|
||||
for _, section := range dungeon.OpaqueSections {
|
||||
if key := sectionKey(section.Name); key == "lost land parameters" || key == "limit time" {
|
||||
t.Fatalf("typed lost-land section retained as opaque: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonLostLandDurationsRejectMalformedValues(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/lost_land_bad.dgn", `
|
||||
[lost land parameters]
|
||||
120000 0
|
||||
[limit time]
|
||||
600
|
||||
`)
|
||||
dungeon := ParseDungeon(4000, "dungeon/lost_land_bad.dgn", doc)
|
||||
if dungeon.Metadata.LostLandParameters.Set || dungeon.Metadata.LimitTime.Set {
|
||||
t.Fatalf("malformed durations parsed: lost_land=%+v limit=%+v", dungeon.Metadata.LostLandParameters, dungeon.Metadata.LimitTime)
|
||||
}
|
||||
if len(dungeon.Diagnostics) != 2 {
|
||||
t.Fatalf("diagnostics=%+v, want two typed duration errors", dungeon.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonLostLandLimitTimeInsideClearCondition(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/lost_land_scoped.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 11200
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[clear condition]
|
||||
[limit time]
|
||||
600 1200
|
||||
[/clear condition]
|
||||
`)
|
||||
dungeon := ParseDungeon(4000, "dungeon/lost_land_scoped.dgn", doc)
|
||||
if got := dungeon.Metadata.LimitTime; !got.Set || got.PrimarySeconds != 600 || got.SecondarySeconds != 1200 {
|
||||
t.Fatalf("scoped limit time=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonMalformedMazeKeepsRawAndDiagnostics(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/bad.dgn", `
|
||||
[name]
|
||||
`+"`Bad Dungeon`"+`
|
||||
[maze info]
|
||||
[size]
|
||||
`+"`wide`"+` 2
|
||||
[map specification]
|
||||
`+"`map`"+` 0 `+"`bad-y`"+` 100
|
||||
[start map]
|
||||
1
|
||||
[future maze node]
|
||||
{ `+"`payload`"+` = 4 }
|
||||
`)
|
||||
dungeon := ParseDungeon(78, "dungeon/bad.dgn", doc)
|
||||
if len(dungeon.Mazes) != 1 || len(dungeon.Mazes[0].Diagnostics) < 3 {
|
||||
t.Fatalf("missing malformed diagnostics: %+v", dungeon.Mazes)
|
||||
}
|
||||
maze := dungeon.Mazes[0]
|
||||
if len(maze.MapSpecifications) != 0 || maze.Start != nil || len(maze.UnknownSections) != 1 || len(maze.Extensions) != 1 {
|
||||
t.Fatalf("malformed values crossed boundaries: %+v", maze)
|
||||
}
|
||||
if maze.Extensions[0].Kind != SectionMixed || len(maze.Extensions[0].Tokens) != 5 {
|
||||
t.Fatalf("future extension tokens lost: %+v", maze.Extensions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDungeonTutorialMetadataAfterMazeKeepsDungeonOwnership(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/tutorial_after_maze.dgn", `
|
||||
[name]
|
||||
`+"`Profession Tutorial`"+`
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[greed]
|
||||
`+"`A`"+`
|
||||
[map specification]
|
||||
`+"`map`"+` 0 0 100
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[tutorial dungeon]
|
||||
1
|
||||
[tutorial dungeon]
|
||||
1
|
||||
`)
|
||||
dungeon := ParseDungeon(7110, "dungeon/tutorial_after_maze.dgn", doc)
|
||||
if !dungeon.Metadata.TutorialDungeon.Set || dungeon.Metadata.TutorialDungeon.Value != 1 {
|
||||
t.Fatalf("post-maze tutorial metadata missing: %+v", dungeon.Metadata.TutorialDungeon)
|
||||
}
|
||||
if len(dungeon.Mazes) != 1 {
|
||||
t.Fatalf("tutorial mazes=%+v", dungeon.Mazes)
|
||||
}
|
||||
for _, section := range dungeon.Mazes[0].UnknownSections {
|
||||
if sectionKey(section.Name) == "tutorial dungeon" {
|
||||
t.Fatalf("tutorial metadata leaked into maze unknown sections: %+v", dungeon.Mazes[0].UnknownSections)
|
||||
}
|
||||
}
|
||||
for _, extension := range dungeon.Mazes[0].Extensions {
|
||||
if sectionKey(extension.Name) == "tutorial dungeon" {
|
||||
t.Fatalf("tutorial metadata leaked into maze extensions: %+v", dungeon.Mazes[0].Extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonExtensionsClassifyLocalPVFValueShapes(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/extensions.dgn", `
|
||||
[local flag]
|
||||
[local integers]
|
||||
1 -2 3
|
||||
[local numbers]
|
||||
1.5 2
|
||||
[local texts]
|
||||
`+"`alpha` beta"+`
|
||||
[local mixed]
|
||||
1 `+"`alpha`"+` { 2 }
|
||||
`)
|
||||
dungeon := ParseDungeon(79, "dungeon/extensions.dgn", doc)
|
||||
if len(dungeon.Extensions) != 5 || len(dungeon.UnknownSections) != 5 {
|
||||
t.Fatalf("extensions/raw = %d/%d", len(dungeon.Extensions), len(dungeon.UnknownSections))
|
||||
}
|
||||
want := []SectionValueKind{SectionFlag, SectionIntegers, SectionNumbers, SectionTexts, SectionMixed}
|
||||
for i, kind := range want {
|
||||
if dungeon.Extensions[i].Kind != kind {
|
||||
t.Errorf("extension %d kind = %s, want %s: %+v", i, dungeon.Extensions[i].Kind, kind, dungeon.Extensions[i])
|
||||
}
|
||||
}
|
||||
if len(dungeon.Extensions[4].Tokens) != 5 || len(dungeon.Extensions[4].Symbols) != 2 {
|
||||
t.Fatalf("mixed extension did not retain structure: %+v", dungeon.Extensions[4])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonNestedMetadataBlockAfterMazeKeepsDungeonOwnership(t *testing.T) {
|
||||
doc := parseTestDocument(t, "dungeon/scoped.dgn", `
|
||||
[maze info]
|
||||
[size]
|
||||
1 1
|
||||
[greed]
|
||||
`+"`A`"+`
|
||||
[start map]
|
||||
0 0
|
||||
[boss map]
|
||||
0 0
|
||||
[monsterapc diff table]
|
||||
[normal]
|
||||
[attack]
|
||||
1.25
|
||||
[future balance field]
|
||||
7
|
||||
[/normal]
|
||||
[/monsterapc diff table]
|
||||
`)
|
||||
dungeon := ParseDungeon(80, "dungeon/scoped.dgn", doc)
|
||||
if len(dungeon.Mazes) != 1 {
|
||||
t.Fatalf("mazes = %+v", dungeon.Mazes)
|
||||
}
|
||||
if len(dungeon.OpaqueSections) != 1 || sectionKey(dungeon.OpaqueSections[0].Name) != "monsterapc diff table" {
|
||||
t.Fatalf("dungeon metadata block = %+v", dungeon.OpaqueSections)
|
||||
}
|
||||
if len(dungeon.UnknownSections) != 3 || len(dungeon.Extensions) != 4 {
|
||||
t.Fatalf("dungeon scoped extensions = unknown=%+v extensions=%+v", dungeon.UnknownSections, dungeon.Extensions)
|
||||
}
|
||||
for _, section := range dungeon.UnknownSections {
|
||||
if len(section.Scope) == 0 || section.Scope[0] != "monsterapc diff table" {
|
||||
t.Fatalf("dungeon extension scope = %+v", section)
|
||||
}
|
||||
}
|
||||
if len(dungeon.Mazes[0].UnknownSections) != 0 {
|
||||
t.Fatalf("metadata children leaked into maze = %+v", dungeon.Mazes[0].UnknownSections)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user