Files
90CNServer/server/internal/services/dnfbridge/town_post_transition_player_state.go
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

406 lines
16 KiB
Go

package dnfbridge
import (
"context"
"fmt"
"strconv"
"longheng.io/server/internal/modules/dnf/dnfenum"
)
// currentTownPostTransitionStage is a durable-in-session wire cursor for the
// selected actor generation created by one typed town op24. A failed write may
// resume only at the unfinished suffix; replaying an already accepted mode0 or
// mode1 can leave the current client bound to a different native object.
type currentTownPostTransitionStage byte
const (
currentTownPostTransitionIdle currentTownPostTransitionStage = iota
currentTownPostTransitionPending
currentTownPostTransitionMode0Sent
currentTownPostTransitionMode1Sent
currentTownPostTransitionPartyActorProjectionCaptured
currentTownPostTransitionGuardianShieldDeckSent
currentTownPostTransitionGuardianShieldAppearanceSent
currentTownPostTransitionChatEmoticonCatalogSent
currentTownPostTransitionCreatureItemSent
currentTownPostTransitionCreatureTableSent
currentTownPostTransitionCreatureGrowthSent
currentTownPostTransitionFinishStateSent
currentTownPostTransitionFinishCompletionSent
currentTownPostTransitionHellPartyStateCleared
currentTownPostTransitionSkillReplaySuppressed
currentTownPostTransitionExpertJobReplaySuppressed
currentTownPostTransitionBuffRegistrationSent
currentTownPostTransitionDarkKnightComboSent
currentTownPostTransitionGuardianGemSent
currentTownPostTransitionPlacementSent
currentTownPostTransitionActorUserStateSent
currentTownPostTransitionComplete
)
// armCurrentTownPostTransitionPlayerState starts a new selected-actor
// generation after an accepted movement/return typed town op24. First
// selection deliberately does not use this cursor: its type1345 callback owns
// only the already-deferred tail and HUD gauge and cannot safely consume
// another actor/finish-loading generation.
func (s *Service) armCurrentTownPostTransitionPlayerState(session *gameSession, source string) {
if session == nil || session.selectedCharacterID == 0 {
return
}
session.townPostTransition.mu.Lock()
defer session.townPostTransition.mu.Unlock()
session.townPostTransition.generation++
session.townPostTransition.characterID = session.selectedCharacterID
session.townPostTransition.ownerChannel = currentTownActorOwnerContext(session)
session.townPostTransition.stage = currentTownPostTransitionPending
session.townPostTransition.source = source
session.townPostTransition.mode0Body = nil
session.townPostTransition.mode1Body = nil
// These gates belong to the actor generation invalidated by op24. The
// staged chain below owns their one replacement for the new generation.
session.currentFinishLoadingStateSent = false
session.currentFinishLoadingCompletionSent = false
session.postFinishLoadingPlayerStateSent = false
session.initialTownSkillInfoSent = false
session.expertJobInfoCharacterID = 0
session.initialTownGuardianShieldDeckSent = false
session.initialTownGuardianShieldAppearanceSent = false
session.chatEmoticonCatalogSent = false
s.logGameEvent(session, "game-town-post-transition-player-state-armed",
"source", source,
"char_id", session.townPostTransition.characterID,
"owner_context", session.townPostTransition.ownerChannel,
"generation", session.townPostTransition.generation,
"stage", session.townPostTransition.stage,
"sequence", "mode0_mode1_op580_optional_slot24_op342_op357_slot26_op14_op105_optional_op102_op37_op30_no_hell_party_data_suppress_op19_suppress_op205_op559_optional_op447_slot32_guardian_gem_op120")
}
// resetCurrentTownPostTransitionPlayerState drops a stale pending generation
// when character selection/session state is reset before another op24.
func resetCurrentTownPostTransitionPlayerState(session *gameSession) {
if session == nil {
return
}
session.townPostTransition.mu.Lock()
session.townPostTransition.characterID = 0
session.townPostTransition.ownerChannel = 0
session.townPostTransition.stage = currentTownPostTransitionIdle
session.townPostTransition.source = ""
session.townPostTransition.mode0Body = nil
session.townPostTransition.mode1Body = nil
session.townPostTransition.mu.Unlock()
}
// sendCurrentTownPostTransitionPlayerState rebuilds the exact current-client
// town generation invalidated by op24. The transport-only DLL does not rewrite
// scene owners, so both actor packets use the selected actor's committed
// context. A local actor may deliberately use context zero while party and
// remote actor records remain keyed by the CHANNELINFO connection owner.
func (s *Service) sendCurrentTownPostTransitionPlayerState(session *gameSession, source string) error {
if session == nil {
return nil
}
session.townPostTransition.mu.Lock()
defer session.townPostTransition.mu.Unlock()
stage := session.townPostTransition.stage
if stage == currentTownPostTransitionIdle || stage == currentTownPostTransitionComplete {
return nil
}
characterID := session.townPostTransition.characterID
ownerChannel := session.townPostTransition.ownerChannel
currentOwner := currentTownActorOwnerContext(session)
if characterID == 0 || characterID != session.selectedCharacterID {
return fmt.Errorf(
"town post-transition character changed before stage %d: armed=%d selected=%d",
stage,
characterID,
session.selectedCharacterID,
)
}
if ownerChannel != currentOwner {
return fmt.Errorf(
"town post-transition owner changed before stage %d: armed=%d current=%d",
stage,
ownerChannel,
currentOwner,
)
}
if source == "" {
source = session.townPostTransition.source
}
generation := session.townPostTransition.generation
if session.townPostTransition.stage < currentTownPostTransitionMode0Sent {
body, err := s.sendSelectedActorMode0GenerationRefreshWithBody(
session,
"town_post_transition",
source+"_mode0_native_owner",
)
if err != nil {
return err
}
session.townPostTransition.mode0Body = body
session.townPostTransition.stage = currentTownPostTransitionMode0Sent
}
if session.townPostTransition.stage < currentTownPostTransitionMode1Sent {
body, err := s.sendSelectedActorMode1AppearanceRefreshWithBody(
session,
"town_post_transition",
source+"_mode1_after_generation_mode0",
)
if err != nil {
return err
}
session.townPostTransition.mode1Body = body
session.townPostTransition.stage = currentTownPostTransitionMode1Sent
}
if session.townPostTransition.stage < currentTownPostTransitionPartyActorProjectionCaptured {
if err := captureCurrentPartyActorProjection(
session,
characterID,
session.townPostTransition.mode0Body,
session.townPostTransition.mode1Body,
); err != nil {
return fmt.Errorf("capture town post-transition party actor projection: %w", err)
}
session.townPostTransition.mode0Body = nil
session.townPostTransition.mode1Body = nil
session.townPostTransition.stage = currentTownPostTransitionPartyActorProjectionCaptured
s.logGameEvent(session, "game-party-actor-full-projection-captured",
"source", source+"_after_mode1",
"char_id", characterID,
"owner_context", ownerChannel,
"character_generation", session.characterGeneration,
"town_post_transition_generation", generation)
}
if session.townPostTransition.stage < currentTownPostTransitionGuardianShieldDeckSent {
if _, err := s.sendSelectedGuardianShieldDeckInfo(
session,
source+"_guardian_shield_deck_after_mode1_before_op24",
); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionGuardianShieldDeckSent
}
if session.townPostTransition.stage < currentTownPostTransitionGuardianShieldAppearanceSent {
if _, err := s.sendSelectedGuardianShieldTownAppearanceRefreshOnce(
session,
source+"_guardian_shield_appearance_after_deck",
); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionGuardianShieldAppearanceSent
}
if session.townPostTransition.stage < currentTownPostTransitionChatEmoticonCatalogSent {
if _, err := s.sendSelectedChatEmoticonCatalog(
session,
source+"_chat_emoticon_catalog_after_mode1_before_op24",
); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionChatEmoticonCatalogSent
}
if session.townPostTransition.stage < currentTownPostTransitionCreatureItemSent {
// Full mode1 recreates the selected actor's slot-26 object, but the
// current EXE consumes the creature's dynamic item/card state only from
// its same-template list-3 op14. Publish that row before op105/op102 so
// their native map lookup cannot observe a half-rehydrated creature.
if err := s.sendSelectedEquippedCreatureItemRefresh(
session,
source+"_equipped_creature_item_after_mode1",
); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionCreatureItemSent
}
if session.townPostTransition.stage < currentTownPostTransitionCreatureTableSent {
// op24 created a new native actor generation, so op105 must be written
// even when the previous generation already consumed the same table.
session.selectedCreatureStateTableSent = false
if err := s.sendSelectedCreatureStateTable(session, source+"_op105_after_actor_rebind"); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionCreatureTableSent
}
if session.townPostTransition.stage < currentTownPostTransitionCreatureGrowthSent {
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
equippedCreature, err := s.currentEquippedCreatureForCharacterWithError(
ctx,
strconv.Itoa(int(characterID)),
)
cancel()
if err != nil {
return err
}
if equippedCreature.valid() {
if err := s.sendSelectedEquippedCreatureGrowthState(
session,
equippedCreature,
source+"_op102_after_op105",
); err != nil {
return err
}
}
session.townPostTransition.stage = currentTownPostTransitionCreatureGrowthSent
}
if session.townPostTransition.stage < currentTownPostTransitionFinishStateSent {
// The current client owns a dungeon-use gate that is released only by the
// authoritative class0/op37 state commit on the new town actor. Historical
// live traffic proves that omitting this frame leaves the next dungeon run
// unable to emit op44 at all. Send it after the complete actor/creature
// rebind, then answer a later client-authored town op37 through the existing
// ACK-only guard so the absolute snapshot is never duplicated.
if err := s.sendCurrentFinishLoadingCharacterStateSnapshot(
session,
source+"_op37_after_actor_and_creature_rebind",
); err != nil {
return err
}
session.currentFinishLoadingStateSent = true
session.townPostTransition.stage = currentTownPostTransitionFinishStateSent
}
if session.townPostTransition.stage < currentTownPostTransitionFinishCompletionSent {
if err := s.sendCurrentIncreaseStatusResult(
session,
source+"_op30_after_finish_state",
); err != nil {
return err
}
session.currentFinishLoadingCompletionSent = true
session.townPostTransition.stage = currentTownPostTransitionFinishCompletionSent
}
if session.townPostTransition.stage < currentTownPostTransitionHellPartyStateCleared {
// op679 is Hell Party encounter data, not a town reset. The authoritative
// op37/op30 actor rebind plus retirement of the old run owns this boundary.
session.townPostTransition.stage = currentTownPostTransitionHellPartyStateCleared
}
if session.townPostTransition.stage < currentTownPostTransitionSkillReplaySuppressed {
// This route leaves and re-enters the same selected character. Replaying a
// complete op19 here is not a skill delta; the current client interprets
// the expert skill in it as a new qualification. Keep the existing manager
// across the selector boundary as well; only a genuinely missing initial
// manager may use the guarded op15 fallback.
s.logGameEvent(session, "game-town-post-transition-skill-replay-suppressed",
"source", source,
"char_id", characterID,
"msg_id", currentSkillInfoMsgID,
"reason", "existing_character_town_rebind_must_not_replay_acquisition_sensitive_skill_snapshot")
session.initialTownSkillInfoPrepared = false
// The staged town rebind deliberately suppresses op19 here; leave the
// request-driven selector fallback armed so its established op19 -> op5 ->
// op27 boundary owns the next complete skill projection.
session.initialTownSkillInfoSent = false
session.initialTownSkillInfo = currentSceneSkillInfoProjection{}
session.townPostTransition.stage = currentTownPostTransitionSkillReplaySuppressed
}
if session.townPostTransition.stage < currentTownPostTransitionExpertJobReplaySuppressed {
// Native op205 is an acquisition/mutation notification on this EXE. Scene
// reconstruction must never publish it; real qualification, abandonment,
// recipe and machine changes retain their dedicated mutation paths.
s.logGameEvent(session, "game-town-post-transition-expert-job-replay-suppressed",
"source", source,
"char_id", characterID,
"msg_id", currentExpertJobInfoNotification,
"reason", "scene_reconstruction_is_not_an_expert_job_mutation")
session.expertJobInfoCharacterID = 0
session.townPostTransition.stage = currentTownPostTransitionExpertJobReplaySuppressed
}
if session.townPostTransition.stage < currentTownPostTransitionBuffRegistrationSent {
// mode1 replaces the selected actor's equipment object graph. Reapply the
// durable BUFF enhancement registrations at the final-owner boundary.
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
err := s.sendSkillSwitchInventoryAllDataRefresh(
ctx,
session,
source+"_buff_registration_after_mode1",
)
cancel()
if err != nil {
return fmt.Errorf("rehydrate town BUFF registration: %w", err)
}
session.townPostTransition.stage = currentTownPostTransitionBuffRegistrationSent
}
if session.townPostTransition.stage < currentTownPostTransitionDarkKnightComboSent {
if err := s.sendSelectedDarkKnightComboInitialization(
session,
source+"_dark_knight_combo_after_mode1",
); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionDarkKnightComboSent
}
// Non-completed dungeon exits use this staged actor rebind instead of the
// completed-return route. Restore the collection-box owner after mode1 so
// returning to town cannot retain the dungeon generation's cache.
if !session.collectboxSceneProjectionSent {
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
err := s.sendCollectboxSceneInitializationStates(ctx, session)
cancel()
if err != nil {
s.logGameEvent(session, "game-collectbox-scene-projection-deferred",
"char_id", characterID,
"source", source+"_town_after_mode1",
"reason", err)
}
}
if session.townPostTransition.stage < currentTownPostTransitionGuardianGemSent {
if err := s.sendSelectedGuardianGemWornMedalRefresh(
session,
source+"_guardian_gem_after_mode1",
); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionGuardianGemSent
}
// The worn title's raw random enchant is in the same create-row gap as the
// medal's guardian-gem socket words, so it is replayed at the same boundary.
if err := s.sendSelectedWornTitleEnchantRowRefresh(
session,
source+"_after_mode1",
); err != nil {
return err
}
if err := s.sendSelectedEquippedEquipmentEffectRuneRefresh(
session,
source+"_after_mode1",
); err != nil {
return err
}
if session.townPostTransition.stage < currentTownPostTransitionPlacementSent {
placementBody := buildCurrentSceneActorPlacementBody()
if err := s.sendGameUpperRawClass(
session,
uint16(dnfenum.CmdPacketRequestBlacklist),
placementBody,
0,
); err != nil {
return err
}
session.postFinishLoadingPlayerStateSent = true
session.townPostTransition.stage = currentTownPostTransitionPlacementSent
}
if session.townPostTransition.stage < currentTownPostTransitionActorUserStateSent {
if err := s.sendSelectedActorUserStateRefreshIfNonDefault(
session,
source+"_op357_after_actor_placement",
); err != nil {
return err
}
session.townPostTransition.stage = currentTownPostTransitionActorUserStateSent
}
session.townPostTransition.stage = currentTownPostTransitionComplete
s.logGameEvent(session, "game-town-post-transition-player-state-sent",
"source", source,
"char_id", characterID,
"owner_context", ownerChannel,
"generation", generation,
"stage", session.townPostTransition.stage,
"sequence", "mode0_mode1_op580_optional_slot24_op342_op357_slot26_op14_op105_optional_op102_op37_op30_no_hell_party_data_suppress_op19_suppress_op205_op559_optional_op447_slot32_guardian_gem_op120_optional_user_state_op357")
return nil
}