服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
205 lines
7.5 KiB
Go
205 lines
7.5 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// sendCurrentChannelReconnectTownEntry rebuilds the selected actor after the
|
|
// target-channel transport reconnect. An account-bound connection that
|
|
// completed CHANNELINFO must use the same u8 channel in every town mode0/mode1
|
|
// row; legacy/high-id fallbacks retain context zero. The connection bootstrap
|
|
// already owns the one allowed class0/op1 and endpoint success.
|
|
func (s *Service) sendCurrentChannelReconnectTownEntry(session *gameSession) error {
|
|
if s == nil || session == nil || !session.channelReconnect || session.selectedCharacterID == 0 {
|
|
return nil
|
|
}
|
|
if session.residentChannel.ID <= 0 || session.residentChannel.ID != session.channel.ID {
|
|
return fmt.Errorf(
|
|
"channel reconnect has no committed target identity: connected=%d resident=%d",
|
|
session.channel.ID,
|
|
session.residentChannel.ID,
|
|
)
|
|
}
|
|
ownerChannel := currentConnectionTownActorOwnerContext(session)
|
|
session.townActorOwnerChannel = ownerChannel
|
|
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
|
defer cancel()
|
|
characterID, characterName, character, hasCharacter := s.selectedCharacterForEnter(ctx, session)
|
|
if characterID == 0 || characterID != session.selectedCharacterID || !hasCharacter {
|
|
return fmt.Errorf(
|
|
"channel reconnect selected character mismatch: selected=%d loaded=%d found=%t",
|
|
session.selectedCharacterID,
|
|
characterID,
|
|
hasCharacter,
|
|
)
|
|
}
|
|
character, townID, areaID, row, mapPath, err := s.currentChannelReconnectTownTransition(
|
|
ctx,
|
|
session,
|
|
characterID,
|
|
character,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("load persisted channel reconnect location: %w", err)
|
|
}
|
|
transitionBody, err := buildCurrentSceneTransitionBody(
|
|
townID,
|
|
areaID,
|
|
[]currentSceneTransitionRow{row},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objectBody := s.buildCurrentSceneObjectListBodyForSessionInContext(
|
|
ctx,
|
|
session,
|
|
characterID,
|
|
characterName,
|
|
character,
|
|
hasCharacter,
|
|
ownerChannel,
|
|
)
|
|
adventureSummary := s.currentAccountAdventureGroupSummaryForPacket(ctx, session, character, hasCharacter)
|
|
mode1Body := s.buildCurrentActorBindingMode1BodyForSelectedInContext(
|
|
ctx,
|
|
session,
|
|
character,
|
|
hasCharacter,
|
|
characterID,
|
|
uint32(adventureSummary.ManageLevel),
|
|
ownerChannel,
|
|
)
|
|
|
|
session.townMu.Lock()
|
|
// Preserve an in-progress route so a failed post-op24 skill write resumes
|
|
// from TransitionSent. A completed route or another actor starts fresh.
|
|
resumeRoute := session.initialTownRouteCharacterID == characterID &&
|
|
session.initialTownRouteStage >= currentInitialTownRouteArmed &&
|
|
session.initialTownRouteStage < currentInitialTownRoutePlayerStateSent
|
|
if !resumeRoute {
|
|
resetCurrentTownPostTransitionPlayerState(session)
|
|
session.initialTownRouteCharacterID = characterID
|
|
session.initialTownRouteStage = currentInitialTownRouteArmed
|
|
session.initialTownActorSceneSnapshotSent = false
|
|
session.initialTownLocationNotificationsSent = false
|
|
session.initialTownQuestSnapshotsSent = false
|
|
session.initialTownQuestHUDRefreshSent = false
|
|
session.initialTownGuildStateProjectionSent = false
|
|
session.initialTownSkillInfoPrepared = false
|
|
session.initialTownSkillInfoSent = false
|
|
session.initialTownSkillInfo = currentSceneSkillInfoProjection{}
|
|
session.initialTownMode3RefreshPending = false
|
|
session.completedTownReturnBuffRegistrationPending = false
|
|
session.initialTownLegacySceneReadyAccepted = false
|
|
clearCurrentGameOptionRestoreSnapshot(session)
|
|
clearCurrentCharacterOptionRestoreSnapshot(session)
|
|
session.initialTownAdventureOverheadRefreshSent = false
|
|
session.initialTownCombatPowerAffixesSent = false
|
|
session.initialTownMoodProjectionSent = false
|
|
} else {
|
|
s.logGameEvent(session, "game-channel-reconnect-town-entry-resume",
|
|
"char_id", characterID,
|
|
"route_stage", session.initialTownRouteStage,
|
|
"skill_info_prepared", session.initialTownSkillInfoPrepared,
|
|
"reason", "same_actor_route_retry_preserves_transition_cursor_and_prepared_post_op24_skill")
|
|
}
|
|
session.townActorOwnerChannel = ownerChannel
|
|
|
|
endpointPolicy := "proactive_class1_op1_context0_no_route_replay"
|
|
if session.currentChannelResidentNoticeSent {
|
|
endpointPolicy = "channelinfo_then_single_requested_class1_op1_no_route_replay"
|
|
}
|
|
s.logGameEvent(session, "game-channel-reconnect-town-entry-send",
|
|
"char_id", characterID,
|
|
"target_channel", session.channel.ID,
|
|
"target_owner_channel", ownerChannel,
|
|
"owner_server", 0,
|
|
"owner_channel", ownerChannel,
|
|
"town_id", townID,
|
|
"area_id", areaID,
|
|
"position_x", row.Value1,
|
|
"position_y", row.Value2,
|
|
"direction", row.Value3,
|
|
"area_state", row.Value4,
|
|
"map_path", mapPath,
|
|
"sequence", "op376_mode0_light_mode1_op800_full_town_init_op124_op9_op120_op22_op23_op24_op21_op574",
|
|
"resident_notice_sent", session.currentChannelResidentNoticeSent,
|
|
"endpoint_policy", endpointPolicy)
|
|
if err := s.sendCurrentChannelReconnectTownActorRoutePacketsLocked(
|
|
session,
|
|
characterID,
|
|
townID,
|
|
areaID,
|
|
row,
|
|
objectBody,
|
|
mode1Body,
|
|
transitionBody,
|
|
); err != nil {
|
|
session.townMu.Unlock()
|
|
return err
|
|
}
|
|
routeStage := session.initialTownRouteStage
|
|
questSnapshotsSeeded := session.initialTownQuestSnapshotsSent
|
|
session.townMu.Unlock()
|
|
s.publishCurrentTownPresence(
|
|
session,
|
|
character,
|
|
townID,
|
|
areaID,
|
|
row,
|
|
0,
|
|
mapPath,
|
|
"channel_reconnect_town_actor_route_complete",
|
|
)
|
|
|
|
// The pre-op24 pair seeds the task manual while the actor is being rebuilt.
|
|
// Refresh it once more after the actor-ready boundary so NPC markers reflect
|
|
// repository/PVF state without accepting or completing a quest.
|
|
if routeStage >= currentInitialTownRouteTransitionSent && questSnapshotsSeeded {
|
|
if err := s.sendCurrentAcceptableQuestListForSession(
|
|
session,
|
|
"channel_reconnect_post_op24_actor_ready_refresh",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := s.sendSelectedActorUserStateRefreshIfNonDefault(
|
|
session,
|
|
"channel_reconnect_post_op24_actor_ready_user_state",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
// Team-channel owners rebuild/clear the native activity table. The new
|
|
// ordinary-channel session is armed during character rebinding and must
|
|
// restore the complete op108 catalog after op24, followed immediately by its
|
|
// normal op1206/op1303 projections. Other reconnects remain progress-only.
|
|
if err := s.sendCurrentSpendTimeInitialStateOnce(
|
|
session,
|
|
"channel_reconnect_post_op24_activity_state",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
// Channel reconnect has its own complete actor route and never enters the
|
|
// deferred select-scene tail. Replay the collection-box cache at this real
|
|
// scene-ready boundary so the target channel receives both the selected Orc
|
|
// Guardian page and the repository-backed state used by its mode-1 stats.
|
|
if err := s.sendCollectboxSceneInitializationStates(ctx, session); err != nil {
|
|
s.logGameEvent(session, "game-collectbox-scene-init-status-deferred", "char_id", session.selectedCharacterID, "source", "channel_reconnect_scene_ready", "reason", err)
|
|
}
|
|
session.channelReconnect = false
|
|
session.channelReconnectNewCharacter = false
|
|
session.sceneBootstrapTailDeferred = false
|
|
session.sceneBootstrapTailSent = true
|
|
session.townActorOwnerChannel = ownerChannel
|
|
s.logGameEvent(session, "game-channel-reconnect-town-entry-finished",
|
|
"char_id", characterID,
|
|
"target_channel", session.channel.ID,
|
|
"route_stage", routeStage,
|
|
"resident_notice_sent", session.currentChannelResidentNoticeSent,
|
|
"town_actor_owner_channel", session.townActorOwnerChannel,
|
|
"report_client_spec_sent", true,
|
|
"final_packet", "class0_op574_post_op24_actor_ready_refresh")
|
|
return nil
|
|
}
|