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,416 @@
|
||||
package worldmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDungeonTopologyBuildsStrictPVFRoomGraph(t *testing.T) {
|
||||
resolver, err := NewResolver(topologyTestTable())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("build topology: %v", err)
|
||||
}
|
||||
rooms := topology.Rooms()
|
||||
if len(rooms) != 5 {
|
||||
t.Fatalf("rooms=%d, want 5: %+v", len(rooms), rooms)
|
||||
}
|
||||
if topology.Start == nil || *topology.Start != (RoomCoordinate{X: 0, Y: 0}) {
|
||||
t.Fatalf("start=%+v", topology.Start)
|
||||
}
|
||||
if len(topology.Bosses) != 1 || topology.Bosses[0] != (RoomCoordinate{X: 2, Y: 1}) {
|
||||
t.Fatalf("bosses=%+v", topology.Bosses)
|
||||
}
|
||||
start, ok := topology.Room(RoomCoordinate{X: 0, Y: 0})
|
||||
if !ok || start.Map == nil || start.Map.Map.ID != 100 || !start.Start || start.Boss {
|
||||
t.Fatalf("start room=%+v ok=%v", start, ok)
|
||||
}
|
||||
if len(start.Neighbors) != 2 || start.Neighbors[0].Direction != RoomDirectionRight || start.Neighbors[1].Direction != RoomDirectionDown {
|
||||
t.Fatalf("start neighbors=%+v", start.Neighbors)
|
||||
}
|
||||
if _, ok := topology.Room(RoomCoordinate{X: 1, Y: 1}); ok {
|
||||
t.Fatal("closed [greed] cell was added to topology")
|
||||
}
|
||||
if unresolved := topology.UnresolvedRooms(); len(unresolved) != 0 {
|
||||
t.Fatalf("unresolved rooms=%+v", unresolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonTopologyPreservesPVFAuthoredMonsterRanksAcrossRoomReads(t *testing.T) {
|
||||
start := RoomCoordinate{}
|
||||
topology := &DungeonTopology{
|
||||
DungeonID: 800,
|
||||
MazeIndex: 0,
|
||||
Start: &start,
|
||||
rooms: map[coordinateKey]DungeonRoom{
|
||||
{}: {
|
||||
Coordinate: start,
|
||||
Start: true,
|
||||
Map: &ResolvedMap{Map: Map{
|
||||
ID: 501,
|
||||
Monsters: []MonsterSpawn{
|
||||
{MonsterID: 1, Rank: "[normal]"},
|
||||
{MonsterID: 2, Rank: "[dummy]"},
|
||||
{MonsterID: 3, Rank: "[champion]"},
|
||||
{MonsterID: 4, Rank: "[super champion]"},
|
||||
{MonsterID: 5, Rank: "[boss]"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
resolutionErrors: make(map[coordinateKey]error),
|
||||
}
|
||||
|
||||
first, ok := topology.Room(start)
|
||||
if !ok || first.Map == nil {
|
||||
t.Fatalf("first room=%+v ok=%t", first, ok)
|
||||
}
|
||||
wantRanks := []string{"[normal]", "[dummy]", "[champion]", "[super champion]", "[boss]"}
|
||||
if got := monsterSpawnRanks(first.Map.Map.Monsters); !reflect.DeepEqual(got, wantRanks) {
|
||||
t.Fatalf("first ranks=%v, want authored ranks %v", got, wantRanks)
|
||||
}
|
||||
first.Map.Map.Monsters[0].Rank = "[champion]"
|
||||
for read := 0; read < 3; read++ {
|
||||
room, roomOK := topology.Room(start)
|
||||
if !roomOK || room.Map == nil {
|
||||
t.Fatalf("read %d room=%+v ok=%t", read, room, roomOK)
|
||||
}
|
||||
if got := monsterSpawnRanks(room.Map.Map.Monsters); !reflect.DeepEqual(got, wantRanks) {
|
||||
t.Fatalf("read %d ranks=%v, want authored %v", read, got, wantRanks)
|
||||
}
|
||||
}
|
||||
rooms := topology.Rooms()
|
||||
if len(rooms) != 1 || rooms[0].Map == nil {
|
||||
t.Fatalf("rooms=%+v", rooms)
|
||||
}
|
||||
if got := monsterSpawnRanks(rooms[0].Map.Map.Monsters); !reflect.DeepEqual(got, wantRanks) {
|
||||
t.Fatalf("Rooms ranks=%v, want authored %v", got, wantRanks)
|
||||
}
|
||||
}
|
||||
|
||||
func monsterSpawnRanks(spawns []MonsterSpawn) []string {
|
||||
ranks := make([]string, len(spawns))
|
||||
for index, spawn := range spawns {
|
||||
ranks[index] = spawn.Rank
|
||||
}
|
||||
return ranks
|
||||
}
|
||||
|
||||
func TestDungeonRunValidatesCurrentEXECoordinatesWithoutFallback(t *testing.T) {
|
||||
resolver, err := NewResolver(topologyTestTable())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
run, err := NewDungeonRun(topology)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if room, ok := run.CurrentRoom(); !ok || room.Coordinate.String() != "0:0" {
|
||||
t.Fatalf("current room=%+v ok=%v", room, ok)
|
||||
}
|
||||
if _, err := run.MoveByte(1, 0); !errors.Is(err, ErrRunCurrentRoomNotCleared) {
|
||||
t.Fatalf("uncleared move error=%v", err)
|
||||
}
|
||||
if err := run.MarkCurrentRoomCleared(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
room, err := run.MoveByte(1, 0)
|
||||
if err != nil || room.Map == nil || room.Map.Map.ID != 101 {
|
||||
t.Fatalf("adjacent move room=%+v err=%v", room, err)
|
||||
}
|
||||
if _, err := run.MoveTo(RoomCoordinate{X: 2, Y: 1}); !errors.Is(err, ErrRoomNotAdjacent) {
|
||||
t.Fatalf("diagonal/far move error=%v", err)
|
||||
}
|
||||
if _, err := run.MoveTo(RoomCoordinate{X: 1, Y: 1}); !errors.Is(err, ErrRoomNotFound) {
|
||||
t.Fatalf("closed cell move error=%v", err)
|
||||
}
|
||||
if err := run.MarkCurrentRoomCleared(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := run.MoveRoomID("2:0"); err != nil {
|
||||
t.Fatalf("room id move: %v", err)
|
||||
}
|
||||
if err := run.Complete(); !errors.Is(err, ErrRunCurrentRoomNotCleared) {
|
||||
t.Fatalf("complete uncleared room error=%v", err)
|
||||
}
|
||||
if err := run.MarkCurrentRoomCleared(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := run.Complete(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshot := run.Snapshot()
|
||||
if snapshot.Status != DungeonRunCompleted || snapshot.Current != (RoomCoordinate{X: 2, Y: 0}) || len(snapshot.Visited) != 3 || len(snapshot.Cleared) != 3 {
|
||||
t.Fatalf("snapshot=%+v", snapshot)
|
||||
}
|
||||
if _, err := run.MoveByte(2, 1); !errors.Is(err, ErrRunNotActive) {
|
||||
t.Fatalf("move after completion error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonRunUsesPVFMoveBeforeClearFlag(t *testing.T) {
|
||||
table := topologyTestTable()
|
||||
table.dungeons[0].Metadata.Flags = map[string]bool{"move map even enemy": true}
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !topology.AllowMoveBeforeClear {
|
||||
t.Fatal("PVF move-before-clear flag was not retained")
|
||||
}
|
||||
run, err := NewDungeonRun(topology)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := run.MoveByte(1, 0); err != nil {
|
||||
t.Fatalf("PVF-authorized move failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonRunUsesDeclaredDirectedWarpTransitions(t *testing.T) {
|
||||
table := topologyTestTable()
|
||||
table.dungeons[0].Metadata.Flags = map[string]bool{"move map even enemy": true}
|
||||
table.dungeons[0].WarpMapConditions = []WarpMapCondition{
|
||||
{Source: Point{X: 0, Y: 0}, Destination: Point{X: 2, Y: 1}},
|
||||
{Source: Point{X: -1, Y: -1}, Destination: Point{X: 2, Y: 0}},
|
||||
}
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(topology.warpTransitions) != 1 {
|
||||
t.Fatalf("warp transitions=%d want=1", len(topology.warpTransitions))
|
||||
}
|
||||
run, err := NewDungeonRun(topology)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
room, err := run.MoveTo(RoomCoordinate{X: 2, Y: 1})
|
||||
if err != nil || room.Map == nil || room.Map.Map.ID != 104 {
|
||||
t.Fatalf("declared warp room=%+v err=%v", room, err)
|
||||
}
|
||||
if _, err := run.MoveTo(RoomCoordinate{X: 0, Y: 0}); !errors.Is(err, ErrRoomNotAdjacent) {
|
||||
t.Fatalf("undeclared reverse warp error=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonRunDoesNotTreatBossEntranceWarpAsTravelEdge(t *testing.T) {
|
||||
table := topologyTestTable()
|
||||
table.dungeons[0].Metadata.Flags = map[string]bool{"move map even enemy": true}
|
||||
maze := &table.dungeons[0].Mazes[0]
|
||||
maze.BossRoomEntranceCondition = &BossRoomEntranceCondition{
|
||||
Action: "clear action",
|
||||
HuntMonsters: []BossRoomEntranceHuntMonster{{TargetID: 69201, Count: 1}},
|
||||
}
|
||||
maze.WarpMapConditions = []WarpMapCondition{
|
||||
{Source: Point{X: 0, Y: 0}, Destination: Point{X: 2, Y: 1}},
|
||||
}
|
||||
table.dungeons[0].WarpMapConditions = append([]WarpMapCondition(nil), maze.WarpMapConditions...)
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(topology.warpTransitions) != 0 {
|
||||
t.Fatalf("boss-entrance warp became travel edges: %+v", topology.warpTransitions)
|
||||
}
|
||||
run, err := NewDungeonRun(topology)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := run.MoveTo(RoomCoordinate{X: 2, Y: 1}); !errors.Is(err, ErrRoomNotAdjacent) {
|
||||
t.Fatalf("boss-entrance destination accepted as direct travel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonTopologyRetainsMalformedAndUnresolvedRooms(t *testing.T) {
|
||||
table := topologyTestTable()
|
||||
table.maps = table.maps[:1]
|
||||
table.mapByID = map[int64]int{100: 0}
|
||||
table.mapByPath = map[string]int{pathKey(table.maps[0].Path): 0}
|
||||
maze := &table.dungeons[0].Mazes[0]
|
||||
maze.Width = OptionalInt{Set: true, Value: 2}
|
||||
maze.Height = OptionalInt{Set: true, Value: 1}
|
||||
maze.Greed = "BBEE"
|
||||
maze.Boss = nil
|
||||
maze.MapSpecifications = maze.MapSpecifications[:1]
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unresolved := topology.UnresolvedRooms()
|
||||
if len(unresolved) != 1 || unresolved[0].Coordinate != (RoomCoordinate{X: 1, Y: 0}) || unresolved[0].ResolutionError == "" {
|
||||
t.Fatalf("unresolved rooms=%+v", unresolved)
|
||||
}
|
||||
run, err := NewDungeonRun(topology)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := run.MarkCurrentRoomCleared(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = run.MoveByte(1, 0)
|
||||
if !errors.Is(err, ErrRoomMapUnresolved) || !errors.Is(err, ErrMapUnresolved) {
|
||||
t.Fatalf("unresolved move error=%v", err)
|
||||
}
|
||||
|
||||
maze.Greed = "1"
|
||||
resolver, err = NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err = BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hasTopologyDiagnostic(topology.Diagnostics, "maze_grid_row_short") {
|
||||
t.Fatalf("missing malformed greed diagnostic: %+v", topology.Diagnostics)
|
||||
}
|
||||
|
||||
maze.Greed = "BBEF"
|
||||
resolver, err = NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err = BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hasTopologyDiagnostic(topology.Diagnostics, "maze_grid_pair_mismatch") {
|
||||
t.Fatalf("missing mismatched pair diagnostic: %+v", topology.Diagnostics)
|
||||
}
|
||||
|
||||
maze.Greed = "BBZZ"
|
||||
resolver, err = NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err = BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hasTopologyDiagnostic(topology.Diagnostics, "maze_grid_code_unknown") {
|
||||
t.Fatalf("missing unknown code diagnostic: %+v", topology.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDungeonTopologyKeepsRepeatedBossCoordinates(t *testing.T) {
|
||||
table := topologyTestTable()
|
||||
table.dungeons[0].Mazes[0].Boss = &MazePoint{X: 2, Y: 1, Params: []int64{2, 0, 99}}
|
||||
resolver, err := NewResolver(table)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topology, err := BuildDungeonTopology(resolver, 800, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(topology.Bosses) != 2 || topology.Bosses[1] != (RoomCoordinate{X: 2, Y: 0}) {
|
||||
t.Fatalf("boss coordinates=%+v", topology.Bosses)
|
||||
}
|
||||
room, ok := topology.Room(RoomCoordinate{X: 2, Y: 0})
|
||||
if !ok || !room.Boss {
|
||||
t.Fatalf("second boss room=%+v ok=%v", room, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomCoordinateParsing(t *testing.T) {
|
||||
coordinate, err := ParseRoomCoordinate(" -2:15 ")
|
||||
if err != nil || coordinate != (RoomCoordinate{X: -2, Y: 15}) || coordinate.String() != "-2:15" {
|
||||
t.Fatalf("coordinate=%+v err=%v", coordinate, err)
|
||||
}
|
||||
for _, value := range []string{"", "1", "1:2:3", "a:2"} {
|
||||
if _, err := ParseRoomCoordinate(value); !errors.Is(err, ErrRoomCoordinateMalformed) {
|
||||
t.Errorf("ParseRoomCoordinate(%q) error=%v", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGreedRoomMasksFollowLocalPVFEncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
code rune
|
||||
mask uint8
|
||||
}{
|
||||
{'A', 0x00},
|
||||
{'B', 0x01},
|
||||
{'C', 0x02},
|
||||
{'E', 0x04},
|
||||
{'I', 0x08},
|
||||
{'P', 0x0f},
|
||||
{'p', 0x0f},
|
||||
}
|
||||
for _, test := range tests {
|
||||
mask, ok := greedRoomMask(test.code)
|
||||
if !ok || mask != test.mask {
|
||||
t.Errorf("greedRoomMask(%q)=(%02x,%v), want %02x", test.code, mask, ok, test.mask)
|
||||
}
|
||||
}
|
||||
if _, ok := greedRoomMask('Z'); ok {
|
||||
t.Fatal("unknown local PVF room code was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func hasTopologyDiagnostic(diagnostics []TopologyDiagnostic, code string) bool {
|
||||
for _, diagnostic := range diagnostics {
|
||||
if diagnostic.Code == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func topologyTestTable() *Table {
|
||||
maps := []Map{
|
||||
{ID: 100, Path: "map/topology/s100(0,0).map"},
|
||||
{ID: 101, Path: "map/topology/101(1,0).map"},
|
||||
{ID: 102, Path: "map/topology/102(2,0).map"},
|
||||
{ID: 103, Path: "map/topology/103(0,1).map"},
|
||||
{ID: 104, Path: "map/topology/104(2,1)boss.map"},
|
||||
}
|
||||
dungeon := Dungeon{
|
||||
ID: 800, Path: "dungeon/topology.dgn",
|
||||
Mazes: []Maze{{
|
||||
Index: 0,
|
||||
Width: OptionalInt{Set: true, Value: 3}, Height: OptionalInt{Set: true, Value: 2},
|
||||
Greed: "JJFFMM\nCCAACC",
|
||||
Start: &MazePoint{X: 0, Y: 0}, Boss: &MazePoint{X: 2, Y: 1},
|
||||
MapSpecifications: []MapSpecification{
|
||||
{Type: "start", Coordinate: Point{X: 0, Y: 0}, MapIDs: []int64{100}},
|
||||
{Type: "boss", Coordinate: Point{X: 2, Y: 1}, MapIDs: []int64{104}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
table := &Table{
|
||||
maps: maps, dungeons: []Dungeon{dungeon},
|
||||
mapByID: make(map[int64]int), mapByPath: make(map[string]int),
|
||||
dungeonByID: map[int64]int{800: 0}, dungeonByPath: map[string]int{pathKey(dungeon.Path): 0},
|
||||
}
|
||||
for index, mapValue := range maps {
|
||||
table.mapByID[mapValue.ID] = index
|
||||
table.mapByPath[pathKey(mapValue.Path)] = index
|
||||
}
|
||||
return table
|
||||
}
|
||||
Reference in new issue
Block a user