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,489 @@
|
||||
package worldmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestRealScriptPVFWorldMapSmoke(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNF_WORLDMAP_REAL_PVF_SMOKE to run the real Script.pvf smoke test")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open real pvf: %v", err)
|
||||
}
|
||||
index, err := dnfpvf.Build(context.Background(), archive, dnfpvf.BuildOptions{Lists: []string{DefaultWorldMapList}})
|
||||
if err != nil {
|
||||
t.Fatalf("build real worldmap index: %v", err)
|
||||
}
|
||||
table, err := Load(context.Background(), index, Options{SkipMaps: true, SkipDungeons: true})
|
||||
if err != nil {
|
||||
t.Fatalf("load real worldmap: %v", err)
|
||||
}
|
||||
if snapshot := table.Snapshot(); snapshot.Areas == 0 || snapshot.DungeonRefs == 0 {
|
||||
t.Fatalf("empty real worldmap snapshot: %+v", snapshot)
|
||||
}
|
||||
|
||||
listText, err := archive.ReadText(DefaultMapList)
|
||||
if err != nil {
|
||||
t.Fatalf("read real map list: %v", err)
|
||||
}
|
||||
listDoc, err := dnfpvf.Parse(DefaultMapList, listText)
|
||||
if err != nil {
|
||||
t.Fatalf("parse real map list: %v", err)
|
||||
}
|
||||
entries := dnfpvf.ParseList(listDoc)
|
||||
if len(entries) == 0 {
|
||||
t.Fatal("real map list is empty")
|
||||
}
|
||||
mapPath := strings.TrimPrefix(strings.ReplaceAll(entries[0].Path, "\\", "/"), "/")
|
||||
if !strings.HasPrefix(strings.ToLower(mapPath), "map/") {
|
||||
mapPath = "map/" + mapPath
|
||||
}
|
||||
mapText, err := archive.ReadText(mapPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read real map %s: %v", mapPath, err)
|
||||
}
|
||||
mapDoc, err := ParseDocument(mapPath, mapText)
|
||||
if err != nil {
|
||||
t.Fatalf("parse real map %s: %v", mapPath, err)
|
||||
}
|
||||
parsed := ParseMap(entries[0].ID, mapPath, mapDoc)
|
||||
if len(parsed.SourceSections) == 0 || len(parsed.PathgatePositions) == 0 || len(parsed.PassiveObjects) == 0 {
|
||||
t.Fatalf("real map core fields missing: path=%s sections=%d pathgates=%d passive=%d", mapPath, len(parsed.SourceSections), len(parsed.PathgatePositions), len(parsed.PassiveObjects))
|
||||
}
|
||||
|
||||
smokeRealDungeonGraph(t, archive)
|
||||
}
|
||||
|
||||
func TestRealScriptPVFProfessionTutorialMetadata(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNF_WORLDMAP_REAL_PVF_SMOKE to verify profession tutorial metadata")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open real pvf: %v", err)
|
||||
}
|
||||
table, err := LoadSource(context.Background(), archive, Options{SkipMaps: true, SkipAreas: true})
|
||||
if err != nil {
|
||||
t.Fatalf("load real dungeon source: %v", err)
|
||||
}
|
||||
professionTutorials := []struct {
|
||||
id int64
|
||||
path string
|
||||
}{
|
||||
{7110, "dungeon/Cataclysm/NewTutorial/Gunner_M_tutoral.dgn"},
|
||||
{7111, "dungeon/Cataclysm/NewTutorial/7111_TheWayToCentralPark.dgn"},
|
||||
{7112, "dungeon/Cataclysm/NewTutorial/Suzhouforest.dgn"},
|
||||
{7113, "dungeon/Cataclysm/NewTutorial/7113_Priest_M_Tutorial.dgn"},
|
||||
{7114, "dungeon/Cataclysm/NewTutorial/7114_Fighter_F_Tutorial.dgn"},
|
||||
{7115, "dungeon/Cataclysm/NewTutorial/Swordman_M.dgn"},
|
||||
{7116, "dungeon/Cataclysm/NewTutorial/Gunner_F.dgn"},
|
||||
{7117, "dungeon/Cataclysm/NewTutorial/Mage_M.dgn"},
|
||||
{7118, "dungeon/Cataclysm/NewTutorial/Tutorial_ATSwordman.dgn"},
|
||||
{7119, "dungeon/Cataclysm/NewTutorial/Thief_F.dgn"},
|
||||
{7124, "dungeon/Cataclysm/NewTutorial/knight_F.dgn"},
|
||||
{7125, "dungeon/Cataclysm/NewTutorial/Priest_F.dgn"},
|
||||
{7145, "dungeon/Cataclysm/NewTutorial/GunBlader_M_tutorial.dgn"},
|
||||
}
|
||||
for _, want := range professionTutorials {
|
||||
dungeon, ok := table.FindDungeonPath(want.path)
|
||||
if !ok {
|
||||
t.Errorf("profession tutorial missing: id=%d path=%s", want.id, want.path)
|
||||
continue
|
||||
}
|
||||
if dungeon.ID != want.id || !dungeon.Metadata.TutorialDungeon.Set || dungeon.Metadata.TutorialDungeon.Value != 1 {
|
||||
t.Errorf("profession tutorial metadata mismatch: id=%d path=%s parsed_id=%d metadata=%+v",
|
||||
want.id, want.path, dungeon.ID, dungeon.Metadata.TutorialDungeon)
|
||||
}
|
||||
}
|
||||
|
||||
for _, dungeonPath := range []string{
|
||||
"dungeon/Impossible/tutorial_Cosmofiend.dgn",
|
||||
"dungeon/Impossible/tutorial_bakalcastle.dgn",
|
||||
} {
|
||||
dungeon, ok := table.FindDungeonPath(dungeonPath)
|
||||
if !ok {
|
||||
t.Errorf("named tutorial negative fixture missing: %s", dungeonPath)
|
||||
continue
|
||||
}
|
||||
if dungeon.Metadata.TutorialDungeon.Set {
|
||||
t.Errorf("name-only tutorial was treated as a profession tutorial: path=%s metadata=%+v",
|
||||
dungeonPath, dungeon.Metadata.TutorialDungeon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFGunbladerTutorialStartRoomTopology(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNF_WORLDMAP_REAL_PVF_SMOKE to verify the Gunblader tutorial start room")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open real pvf: %v", err)
|
||||
}
|
||||
table, err := LoadSource(context.Background(), archive, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("load real worldmap source: %v", err)
|
||||
}
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatalf("build real worldmap resolver: %v", err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 7145, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("build Gunblader tutorial topology: %v", err)
|
||||
}
|
||||
start, ok := topology.Room(RoomCoordinate{X: 0, Y: 0})
|
||||
if !ok || start.Map == nil {
|
||||
t.Fatalf("Gunblader start room missing: %+v found=%t diagnostics=%+v", start, ok, topology.Diagnostics)
|
||||
}
|
||||
actionText, err := archive.ReadText("map/Cataclysm/NewTutorial/GunBlader_M/Action/70576_6801.act")
|
||||
if err != nil {
|
||||
t.Fatalf("read Gunblader start basic action: %v", err)
|
||||
}
|
||||
if start.Map.Map.ID != 70576 || len(start.Neighbors) != 1 ||
|
||||
start.Neighbors[0].Direction != RoomDirectionRight ||
|
||||
start.Neighbors[0].Coordinate != (RoomCoordinate{X: 1, Y: 0}) {
|
||||
t.Fatalf("Gunblader start route=%+v want map 70576 with one right exit", start)
|
||||
}
|
||||
if len(topology.Diagnostics) != 0 {
|
||||
t.Fatalf("Gunblader topology diagnostics=%+v", topology.Diagnostics)
|
||||
}
|
||||
if start.Map.Map.BasicAction != "Action/70576_6801.act" ||
|
||||
!strings.Contains(actionText, "[END BY MOVING MAP]") {
|
||||
t.Fatalf("Gunblader start action=%q lacks move-map tutorial boundary", start.Map.Map.BasicAction)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFTowerOfDespairSingleRoomLayout(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNF_WORLDMAP_REAL_PVF_SMOKE to verify tower single-room map ownership")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open real pvf: %v", err)
|
||||
}
|
||||
table, err := LoadSource(context.Background(), archive, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("load full real pvf source: %v", err)
|
||||
}
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatalf("index real pvf resolver: %v", err)
|
||||
}
|
||||
if _, err := resolver.PoolCandidates(MapPoolRequest{
|
||||
DungeonID: 11008,
|
||||
MazeIndex: 0,
|
||||
FileType: MapFileBoss,
|
||||
}); !errors.Is(err, ErrMapPoolEmpty) {
|
||||
t.Fatalf("real tower boss pool should be empty before start==boss fallback: %v", err)
|
||||
}
|
||||
topology, err := BuildDungeonLayout(resolver, 11008, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build real tower of despair floor 1 layout: %v", err)
|
||||
}
|
||||
room, ok := topology.Room(RoomCoordinate{X: 0, Y: 0})
|
||||
if !ok || !room.Start || !room.Boss || room.Map == nil || room.Map.Map.ID != 15130 ||
|
||||
!strings.EqualFold(room.Map.Map.Path, "map/towerofdespair_down/15130despair001.map") {
|
||||
t.Fatalf("real tower room = %+v ok=%v", room, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFFullSourceLoad(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNF_WORLDMAP_REAL_PVF_SMOKE to run the full real Script.pvf source smoke test")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open full real pvf: %v", err)
|
||||
}
|
||||
table, err := LoadSource(context.Background(), archive, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("load full real pvf source: %v", err)
|
||||
}
|
||||
snapshot := table.Snapshot()
|
||||
if snapshot.Maps == 0 || snapshot.Dungeons != 1450 || snapshot.Areas == 0 || snapshot.Mazes == 0 {
|
||||
t.Fatalf("unexpected full real pvf snapshot: %+v", snapshot)
|
||||
}
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatalf("index full real pvf resolver: %v", err)
|
||||
}
|
||||
if len(resolver.byDungeon) != snapshot.Dungeons {
|
||||
t.Fatalf("resolver dungeons=%d, table dungeons=%d", len(resolver.byDungeon), snapshot.Dungeons)
|
||||
}
|
||||
topologies := 0
|
||||
topologyFailures := 0
|
||||
rooms := 0
|
||||
unresolvedRooms := 0
|
||||
mapUnresolved := 0
|
||||
mapAmbiguous := 0
|
||||
startMissing := 0
|
||||
startUnresolved := 0
|
||||
startIsolated := 0
|
||||
gridShort := 0
|
||||
gridMalformed := 0
|
||||
gridPairMismatch := 0
|
||||
gridCodeUnknown := 0
|
||||
var startIssueSamples []string
|
||||
var gridIssueSamples []string
|
||||
for _, dungeon := range table.dungeons {
|
||||
for mazeIndex := range dungeon.Mazes {
|
||||
topology, err := BuildDungeonTopology(resolver, dungeon.ID, mazeIndex)
|
||||
if err != nil {
|
||||
topologyFailures++
|
||||
continue
|
||||
}
|
||||
topologies++
|
||||
rooms += len(topology.rooms)
|
||||
unresolvedRooms += len(topology.UnresolvedRooms())
|
||||
for _, resolutionError := range topology.resolutionErrors {
|
||||
switch {
|
||||
case errors.Is(resolutionError, ErrMapAmbiguous):
|
||||
mapAmbiguous++
|
||||
case errors.Is(resolutionError, ErrMapUnresolved):
|
||||
mapUnresolved++
|
||||
}
|
||||
}
|
||||
if topology.Start == nil {
|
||||
startMissing++
|
||||
if len(startIssueSamples) < 5 {
|
||||
startIssueSamples = append(startIssueSamples, fmt.Sprintf("%d/%d:missing", dungeon.ID, mazeIndex))
|
||||
}
|
||||
} else if start, ok := topology.Room(*topology.Start); !ok || start.Map == nil {
|
||||
startUnresolved++
|
||||
if len(startIssueSamples) < 5 {
|
||||
startIssueSamples = append(startIssueSamples, fmt.Sprintf("%d/%d:%s", dungeon.ID, mazeIndex, topology.Start))
|
||||
}
|
||||
} else if len(start.Neighbors) == 0 && len(topology.rooms) > 1 {
|
||||
startIsolated++
|
||||
}
|
||||
for _, diagnostic := range topology.Diagnostics {
|
||||
switch diagnostic.Code {
|
||||
case "maze_grid_short", "maze_grid_rows_short", "maze_grid_row_short":
|
||||
gridShort++
|
||||
case "maze_grid_malformed":
|
||||
gridMalformed++
|
||||
case "maze_grid_pair_mismatch":
|
||||
gridPairMismatch++
|
||||
case "maze_grid_code_unknown":
|
||||
gridCodeUnknown++
|
||||
}
|
||||
if strings.HasPrefix(diagnostic.Code, "maze_grid_") && len(gridIssueSamples) < 5 {
|
||||
gridIssueSamples = append(
|
||||
gridIssueSamples,
|
||||
fmt.Sprintf("%d/%d:%s:%s", dungeon.ID, mazeIndex, diagnostic.Code, diagnostic.Message),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if topologies == 0 || rooms == 0 {
|
||||
t.Fatalf("real PVF yielded no dungeon topology: built=%d failures=%d rooms=%d", topologies, topologyFailures, rooms)
|
||||
}
|
||||
t.Logf(
|
||||
"full real PVF source: %+v resolver_dungeons=%d topologies=%d topology_failures=%d rooms=%d unresolved_rooms=%d map_unresolved=%d map_ambiguous=%d start_missing=%d start_unresolved=%d start_isolated=%d grid_short=%d grid_malformed=%d grid_pair_mismatch=%d grid_code_unknown=%d",
|
||||
snapshot, len(resolver.byDungeon), topologies, topologyFailures, rooms, unresolvedRooms,
|
||||
mapUnresolved, mapAmbiguous, startMissing, startUnresolved, startIsolated,
|
||||
gridShort, gridMalformed, gridPairMismatch, gridCodeUnknown,
|
||||
)
|
||||
t.Logf("real PVF topology issue samples: starts=%v grids=%v", startIssueSamples, gridIssueSamples)
|
||||
}
|
||||
|
||||
func smokeRealDungeonGraph(t *testing.T, archive *platformpvf.Archive) {
|
||||
t.Helper()
|
||||
dungeonTable, err := LoadSource(context.Background(), archive, Options{SkipMaps: true, SkipAreas: true})
|
||||
if err != nil {
|
||||
t.Fatalf("load real dungeon source: %v", err)
|
||||
}
|
||||
mapListText, err := archive.ReadText(DefaultMapList)
|
||||
if err != nil {
|
||||
t.Fatalf("read real map list for dungeon graph: %v", err)
|
||||
}
|
||||
mapListDoc, err := dnfpvf.Parse(DefaultMapList, mapListText)
|
||||
if err != nil {
|
||||
t.Fatalf("parse real map list for dungeon graph: %v", err)
|
||||
}
|
||||
mapPaths := make(map[int64]string)
|
||||
for _, entry := range dnfpvf.ParseList(mapListDoc) {
|
||||
mapPaths[entry.ID] = prefixedArchivePath("map", entry.Path)
|
||||
}
|
||||
|
||||
parsedDocuments := len(dungeonTable.dungeons)
|
||||
extensionNames := make(map[string]struct{})
|
||||
randomizedScripts := 0
|
||||
randomizedObjects := 0
|
||||
clearConditions := 0
|
||||
var selected Dungeon
|
||||
selectedMaze := -1
|
||||
for _, dungeon := range dungeonTable.dungeons {
|
||||
dungeonPath := dungeon.Path
|
||||
if len(dungeon.Extensions) < len(dungeon.UnknownSections) {
|
||||
t.Fatalf("real dungeon extension/raw mismatch: path=%s unknown=%d extensions=%d", dungeonPath, len(dungeon.UnknownSections), len(dungeon.Extensions))
|
||||
}
|
||||
for _, extension := range dungeon.Extensions {
|
||||
extensionNames[sectionKey(extension.Name)] = struct{}{}
|
||||
}
|
||||
for mazePos, maze := range dungeon.Mazes {
|
||||
if len(maze.Extensions) < len(maze.UnknownSections) {
|
||||
t.Fatalf("real maze extension/raw mismatch: path=%s maze=%d unknown=%d extensions=%d", dungeonPath, mazePos, len(maze.UnknownSections), len(maze.Extensions))
|
||||
}
|
||||
for _, extension := range maze.Extensions {
|
||||
extensionNames[sectionKey(extension.Name)] = struct{}{}
|
||||
}
|
||||
randomizedScripts += len(maze.RandomizedObjects)
|
||||
clearConditions += len(maze.ClearConditions)
|
||||
for _, script := range maze.RandomizedObjects {
|
||||
randomizedObjects += len(script.Objects)
|
||||
}
|
||||
if selectedMaze < 0 && len(maze.MapSpecifications) > 0 && allSpecificationMapsPresent(maze, mapPaths) {
|
||||
selected = dungeon
|
||||
selectedMaze = mazePos
|
||||
}
|
||||
}
|
||||
}
|
||||
if parsedDocuments == 0 || selectedMaze < 0 {
|
||||
t.Fatalf("real dungeon graph unavailable: parsed=%d selected_maze=%d", parsedDocuments, selectedMaze)
|
||||
}
|
||||
if len(extensionNames) == 0 {
|
||||
t.Fatal("real PVF contains no parsed C#-missing dungeon extensions")
|
||||
}
|
||||
if randomizedScripts == 0 || randomizedObjects == 0 || clearConditions == 0 {
|
||||
t.Fatalf(
|
||||
"real complex dungeon structures missing: randomized_scripts=%d randomized_objects=%d clear_conditions=%d",
|
||||
randomizedScripts, randomizedObjects, clearConditions,
|
||||
)
|
||||
}
|
||||
|
||||
selected.Mazes = []Maze{selected.Mazes[selectedMaze]}
|
||||
selected.Mazes[0].Index = 0
|
||||
maps := loadSmokeSpecificationMaps(t, archive, selected.Mazes[0], mapPaths)
|
||||
table := smokeResolverTable(selected, maps)
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatalf("index real dungeon graph: %v", err)
|
||||
}
|
||||
resolved := false
|
||||
for _, specification := range selected.Mazes[0].MapSpecifications {
|
||||
request := ResolveRequest{
|
||||
DungeonID: selected.ID, MazeIndex: 0,
|
||||
X: specification.Coordinate.X, Y: specification.Coordinate.Y,
|
||||
}
|
||||
if _, err := resolver.Resolve(request); err == nil {
|
||||
resolved = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !resolved {
|
||||
t.Fatalf("real map specifications did not yield one deterministic room: dungeon=%d path=%s", selected.ID, selected.Path)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, selected.ID, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("build real dungeon topology: dungeon=%d path=%s: %v", selected.ID, selected.Path, err)
|
||||
}
|
||||
run, err := NewDungeonRun(topology)
|
||||
if err != nil {
|
||||
t.Fatalf("start real dungeon run: dungeon=%d path=%s: %v", selected.ID, selected.Path, err)
|
||||
}
|
||||
current, ok := run.CurrentRoom()
|
||||
if !ok || current.Map == nil {
|
||||
t.Fatalf("real dungeon start room unresolved: dungeon=%d room=%+v", selected.ID, current)
|
||||
}
|
||||
if err := run.MarkCurrentRoomCleared(); err != nil {
|
||||
t.Fatalf("clear real dungeon start room: %v", err)
|
||||
}
|
||||
moved := false
|
||||
for _, neighbor := range current.Neighbors {
|
||||
if neighbor.Coordinate.X < 0 || neighbor.Coordinate.X > 255 || neighbor.Coordinate.Y < 0 || neighbor.Coordinate.Y > 255 {
|
||||
continue
|
||||
}
|
||||
room, ok := topology.Room(neighbor.Coordinate)
|
||||
if !ok || room.Map == nil {
|
||||
continue
|
||||
}
|
||||
if _, err := run.MoveByte(byte(neighbor.Coordinate.X), byte(neighbor.Coordinate.Y)); err != nil {
|
||||
t.Fatalf("move through real dungeon topology: %v", err)
|
||||
}
|
||||
moved = true
|
||||
break
|
||||
}
|
||||
if !moved {
|
||||
t.Fatalf("real dungeon start has no EXE-coordinate compatible resolved neighbor: dungeon=%d room=%s", selected.ID, current.Coordinate)
|
||||
}
|
||||
t.Logf(
|
||||
"real dungeon alignment: parsed=%d parse_failures=0 extension_tags=%d randomized_scripts=%d randomized_objects=%d clear_conditions=%d selected_dungeon=%d selected_path=%s maze_specs=%d loaded_maps=%d topology_rooms=%d unresolved_rooms=%d moved_to=%s",
|
||||
parsedDocuments, len(extensionNames), randomizedScripts, randomizedObjects, clearConditions, selected.ID, selected.Path,
|
||||
len(selected.Mazes[0].MapSpecifications), len(maps), len(topology.rooms), len(topology.UnresolvedRooms()), run.Snapshot().Current,
|
||||
)
|
||||
}
|
||||
|
||||
func allSpecificationMapsPresent(maze Maze, paths map[int64]string) bool {
|
||||
for _, specification := range maze.MapSpecifications {
|
||||
for _, mapID := range specification.MapIDs {
|
||||
if paths[mapID] == "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func loadSmokeSpecificationMaps(t *testing.T, archive *platformpvf.Archive, maze Maze, paths map[int64]string) []Map {
|
||||
t.Helper()
|
||||
seen := make(map[int64]struct{})
|
||||
var maps []Map
|
||||
for _, specification := range maze.MapSpecifications {
|
||||
for _, mapID := range specification.MapIDs {
|
||||
if _, ok := seen[mapID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[mapID] = struct{}{}
|
||||
mapPath := paths[mapID]
|
||||
text, err := archive.ReadText(mapPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read real specification map %d %s: %v", mapID, mapPath, err)
|
||||
}
|
||||
doc, err := ParseDocument(mapPath, text)
|
||||
if err != nil {
|
||||
t.Fatalf("parse real specification map %d %s: %v", mapID, mapPath, err)
|
||||
}
|
||||
maps = append(maps, ParseMap(mapID, mapPath, doc))
|
||||
}
|
||||
}
|
||||
return maps
|
||||
}
|
||||
|
||||
func smokeResolverTable(dungeon Dungeon, maps []Map) *Table {
|
||||
table := &Table{
|
||||
maps: maps, dungeons: []Dungeon{dungeon},
|
||||
mapByID: make(map[int64]int), mapByPath: make(map[string]int),
|
||||
dungeonByID: map[int64]int{dungeon.ID: 0}, dungeonByPath: map[string]int{pathKey(dungeon.Path): 0},
|
||||
}
|
||||
for pos, mapValue := range maps {
|
||||
table.mapByID[mapValue.ID] = pos
|
||||
table.mapByPath[pathKey(mapValue.Path)] = pos
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
func prefixedArchivePath(prefix, value string) string {
|
||||
value = strings.TrimPrefix(strings.ReplaceAll(value, "\\", "/"), "/")
|
||||
if !strings.HasPrefix(strings.ToLower(value), strings.ToLower(prefix)+"/") {
|
||||
return prefix + "/" + value
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in new issue
Block a user