DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,935 @@
package dnfbridge
import (
"encoding/binary"
"fmt"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/raid"
)
// The two fixtures below mirror etc/raid/luke/luke.etc exactly; the real values
// are pinned against the shipped PVF by
// raid.TestRealScriptPVFLukePhaseMechanismTables.
func lukeTeamPhaseOneState() *runtimeRaidState {
objectives := map[int]runtimeRaidDungeonObjective{}
for _, gate := range []struct {
mapID int
state raid.AntonDungeonState
}{
{lukeBirthSanctumMapID, raid.AntonDungeonOpen},
{lukeExtinctionSanctumMapID, raid.AntonDungeonHide},
{lukeSpreadSanctumMapID, raid.AntonDungeonHide},
} {
objectives[gate.mapID] = runtimeRaidDungeonObjective{
Phase: runtimeRaidDungeonPhaseOne, MapID: gate.mapID, State: gate.state,
MaxHP: 3, RemainingHP: 3, PartyMax: 1,
}
}
state := &runtimeRaidState{
RaidKey: 0x00200001,
RaidMode: runtimeRaidOwnerLuke,
Lifecycle: runtimeRaidLifecycleState{
Status: runtimeRaidLifecyclePhaseActive,
Phase: runtimeRaidDungeonPhaseOne,
Objectives: objectives,
},
}
state.ensureRuntimeRaidLifecycleMapsLocked()
initRuntimeRaidLukePhaseOneLocked(state)
return state
}
func lukeTeamPhaseTwoState() *runtimeRaidState {
objectives := map[int]runtimeRaidDungeonObjective{}
// The init states are the ones etc/raid/luke/luke.etc actually declares: the
// four energy/altar rooms start `close`, everything else `hide`.
for mapID, initState := range map[int]raid.AntonDungeonState{
lukeLightEntranceMapID: raid.AntonDungeonHide,
lukeDarkEntranceMapID: raid.AntonDungeonHide,
lukeLightEnergyMapID: raid.AntonDungeonClosed,
lukeDarkEnergyMapID: raid.AntonDungeonClosed,
lukeLightAltarMapID: raid.AntonDungeonClosed,
lukeDarkAltarMapID: raid.AntonDungeonClosed,
lukeEnergyControlMapID: raid.AntonDungeonHide,
lukeLightTempleMapID: raid.AntonDungeonHide,
lukeDarkTempleMapID: raid.AntonDungeonHide,
lukeLightThroneMapID: raid.AntonDungeonHide,
lukeDarkThroneMapID: raid.AntonDungeonHide,
lukeEnergySaveMapID: raid.AntonDungeonHide,
} {
objectives[mapID] = runtimeRaidDungeonObjective{
Phase: runtimeRaidDungeonPhaseTwo, MapID: mapID, State: initState,
MaxHP: 1, RemainingHP: 1, PartyMax: 1,
}
}
state := &runtimeRaidState{
RaidKey: 0x00200001,
RaidMode: runtimeRaidOwnerLuke,
Started: true,
Lifecycle: runtimeRaidLifecycleState{
Status: runtimeRaidLifecyclePhaseActive,
Phase: runtimeRaidDungeonPhaseTwo,
Objectives: objectives,
},
}
state.ensureRuntimeRaidLifecycleMapsLocked()
return state
}
func lukeGateState(t *testing.T, state *runtimeRaidState, mapID int) raid.AntonDungeonState {
t.Helper()
objective, ok := state.Lifecycle.Objectives[mapID]
if !ok {
t.Fatalf("gate %d missing", mapID)
}
return objective.State
}
// lukeClear drives one committed clear all the way through the shared objective
// advance, which is what the dungeon lifecycle does on a real clear.
func lukeClear(t *testing.T, state *runtimeRaidState, mapID int, now time.Time) []runtimeRaidMechanismTimer {
t.Helper()
if err := advanceRuntimeRaidObjectiveLocked(state, state.Lifecycle.Phase, mapID); err != nil {
t.Fatalf("advance gate %d: %v", mapID, err)
}
if state.Lifecycle.Phase != runtimeRaidDungeonPhaseTwo {
return nil
}
return applyRuntimeRaidLukePhaseTwoClearLocked(state, mapID, now)
}
func TestLukePhaseOneOpensBranchSanctumsOnEntryAndRecordsFirstBranch(t *testing.T) {
state := lukeTeamPhaseOneState()
if got := lukeGateState(t, state, lukeExtinctionSanctumMapID); got != raid.AntonDungeonHide {
t.Fatalf("extinction sanctum starts %q, want hide", got)
}
// [ON ENTER DUNGEON] 5054 with [2PHASE DUNGEON TYPE] still 0.
if !runtimeRaidLukeEnterDungeonLocked(state, lukeBirthSanctumMapID) {
t.Fatal("entering the birth sanctum opened nothing")
}
for _, mapID := range []int{lukeExtinctionSanctumMapID, lukeSpreadSanctumMapID} {
if got := lukeGateState(t, state, mapID); got != raid.AntonDungeonOpen {
t.Fatalf("gate %d = %q after entry, want open", mapID, got)
}
}
// Three clears finish a sanctum; 扩散 falling first sets the route marker to 2.
now := time.Unix(1700000000, 0).UTC()
for i := 0; i < 3; i++ {
lukeClear(t, state, lukeSpreadSanctumMapID, now)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolPhaseTwoDungeonType); got != lukePhaseTwoDungeonTypeSpread {
t.Fatalf("2PHASE DUNGEON TYPE=%d want=%d", got, lukePhaseTwoDungeonTypeSpread)
}
// The marker is decided once: 灭亡 falling later must not overwrite it.
for i := 0; i < 3; i++ {
lukeClear(t, state, lukeExtinctionSanctumMapID, now)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolPhaseTwoDungeonType); got != lukePhaseTwoDungeonTypeSpread {
t.Fatalf("2PHASE DUNGEON TYPE overwritten to %d", got)
}
if runtimeRaidPhaseObjectivesComplete(*state, runtimeRaidDungeonPhaseOne) {
t.Fatal("phase one complete with the birth sanctum still standing")
}
for i := 0; i < 3; i++ {
lukeClear(t, state, lukeBirthSanctumMapID, now)
}
if !runtimeRaidPhaseObjectivesComplete(*state, runtimeRaidDungeonPhaseOne) {
t.Fatal("phase one did not complete with all three sanctums down")
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolPhaseOneSuccess); got != 1 {
t.Fatalf("[1PHASE SUCCESS]=%d want=1", got)
}
}
func TestLukePhaseOneGiveUpResetsThatSanctumObjectCounter(t *testing.T) {
state := lukeTeamPhaseOneState()
setRuntimeRaidLukeSymbolLocked(state, lukeSymbolBirthSanctumObject, 4)
setRuntimeRaidLukeSymbolLocked(state, lukeSymbolSpreadSanctumObject, 2)
runtimeRaidLukeGiveUpDungeonLocked(state, lukeBirthSanctumMapID)
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolBirthSanctumObject); got != 0 {
t.Fatalf("birth sanctum objects=%d want=0", got)
}
// Only the abandoned sanctum resets.
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolSpreadSanctumObject); got != 2 {
t.Fatalf("spread sanctum objects=%d want=2", got)
}
}
func TestLukePhaseTwoWalksBothChainsToTheTwoThrones(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
armed := initRuntimeRaidLukePhaseTwoLocked(state, now)
if len(armed) != 1 || armed[0].MapID != lukeBirthSanctumMapID {
t.Fatalf("phase two opening timer=%+v", armed)
}
for _, mapID := range []int{lukeLightEntranceMapID, lukeDarkEntranceMapID} {
if got := lukeGateState(t, state, mapID); got != raid.AntonDungeonOpen {
t.Fatalf("entrance %d=%q want open", mapID, got)
}
}
// One entrance is not enough to open the altars.
lukeClear(t, state, lukeLightEntranceMapID, now)
if got := lukeGateState(t, state, lukeLightEnergyMapID); got == raid.AntonDungeonOpen {
t.Fatalf("light energy opened on one entrance: %q", got)
}
lukeClear(t, state, lukeDarkEntranceMapID, now)
for _, mapID := range []int{lukeLightEnergyMapID, lukeDarkEnergyMapID, lukeEnergyControlMapID} {
if got := lukeGateState(t, state, mapID); got != raid.AntonDungeonOpen {
t.Fatalf("gate %d=%q after both entrances, want open", mapID, got)
}
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergyControlEffect); got != 1 {
t.Fatalf("[ENERGY CONTROL EFFECT]=%d want=1", got)
}
// Each energy room opens its own altar, and both altars open both temples.
lukeClear(t, state, lukeLightEnergyMapID, now)
if got := lukeGateState(t, state, lukeLightAltarMapID); got != raid.AntonDungeonOpen {
t.Fatalf("light altar=%q want open", got)
}
lukeClear(t, state, lukeDarkEnergyMapID, now)
lukeClear(t, state, lukeLightAltarMapID, now)
if got := lukeGateState(t, state, lukeLightTempleMapID); got != raid.AntonDungeonHide {
t.Fatalf("light temple opened on one altar: %q", got)
}
lukeClear(t, state, lukeDarkAltarMapID, now)
for _, mapID := range []int{lukeLightTempleMapID, lukeDarkTempleMapID} {
if got := lukeGateState(t, state, mapID); got != raid.AntonDungeonOpen {
t.Fatalf("temple %d=%q want open", mapID, got)
}
}
// Both altars down also shuts the energy control room for good.
if got := lukeGateState(t, state, lukeEnergyControlMapID); got != raid.AntonDungeonHide {
t.Fatalf("energy control=%q want hide", got)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergyControlEffect); got != 0 {
t.Fatalf("[ENERGY CONTROL EFFECT]=%d want=0", got)
}
// Both temples open 机械王座:明 only; 暗 follows the light throne's fall.
lukeClear(t, state, lukeLightTempleMapID, now)
lukeClear(t, state, lukeDarkTempleMapID, now)
if got := lukeGateState(t, state, lukeLightThroneMapID); got != raid.AntonDungeonOpen {
t.Fatalf("light throne=%q want open", got)
}
if got := lukeGateState(t, state, lukeDarkThroneMapID); got != raid.AntonDungeonHide {
t.Fatalf("dark throne opened early: %q", got)
}
lukeClear(t, state, lukeLightThroneMapID, now)
if got := lukeGateState(t, state, lukeDarkThroneMapID); got != raid.AntonDungeonOpen {
t.Fatalf("dark throne=%q want open", got)
}
if got := state.Lifecycle.MovieSymbols[lukeSymbolKillAllCharacter]; got != 1 {
t.Fatalf("[KILL ALL CHARACTER]=%d want=1 between the two throne stages", got)
}
if runtimeRaidPhaseObjectivesComplete(*state, runtimeRaidDungeonPhaseTwo) {
t.Fatal("phase two complete before the dark throne fell")
}
lukeClear(t, state, lukeDarkThroneMapID, now)
if !runtimeRaidPhaseObjectivesComplete(*state, runtimeRaidDungeonPhaseTwo) {
t.Fatal("phase two did not complete on the dark throne")
}
}
// TestLukePhaseTwoInitOpensEntrancesBeforeLifecyclePhaseIsAssigned reproduces
// the real call ordering inside activateRuntimeRaidPhase, which builds the new
// phase's objectives and runs the phase-two init BEFORE assigning
// Lifecycle.Phase. The gate setter used to compare an objective's phase against
// Lifecycle.Phase, so during activation every opening was silently dropped:
// live 2026-09-23T05:38:55Z op585 went out with 5311 and 5313 still at their
// PVF `hide` (state 2) and the whole phase-two route was unreachable, while the
// symbols -- which have no such guard -- went out correctly.
func TestLukePhaseTwoInitOpensEntrancesBeforeLifecyclePhaseIsAssigned(t *testing.T) {
state := lukeTeamPhaseTwoState()
// The objectives belong to phase two; the lifecycle still says phase one.
state.Lifecycle.Phase = runtimeRaidDungeonPhaseOne
initRuntimeRaidLukePhaseTwoLocked(state, time.Unix(1700000000, 0).UTC())
for _, mapID := range []int{lukeLightEntranceMapID, lukeDarkEntranceMapID} {
if got := lukeGateState(t, state, mapID); got != raid.AntonDungeonOpen {
t.Fatalf("entrance %d=%q want open during activation", mapID, got)
}
}
// Everything else keeps the state the PVF gave it.
if got := lukeGateState(t, state, lukeLightEnergyMapID); got == raid.AntonDungeonOpen {
t.Fatal("the light altar opened before both entrances were cleared")
}
}
// TestLukePhaseTwoRoutesAreMutuallyExclusive covers the reported "两条线都能进".
// Nothing read [LOCKOUT TYPE], so any member could walk into both routes.
func TestLukePhaseTwoRoutesAreMutuallyExclusive(t *testing.T) {
state := lukeTeamPhaseTwoState()
initRuntimeRaidLukePhaseTwoLocked(state, time.Unix(1700000000, 0).UTC())
const light, dark = uint16(1001), uint16(1002)
// Before anyone commits, both routes are open to both members.
for _, mapID := range []int{lukeLightEntranceMapID, lukeDarkEntranceMapID} {
for _, charID := range []uint16{light, dark} {
if !runtimeRaidLukeRouteEntryAllowedLocked(state, charID, mapID) {
t.Fatalf("char %d refused gate %d before binding", charID, mapID)
}
}
}
if route, bound := bindRuntimeRaidLukeRouteLocked(state, light, lukeLightEntranceMapID); !bound || route != lukeRouteLight {
t.Fatalf("light bind=(%d,%t)", route, bound)
}
if route, bound := bindRuntimeRaidLukeRouteLocked(state, dark, lukeDarkEntranceMapID); !bound || route != lukeRouteDark {
t.Fatalf("dark bind=(%d,%t)", route, bound)
}
// Each member keeps its own whole route and is refused every gate of the other.
lightGates := []int{lukeLightEnergyMapID, lukeLightAltarMapID, lukeLightTempleMapID, lukeLightThroneMapID}
darkGates := []int{lukeDarkEnergyMapID, lukeDarkAltarMapID, lukeDarkTempleMapID, lukeDarkThroneMapID}
for _, mapID := range lightGates {
if !runtimeRaidLukeRouteEntryAllowedLocked(state, light, mapID) {
t.Fatalf("light member refused its own gate %d", mapID)
}
if runtimeRaidLukeRouteEntryAllowedLocked(state, dark, mapID) {
t.Fatalf("dark member entered light gate %d", mapID)
}
}
for _, mapID := range darkGates {
if !runtimeRaidLukeRouteEntryAllowedLocked(state, dark, mapID) {
t.Fatalf("dark member refused its own gate %d", mapID)
}
if runtimeRaidLukeRouteEntryAllowedLocked(state, light, mapID) {
t.Fatalf("light member entered dark gate %d", mapID)
}
}
// The two shared rooms bind nobody and block nobody.
for _, mapID := range []int{lukeEnergyControlMapID, lukeEnergySaveMapID} {
for _, charID := range []uint16{light, dark} {
if !runtimeRaidLukeRouteEntryAllowedLocked(state, charID, mapID) {
t.Fatalf("char %d refused shared room %d", charID, mapID)
}
}
if _, bound := bindRuntimeRaidLukeRouteLocked(state, 1003, mapID); bound {
t.Fatalf("shared room %d bound a route", mapID)
}
}
// A single raid is never route-locked.
state.TailFlag = 1
if !runtimeRaidLukeRouteEntryAllowedLocked(state, dark, lukeLightThroneMapID) {
t.Fatal("a single raid was route-locked")
}
}
func TestLukeEnergyControlRecoveryFollowsGeneratorTier(t *testing.T) {
// [1PHASE OBJ DESTROY] 0 0 5 / 1 6 10 / 2 11 15 picks 133, and luke.etc
// arms 45 / 90 / 120 s from it; the window is the same on every cycle.
for _, test := range []struct {
name string
destroyed [3]uint32
tier uint32
want time.Duration
}{
{name: "no generators", destroyed: [3]uint32{0, 0, 0}, tier: 0, want: 45 * time.Second},
{name: "six generators", destroyed: [3]uint32{5, 1, 0}, tier: 1, want: 90 * time.Second},
{name: "all fifteen", destroyed: [3]uint32{5, 5, 5}, tier: 2, want: 120 * time.Second},
} {
t.Run(test.name, func(t *testing.T) {
lukeEnergyControlCycles(t, test.destroyed, test.tier, test.want)
})
}
}
func lukeEnergyControlCycles(t *testing.T, destroyed [3]uint32, tier uint32, want time.Duration) {
t.Helper()
state := lukeTeamPhaseTwoState()
state.LukeConfig = lukeGeneratorTestConfig()
setRuntimeRaidLukeSymbolLocked(state, lukeSymbolBirthSanctumObject, destroyed[0])
setRuntimeRaidLukeSymbolLocked(state, lukeSymbolExtinctionSanctumObject, destroyed[1])
setRuntimeRaidLukeSymbolLocked(state, lukeSymbolSpreadSanctumObject, destroyed[2])
now := time.Unix(1700000000, 0).UTC()
initRuntimeRaidLukePhaseTwoLocked(state, now)
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergyControlOpen); got != tier {
t.Fatalf("[ENERGY CONTROL OPEN]=%d want tier %d", got, tier)
}
lukeClear(t, state, lukeLightEntranceMapID, now)
lukeClear(t, state, lukeDarkEntranceMapID, now)
for index := 0; index < 4; index++ {
armed := lukeClear(t, state, lukeEnergyControlMapID, now)
if len(armed) != 1 || armed[0].Kind != runtimeRaidMechanismRecovery || armed[0].MapID != lukeEnergyControlMapID {
t.Fatalf("cycle %d armed=%+v", index, armed)
}
if got := armed[0].Deadline.Sub(now); got != want {
t.Fatalf("cycle %d recovery=%s want=%s", index, got, want)
}
if got := lukeGateState(t, state, lukeEnergyControlMapID); got != raid.AntonDungeonClosed {
t.Fatalf("cycle %d energy control=%q want close", index, got)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergyControlEffect); got != 0 {
t.Fatalf("cycle %d [ENERGY CONTROL EFFECT]=%d want=0 while closed", index, got)
}
next, outcome := fireRuntimeRaidLukeMechanismLocked(state, armed[0], now.Add(want))
if outcome != "energy_control_reopened" || len(next) != 0 {
t.Fatalf("cycle %d reopen outcome=%q next=%+v", index, outcome, next)
}
if got := lukeGateState(t, state, lukeEnergyControlMapID); got != raid.AntonDungeonOpen {
t.Fatalf("cycle %d energy control=%q want open", index, got)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergyControlEffect); got != 1 {
t.Fatalf("cycle %d [ENERGY CONTROL EFFECT]=%d want=1 while open", index, got)
}
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergyControlOpen); got != tier {
t.Fatalf("cycles moved [ENERGY CONTROL OPEN] to %d; it is the tier %d, not a cycle counter", got, tier)
}
}
func TestLukeEnergySaveWipesTheRosterWhenItsClockRunsOut(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
setRuntimeRaidLukeDungeonStateLocked(state, lukeLightThroneMapID, raid.AntonDungeonOpen)
// Reaching the throne's boss room opens the energy save room and starts its clock.
opened, armed := runtimeRaidLukeEnterMapLocked(state, lukeLightThroneMapID, lukeLightThroneBossRoomMapID, now)
if !opened {
t.Fatal("entering the throne boss room did not open the energy save room")
}
if len(armed) != 1 || armed[0].Deadline.Sub(now) != lukeEnergySaveWindow {
t.Fatalf("energy save window=%+v want %s", armed, lukeEnergySaveWindow)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergySaveEffect); got != lukeEnergySaveEffectActive {
t.Fatalf("[ENERGY SAVE EFFECT]=%d want=%d", got, lukeEnergySaveEffectActive)
}
next, outcome := fireRuntimeRaidLukeMechanismLocked(state, armed[0], now.Add(lukeEnergySaveWindow))
if outcome != "energy_save_expired" || len(next) != 0 {
t.Fatalf("expiry outcome=%q next=%+v", outcome, next)
}
if got := state.Lifecycle.MovieSymbols[lukeSymbolKillAllCharacter]; got != 1 {
t.Fatalf("[KILL ALL CHARACTER]=%d want=1", got)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergySaveEffect); got != lukeEnergySaveEffectFailed {
t.Fatalf("[ENERGY SAVE EFFECT]=%d want=%d", got, lukeEnergySaveEffectFailed)
}
if got := lukeGateState(t, state, lukeEnergySaveMapID); got != raid.AntonDungeonHide {
t.Fatalf("energy save room=%q want hide", got)
}
}
func TestLukeEnergySaveClockIsNotRestartedWhileItIsAlreadyRunning(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
setRuntimeRaidLukeDungeonStateLocked(state, lukeLightThroneMapID, raid.AntonDungeonOpen)
setRuntimeRaidLukeDungeonStateLocked(state, lukeDarkThroneMapID, raid.AntonDungeonOpen)
opened, armed := runtimeRaidLukeEnterMapLocked(state, lukeLightThroneMapID, lukeLightThroneBossRoomMapID, now)
if !opened || len(armed) != 1 {
t.Fatalf("boss room opened=%t window=%+v want one timer", opened, armed)
}
deadline := armed[0].Deadline
// The rest of the squad reaching the boss room, the other throne's squad
// reaching its own boss room, and squads walking in and out of 能源储藏室 must
// all leave the clock alone.
later := now.Add(90 * time.Second)
for _, step := range []struct {
dungeon int
room int64
}{
{lukeLightThroneMapID, lukeLightThroneBossRoomMapID},
{lukeDarkThroneMapID, lukeDarkThroneBossRoomMapID},
{lukeEnergySaveMapID, lukeLightThroneBossRoomMapID},
} {
if again, rearmed := runtimeRaidLukeEnterMapLocked(state, step.dungeon, step.room, later); again || len(rearmed) != 0 {
t.Fatalf("entering %d/%d reopened=%t rearmed=%+v", step.dungeon, step.room, again, rearmed)
}
}
if runtimeRaidLukeEnterDungeonLocked(state, lukeEnergySaveMapID) {
t.Fatal("loading into 能源储藏室 reopened it")
}
if again := armRuntimeRaidLukeEnergySaveWindowLocked(state, later); len(again) != 0 {
t.Fatalf("re-arm returned %+v want none", again)
}
current, ok := state.Lifecycle.Mechanisms[runtimeRaidMechanismKey{
Kind: runtimeRaidMechanismActive, MapID: lukeEnergySaveMapID,
}]
if !ok || !current.Deadline.Equal(deadline) {
t.Fatalf("wipe clock deadline=%v ok=%t want the original %v", current.Deadline, ok, deadline)
}
}
// luke.etc opens 能源储藏室 on [ON ENTER MAP] 55845 / 55846, the throne's second
// (boss) room -- not on entering the throne. A squad standing in the start room
// must not have started anybody's 150-second clock.
func TestLukeEnergySaveOpensOnTheThroneBossRoomNotTheThroneGate(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
setRuntimeRaidLukeDungeonStateLocked(state, lukeLightThroneMapID, raid.AntonDungeonOpen)
if runtimeRaidLukeEnterDungeonLocked(state, lukeLightThroneMapID) {
t.Fatal("loading into the throne changed the graph")
}
const lightThroneStartRoomMapID int64 = 55849
if opened, armed := runtimeRaidLukeEnterMapLocked(state, lukeLightThroneMapID, lightThroneStartRoomMapID, now); opened || len(armed) != 0 {
t.Fatalf("throne start room opened=%t armed=%+v", opened, armed)
}
// The boss room of a throne only counts inside a throne gate.
if opened, _ := runtimeRaidLukeEnterMapLocked(state, lukeLightTempleMapID, lukeLightThroneBossRoomMapID, now); opened {
t.Fatal("a non-throne gate opened the energy save room")
}
if got := lukeGateState(t, state, lukeEnergySaveMapID); got != raid.AntonDungeonHide {
t.Fatalf("energy save room=%q before the boss room, want hide", got)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergySaveEffect); got != 0 {
t.Fatalf("[ENERGY SAVE EFFECT]=%d before the boss room, want 0", got)
}
bossAt := now.Add(40 * time.Second)
opened, armed := runtimeRaidLukeEnterMapLocked(state, lukeLightThroneMapID, lukeLightThroneBossRoomMapID, bossAt)
if !opened || len(armed) != 1 || !armed[0].Deadline.Equal(bossAt.Add(lukeEnergySaveWindow)) {
t.Fatalf("boss room opened=%t armed=%+v want the clock from %v", opened, armed, bossAt)
}
if got := lukeGateState(t, state, lukeEnergySaveMapID); got != raid.AntonDungeonOpen {
t.Fatalf("energy save room=%q after the boss room, want open", got)
}
}
// The storage room is a repeatable objective: after a clear it closes, reopens
// 120 s later with a fresh 150 s clock, and must be enterable and clearable
// again -- every time. This walks the committed-clear path twice with the same
// gate conditions validateAntonRaidDungeonEntry checks.
func TestLukeEnergySaveCanBeClearedAgainAfterItRespawns(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
setRuntimeRaidLukeDungeonStateLocked(state, lukeLightThroneMapID, raid.AntonDungeonOpen)
if opened, _ := runtimeRaidLukeEnterMapLocked(state, lukeLightThroneMapID, lukeLightThroneBossRoomMapID, now); !opened {
t.Fatal("boss room did not open the energy save room")
}
enterable := func(label string) {
t.Helper()
objective := state.Lifecycle.Objectives[lukeEnergySaveMapID]
if objective.State != raid.AntonDungeonOpen || objective.RemainingHP <= 0 {
t.Fatalf("%s: energy save state=%q remaining=%d, not enterable", label, objective.State, objective.RemainingHP)
}
if !runtimeRaidLukeRouteEntryAllowedLocked(state, 42, lukeEnergySaveMapID) {
t.Fatalf("%s: route lock refuses the energy save room", label)
}
}
clock := now
for cycle := 1; cycle <= 3; cycle++ {
enterable(fmt.Sprintf("cycle %d before clear", cycle))
clock = clock.Add(60 * time.Second)
if err := advanceRuntimeRaidObjectiveLocked(state, runtimeRaidDungeonPhaseTwo, lukeEnergySaveMapID); err != nil {
t.Fatalf("cycle %d advance: %v", cycle, err)
}
armed := applyRuntimeRaidLukePhaseTwoClearLocked(state, lukeEnergySaveMapID, clock)
if len(armed) != 1 || armed[0].Kind != runtimeRaidMechanismRecovery {
t.Fatalf("cycle %d clear armed=%+v want the 120 s recovery", cycle, armed)
}
if _, running := state.Lifecycle.Mechanisms[runtimeRaidMechanismKey{Kind: runtimeRaidMechanismActive, MapID: lukeEnergySaveMapID}]; running {
t.Fatalf("cycle %d: the wipe clock survived the clear", cycle)
}
clock = armed[0].Deadline
next, outcome := fireRuntimeRaidLukeMechanismLocked(state, armed[0], clock)
if outcome != "energy_save_reopened" || len(next) != 1 || next[0].Kind != runtimeRaidMechanismActive {
t.Fatalf("cycle %d reopen outcome=%q next=%+v", cycle, outcome, next)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergySaveEffect); got != lukeEnergySaveEffectActive {
t.Fatalf("cycle %d [ENERGY SAVE EFFECT]=%d after reopen", cycle, got)
}
}
enterable("after the third respawn")
}
func TestLukeThroneGiveUpDisarmsTheEnergySaveClock(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
setRuntimeRaidLukeDungeonStateLocked(state, lukeLightThroneMapID, raid.AntonDungeonOpen)
if opened, _ := runtimeRaidLukeEnterMapLocked(state, lukeLightThroneMapID, lukeLightThroneBossRoomMapID, now); !opened {
t.Fatal("entering the throne boss room did not open the energy save room")
}
runtimeRaidLukeGiveUpDungeonLocked(state, lukeLightThroneMapID)
// [ON CHANGE DUNGEON STATE] 5066 `hide` resets timers 1 and 2: an abandoned
// throne must not be wiped by a clock that outlived it.
for _, kind := range []byte{runtimeRaidMechanismActive, runtimeRaidMechanismRecovery} {
key := runtimeRaidMechanismKey{Kind: kind, MapID: lukeEnergySaveMapID}
if _, armed := state.Lifecycle.Mechanisms[key]; armed {
t.Fatalf("timer kind=%d survived the throne give-up", kind)
}
}
if got := lukeGateState(t, state, lukeEnergySaveMapID); got != raid.AntonDungeonHide {
t.Fatalf("energy save room=%q want hide", got)
}
}
func TestLukeEnergySaveClearedRoomReopensAfterTwoMinutes(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
setRuntimeRaidLukeDungeonStateLocked(state, lukeEnergySaveMapID, raid.AntonDungeonOpen)
armRuntimeRaidLukeEnergySaveWindowLocked(state, now)
armed := applyRuntimeRaidLukePhaseTwoClearLocked(state, lukeEnergySaveMapID, now)
if len(armed) != 1 || armed[0].Kind != runtimeRaidMechanismRecovery ||
armed[0].Deadline.Sub(now) != lukeEnergySaveRecovery {
t.Fatalf("energy save recovery=%+v want %s", armed, lukeEnergySaveRecovery)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergySaveEffect); got != 0 {
t.Fatalf("[ENERGY SAVE EFFECT]=%d want=0 after the clear", got)
}
// The wipe clock must be gone, or a cleared room would still kill the roster.
if _, armedStill := state.Lifecycle.Mechanisms[runtimeRaidMechanismKey{
Kind: runtimeRaidMechanismActive, MapID: lukeEnergySaveMapID,
}]; armedStill {
t.Fatal("the 150s wipe clock survived the clear")
}
next, outcome := fireRuntimeRaidLukeMechanismLocked(state, armed[0], now.Add(lukeEnergySaveRecovery))
if outcome != "energy_save_reopened" {
t.Fatalf("reopen outcome=%q", outcome)
}
if len(next) != 1 || next[0].Kind != runtimeRaidMechanismActive ||
next[0].Deadline.Sub(now.Add(lukeEnergySaveRecovery)) != lukeEnergySaveWindow {
t.Fatalf("reopened room armed=%+v, want a fresh %s window", next, lukeEnergySaveWindow)
}
}
func TestLukeGiveUpThroneClosesTheEnergySaveRoom(t *testing.T) {
state := lukeTeamPhaseTwoState()
now := time.Unix(1700000000, 0).UTC()
setRuntimeRaidLukeDungeonStateLocked(state, lukeEnergySaveMapID, raid.AntonDungeonOpen)
armRuntimeRaidLukeEnergySaveWindowLocked(state, now)
runtimeRaidLukeGiveUpDungeonLocked(state, lukeLightThroneMapID)
if got := lukeGateState(t, state, lukeEnergySaveMapID); got != raid.AntonDungeonHide {
t.Fatalf("energy save room=%q want hide", got)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergySaveEffect); got != 0 {
t.Fatalf("[ENERGY SAVE EFFECT]=%d want=0", got)
}
}
// TestLukeTeamSnapshotCarriesEveryGateRow is the regression for the empty
// 战况表 and the empty dungeon selection page. Live 2026-09-23T01:59:34Z, raid
// 1835009: op585 went out as 00 00 00000000 -- zero dungeon rows -- because the
// snapshot builder skipped any gate whose map id had no "<id> DGN HP" symbol,
// and no Luke team gate had one. op583 carried only the four phase-one symbols
// this runtime sets itself (107, 117, 118, 119).
func TestLukeTeamSnapshotCarriesEveryGateRow(t *testing.T) {
for _, phase := range []struct {
name string
state *runtimeRaidState
gates map[int]uint32
}{
{
name: "phase one",
state: lukeTeamPhaseOneState(),
gates: map[int]uint32{5054: 67, 5055: 68, 5056: 69},
},
{
name: "phase two",
state: lukeTeamPhaseTwoState(),
gates: map[int]uint32{
5311: 82, 5313: 84, 5057: 70, 5058: 71, 5059: 73, 5060: 74,
5063: 85, 5064: 86, 5065: 87, 5061: 75, 5062: 76, 5066: 88,
},
},
} {
t.Run(phase.name, func(t *testing.T) {
// 本树:viewer 版是独立函数(原 1 参版保持共享视图,签名不变),
// 且多返回一个 dungeonRows。
symbolBody, dungeonBody, _, err := runtimeRaidPhaseSnapshotBodiesForViewer(*phase.state, 0)
if err != nil {
t.Fatalf("snapshot bodies: %v", err)
}
// op585 is [reserved][count] then five bytes per row.
if len(dungeonBody) < 2 || int(dungeonBody[1]) != len(phase.gates) {
t.Fatalf("dungeon rows=%d want=%d body=% X", dungeonBody[1], len(phase.gates), dungeonBody)
}
seen := map[uint32]bool{}
for index := 0; index < len(phase.gates); index++ {
offset := 2 + index*5
seen[binary.LittleEndian.Uint32(dungeonBody[offset:offset+4])] = true
}
for mapID := range phase.gates {
if !seen[uint32(mapID)] {
t.Fatalf("gate %d missing from op585: % X", mapID, dungeonBody)
}
}
// Every gate also owns a DGN HP symbol from etc/raid/luke/luke.etc.symbol.
for mapID, wantKey := range phase.gates {
key, ok := runtimeRaidObjectiveSymbolKey(mapID)
if !ok || key != wantKey {
t.Fatalf("gate %d symbol=(%d,%t) want=%d", mapID, key, ok, wantKey)
}
}
if len(symbolBody) == 0 || int(symbolBody[0]) < len(phase.gates) {
t.Fatalf("symbol rows=%v want at least %d", symbolBody, len(phase.gates))
}
})
}
}
// A gate the symbol table does not know must still reach the selection page.
func TestRaidSnapshotKeepsGateRowWithoutASymbolKey(t *testing.T) {
state := lukeTeamPhaseOneState()
state.Lifecycle.Objectives[9999] = runtimeRaidDungeonObjective{
Phase: runtimeRaidDungeonPhaseOne, MapID: 9999, State: raid.AntonDungeonOpen,
MaxHP: 1, RemainingHP: 1, PartyMax: 1,
}
if _, ok := runtimeRaidObjectiveSymbolKey(9999); ok {
t.Fatal("9999 unexpectedly has a symbol key")
}
_, dungeonBody, _, err := runtimeRaidPhaseSnapshotBodiesForViewer(*state, 0)
if err != nil {
t.Fatalf("snapshot bodies: %v", err)
}
if int(dungeonBody[1]) != 4 {
t.Fatalf("dungeon rows=%d want=4 (three sanctums plus the symbol-less gate)", dungeonBody[1])
}
}
// TestLukeTeamGatesAreRaidEntriesNotOrdinaryDungeons is the regression for
// "选择进入副本 无法进入". runtimeRaidDungeonPhaseForMap is what marks a
// SelectDungeon request as a raid entry, and it knew no Luke team map, so
// entering 5054 from the Jelva waiting area was judged as an ordinary dungeon:
//
// game-dungeon-select-entry-policy anton_raid=false raid_started=false
// game-dungeon-select-blocked reason=ordinary_dungeon_town_return_origin_unavailable
// origin 1/32/4 does not match runtime 1/38/1
//
// 32/4 is the Luke raid waiting room ([WAITING ROOM] 32 4); 38/1 is the ordinary
// town the gate's worldmap entry belongs to.
func TestLukeTeamGatesAreRaidEntriesNotOrdinaryDungeons(t *testing.T) {
phaseOne := []int{5054, 5055, 5056}
phaseTwo := []int{5311, 5313, 5057, 5058, 5059, 5060, 5063, 5064, 5065, 5061, 5062, 5066}
for _, mapID := range phaseOne {
phase, ok := runtimeRaidDungeonPhaseForMap(mapID)
if !ok || phase != runtimeRaidDungeonPhaseOne {
t.Fatalf("gate %d phase=(%d,%t), want phase one", mapID, phase, ok)
}
}
for _, mapID := range phaseTwo {
phase, ok := runtimeRaidDungeonPhaseForMap(mapID)
if !ok || phase != runtimeRaidDungeonPhaseTwo {
t.Fatalf("gate %d phase=(%d,%t), want phase two", mapID, phase, ok)
}
}
// This predicate is also what isAntonRaidEntryDungeonID answers, which is
// what skips the ordinary level/fatigue policy and what routes the
// town-return origin to the raid waiting room instead of an ordinary town.
for _, mapID := range append(append([]int{}, phaseOne...), phaseTwo...) {
if !isAntonRaidEntryDungeonID(int64(mapID)) {
t.Fatalf("gate %d is not treated as a raid entry", mapID)
}
}
// The Anton ranges and the Luke single route keep their own answers.
for mapID, want := range map[int]byte{
210: runtimeRaidDungeonPhaseOne, 216: runtimeRaidDungeonPhaseOne,
218: runtimeRaidDungeonPhaseTwo, 224: runtimeRaidDungeonPhaseTwo,
5314: runtimeRaidDungeonPhaseOne, 5323: runtimeRaidDungeonPhaseOne,
} {
phase, ok := runtimeRaidDungeonPhaseForMap(mapID)
if !ok || phase != want {
t.Fatalf("gate %d phase=(%d,%t) want=%d", mapID, phase, ok, want)
}
}
// An ordinary dungeon must stay ordinary.
if _, ok := runtimeRaidDungeonPhaseForMap(5100); ok {
t.Fatal("5100 was promoted to a raid gate")
}
}
// TestLukeRouteLockedGatesDrawClosedForThatMemberOnly pins the per-viewer
// snapshot: a member bound to one route sees the other route's open gates as
// closed (wire 1), which is the locked cell the board already draws for a gate it
// cannot enter. An unbound member keeps seeing them open, and the objective's own
// state is never changed.
func TestLukeRouteLockedGatesDrawClosedForThatMemberOnly(t *testing.T) {
state := lukeTeamPhaseTwoState()
initRuntimeRaidLukePhaseTwoLocked(state, time.Unix(1700000000, 0).UTC())
const dark, unbound = uint16(1001), uint16(1002)
bindRuntimeRaidLukeRouteLocked(state, dark, lukeDarkEntranceMapID)
gateState := func(body []byte, mapID uint32) (uint32, bool) {
rows := int(body[1])
for index := 0; index < rows; index++ {
offset := 2 + index*5
if binary.LittleEndian.Uint32(body[offset:offset+4]) == mapID {
return uint32(body[offset+4]), true
}
}
return 0, false
}
// The dark-bound member sees the light entrance closed...
_, darkBody, _, err := runtimeRaidPhaseSnapshotBodiesForViewer(*state, dark)
if err != nil {
t.Fatalf("dark viewer snapshot: %v", err)
}
got, ok := gateState(darkBody, lukeLightEntranceMapID)
if !ok || got != runtimeRaidObjectiveWireState(raid.AntonDungeonClosed) {
t.Fatalf("light entrance for the dark member = (%d,%t), want closed", got, ok)
}
// ...and its own route still open.
if got, ok := gateState(darkBody, lukeDarkEntranceMapID); !ok || got != runtimeRaidObjectiveWireState(raid.AntonDungeonOpen) {
t.Fatalf("dark entrance for the dark member = (%d,%t), want open", got, ok)
}
// An unbound member sees both routes open.
_, freeBody, _, err := runtimeRaidPhaseSnapshotBodiesForViewer(*state, unbound)
if err != nil {
t.Fatalf("unbound viewer snapshot: %v", err)
}
for _, mapID := range []uint32{lukeLightEntranceMapID, lukeDarkEntranceMapID} {
if got, ok := gateState(freeBody, mapID); !ok || got != runtimeRaidObjectiveWireState(raid.AntonDungeonOpen) {
t.Fatalf("gate %d for an unbound member = (%d,%t), want open", mapID, got, ok)
}
}
// The authoritative objective is untouched; only the view changed.
if got := lukeGateState(t, state, lukeLightEntranceMapID); got != raid.AntonDungeonOpen {
t.Fatalf("light entrance objective = %q, want open", got)
}
}
// TestGatePartyMaxAdmitsExactlyTheDeclaredSquadCount covers "同一圣所同时只容一支
// 队伍". [PARTY MAX] was parsed and stored but never enforced, so a second squad
// walked into a gate another was already fighting: live 2026-09-23T08:27Z both
// members opened their own run of 5313 with separate lifecycle tokens.
func TestGatePartyMaxAdmitsExactlyTheDeclaredSquadCount(t *testing.T) {
const gate = lukeDarkEntranceMapID
seatOf := func(group uint8) runtimeRaidDungeonSeat {
return runtimeRaidDungeonSeat{Group: group}
}
newState := func(partyMax int, occupants ...uint8) *runtimeRaidState {
state := &runtimeRaidState{
RaidMode: runtimeRaidOwnerLuke,
Lifecycle: runtimeRaidLifecycleState{
Phase: runtimeRaidDungeonPhaseTwo,
Objectives: map[int]runtimeRaidDungeonObjective{
gate: {
Phase: runtimeRaidDungeonPhaseTwo, MapID: gate,
State: raid.AntonDungeonOpen, MaxHP: 1, RemainingHP: 1,
PartyMax: partyMax,
},
},
Dungeons: map[runtimeRaidDungeonRunKey]runtimeRaidDungeonProgress{},
},
}
for _, group := range occupants {
state.Lifecycle.Dungeons[runtimeRaidDungeonRunKey{MapID: gate, Seat: seatOf(group)}] =
runtimeRaidDungeonProgress{
Phase: runtimeRaidDungeonPhaseTwo, MapID: gate, Seat: seatOf(group),
Status: runtimeRaidDungeonProgressLoaded,
}
}
return state
}
// Luke: one squad already inside, a different squad is refused.
if _, _, full := runtimeRaidGatePartyMaxReachedLocked(newState(1, 1), seatOf(2), gate); !full {
t.Fatal("a second squad entered a PARTY MAX 1 gate")
}
// The squad already inside is not blocked by its own run.
if _, _, full := runtimeRaidGatePartyMaxReachedLocked(newState(1, 1), seatOf(1), gate); full {
t.Fatal("the occupying squad was refused its own gate")
}
// An empty gate admits the first squad.
if _, _, full := runtimeRaidGatePartyMaxReachedLocked(newState(1), seatOf(1), gate); full {
t.Fatal("the first squad was refused an empty gate")
}
// A completed run does not hold the gate.
completed := newState(1, 1)
for key, progress := range completed.Lifecycle.Dungeons {
progress.Status = runtimeRaidDungeonProgressCompleted
completed.Lifecycle.Dungeons[key] = progress
}
if _, _, full := runtimeRaidGatePartyMaxReachedLocked(completed, seatOf(2), gate); full {
t.Fatal("a completed run still held the gate")
}
// Anton declares 5 on its gates and must still admit several squads; the
// blanket cross-squad refusal this replaces was removed for exactly that.
if _, _, full := runtimeRaidGatePartyMaxReachedLocked(newState(5, 1, 2, 3), seatOf(4), gate); full {
t.Fatal("a fourth squad was refused a PARTY MAX 5 gate")
}
if _, _, full := runtimeRaidGatePartyMaxReachedLocked(newState(5, 1, 2, 3, 4, 5), seatOf(6), gate); !full {
t.Fatal("a sixth squad entered a PARTY MAX 5 gate")
}
}
func TestLukeSymbolRowsRideTheSnapshotSortedAndOwnerScoped(t *testing.T) {
state := lukeTeamPhaseTwoState()
initRuntimeRaidLukePhaseTwoLocked(state, time.Unix(1700000000, 0).UTC())
rows := runtimeRaidLukeSymbolRows(*state)
if len(rows) == 0 {
t.Fatal("no Luke symbol rows")
}
for index := 1; index < len(rows); index++ {
if rows[index-1].Key >= rows[index].Key {
t.Fatalf("rows not sorted by key: %+v", rows)
}
}
seen := map[uint32]bool{}
for _, row := range rows {
seen[row.Key] = true
}
for _, key := range []uint32{lukeSymbolPhaseTwoInit, lukeSymbolAltarEntranceClear, lukeSymbolEnergyControlEffect} {
if !seen[key] {
t.Fatalf("symbol %d missing from the snapshot rows", key)
}
}
// An Anton raid must never receive Luke's symbol namespace.
state.RaidMode = runtimeRaidOwnerAnton
if rows := runtimeRaidLukeSymbolRows(*state); len(rows) != 0 {
t.Fatalf("anton snapshot carried Luke rows: %+v", rows)
}
}
// The raid-skip-to-luke-throne test aid walks the real clear hooks, so it must
// leave exactly the state a played phase two leaves in front of the thrones.
func TestLukeThroneFastForwardLeavesTheFinalStageOpen(t *testing.T) {
state := lukeTeamPhaseTwoState()
state.FrozenMembers = []runtimeRaidMemberState{{CharID: 1, GroupIndex: 1}, {CharID: 42, GroupIndex: 2}}
now := time.Unix(1700000000, 0).UTC()
initRuntimeRaidLukePhaseTwoLocked(state, now)
fastForwardRuntimeRaidLukeToThroneLocked(state, now)
for mapID, want := range map[int]raid.AntonDungeonState{
lukeLightThroneMapID: raid.AntonDungeonOpen,
lukeDarkThroneMapID: raid.AntonDungeonHide,
lukeEnergySaveMapID: raid.AntonDungeonHide,
lukeEnergyControlMapID: raid.AntonDungeonHide,
} {
if got := lukeGateState(t, state, mapID); got != want {
t.Fatalf("gate %d=%q want %q", mapID, got, want)
}
}
for _, mapID := range lukeThroneFastForwardGates {
if got := state.Lifecycle.Objectives[mapID].RemainingHP; got != 0 {
t.Fatalf("gate %d remaining=%d want cleared", mapID, got)
}
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolEnergyControlEffect); got != 0 {
t.Fatalf("[ENERGY CONTROL EFFECT]=%d after the altars, want 0", got)
}
if got := runtimeRaidLukeSymbolLocked(state, lukeSymbolTempleClear); got != lukeTempleRequired {
t.Fatalf("[TEMPLE CLEAR]=%d want %d", got, lukeTempleRequired)
}
}