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

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

1249 lines
43 KiB
Go

package dnfbridge
import (
"context"
"errors"
"fmt"
"strings"
"longheng.io/server/internal/modules/dnf/dungeoncmd"
dnfquest "longheng.io/server/internal/modules/dnf/quest"
"longheng.io/server/internal/modules/dnf/worldmap"
)
var (
errDungeonMoveRuntimeOwnerMismatch = errors.New("dnf dungeon move runtime owner mismatch")
errDungeonMoveRequestTailUnknown = errors.New("dnf dungeon move request has an unproven tail")
errDungeonElevatorIncomplete = errors.New("dnf dungeon elevator encounter is not complete")
)
type currentDungeonMovePlan struct {
Source worldmap.DungeonRoomScene
Target worldmap.DungeonRoomScene
SourceVisit *runtimeDungeonRoomVisit
TargetVisit *runtimeDungeonRoomVisit
TargetRoom *runtimeDungeonRoom
NextObjectKey uint32
Seed uint32
Revisit bool
Layered bool
LayerReturn bool
LayerExit bool
LayerBaseAck bool
LayerIndex int
LayerBaseKey runtimeDungeonRoomKey
StoryStage bool
StoryStageIndex int
BossEntranceMapOverride bool
BossEntranceResolvedMap worldmap.ResolvedMap
Operation byte
PayloadMode byte
PassiveDrops currentDungeonPassiveObjectDropPlan
}
type runtimeDungeonRoomKey struct {
X int64
Y int64
MapID int64
}
type runtimeDungeonRoomVisit struct {
Scene worldmap.DungeonRoomScene
Room *runtimeDungeonRoom
Seed uint32
DropRNG uint32
FatigueConsumed bool
FatigueConsumedAmount int64
FatigueBatteryConsumed bool
PassiveObjectExtraEntries []currentDungeonStartMapExtraEntry
PassiveObjectDropsPlanned bool
QuestPassiveObjectDeaths map[int64]uint32
}
type runtimeDungeonLayerChain struct {
BaseKey runtimeDungeonRoomKey
Consumed bool
FinalAckPending bool
}
func runtimeDungeonRoomKeyFromScene(scene worldmap.DungeonRoomScene) runtimeDungeonRoomKey {
return runtimeDungeonRoomKey{
X: scene.Coordinate.X,
Y: scene.Coordinate.Y,
MapID: scene.Map.Map.ID,
}
}
func (runtime *runtimeDungeonState) currentDungeonRoomVisit(
scene worldmap.DungeonRoomScene,
) (*runtimeDungeonRoomVisit, error) {
if runtime == nil || runtime.Room == nil || runtime.Rooms == nil {
return nil, errDungeonWorldMapUnavailable
}
key := runtimeDungeonRoomKeyFromScene(scene)
visit := runtime.Rooms[key]
if visit == nil || visit.Room == nil {
return nil, fmt.Errorf("%w: room=%s map=%d visit_missing", errDungeonMoveRuntimeOwnerMismatch, scene.Coordinate, scene.Map.Map.ID)
}
snapshot := visit.Room.Snapshot()
if visit.Room != runtime.Room || visit.Seed != runtime.Seed ||
snapshot.Coordinate != scene.Coordinate || snapshot.MapID != scene.Map.Map.ID {
return nil, fmt.Errorf(
"%w: scene=%s/%d runtime_room=%p cached_room=%p cached_owner=%s/%d runtime_seed=%d cached_seed=%d",
errDungeonMoveRuntimeOwnerMismatch,
scene.Coordinate,
scene.Map.Map.ID,
runtime.Room,
visit.Room,
snapshot.Coordinate,
snapshot.MapID,
runtime.Seed,
visit.Seed,
)
}
return visit, nil
}
func (runtime *runtimeDungeonState) cacheCurrentDungeonRoom() error {
if runtime == nil || runtime.Session == nil || runtime.Room == nil {
return errDungeonWorldMapUnavailable
}
scene, ok := runtime.Session.Scene()
if !ok {
return worldmap.ErrDungeonRunRequired
}
snapshot := runtime.Room.Snapshot()
if snapshot.Coordinate != scene.Coordinate || snapshot.MapID != scene.Map.Map.ID {
return fmt.Errorf(
"%w: session_room=%s session_map=%d runtime_room=%s runtime_map=%d",
errDungeonMoveRuntimeOwnerMismatch,
scene.Coordinate,
scene.Map.Map.ID,
snapshot.Coordinate,
snapshot.MapID,
)
}
if runtime.Rooms == nil {
runtime.Rooms = make(map[runtimeDungeonRoomKey]*runtimeDungeonRoomVisit)
}
key := runtimeDungeonRoomKeyFromScene(scene)
if previous := runtime.Rooms[key]; previous != nil && previous.Room != runtime.Room {
return fmt.Errorf(
"%w: room=%s map=%d cached_room=%p current_room=%p",
errDungeonMoveRuntimeOwnerMismatch,
scene.Coordinate,
scene.Map.Map.ID,
previous.Room,
runtime.Room,
)
}
// Dungeon drop rolls are room-scoped and sequential. Preserve their LCG
// state when caching a room after a death; resetting it to the start-map
// seed would make every monster in the room receive the same roll sequence.
dropRNG := currentPartyDungeonPrivateDropRNG(runtime, runtime.Seed)
var passiveObjectExtraEntries []currentDungeonStartMapExtraEntry
passiveObjectDropsPlanned := false
fatigueConsumed := false
fatigueConsumedAmount := int64(0)
fatigueBatteryConsumed := false
var questPassiveObjectDeaths map[int64]uint32
if previous := runtime.Rooms[key]; previous != nil && previous.Room == runtime.Room && previous.Seed == runtime.Seed {
dropRNG = previous.DropRNG
fatigueConsumed = previous.FatigueConsumed
fatigueConsumedAmount = previous.FatigueConsumedAmount
fatigueBatteryConsumed = previous.FatigueBatteryConsumed
passiveObjectExtraEntries = append([]currentDungeonStartMapExtraEntry(nil), previous.PassiveObjectExtraEntries...)
passiveObjectDropsPlanned = previous.PassiveObjectDropsPlanned
questPassiveObjectDeaths = previous.QuestPassiveObjectDeaths
}
runtime.Rooms[key] = &runtimeDungeonRoomVisit{
Scene: scene, Room: runtime.Room, Seed: runtime.Seed, DropRNG: dropRNG,
FatigueConsumed: fatigueConsumed,
FatigueConsumedAmount: fatigueConsumedAmount,
FatigueBatteryConsumed: fatigueBatteryConsumed,
PassiveObjectExtraEntries: passiveObjectExtraEntries,
PassiveObjectDropsPlanned: passiveObjectDropsPlanned,
QuestPassiveObjectDeaths: questPassiveObjectDeaths,
}
return nil
}
// currentPartyDungeonPrivateDropRNG keeps the shared room seed authoritative
// for topology while deriving an independent, repeatable drop stream for each
// immutable party member. Solo runs preserve the historical room seed.
func currentPartyDungeonPrivateDropRNG(runtime *runtimeDungeonState, roomSeed uint32) uint32 {
if runtime == nil || runtime.partyDungeonOwner == nil || !runtime.partyMemberIndexed {
return roomSeed
}
var member runtimePartyDungeonMember
found := false
for _, candidate := range runtime.partyDungeonOwner.roster() {
if candidate.slot == runtime.partyMemberIndex {
member = candidate
found = true
break
}
}
if !found {
return roomSeed
}
value := roomSeed ^ uint32(runtime.partyDungeonOwner.partyID)<<16
value ^= uint32(member.userID)*0x9e3779b9 ^ uint32(member.slot+1)*0x85ebca6b
value ^= value >> 16
value *= 0x7feb352d
value ^= value >> 15
value *= 0x846ca68b
value ^= value >> 16
if value == 0 {
value = 0xa341316c ^ uint32(member.slot+1)
}
return value
}
func (s *Service) planCurrentDungeonMove(
runtime *runtimeDungeonState,
request dungeoncmd.MoveMapRequest,
) (currentDungeonMovePlan, error) {
if runtime == nil || runtime.Session == nil || runtime.Room == nil {
return currentDungeonMovePlan{}, errDungeonWorldMapUnavailable
}
if len(request.OpaqueTail) != 0 {
return currentDungeonMovePlan{}, fmt.Errorf("%w: bytes=%d", errDungeonMoveRequestTailUnknown, len(request.OpaqueTail))
}
source, ok := runtime.Session.Scene()
if !ok {
return currentDungeonMovePlan{}, worldmap.ErrDungeonRunRequired
}
sourceRoom := runtime.Room.Snapshot()
if source.Coordinate != sourceRoom.Coordinate || source.Map.Map.ID != sourceRoom.MapID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: session_room=%s session_map=%d runtime_room=%s runtime_map=%d",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
sourceRoom.Coordinate,
sourceRoom.MapID,
)
}
if currentSuspiciousVillageElevatorMoveBlocked(runtime, source, sourceRoom) {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: dungeon=%d maze=%d room=%s map=%d counter=%d state=%d",
errDungeonElevatorIncomplete,
runtime.Dungeon.ID,
runtime.MazeIndex,
source.Coordinate,
source.Map.Map.ID,
runtime.suspiciousVillageElevator.Counter,
runtime.suspiciousVillageElevator.State,
)
}
sourceVisit, err := runtime.currentDungeonRoomVisit(source)
if err != nil {
return currentDungeonMovePlan{}, err
}
if currentDungeonHellPartyMoveBlocked(runtime, source) {
return currentDungeonMovePlan{}, errCurrentHellPartyMoveBlocked
}
if stage, stageIndex, pending := currentDungeonNextStoryStage(runtime); pending {
if int64(request.NextX) == stage.Coordinate.X && int64(request.NextY) == stage.Coordinate.Y {
return s.planCurrentDungeonStoryStageMove(runtime, source, sourceVisit, stage, stageIndex)
}
if runtime.StoryStageIndex >= 0 {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: source=%s/%d story_stage=%d expected_target=%s requested_target=%d:%d",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
stageIndex,
stage.Coordinate,
request.NextX,
request.NextY,
)
}
}
if target, resolved, found, overrideErr := s.currentDungeonBossEntranceMapOverride(runtime, request); overrideErr != nil {
return currentDungeonMovePlan{}, overrideErr
} else if found {
return s.planCurrentDungeonBossEntranceMapOverrideMove(
runtime,
source,
sourceVisit,
target,
resolved,
)
}
if request.MoveKind == 1 &&
int64(request.NextX) == source.Coordinate.X &&
int64(request.NextY) == source.Coordinate.Y {
return s.planCurrentDungeonLayerMove(runtime, request, source, sourceVisit)
}
layerExit, layerBaseKey, err := s.validateCurrentDungeonTerminalLayerExit(runtime, request, source)
if err != nil {
return currentDungeonMovePlan{}, err
}
preview, err := runtime.Session.PreviewMoveByteTransition(request.NextX, request.NextY)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("preview server-owned dungeon move: %w", err)
}
if preview.Revisit {
targetVisit := runtime.Rooms[runtimeDungeonRoomKeyFromScene(preview.Scene)]
if targetVisit == nil || targetVisit.Room == nil {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s/%d revisit_cache_missing",
errDungeonMoveRuntimeOwnerMismatch,
preview.Scene.Coordinate,
preview.Scene.Map.Map.ID,
)
}
validated, validateErr := runtime.Session.ValidateMoveByteTransition(
request.NextX,
request.NextY,
&targetVisit.Scene,
)
if validateErr != nil {
return currentDungeonMovePlan{}, fmt.Errorf("validate cached dungeon revisit: %w", validateErr)
}
targetSnapshot := targetVisit.Room.Snapshot()
if targetSnapshot.Coordinate != validated.Scene.Coordinate || targetSnapshot.MapID != validated.Scene.Map.Map.ID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: cached_scene=%s/%d cached_runtime=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
validated.Scene.Coordinate,
validated.Scene.Map.Map.ID,
targetSnapshot.Coordinate,
targetSnapshot.MapID,
)
}
return currentDungeonMovePlan{
Source: source,
Target: validated.Scene,
SourceVisit: sourceVisit,
TargetVisit: targetVisit,
TargetRoom: targetVisit.Room,
NextObjectKey: runtime.NextObjectKey,
Seed: targetVisit.Seed,
Revisit: true,
LayerExit: layerExit,
LayerIndex: -1,
LayerBaseKey: layerBaseKey,
Operation: currentDungeonStartMapOperationCurrent,
PayloadMode: currentDungeonStartMapPayloadCached,
}, nil
}
targetValidation, err := runtime.Session.ValidateMoveByteTransition(request.NextX, request.NextY, nil)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("validate first dungeon room visit: %w", err)
}
target := targetValidation.Scene
if existing := runtime.Rooms[runtimeDungeonRoomKeyFromScene(target)]; existing != nil {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s/%d first_visit_cache_already_exists",
errDungeonMoveRuntimeOwnerMismatch,
target.Coordinate,
target.Map.Map.ID,
)
}
monsterCatalog, err := s.dungeonMonsterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
targetRoom, nextObjectKey, err := newRuntimeDungeonRoom(target, monsterCatalog, runtime.NextObjectKey)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan target dungeon monster owner: %w", err)
}
var aiCatalog *pvfDungeonAICharacterCatalog
if len(target.AICharacters) != 0 {
aiCatalog, err = s.dungeonAICharacterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
}
extendedPlan, err := planRuntimeDungeonExtendedActors(
target,
monsterCatalog,
aiCatalog,
runtime.Dungeon.Metadata.BasisLevel,
nextObjectKey,
)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan target dungeon extended actor owner: %w", err)
}
if runtime.HellParty != nil {
extendedPlan, err = s.appendCurrentDungeonHellPartyActors(
extendedPlan,
runtime.HellParty,
target,
runtime.Dungeon.Metadata.BasisLevel,
monsterCatalog,
aiCatalog,
)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan target hell party actors: %w", err)
}
}
if err := targetRoom.AttachExtendedActors(extendedPlan); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("attach target dungeon extended actor owner: %w", err)
}
if _, err := s.configurePVFTutorialBasicActionRoom(runtime.Dungeon, target, targetRoom); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("configure target tutorial basic-action room: %w", err)
}
seed, err := s.chooseDungeonSeed()
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("choose target dungeon room seed: %w", err)
}
return currentDungeonMovePlan{
Source: source,
Target: target,
SourceVisit: sourceVisit,
TargetRoom: targetRoom,
NextObjectKey: extendedPlan.NextObjectKey,
Seed: seed,
Revisit: false,
LayerExit: layerExit,
LayerIndex: -1,
LayerBaseKey: layerBaseKey,
Operation: currentDungeonStartMapOperationCurrent,
PayloadMode: currentDungeonStartMapPayloadBuild,
}, nil
}
// validateCurrentDungeonTerminalLayerExit allows the client to leave the last
// resolved layer through a normal explicit adjacent move. Earlier layers still
// require their same-coordinate MoveKind=1 transition, preventing a player
// from skipping a pending story layer.
func (s *Service) validateCurrentDungeonTerminalLayerExit(
runtime *runtimeDungeonState,
request dungeoncmd.MoveMapRequest,
source worldmap.DungeonRoomScene,
) (bool, runtimeDungeonRoomKey, error) {
if runtime == nil || !runtime.LayeredMapActive {
return false, runtimeDungeonRoomKey{}, nil
}
if request.MoveKind != 0 ||
(int64(request.NextX) == source.Coordinate.X && int64(request.NextY) == source.Coordinate.Y) {
return false, runtimeDungeonRoomKey{}, fmt.Errorf(
"%w: source=%s/%d active_layer_requires_terminal_explicit_target_or_same_coordinate_move_kind_1",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
)
}
chain := runtime.LayerChains[source.Coordinate]
if chain == nil || chain.Consumed || runtime.LayeredMapIndex < 0 {
return false, runtimeDungeonRoomKey{}, fmt.Errorf(
"%w: source=%s/%d active_layer_chain_unavailable",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
)
}
if !source.Cleared {
return false, runtimeDungeonRoomKey{}, fmt.Errorf(
"%w: source=%s/%d layered_terminal_combat_gate_not_cleared",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
)
}
_, resolver, err := s.dungeonWorldMap()
if err != nil {
return false, runtimeDungeonRoomKey{}, err
}
current, err := resolver.ResolveLayered(
runtime.Dungeon.ID,
runtime.MazeIndex,
source.Coordinate,
runtime.LayeredMapIndex,
)
if err != nil || current.Map.ID != source.Map.Map.ID {
return false, runtimeDungeonRoomKey{}, fmt.Errorf(
"%w: source=%s/%d active_layer_index=%d resolution_error=%v",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
runtime.LayeredMapIndex,
err,
)
}
_, err = resolver.ResolveLayered(
runtime.Dungeon.ID,
runtime.MazeIndex,
source.Coordinate,
runtime.LayeredMapIndex+1,
)
if err == nil {
return false, runtimeDungeonRoomKey{}, fmt.Errorf(
"%w: source=%s/%d active_layer_has_pending_successor",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
)
}
if !errors.Is(err, worldmap.ErrLayeredIndexInvalid) {
return false, runtimeDungeonRoomKey{}, fmt.Errorf("resolve terminal layered dungeon map: %w", err)
}
return true, chain.BaseKey, nil
}
func currentDungeonNextStoryStage(
runtime *runtimeDungeonState,
) (worldmap.DungeonStoryStage, int, bool) {
if runtime == nil || len(runtime.StoryStages) == 0 {
return worldmap.DungeonStoryStage{}, -1, false
}
nextIndex := runtime.StoryStageIndex + 1
if nextIndex < 0 || nextIndex >= len(runtime.StoryStages) {
return worldmap.DungeonStoryStage{}, nextIndex, false
}
stage := runtime.StoryStages[nextIndex]
if stage.Index != nextIndex || stage.MapID <= 0 {
return worldmap.DungeonStoryStage{}, nextIndex, false
}
return stage, nextIndex, true
}
func (s *Service) currentDungeonBossEntranceMapOverride(
runtime *runtimeDungeonState,
request dungeoncmd.MoveMapRequest,
) (worldmap.RoomCoordinate, worldmap.ResolvedMap, bool, error) {
if runtime == nil || request.MoveKind != 0 ||
!runtime.bossEntranceConditionComplete || !runtime.bossEntranceGateNotificationSent ||
runtime.MazeIndex < 0 || runtime.MazeIndex >= len(runtime.Dungeon.Mazes) {
return worldmap.RoomCoordinate{}, worldmap.ResolvedMap{}, false, nil
}
maze := runtime.Dungeon.Mazes[runtime.MazeIndex]
if maze.BossRoomEntranceCondition == nil ||
!strings.EqualFold(strings.TrimSpace(maze.BossRoomEntranceCondition.Action), "clear action") {
return worldmap.RoomCoordinate{}, worldmap.ResolvedMap{}, false, nil
}
target := worldmap.RoomCoordinate{X: int64(request.NextX), Y: int64(request.NextY)}
var matched *worldmap.WarpMapCondition
for index := range maze.WarpMapConditions {
condition := &maze.WarpMapConditions[index]
if condition.Source.X != target.X || condition.Source.Y != target.Y {
continue
}
if matched != nil {
return worldmap.RoomCoordinate{}, worldmap.ResolvedMap{}, false, fmt.Errorf(
"%w: dungeon=%d maze=%d source=%s reason=ambiguous_boss_entrance_warp",
errDungeonMoveRuntimeOwnerMismatch,
runtime.Dungeon.ID,
runtime.MazeIndex,
target,
)
}
matched = condition
}
if matched == nil {
return worldmap.RoomCoordinate{}, worldmap.ResolvedMap{}, false, nil
}
_, resolver, err := s.dungeonWorldMap()
if err != nil {
return worldmap.RoomCoordinate{}, worldmap.ResolvedMap{}, false, err
}
resolved, err := resolver.Resolve(worldmap.ResolveRequest{
DungeonID: runtime.Dungeon.ID,
MazeIndex: runtime.MazeIndex,
X: matched.Destination.X,
Y: matched.Destination.Y,
})
if err != nil {
return worldmap.RoomCoordinate{}, worldmap.ResolvedMap{}, false, fmt.Errorf(
"resolve boss-entrance warp destination map dungeon=%d maze=%d source=%s destination=%d:%d: %w",
runtime.Dungeon.ID,
runtime.MazeIndex,
target,
matched.Destination.X,
matched.Destination.Y,
err,
)
}
return target, resolved, true, nil
}
func (s *Service) planCurrentDungeonBossEntranceMapOverrideMove(
runtime *runtimeDungeonState,
source worldmap.DungeonRoomScene,
sourceVisit *runtimeDungeonRoomVisit,
targetCoordinate worldmap.RoomCoordinate,
resolved worldmap.ResolvedMap,
) (currentDungeonMovePlan, error) {
if runtime == nil || runtime.Session == nil || runtime.Room == nil || sourceVisit == nil ||
runtime.LayeredMapActive || resolved.Map.ID <= 0 {
return currentDungeonMovePlan{}, errDungeonWorldMapUnavailable
}
transition, err := runtime.Session.PreviewMoveMapOverride(targetCoordinate, resolved)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("preview boss-entrance map override: %w", err)
}
target := transition.Scene
if target.Coordinate != targetCoordinate || target.Map.Map.ID != resolved.Map.ID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: boss_entrance_target=%s/%d session=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
targetCoordinate,
resolved.Map.ID,
target.Coordinate,
target.Map.Map.ID,
)
}
targetKey := runtimeDungeonRoomKeyFromScene(target)
if existing := runtime.Rooms[targetKey]; existing != nil {
if existing.Room == nil {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s/%d boss_entrance_cache_room_missing",
errDungeonMoveRuntimeOwnerMismatch,
target.Coordinate,
target.Map.Map.ID,
)
}
validated, validateErr := runtime.Session.ValidateMoveMapOverride(
targetCoordinate,
resolved,
&existing.Scene,
)
if validateErr != nil {
return currentDungeonMovePlan{}, fmt.Errorf("validate cached boss-entrance map override: %w", validateErr)
}
targetSnapshot := existing.Room.Snapshot()
if validated.Scene.Coordinate != target.Coordinate || validated.Scene.Map.Map.ID != target.Map.Map.ID ||
targetSnapshot.Coordinate != target.Coordinate || targetSnapshot.MapID != target.Map.Map.ID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: boss_entrance_cached_scene=%s/%d runtime=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
validated.Scene.Coordinate,
validated.Scene.Map.Map.ID,
targetSnapshot.Coordinate,
targetSnapshot.MapID,
)
}
return currentDungeonMovePlan{
Source: source,
Target: validated.Scene,
SourceVisit: sourceVisit,
TargetVisit: existing,
TargetRoom: existing.Room,
NextObjectKey: runtime.NextObjectKey,
Seed: existing.Seed,
Revisit: validated.Revisit,
LayerIndex: -1,
BossEntranceMapOverride: true,
BossEntranceResolvedMap: resolved,
Operation: currentDungeonStartMapOperationCurrent,
PayloadMode: currentDungeonStartMapPayloadCached,
}, nil
}
validated, err := runtime.Session.ValidateMoveMapOverride(targetCoordinate, resolved, nil)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("validate fresh boss-entrance map override: %w", err)
}
target = validated.Scene
monsterCatalog, err := s.dungeonMonsterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
targetRoom, nextObjectKey, err := newRuntimeDungeonRoom(target, monsterCatalog, runtime.NextObjectKey)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan boss-entrance target monster owner: %w", err)
}
var aiCatalog *pvfDungeonAICharacterCatalog
if len(target.AICharacters) != 0 {
aiCatalog, err = s.dungeonAICharacterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
}
extendedPlan, err := planRuntimeDungeonExtendedActors(
target,
monsterCatalog,
aiCatalog,
runtime.Dungeon.Metadata.BasisLevel,
nextObjectKey,
)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan boss-entrance target extended actor owner: %w", err)
}
if runtime.HellParty != nil {
extendedPlan, err = s.appendCurrentDungeonHellPartyActors(
extendedPlan,
runtime.HellParty,
target,
runtime.Dungeon.Metadata.BasisLevel,
monsterCatalog,
aiCatalog,
)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan boss-entrance hell-party actors: %w", err)
}
}
if err := targetRoom.AttachExtendedActors(extendedPlan); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("attach boss-entrance target extended actor owner: %w", err)
}
if _, err := s.configurePVFTutorialBasicActionRoom(runtime.Dungeon, target, targetRoom); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("configure boss-entrance target basic-action room: %w", err)
}
seed, err := s.chooseDungeonSeed()
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("choose boss-entrance target seed: %w", err)
}
payloadMode := currentDungeonStartMapPayloadBuild
if validated.Revisit {
payloadMode = currentDungeonStartMapPayloadRefresh
}
return currentDungeonMovePlan{
Source: source,
Target: target,
SourceVisit: sourceVisit,
TargetRoom: targetRoom,
NextObjectKey: extendedPlan.NextObjectKey,
Seed: seed,
Revisit: validated.Revisit,
LayerIndex: -1,
BossEntranceMapOverride: true,
BossEntranceResolvedMap: resolved,
Operation: currentDungeonStartMapOperationCurrent,
PayloadMode: payloadMode,
}, nil
}
func (s *Service) planCurrentDungeonStoryStageMove(
runtime *runtimeDungeonState,
source worldmap.DungeonRoomScene,
sourceVisit *runtimeDungeonRoomVisit,
stage worldmap.DungeonStoryStage,
stageIndex int,
) (currentDungeonMovePlan, error) {
if runtime == nil || runtime.Session == nil || runtime.Room == nil || sourceVisit == nil || runtime.LayeredMapActive {
return currentDungeonMovePlan{}, errDungeonWorldMapUnavailable
}
transition, err := runtime.Session.PreviewStoryStage(stageIndex)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("preview dungeon story stage: %w", err)
}
target := transition.Scene
if target.Coordinate != stage.Coordinate || target.Map.Map.ID != stage.MapID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: story_stage=%d runtime=%s/%d session=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
stageIndex,
stage.Coordinate,
stage.MapID,
target.Coordinate,
target.Map.Map.ID,
)
}
_, resolver, err := s.dungeonWorldMap()
if err != nil {
return currentDungeonMovePlan{}, err
}
resolved, descriptor, err := resolver.ResolveStoryStage(runtime.Dungeon.ID, runtime.MazeIndex, stageIndex)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("resolve dungeon story stage: %w", err)
}
if descriptor != stage || resolved.Map.ID != stage.MapID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: story_stage=%d cached=%+v resolved=%+v map_id=%d",
errDungeonMoveRuntimeOwnerMismatch,
stageIndex,
stage,
descriptor,
resolved.Map.ID,
)
}
if existing := runtime.Rooms[runtimeDungeonRoomKeyFromScene(target)]; existing != nil {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s/%d story_stage_visit_cache_already_exists",
errDungeonMoveRuntimeOwnerMismatch,
target.Coordinate,
target.Map.Map.ID,
)
}
monsterCatalog, err := s.dungeonMonsterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
targetRoom, nextObjectKey, err := newRuntimeDungeonRoom(target, monsterCatalog, runtime.NextObjectKey)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan dungeon story-stage monster owner: %w", err)
}
var aiCatalog *pvfDungeonAICharacterCatalog
if len(target.AICharacters) != 0 {
aiCatalog, err = s.dungeonAICharacterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
}
extendedPlan, err := planRuntimeDungeonExtendedActors(
target,
monsterCatalog,
aiCatalog,
runtime.Dungeon.Metadata.BasisLevel,
nextObjectKey,
)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan dungeon story-stage extended actor owner: %w", err)
}
if err := targetRoom.AttachExtendedActors(extendedPlan); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("attach dungeon story-stage extended actor owner: %w", err)
}
if _, err := s.configurePVFTutorialBasicActionRoom(runtime.Dungeon, target, targetRoom); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("configure dungeon story-stage basic-action room: %w", err)
}
seed, err := s.chooseDungeonSeed()
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("choose dungeon story-stage seed: %w", err)
}
payloadMode := currentDungeonStartMapPayloadBuild
if transition.Revisit {
payloadMode = currentDungeonStartMapPayloadRefresh
}
return currentDungeonMovePlan{
Source: source,
Target: target,
SourceVisit: sourceVisit,
TargetRoom: targetRoom,
NextObjectKey: extendedPlan.NextObjectKey,
Seed: seed,
Revisit: transition.Revisit,
LayerIndex: -1,
StoryStage: true,
StoryStageIndex: stageIndex,
Operation: currentDungeonStartMapOperationAdvanceLayer,
PayloadMode: payloadMode,
}, nil
}
// planCurrentDungeonLayerMove owns the current-EXE's explicit same-coordinate
// MOVE_MAP form. The live client sets MoveKind=1 when advancing a PVF
// [layered map specification]; ordinary same-coordinate moves continue through
// DungeonRun's adjacency validator and remain rejected.
func (s *Service) planCurrentDungeonLayerMove(
runtime *runtimeDungeonState,
request dungeoncmd.MoveMapRequest,
source worldmap.DungeonRoomScene,
sourceVisit *runtimeDungeonRoomVisit,
) (currentDungeonMovePlan, error) {
if runtime == nil || runtime.Session == nil || runtime.Room == nil || sourceVisit == nil {
return currentDungeonMovePlan{}, errDungeonWorldMapUnavailable
}
chain := runtime.LayerChains[source.Coordinate]
if chain != nil && chain.Consumed {
if chain.FinalAckPending && !runtime.LayeredMapActive {
return s.planCurrentDungeonLayerBaseAck(runtime, source, sourceVisit, chain)
}
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s/%d layered_transition_already_consumed",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
)
}
nextLayerIndex := 0
if runtime.LayeredMapActive {
if chain == nil {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s active_layer_chain_missing",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
)
}
nextLayerIndex = runtime.LayeredMapIndex + 1
}
_, resolver, err := s.dungeonWorldMap()
if err != nil {
return currentDungeonMovePlan{}, err
}
resolved, err := resolver.ResolveLayered(
runtime.Dungeon.ID,
runtime.MazeIndex,
source.Coordinate,
nextLayerIndex,
)
if runtime.LayeredMapActive && errors.Is(err, worldmap.ErrLayeredIndexInvalid) {
return s.planCurrentDungeonLayerReturn(runtime, source, sourceVisit, chain)
}
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("resolve explicit layered dungeon map: %w", err)
}
baseKey := runtimeDungeonRoomKeyFromScene(source)
if chain != nil {
baseKey = chain.BaseKey
} else {
base, baseErr := runtime.Session.ValidateLayeredBase(source.Coordinate, sourceVisit.Scene)
if baseErr != nil {
return currentDungeonMovePlan{}, fmt.Errorf("validate layered dungeon base before entry: %w", baseErr)
}
if base.Coordinate != source.Coordinate || base.Map.Map.ID != source.Map.Map.ID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: layered_base=%s/%d source=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
base.Coordinate,
base.Map.Map.ID,
source.Coordinate,
source.Map.Map.ID,
)
}
}
target, err := runtime.Session.PreviewLayered(source.Coordinate, nextLayerIndex)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("preview explicit layered dungeon move: %w", err)
}
if target.Coordinate != source.Coordinate || target.Map.Map.ID != resolved.Map.ID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: layered_resolver=%s/%d layered_session=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
resolved.Map.ID,
target.Coordinate,
target.Map.Map.ID,
)
}
if existing := runtime.Rooms[runtimeDungeonRoomKeyFromScene(target)]; existing != nil {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s/%d layered_visit_cache_already_exists",
errDungeonMoveRuntimeOwnerMismatch,
target.Coordinate,
target.Map.Map.ID,
)
}
monsterCatalog, err := s.dungeonMonsterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
targetRoom, nextObjectKey, err := newRuntimeDungeonRoom(target, monsterCatalog, runtime.NextObjectKey)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan layered dungeon monster owner: %w", err)
}
var aiCatalog *pvfDungeonAICharacterCatalog
if len(target.AICharacters) != 0 {
aiCatalog, err = s.dungeonAICharacterCatalog()
if err != nil {
return currentDungeonMovePlan{}, err
}
}
extendedPlan, err := planRuntimeDungeonExtendedActors(
target,
monsterCatalog,
aiCatalog,
runtime.Dungeon.Metadata.BasisLevel,
nextObjectKey,
)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("plan layered dungeon extended actor owner: %w", err)
}
if err := targetRoom.AttachExtendedActors(extendedPlan); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("attach layered dungeon extended actor owner: %w", err)
}
if _, err := s.configurePVFTutorialBasicActionRoom(runtime.Dungeon, target, targetRoom); err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("configure layered tutorial basic-action room: %w", err)
}
seed, err := s.chooseDungeonSeed()
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("choose layered dungeon room seed: %w", err)
}
return currentDungeonMovePlan{
Source: source,
Target: target,
SourceVisit: sourceVisit,
TargetRoom: targetRoom,
NextObjectKey: extendedPlan.NextObjectKey,
Seed: seed,
Layered: true,
LayerIndex: nextLayerIndex,
LayerBaseKey: baseKey,
Operation: currentDungeonStartMapOperationAdvanceLayer,
PayloadMode: func() byte {
if nextLayerIndex == 0 {
return currentDungeonStartMapPayloadBuild
}
return currentDungeonStartMapPayloadRefresh
}(),
}, nil
}
// planCurrentDungeonLayerBaseAck owns the final automatic CHANGE MAP request
// emitted after flag 1 has restored the same-coordinate base room. The client
// is already in the correct room, so this is a short ordinary-room revisit ACK
// and must not rebuild actors or restart layer zero.
func (s *Service) planCurrentDungeonLayerBaseAck(
runtime *runtimeDungeonState,
source worldmap.DungeonRoomScene,
sourceVisit *runtimeDungeonRoomVisit,
chain *runtimeDungeonLayerChain,
) (currentDungeonMovePlan, error) {
if runtime == nil || runtime.Session == nil || runtime.Room == nil || sourceVisit == nil || chain == nil {
return currentDungeonMovePlan{}, errDungeonWorldMapUnavailable
}
if !chain.Consumed || !chain.FinalAckPending || runtime.LayeredMapActive {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: layered base ACK requested without completed layer return",
errDungeonMoveRuntimeOwnerMismatch,
)
}
if runtimeDungeonRoomKeyFromScene(source) != chain.BaseKey ||
sourceVisit.Room != runtime.Room || sourceVisit.Seed != runtime.Seed {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: layered_base_ack source=%s/%d base=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
source.Coordinate,
source.Map.Map.ID,
worldmap.RoomCoordinate{X: chain.BaseKey.X, Y: chain.BaseKey.Y},
chain.BaseKey.MapID,
)
}
return currentDungeonMovePlan{
Source: source,
Target: source,
SourceVisit: sourceVisit,
TargetVisit: sourceVisit,
TargetRoom: sourceVisit.Room,
NextObjectKey: runtime.NextObjectKey,
Seed: sourceVisit.Seed,
Revisit: true,
LayerBaseAck: true,
LayerIndex: -1,
LayerBaseKey: chain.BaseKey,
Operation: currentDungeonStartMapOperationRestoreBase,
PayloadMode: currentDungeonStartMapPayloadCached,
}, nil
}
// planCurrentDungeonLayerReturn restores the cached topology-owned base room
// after the explicit layer list at the current coordinate is exhausted.
func (s *Service) planCurrentDungeonLayerReturn(
runtime *runtimeDungeonState,
source worldmap.DungeonRoomScene,
sourceVisit *runtimeDungeonRoomVisit,
chain *runtimeDungeonLayerChain,
) (currentDungeonMovePlan, error) {
if runtime == nil || runtime.Session == nil || runtime.Room == nil || sourceVisit == nil || chain == nil {
return currentDungeonMovePlan{}, errDungeonWorldMapUnavailable
}
if !runtime.LayeredMapActive || runtime.LayeredMapIndex < 0 || chain.Consumed {
return currentDungeonMovePlan{}, fmt.Errorf("%w: layered base return requested without an active layer", errDungeonMoveRuntimeOwnerMismatch)
}
targetVisit := runtime.Rooms[chain.BaseKey]
if targetVisit == nil || targetVisit.Room == nil {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: target=%s/%d layered_base_cache_missing",
errDungeonMoveRuntimeOwnerMismatch,
worldmap.RoomCoordinate{X: chain.BaseKey.X, Y: chain.BaseKey.Y},
chain.BaseKey.MapID,
)
}
target, err := runtime.Session.ValidateLayeredBase(source.Coordinate, targetVisit.Scene)
if err != nil {
return currentDungeonMovePlan{}, fmt.Errorf("validate cached layered dungeon base: %w", err)
}
if runtimeDungeonRoomKeyFromScene(target) != chain.BaseKey {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: cached_base=%s/%d topology_base=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
worldmap.RoomCoordinate{X: chain.BaseKey.X, Y: chain.BaseKey.Y},
chain.BaseKey.MapID,
target.Coordinate,
target.Map.Map.ID,
)
}
targetSnapshot := targetVisit.Room.Snapshot()
if targetSnapshot.Coordinate != target.Coordinate || targetSnapshot.MapID != target.Map.Map.ID {
return currentDungeonMovePlan{}, fmt.Errorf(
"%w: cached_scene=%s/%d cached_runtime=%s/%d",
errDungeonMoveRuntimeOwnerMismatch,
target.Coordinate,
target.Map.Map.ID,
targetSnapshot.Coordinate,
targetSnapshot.MapID,
)
}
return currentDungeonMovePlan{
Source: source,
Target: target,
SourceVisit: sourceVisit,
TargetVisit: targetVisit,
TargetRoom: targetVisit.Room,
NextObjectKey: runtime.NextObjectKey,
Seed: targetVisit.Seed,
LayerReturn: true,
LayerIndex: -1,
LayerBaseKey: chain.BaseKey,
Operation: currentDungeonStartMapOperationRestoreBase,
PayloadMode: currentDungeonStartMapPayloadCached,
}, nil
}
// currentDungeonPendingLayer reports whether the active PVF maze has another
// explicit layer at the current coordinate. Missing specifications and a
// consumed layer list are normal terminal states; ambiguous or unresolved PVF
// data fails closed so settlement cannot skip an intended story layer.
func (s *Service) currentDungeonPendingLayer(
runtime *runtimeDungeonState,
scene worldmap.DungeonRoomScene,
) (bool, int, int64, error) {
if runtime == nil || runtime.Session == nil {
return false, -1, 0, errDungeonWorldMapUnavailable
}
if chain := runtime.LayerChains[scene.Coordinate]; chain != nil && chain.Consumed {
return false, -1, 0, nil
}
if !currentDungeonCoordinateDeclaresLayer(runtime, scene.Coordinate) {
return false, -1, 0, nil
}
nextLayerIndex := 0
if runtime.LayeredMapActive {
nextLayerIndex = runtime.LayeredMapIndex + 1
}
_, resolver, err := s.dungeonWorldMap()
if err != nil {
return false, nextLayerIndex, 0, err
}
resolved, err := resolver.ResolveLayered(
runtime.Dungeon.ID,
runtime.MazeIndex,
scene.Coordinate,
nextLayerIndex,
)
if errors.Is(err, worldmap.ErrLayeredSpecMissing) || errors.Is(err, worldmap.ErrLayeredIndexInvalid) {
return false, nextLayerIndex, 0, nil
}
if err != nil {
return false, nextLayerIndex, 0, fmt.Errorf("resolve pending layered dungeon map: %w", err)
}
if !runtime.LayeredMapActive {
catalog, catalogErr := s.loadQuestCatalog(context.Background())
if catalogErr == nil && currentDungeonConnectedClearMapQuestOwnsBaseMap(
runtime,
scene.Map.Map.ID,
resolved.Map.ID,
catalog,
) {
return false, nextLayerIndex, resolved.Map.ID, nil
}
}
return true, nextLayerIndex, resolved.Map.ID, nil
}
// currentDungeonConnectedClearMapQuestOwnsBaseMap recognizes the narrow PVF
// case where a maze declares a story layer but its active clear-map quest
// explicitly targets the base map. Unknown or multi-target definitions remain
// pending so an intended story layer cannot be skipped.
func currentDungeonConnectedClearMapQuestOwnsBaseMap(
runtime *runtimeDungeonState,
currentMapID int64,
nextLayerMapID int64,
catalog *dnfquest.Catalog,
) bool {
if runtime == nil || catalog == nil || runtime.LayeredMapActive ||
runtime.MazeIndex < 0 || runtime.MazeIndex >= len(runtime.Dungeon.Mazes) {
return false
}
connection := runtime.Dungeon.Mazes[runtime.MazeIndex].QuestConnection
if len(connection) < 2 || connection[0] != 0 || connection[1] <= 0 {
return false
}
if currentMapID <= 0 || currentMapID == nextLayerMapID {
return false
}
definition, ok := catalog.Find(connection[1])
return ok &&
normalizeDungeonPVFSymbol(definition.Type) == "clear map" &&
len(definition.IntData) == 1 &&
definition.IntData[0] == currentMapID
}
func currentDungeonCoordinateDeclaresLayer(
runtime *runtimeDungeonState,
coordinate worldmap.RoomCoordinate,
) bool {
if runtime == nil || runtime.MazeIndex < 0 || runtime.MazeIndex >= len(runtime.Dungeon.Mazes) {
return false
}
maze := runtime.Dungeon.Mazes[runtime.MazeIndex]
for _, specification := range maze.MapSpecifications {
if strings.EqualFold(strings.Trim(specification.Type, " []"), "layered") &&
specification.Coordinate.X == coordinate.X && specification.Coordinate.Y == coordinate.Y {
return true
}
}
for _, specification := range maze.LayeredSpecifications {
if specification.Coordinate.X == coordinate.X && specification.Coordinate.Y == coordinate.Y {
return true
}
}
return false
}
func buildCurrentDungeonMoveStartMapBody(
runtime *runtimeDungeonState,
plan currentDungeonMovePlan,
) ([]byte, error) {
if runtime == nil || plan.TargetRoom == nil {
return nil, errDungeonWorldMapUnavailable
}
if runtime.altar != nil {
body, err := buildCurrentDungeonAltarStartMapBody(plan.Target, plan.Seed, plan.Revisit)
if err != nil {
return nil, fmt.Errorf("build altar target-room start-map body: %w", err)
}
return body, nil
}
if plan.Target.Coordinate.X < 0 || plan.Target.Coordinate.X > 0xff ||
plan.Target.Coordinate.Y < 0 || plan.Target.Coordinate.Y > 0xff {
return nil, fmt.Errorf("%w: room=%s", errDungeonStartMapCoordinateRange, plan.Target.Coordinate)
}
if plan.PayloadMode == currentDungeonStartMapPayloadCached {
if plan.TargetVisit == nil || plan.TargetVisit.Room != plan.TargetRoom {
return nil, fmt.Errorf("%w: cached target owner missing", errDungeonMoveRuntimeOwnerMismatch)
}
startMap := currentDungeonCachedStartMap{
X: byte(plan.Target.Coordinate.X),
Y: byte(plan.Target.Coordinate.Y),
Operation: plan.Operation,
Seed: plan.Seed,
RoomStateValue: 1,
PartyMemberIndex: currentDungeonRuntimePartyMemberIndex(runtime),
}
if runtime.HellParty != nil {
startMap.HellPartyMode = runtime.HellParty.Mode
}
body, err := startMap.Build()
if err != nil {
return nil, fmt.Errorf("build cached target-room start-map body: %w", err)
}
return body, nil
}
if plan.PayloadMode != currentDungeonStartMapPayloadBuild &&
plan.PayloadMode != currentDungeonStartMapPayloadRefresh {
return nil, fmt.Errorf("unsupported full start-map payload mode=%d", plan.PayloadMode)
}
targetRuntime := *runtime
targetRuntime.Room = plan.TargetRoom
targetRuntime.NextObjectKey = plan.NextObjectKey
targetRuntime.Seed = plan.Seed
startState := currentDungeonStartMapState{
LayeredRoomFlag: plan.Operation,
Seed: plan.Seed,
RoomStateValue: 1,
RoomStateFlag: plan.PayloadMode,
ExtraEntries: plan.PassiveDrops.ExtraEntries,
RidableGroups: currentDungeonRidableGroupsForRoom(runtime.RandomizedObjectGroups, plan.Target.Coordinate),
PartyMemberIndex: currentDungeonRuntimePartyMemberIndex(runtime),
}
if runtime.HellParty != nil {
startState.HellPartyMode = runtime.HellParty.Mode
startState.HellPartyFogFlag = 1
}
packet, err := currentDungeonStartMapFromRuntime(&targetRuntime, plan.Target, startState)
if err != nil {
return nil, fmt.Errorf("plan target-room start-map body: %w", err)
}
body, err := packet.Build()
if err != nil {
return nil, fmt.Errorf("build target-room start-map body: %w", err)
}
return body, nil
}