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

695 lines
29 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"context"
"sort"
"strings"
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/premium"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
type currentDeferredSelectSceneTailPostStage byte
const (
currentDeferredSelectSceneTailPackets currentDeferredSelectSceneTailPostStage = iota
currentDeferredSelectSceneTailFinishGate
currentDeferredSelectSceneTailCreature
currentDeferredSelectSceneTailPremium
currentDeferredSelectSceneTailCrystal
currentDeferredSelectSceneTailRental
currentDeferredSelectSceneTailMailbox
currentDeferredSelectSceneTailCarryGold
currentDeferredSelectSceneTailDamageFont
currentDeferredSelectSceneTailRepresentName
currentDeferredSelectSceneTailEvents
currentDeferredSelectSceneTailSeriaLuck
currentDeferredSelectSceneTailCharacterOptions
currentDeferredSelectSceneTailFriends
currentDeferredSelectSceneTailComplete
)
func resetCurrentDeferredSelectSceneTailProgress(session *gameSession) {
if session == nil {
return
}
session.sceneBootstrapActorGeneration++
if session.sceneBootstrapActorGeneration == 0 {
session.sceneBootstrapActorGeneration++
}
session.sceneBootstrapTailPacketIndex = 0
session.sceneBootstrapTailObjectMode1Pending = false
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailPackets
session.sceneBootstrapFriendListPacketIndex = 0
session.skillSwitchInventoryAllDataCharacterID = 0
session.skillSwitchInventoryAllDataGeneration = 0
}
// sendCurrentSelectSceneReadyBoundary is the common actor-tail boundary for the
// DPROTO callback, CheckUserConnection, and legacy type1345 routes.
func (s *Service) sendCurrentSelectSceneReadyBoundary(session *gameSession, source string) error {
if session == nil {
return nil
}
if err := s.sendDeferredSelectSceneTail(session, source); err != nil {
return err
}
if session.sceneBootstrapTailDeferred || !session.sceneBootstrapTailSent {
return s.sendInitialSkillSwitchInventoryAllDataAfterDprotoFallback(session, source)
}
if err := s.sendSelectedDarkKnightComboAfterFinalTownSceneOnce(
session,
source+"_after_final_deferred_mode1",
); err != nil {
return err
}
session.townMu.Lock()
legacyHUDReady := session.initialTownLegacySceneReadyAccepted
session.townMu.Unlock()
if !legacyHUDReady {
return nil
}
if err := s.sendCurrentGameOptionAfterLegacySceneReadyOnce(
session,
source+"_game_option_after_hud_ready_tail",
); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
err := s.sendInitialSkillSwitchInventoryAllData(
ctx,
session,
source+"_after_mode1_and_legacy_scene_ready",
)
cancel()
if err != nil {
return err
}
// A return-to-selector keeps the account inventory singleton alive even
// though the selected-character bootstrap resets its containers. The full
// pre-op24 list seeds storage, then this proved HUD-ready boundary rebinds
// only the twelve account-owned crystal/soul rows to their surviving widgets.
if err := s.sendCurrentAccountSharedInventoryTownUIReadyRefresh(
session,
source+"_account_shared_inventory_after_hud_ready",
); err != nil {
return err
}
if err := s.sendInitialTownAdventureOverheadRefresh(session, source+"_adventure_overhead_hud"); err != nil {
return err
}
if err := s.sendInitialTownCombatPowerAffixes(session, source+"_combat_power_hud"); err != nil {
return err
}
if err := s.sendInitialTownMoodProjection(session, source+"_mood"); err != nil {
return err
}
return s.sendInitialTownActorUserStateAfterHUDReadyOnce(
session,
source+"_user_state_after_hud_ready_tail",
)
}
// sendInitialSkillSwitchInventoryAllDataAfterDprotoFallback covers the live
// current-client route that completes the selected actor but never emits the
// older type1345/u32(2) acknowledgement. It deliberately does not complete
// the deferred tail: only the established BUFF all-data projection is safe at
// this later DPROTO callback. A later proved type1345 still owns the tail and
// is harmless because sendInitialSkillSwitchInventoryAllData is generation
// gated after this write succeeds.
func (s *Service) sendInitialSkillSwitchInventoryAllDataAfterDprotoFallback(session *gameSession, source string) error {
if s == nil || session == nil || !strings.HasPrefix(source, "upper_dproto_") {
return nil
}
session.townMu.Lock()
ready := session.initialTownRouteCharacterID != 0 &&
session.initialTownRouteCharacterID == session.selectedCharacterID &&
session.initialTownRouteStage >= currentInitialTownRoutePlayerStateSent &&
session.sceneBootstrapTailDeferred &&
!session.sceneBootstrapTailSent &&
!session.initialTownLegacySceneReadyAccepted
session.townMu.Unlock()
if !ready {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
err := s.sendInitialSkillSwitchInventoryAllData(
ctx,
session,
source+"_after_mode1_and_op24_without_legacy_type1345",
)
cancel()
return err
}
// sendInitialTownAdventureOverheadRefresh replays the authoritative account
// model only after the legacy type1345 HUD-ready boundary. The first op1340 /
// op1346 pair is necessarily sent before mode1 during actor construction; on
// the current EXE that early pair populates its internal adventure model but
// can be ignored by the already-created overhead-name widget. This one replay
// reaches the same native data path after the selected actor and HUD exist.
func (s *Service) sendInitialTownAdventureOverheadRefresh(session *gameSession, source string) error {
if s == nil || session == nil {
return nil
}
session.townMu.Lock()
defer session.townMu.Unlock()
if session.initialTownAdventureOverheadRefreshSent ||
session.initialTownRouteCharacterID == 0 ||
session.initialTownRouteCharacterID != session.selectedCharacterID ||
session.initialTownRouteStage < currentInitialTownRoutePlayerStateSent {
return nil
}
objectKey := session.selectedCharacterID
if err := s.sendCurrentAdventureInfoPushFromAccount(session, objectKey, source+"_op1340"); err != nil {
return err
}
if err := s.sendCurrentAdventureActorRefreshFromAccount(session, objectKey, source+"_op1346"); err != nil {
return err
}
name, _, _, err := s.currentRepresentAccountIdentity(context.Background(), session)
if err != nil {
s.logGameEvent(session, "game-adventure-overhead-name-replay-skipped",
"source", source,
"reason", "represent_account_identity_unavailable",
"error", err)
} else if strings.TrimSpace(name) != "" {
if err := s.sendRepresentAccountNameState(session, name, false, source+"_op1371"); err != nil {
return err
}
}
session.initialTownAdventureOverheadRefreshSent = true
return nil
}
// sendCurrentLegacyTownSceneReadyBoundary consumes the current NoPack's proved
// type1345/u32(2) callback after the first typed op24. This callback does not
// own another actor/finish-loading generation: live traffic proved that
// replaying mode0/mode1/op105/op37/op30 here leaves the client update loop with
// a null scene actor. Complete only the already-owned deferred tail. Skills
// were installed at the accepted pre-op24 boundary. The selected-town route
// deliberately does not send mode3 here: on the current EXE that packet is
// the user-initiated personal-information panel route.
func (s *Service) sendCurrentLegacyTownSceneReadyBoundary(session *gameSession, source string) error {
if session == nil {
return nil
}
session.townMu.Lock()
if session.initialTownRouteCharacterID == 0 ||
session.initialTownRouteCharacterID != session.selectedCharacterID ||
session.initialTownRouteStage < currentInitialTownRoutePlayerStateSent {
session.townMu.Unlock()
return nil
}
session.initialTownLegacySceneReadyAccepted = true
session.townMu.Unlock()
if err := s.sendCurrentSelectSceneReadyBoundary(session, source); err != nil {
return err
}
if err := s.sendCurrentQuestHUDReadyRefresh(session, source+"_after_confirmed_actor_and_hud"); err != nil {
return err
}
session.initialTownMode3RefreshPending = false
return nil
}
func (s *Service) sendInitialTownCombatPowerAffixes(session *gameSession, source string) error {
if session == nil {
return nil
}
session.townMu.Lock()
defer session.townMu.Unlock()
if session.initialTownCombatPowerAffixesSent ||
session.initialTownRouteStage < currentInitialTownRouteTransitionSent {
return nil
}
sent, err := s.sendSelectedCurrentCombatPowerAffixProjection(session, source)
if err != nil {
return err
}
if sent {
session.initialTownCombatPowerAffixesSent = true
}
return nil
}
func (s *Service) sendInitialTownMoodProjection(session *gameSession, source string) error {
if s == nil || session == nil {
return nil
}
session.townMu.Lock()
if session.initialTownMoodProjectionSent || session.initialTownRouteStage < currentInitialTownRouteTransitionSent {
session.townMu.Unlock()
return nil
}
session.townMu.Unlock()
moodValue, found, err := s.selectedCurrentMoodValue(session)
if err != nil {
s.logGameEvent(session, "game-mood-private-projection-deferred", "source", source, "char_id", session.selectedCharacterID, "reason", "character_load_failed", "error", err)
return nil
}
if found && moodValue != 0 {
if err := s.sendCurrentMoodProjection(session, moodValue, source); err != nil {
return err
}
}
session.townMu.Lock()
session.initialTownMoodProjectionSent = true
session.townMu.Unlock()
return nil
}
func currentInitialTownWaitsForLegacySceneReady(session *gameSession) bool {
if session == nil {
return false
}
session.townMu.Lock()
waiting := session.initialTownRouteCharacterID != 0 &&
session.initialTownRouteCharacterID == session.selectedCharacterID &&
session.initialTownRouteStage >= currentInitialTownRouteTransitionSent &&
!session.initialTownLegacySceneReadyAccepted
session.townMu.Unlock()
return waiting
}
// currentDeferredSceneTailWaitFields describes the state the scene tail is
// waiting in.
//
// A bare "the tail was deferred" line (160k of them in the measured 11 GB log)
// cannot answer which gate refused it: the two reasons are a client callback that
// never came and a route whose flags were reset under it, and the difference is
// entirely in these fields. They go into the packet log and the scene-loading
// diagnostic record (records/scene-*.jsonl), which is not a raid category: the
// tail deferral is unrelated to raids and at several lines per second it used to
// drown the real raid events and rotate the raid files out of retention.
func currentDeferredSceneTailWaitFields(session *gameSession) map[string]any {
if session == nil {
return map[string]any{}
}
session.townMu.Lock()
fields := map[string]any{
"route_char_id": session.initialTownRouteCharacterID,
"route_stage": int(session.initialTownRouteStage),
"legacy_ready_accepted": session.initialTownLegacySceneReadyAccepted,
"town_scene_ready_char_id": session.townSceneReadyCharacterID,
"return_select_pending": session.returnSelectTownReentryPending,
"back_to_village_pending": session.backToVillageEnterSelectPending,
}
session.townMu.Unlock()
fields["tail_deferred"] = session.sceneBootstrapTailDeferred
fields["tail_sent"] = session.sceneBootstrapTailSent
fields["tail_packet_index"] = session.sceneBootstrapTailPacketIndex
fields["tail_post_stage"] = int(session.sceneBootstrapTailPostStage)
fields["tail_object_mode1_pending"] = session.sceneBootstrapTailObjectMode1Pending
fields["post_start_map_player_state_sent"] = session.postStartMapPlayerStateSent
fields["tail_actor_generation"] = session.sceneBootstrapActorGeneration
return fields
}
// logDeferredSceneTail records one refused tail publication with the wait state
// attached, in both the packet log and the scene-loading diagnostic record
// (records/scene-*.jsonl; it used to go into the raid record, see recordScene).
func (s *Service) logDeferredSceneTail(session *gameSession, source string, reason string) {
s.logDeferredSceneTailFields(session, source, "", reason)
}
// logDeferredSceneTailWithActorSource keeps the actor-readiness source visible on
// the one gate that used to report it, while still emitting the full wait state.
func (s *Service) logDeferredSceneTailWithActorSource(
session *gameSession,
source string,
actorReadySource string,
reason string,
) {
s.logDeferredSceneTailFields(session, source, actorReadySource, reason)
}
func (s *Service) logDeferredSceneTailFields(
session *gameSession,
source string,
actorReadySource string,
reason string,
) {
if s == nil || session == nil {
return
}
fields := currentDeferredSceneTailWaitFields(session)
args := make([]any, 0, 2*len(fields)+6)
args = append(args,
"source", source,
"char_id", session.selectedCharacterID,
"reason", reason)
if actorReadySource != "" {
args = append(args, "actor_ready_source", actorReadySource)
}
for _, key := range sortedMapKeys(fields) {
args = append(args, key, fields[key])
}
s.logGameEvent(session, "game-upper-select-character-scene-tail-deferred", args...)
fields["source"] = source
fields["defer_reason"] = reason
fields["char_id"] = session.selectedCharacterID
fields["result"] = "deferred"
if actorReadySource != "" {
fields["actor_ready_source"] = actorReadySource
}
// 为什么搬去 scene 类别:这是选人选场景/进图时的引导包诊断,与团本无关;
// 它每秒能产生好几条,写进 raid 会把真正的团本事件淹没,还会把 raid-*.jsonl
// 顶到轮转上限、把团本记录挤出"每类保留最近 14 个文件"的保留窗口。
// 上面的 logGameEvent(packet_log)保持不变:高负载下它会丢约 25% 的行,
// 所以这份记录必须一条不少地留着,只是换个文件而已。
s.recordScene(session, "scene_tail_deferred", fields)
}
// sortedMapKeys keeps the log lines byte-stable for the same state, so two
// deferrals that differ only in a field value are comparable by eye.
func sortedMapKeys(fields map[string]any) []string {
keys := make([]string, 0, len(fields))
for key := range fields {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func (s *Service) sendDeferredSelectSceneTail(session *gameSession, source string) error {
if session == nil {
return nil
}
session.sceneBootstrapTailMu.Lock()
defer session.sceneBootstrapTailMu.Unlock()
if !session.sceneBootstrapTailDeferred {
return nil
}
if currentInitialTownWaitsForLegacySceneReady(session) {
s.logDeferredSceneTail(session, source, "wait_for_current_exe_legacy_type1345_u32_2_scene_ready")
return nil
}
transitionReady, transitionReadySource := s.currentSceneTransitionReadyForState(session)
if !transitionReady {
if err := s.resumeCurrentInitialTownRouteAfterSelectHeartbeat(session, source); err != nil {
return err
}
if currentInitialTownWaitsForLegacySceneReady(session) {
s.logDeferredSceneTail(session, source, "initial_town_transition_committed_wait_for_current_exe_legacy_type1345_u32_2")
return nil
}
transitionReady, transitionReadySource = s.currentSceneTransitionReadyForState(session)
}
if !transitionReady {
s.logDeferredSceneTail(session, source, "wait_for_selected_actor_and_scene_transition_owner")
return nil
}
if transitionReadySource == "initial_town_actor_bound_and_transition_sent" {
if err := s.sendCurrentInitialTownPlayerState(session, source+"_after_town_transition"); err != nil {
return err
}
}
actorReady, actorReadySource := s.currentSceneActorReadyForState(session)
if !actorReady {
s.logDeferredSceneTailWithActorSource(session, source, actorReadySource, "wait_for_full_player_state_and_scene_finalizer")
return nil
}
if session.sceneBootstrapTailSent {
s.logGameEvent(session, "game-upper-select-character-scene-tail-duplicate-skipped",
"source", source,
"char_id", session.selectedCharacterID)
return nil
}
packets := deferredSelectSceneTailPackets()
s.logGameEvent(session, "game-upper-select-character-scene-tail-send",
"source", source,
"char_id", session.selectedCharacterID,
"actor_ready_source", actorReadySource,
"packet_count", len(packets),
"resume_packet_index", session.sceneBootstrapTailPacketIndex,
"resume_object_mode1_pending", session.sceneBootstrapTailObjectMode1Pending,
"resume_post_stage", session.sceneBootstrapTailPostStage)
// 同上:引导包诊断改记 scene 类别,不占团本记录的文件轮转窗口。
s.recordScene(session, "scene_tail_deferred", map[string]any{
"source": source,
"char_id": session.selectedCharacterID,
"actor_ready_source": actorReadySource,
"result": "sent",
"packet_count": len(packets),
"resume_packet_index": session.sceneBootstrapTailPacketIndex,
"resume_post_stage": int(session.sceneBootstrapTailPostStage),
"tail_actor_generation": session.sceneBootstrapActorGeneration,
"defer_reason": "",
})
for session.sceneBootstrapTailPacketIndex < len(packets) {
packetIndex := session.sceneBootstrapTailPacketIndex
packet := packets[packetIndex]
if packet.kind == csharpCurrentSceneObjectListKind &&
!session.sceneBootstrapTailObjectMode1Pending &&
session.currentSceneObjectListSent {
s.logGameEvent(session, "game-upper-select-character-scene-tail-object-duplicate-skipped",
"source", source,
"char_id", session.selectedCharacterID,
"packet_index", packetIndex,
"reason", "initial_town_or_return_route_already_committed_current_object_list")
session.sceneBootstrapTailPacketIndex++
continue
}
body := packet.body
if packet.kind == csharpCurrentSceneObjectListKind &&
!session.sceneBootstrapTailObjectMode1Pending {
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
var err error
body, err = s.buildCurrentSceneObjectListBodyForSessionInContextStrict(
ctx,
session,
0,
"",
dnfrepo.CharacterRecord{},
false,
currentTownActorOwnerContext(session),
)
cancel()
if err != nil {
return err
}
}
if !session.sceneBootstrapTailObjectMode1Pending {
if err := s.sendCSharpSelectInitPacket(session, packet, body); err != nil {
return err
}
if packet.kind == csharpCurrentSceneObjectListKind {
// The object packet is already accepted. If the paired mode1
// write fails, resume at mode1 instead of replaying mode0.
session.sceneBootstrapTailObjectMode1Pending = true
} else {
session.sceneBootstrapTailPacketIndex++
continue
}
}
if packet.kind == csharpCurrentSceneObjectListKind {
if err := s.sendSelectedSceneUserInfo23Mode1Only(session, source+"_after_current_object_list"); err != nil {
return err
}
session.sceneBootstrapTailObjectMode1Pending = false
}
session.sceneBootstrapTailPacketIndex++
}
if session.sceneBootstrapTailPostStage < currentDeferredSelectSceneTailFinishGate {
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailFinishGate
}
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailFinishGate {
if actorReadySource != "initial_town_player_state_finalized" {
if err := s.sendUpperFinishLoadingGate(session, source); err != nil {
return err
}
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailCreature
}
// Do not publish skills from the generic tail. type1345 owns one explicit
// retained op19 write only after this complete tail has committed; generic
// CheckUserConnection/DPROTO callbacks must not consume that HUD boundary.
// The actor-bound pre-mode1 stage already installs the complete current
// item-list family. Replaying list 0/1/2/7/38 here rebuilds the same client
// containers after op24 and can overwrite scene-local item state. Keep
// scene-ready projections narrowly owned by their dedicated packets below.
// The select-character ACK folds Devil Contract services into type 58, but
// it cannot initialize the eight independent service deadlines consumed by
// the coupon box and the Cera-shop checkout UI. Publish their authoritative
// op903 state once after the town actor and HUD are ready.
// Durable aura expansion is already projected by selected-user subtype0's
// repository-backed aura_flag. Do not emit unsolicited op863 here: its
// native result handler is request/UI-owned and opens the avatar panel.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailCreature {
if err := s.sendSelectedCreatureSceneReadyProjection(session, source+"_scene_ready_creature"); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailPremium
}
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailPremium {
// serviceType 1 is the variant that applies the body to the contract
// singleton the coupon box and Cera checkout read. It also consumes the
// client's one-shot "show the contract panel" flag, which is the intended
// login-time popup; every later send stays silent (see
// currentPremiumServiceTypeForSession).
if err := s.sendCurrentPremiumServiceState(session, source+"_after_scene_actor_commit", premium.ServiceTypePanel); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailCrystal
}
// Do not send op898 from the scene tail. The actor-bound player-state path
// owns the cold-login class0 notification after every inventory container
// has been installed; the first client-authored op36 is only an idempotent
// fallback for routes that bypass that bootstrap. Sending from this earlier
// tail would again require the rejected ABI-unsafe deferred call into
// sub_1E7F810 and can corrupt adjacent UI state.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailCrystal {
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailRental
}
// The select-character container bootstrap does not carry rental points.
// Publish the absolute wallet once, after the scene actor is committed.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailRental {
if _, err := s.sendSelectedCurrentRentalPointState(session, source+"_after_scene_actor_commit", true); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailMailbox
}
// class0/op63 is the current client's mailbox-system bootstrap. It is a
// one-WORD unread-count projection, not an unsolicited op97 mailbox list,
// and runs only after the selected actor and UI-safe tail are complete.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailMailbox {
if err := s.sendMailboxAlarmForSession(
session,
session.selectedCharacterID,
source+"_after_scene_actor_commit",
); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailCarryGold
}
// The gold-carry cap stage owns its own resume point: without it the client
// falls back to stage 0 after a relog and would offer an already-purchased
// upgrade again. A failed write therefore neither replays the mailbox alarm
// nor skips the damage-font projection.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailCarryGold {
if err := s.sendCurrentCarryGoldStage(
session,
source+"_after_scene_actor_commit",
); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailDamageFont
}
// Damage-font ownership is independent of the inventory bootstrap. The
// current client installs it through a class-0 NOTI 1239 projection once
// the selected actor and other scene-owned stores are ready.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailDamageFont {
if err := s.sendSelectedDamageFontState(session, source+"_after_scene_actor_commit"); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailRepresentName
}
// This is the first point at which the selected town scene has received the
// complete bootstrap tail. First-time adventure-group registration must not
// interrupt the role selector or any earlier actor/state initialization.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailRepresentName {
if err := s.sendRepresentAccountNameRegistrationAfterScene(session, source+"_after_scene_init_complete"); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailEvents
}
// The activity calendar base rows and cumulative online/joust/Seria-luck
// state are published together by the final compressed op108 descriptor.
// Calendar rows remain base-only records; only the three live event families
// are included in op108's active-state map.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailEvents {
// 公会贡献币 earned before the last logout are delivered here, once the
// scene-init event tail has committed. Upstream places the flush in exactly
// this slot, ahead of the spend-time state.
if err := s.flushCurrentGuildContribution(session); err != nil {
return err
}
if err := s.sendCurrentSpendTimeInitialStateOnce(session, source+"_after_scene_init_events"); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailSeriaLuck
}
// BOOSTER_GAGE is the persisted left-top HUD gauge, not the random-box
// result panel. Send it only after every selected-scene initialization
// packet has committed; absent or invalid durable state is intentionally
// skipped by the helper instead of being projected as a synthetic zero.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailSeriaLuck {
if err := s.sendInitialSeriaLuckGauge(session, source+"_after_scene_init_complete"); err != nil {
return err
}
if err := s.sendInitialTownMoodProjection(session, source+"_mood"); err != nil {
return err
}
if err := s.sendCollectboxSceneInitializationStates(context.Background(), session); err != nil {
s.logGameEvent(session, "game-collectbox-scene-init-status-deferred", "char_id", session.selectedCharacterID, "source", source, "reason", err)
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailCharacterOptions
}
// op390 rebuilds the current actor's option collections. Keep it inside the
// retryable tail after the selected scene and local actor have replaced any
// previous channel state; a failed write therefore resumes at op390 without
// replaying the already committed prefix.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailCharacterOptions {
if err := s.sendCurrentCharacterOptionSnapshot(session, source+"_after_scene_init_complete"); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailFriends
}
// The stock eight-zero op273 bootstrap only clears the UI. Publish the
// durable one-way list after the final actor generation is bound; the
// duplicate full snapshot is intentional because this EXE's first mode-0
// pass creates hidden rows and the second pass marks existing rows visible.
if session.sceneBootstrapTailPostStage == currentDeferredSelectSceneTailFriends {
if err := s.sendCurrentUnitedFriendListResumable(session, "", source+"_after_scene_init_complete", &session.sceneBootstrapFriendListPacketIndex); err != nil {
return err
}
session.sceneBootstrapTailPostStage = currentDeferredSelectSceneTailComplete
}
session.sceneBootstrapTailSent = true
session.sceneBootstrapTailDeferred = false
// The blacklist answer was held back while this tail was pending (a row the client
// builds before the tail commits loses its 黑名单日期). Flush it here so a session
// whose client never asks again still gets its list; a failure keeps the pending
// flag set, and the retryable tail replays this point.
if session.blacklistListAnswerPending {
if err := s.sendCurrentBlacklistListAnswer(session, source+"_after_scene_tail", nil); err != nil {
return err
}
}
s.logGameEvent(session, "game-select-scene-tail-active-finished",
"source", source,
"char_id", session.selectedCharacterID,
"packet_count", len(packets)+1)
return nil
}
func (s *Service) sendCurrentCargoPadResetBeforeItemLists(session *gameSession, source string) error {
body := buildCurrentCancelCargoPadTransportBody()
s.logPacketEvent("game-upper-current-cargo-pad-reset-send",
"conn_id", session.connID,
"channel_id", session.channel.ID,
"source", source,
"char_id", session.selectedCharacterID,
"msg_id", uint16(dnfenum.CmdPacketCancelCargoPad),
"classification", 0,
"header_marker", 1,
"body_encoded", true,
"body_codec", currentCancelCargoPadTransportCodec,
"expanded_body_len", currentCancelCargoPadHeaderSize+currentCancelCargoPadSlotCount*4,
"slot_count", currentCancelCargoPadSlotCount,
"protected_tail_len", currentCancelCargoPadProtectedTailSize,
"state_source", "current_exe_protected_cargo_pad_reset_before_repository_item_lists")
return s.sendCurrentProtectedClass0Packet(
session,
uint16(dnfenum.CmdPacketCancelCargoPad),
body,
currentCancelCargoPadTransportCodec,
"initial_town_cargo_pad_reset_current_protected_state",
)
}