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

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
}