DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,435 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/premium"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
func (s *Service) sendUpperCSharpSelectInit(session *gameSession, request []byte) (err error) {
|
||||
packets := expandedCSharpUpperSelectInitTemplate()
|
||||
var legacyRepo dnfrepo.LegacyUserInfoRepository
|
||||
var selectedQuests dnfrepo.QuestRecord
|
||||
hasSelectedQuests := false
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
defer cancel()
|
||||
|
||||
charID, charName, selectedCharacter, hasSelectedCharacter, slot := s.resolveSelectedCharacter(ctx, session, request)
|
||||
if !hasSelectedCharacter {
|
||||
s.logPacketEvent("game-upper-select-character-init-blocked",
|
||||
"conn_id", session.connID,
|
||||
"channel_id", session.channel.ID,
|
||||
"slot", slot,
|
||||
"char_id", charID,
|
||||
"request_body_len", len(request),
|
||||
"reason", "select_request_not_decoded")
|
||||
return nil
|
||||
}
|
||||
if beginCurrentSelectCharacterInit(session, charID) {
|
||||
s.logGameEvent(session, "game-upper-select-character-init-duplicate-suppressed",
|
||||
"char_id", charID,
|
||||
"request_body_len", len(request),
|
||||
"reason", "same_character_select_lifecycle_already_active")
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
abortCurrentSelectCharacterInit(session, charID)
|
||||
}
|
||||
}()
|
||||
selectedCharacter, _, err = s.reconcileCurrentGameOptionVisibilityForSelectedCharacter(
|
||||
ctx,
|
||||
session,
|
||||
selectedCharacter,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reconcile selected character game-option visibility: %w", err)
|
||||
}
|
||||
s.bindGameSessionCharacter(session, charID)
|
||||
sameCharacterReturn := sameCharacterReturnSelect(session, charID)
|
||||
if activationErr := s.reconcileCurrentPremiumInventoryBeforeList(
|
||||
ctx,
|
||||
session,
|
||||
"select_character_before_premium_ack_and_inventory_bootstrap",
|
||||
false,
|
||||
); activationErr != nil {
|
||||
// Selection remains recoverable: the untouched item is projected and
|
||||
// can still be used manually, while the next list snapshot retries the
|
||||
// automatic conversion.
|
||||
s.logGameEvent(session, "game-upper-select-character-premium-inventory-auto-activation-deferred",
|
||||
"char_id", charID,
|
||||
"reason", activationErr,
|
||||
"fallback", "retain_contract_item_and_retry_before_list0")
|
||||
}
|
||||
var blackDiamondAccount dnfrepo.AccountRecord
|
||||
blackDiamondAccountFound := false
|
||||
blackDiamondResult, blackDiamondErr := s.reconcileCurrentBlackDiamondSelection(ctx, session, selectedCharacter)
|
||||
if blackDiamondErr != nil {
|
||||
// Selection stays available if storage is temporarily unavailable. The
|
||||
// next login retries migration/fatigue reconciliation and the account
|
||||
// expiry itself remains authoritative for settlement rewards.
|
||||
s.logGameEvent(session, "game-black-diamond-selection-reconcile-deferred",
|
||||
"char_id", charID,
|
||||
"reason", blackDiamondErr)
|
||||
} else if blackDiamondResult.Found {
|
||||
selectedCharacter = blackDiamondResult.Character
|
||||
blackDiamondAccount = blackDiamondResult.Account
|
||||
blackDiamondAccountFound = true
|
||||
s.logGameEvent(session, "game-black-diamond-selection-reconciled",
|
||||
"char_id", charID,
|
||||
"active", blackDiamondResult.Active,
|
||||
"expires_at", blackDiamondResult.ExpiresAt,
|
||||
"fatigue", selectedCharacter.Stats["fatigue"],
|
||||
"fatigue_limit", selectedCharacter.Stats["fatigue_limit"],
|
||||
"wire_type", premium.TypeBlackDiamondWire,
|
||||
"entitlement_type", premium.TypeBlackDiamondEntitlement)
|
||||
}
|
||||
dailyFatigueResult, dailyFatigueErr := s.reconcileCurrentDailyFatigueSelection(ctx, session)
|
||||
if dailyFatigueErr != nil {
|
||||
s.logGameEvent(session, "game-daily-fatigue-selection-reconcile-deferred",
|
||||
"char_id", charID,
|
||||
"reason", dailyFatigueErr)
|
||||
} else {
|
||||
if dailyFatigueResult.Character.CharacterID != "" {
|
||||
selectedCharacter = dailyFatigueResult.Character
|
||||
s.logGameEvent(session, "game-daily-fatigue-selection-reconciled",
|
||||
"char_id", charID,
|
||||
"service_day_id", dailyFatigueResult.ServiceDayID,
|
||||
"changed", dailyFatigueResult.Changed,
|
||||
"fatigue_restored", dailyFatigueResult.FatigueRestored,
|
||||
"potion_usage_reset", dailyFatigueResult.PotionUsageReset,
|
||||
"fatigue", selectedCharacter.Stats["fatigue"],
|
||||
"fatigue_limit", selectedCharacter.Stats["fatigue_limit"])
|
||||
}
|
||||
}
|
||||
dailyDungeonResult, dailyDungeonErr := s.reconcileCurrentDailyDungeonSelection(ctx, session)
|
||||
if dailyDungeonErr != nil {
|
||||
s.logGameEvent(session, "game-daily-dungeon-selection-reconcile-deferred",
|
||||
"char_id", charID,
|
||||
"reason", dailyDungeonErr)
|
||||
} else if !dailyDungeonResult.Window.Start.IsZero() {
|
||||
s.logGameEvent(session, "game-daily-dungeon-selection-reconciled",
|
||||
"char_id", charID,
|
||||
"service_window_start", dailyDungeonResult.Window.Start.Format(time.RFC3339),
|
||||
"changed", dailyDungeonResult.Changed,
|
||||
"counters_changed", dailyDungeonResult.CountersChanged,
|
||||
"inventory_changed", dailyDungeonResult.InventoryChanged,
|
||||
"reset_policy_count", len(dailyDungeonResult.ResetPolicyKeys),
|
||||
"invitation_count", len(dailyDungeonResult.Invitations))
|
||||
}
|
||||
if dailyFatigueErr != nil || dailyDungeonErr != nil {
|
||||
if retryErr := s.retryCurrentDailyFatigueReset(session); retryErr != nil {
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-retry-schedule-failed",
|
||||
"char_id", charID,
|
||||
"reason", "selection_daily_reconcile_failed",
|
||||
"error", retryErr)
|
||||
}
|
||||
} else if timerErr := s.armCurrentDailyFatigueReset(session); timerErr != nil {
|
||||
s.logGameEvent(session, "game-daily-fatigue-reset-timer-arm-deferred",
|
||||
"char_id", charID,
|
||||
"error", timerErr)
|
||||
}
|
||||
s.persistCurrentSelectorAdventureInfoSlot(ctx, session, slot)
|
||||
session.selectedCreatureStateTableSent = false
|
||||
s.resetCurrentInitialTownRoute(session)
|
||||
if stats, ok := s.characterPVFStatsForUserInfo(ctx, session, selectedCharacter, hasSelectedCharacter); ok {
|
||||
applyCharacterPVFStats(&selectedCharacter, stats)
|
||||
}
|
||||
if preSyncErr := s.preSyncGuardianShieldMainProjection(ctx, session, selectedCharacter, hasSelectedCharacter); preSyncErr != nil {
|
||||
s.logGameEvent(session, "game-upper-select-guardian-shield-main-projection-presync-deferred",
|
||||
"char_id", charID,
|
||||
"error", preSyncErr)
|
||||
}
|
||||
ackTownID, townErr := currentSelectAckTownID(session, selectedCharacter)
|
||||
if townErr != nil {
|
||||
s.logPacketEvent("game-upper-select-character-init-blocked",
|
||||
"conn_id", session.connID,
|
||||
"channel_id", session.channel.ID,
|
||||
"slot", slot,
|
||||
"char_id", charID,
|
||||
"request_body_len", len(request),
|
||||
"reason", townErr)
|
||||
return townErr
|
||||
}
|
||||
if repos, ok := s.repositoryGroup(); ok {
|
||||
legacyRepo = repos.LegacyUserInfo
|
||||
if repos.Quest != nil {
|
||||
characterID := selectedCharacter.CharacterID
|
||||
if characterID == "" {
|
||||
characterID = strconv.Itoa(int(charID))
|
||||
}
|
||||
var err error
|
||||
selectedQuests, hasSelectedQuests, err = repos.Quest.Load(ctx, characterID)
|
||||
if err != nil {
|
||||
hasSelectedQuests = false
|
||||
s.logGameEvent(session, "game-upper-select-character-quest-load-failed",
|
||||
"char_id", charID,
|
||||
"error", err)
|
||||
} else if hasSelectedQuests {
|
||||
// The select ACK carries the quest projection the client boots
|
||||
// from; retire the legacy Anton team mission before it is built
|
||||
// so a Luke-channel login sees the mission as already complete.
|
||||
var defaultCompleted bool
|
||||
selectedQuests, defaultCompleted, err = s.reconcileAntonRaidMissionDefaultCompletion(
|
||||
ctx,
|
||||
session,
|
||||
repos,
|
||||
characterID,
|
||||
selectedQuests,
|
||||
)
|
||||
if err != nil {
|
||||
s.logGameEvent(session, "game-upper-select-character-anton-raid-mission-default-completion-deferred",
|
||||
"char_id", charID,
|
||||
"error", err)
|
||||
} else if defaultCompleted {
|
||||
hasSelectedQuests = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.logPacketEvent("game-upper-select-character-csharp-init-send",
|
||||
"conn_id", session.connID,
|
||||
"channel_id", session.channel.ID,
|
||||
"slot", slot,
|
||||
"char_id", charID,
|
||||
"packet_count", len(packets))
|
||||
accountCera := uint32(0)
|
||||
premiumEntries := []byte{0}
|
||||
if repos, ok := s.repositoryGroup(); ok {
|
||||
if balance := s.loadCurrentAccountCera(ctx, repos, session); balance > 0 {
|
||||
if balance > int64(^uint32(0)) {
|
||||
balance = int64(^uint32(0))
|
||||
}
|
||||
accountCera = uint32(balance)
|
||||
}
|
||||
if blackDiamondAccountFound {
|
||||
premiumEntries = currentSelectAckPremiumEntries(blackDiamondAccount, s.gameplayNow().UTC())
|
||||
} else if repos.Account != nil {
|
||||
if account, found, err := repos.Account.Load(ctx, s.accountIDForSession(session)); err == nil && found {
|
||||
premiumEntries = currentSelectAckPremiumEntries(account, s.gameplayNow().UTC())
|
||||
s.setCurrentBlackDiamondExpiry(session, currentBlackDiamondExpiration(account))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, packet := range packets {
|
||||
body := packet.body
|
||||
if packet.kind == "userinfo" {
|
||||
s.logGameEvent(session, "game-upper-select-character-userinfo-deferred",
|
||||
"char_id", charID,
|
||||
"occurrence", packet.occurrence,
|
||||
"reason", "selected_userinfo_refresh_waits_for_enter_scene_state")
|
||||
continue
|
||||
}
|
||||
if packet.kind == "select_deferred" {
|
||||
s.logGameEvent(session, "game-upper-select-character-packet-deferred",
|
||||
"char_id", charID,
|
||||
"msg_id", packet.msgID,
|
||||
"reason", "client_crashes_when_sent_before_enter_scene_state")
|
||||
continue
|
||||
}
|
||||
if !selectInitPacketAllowedBeforeScene(packet) {
|
||||
s.logGameEvent(session, "game-upper-select-character-packet-deferred",
|
||||
"char_id", charID,
|
||||
"msg_id", packet.msgID,
|
||||
"kind", packet.kind,
|
||||
"reason", "wait_for_client_request_or_scene_state")
|
||||
continue
|
||||
}
|
||||
switch packet.kind {
|
||||
case "select_ack":
|
||||
body = buildCurrentSelectCharacterAckBody(selectedCharacter, hasSelectedCharacter, selectedQuests, hasSelectedQuests, charID, ackTownID, accountCera, premiumEntries)
|
||||
fixedQuestRows, overflowQuestRows, questRowsOK := currentSelectAckQuestRowCounts(body)
|
||||
ackCharacterID, ackCharacterIDOK := currentSelectAckCharacterID(body)
|
||||
ackTownIDFromBody, ackTownIDOK := currentSelectAckTownIDFromBody(body)
|
||||
ackIntermediateState, ackIntermediateOffset, ackIntermediateOK := currentSelectAckIntermediateState(body)
|
||||
tutorialFlag, tutorialCount, tutorialStateOK := currentSelectAckTutorialState(body)
|
||||
tutorialIndexes, tutorialIndexesOK := currentSelectAckTutorialIndexes(body)
|
||||
fatigue := currentCharacterFatigueState(selectedCharacter, hasSelectedCharacter)
|
||||
s.logPacketEvent("game-upper-select-character-ack-current-fields",
|
||||
"conn_id", session.connID,
|
||||
"channel_id", session.channel.ID,
|
||||
"char_id", charID,
|
||||
"slot", slot,
|
||||
"quest_record_found", hasSelectedQuests,
|
||||
"quest_state_count", len(selectedQuests.States),
|
||||
"fixed_quest_rows", fixedQuestRows,
|
||||
"overflow_quest_rows", overflowQuestRows,
|
||||
"quest_rows_ok", questRowsOK,
|
||||
"ack_character_id", ackCharacterID,
|
||||
"ack_character_id_ok", ackCharacterIDOK,
|
||||
"town_id", ackTownIDFromBody,
|
||||
"town_id_ok", ackTownIDOK,
|
||||
"intermediate_state_offset", ackIntermediateOffset,
|
||||
"intermediate_state_ok", ackIntermediateOK,
|
||||
"intermediate_state0", ackIntermediateState[0],
|
||||
"intermediate_state1", ackIntermediateState[1],
|
||||
"intermediate_state2", ackIntermediateState[2],
|
||||
"intermediate_state3", ackIntermediateState[3],
|
||||
"tutorial_ignored_flag", tutorialFlag,
|
||||
"tutorial_count", tutorialCount,
|
||||
"tutorial_state_ok", tutorialStateOK,
|
||||
"tutorial_indexes", tutorialIndexes,
|
||||
"tutorial_indexes_ok", tutorialIndexesOK,
|
||||
"server_tutorial_completed", hasPersistedDungeonTutorialCompletion(selectedCharacter),
|
||||
"fatigue_used", fatigue.used,
|
||||
"fatigue_limit", fatigue.limit,
|
||||
"premium_count", premiumEntries[0],
|
||||
"same_character_return_select", sameCharacterReturn,
|
||||
"body_len", len(body),
|
||||
"body_source", "current_exe_sub_1D01C50_sub_1A0C3E0_4u32_character_quest_slot_typed")
|
||||
case "select_scene_userinfo":
|
||||
body = s.buildCSharpSelectedUserInfoBody(ctx, session, legacyRepo, packet.occurrence, selectedCharacter, hasSelectedCharacter, charID, charName)
|
||||
s.logPacketEvent("game-upper-select-character-scene-userinfo-send",
|
||||
"conn_id", session.connID,
|
||||
"channel_id", session.channel.ID,
|
||||
"char_id", charID,
|
||||
"occurrence", packet.occurrence,
|
||||
"msg_id", packet.msgID,
|
||||
"body_len", len(body),
|
||||
"source", "csharp_select_init_before_scene_object_chain")
|
||||
case "userinfo":
|
||||
body = s.buildCSharpSelectedUserInfoBody(ctx, session, legacyRepo, packet.occurrence, selectedCharacter, hasSelectedCharacter, charID, charName)
|
||||
case csharpLegacyUserInfoKind:
|
||||
var ok bool
|
||||
body, ok = s.buildCSharpLegacyUserInfoBody(ctx, session, legacyRepo, charID, packet.msgID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
case csharpCurrentActionTableStateKind:
|
||||
s.logPacketEvent("game-upper-current-action-table-state-send",
|
||||
"conn_id", session.connID,
|
||||
"channel_id", session.channel.ID,
|
||||
"char_id", charID,
|
||||
"msg_id", packet.msgID,
|
||||
"body_len", len(body),
|
||||
"body_source", "current_exe_sub_217D850_u8_u8_u8",
|
||||
"selector_source", "decoded_dword_51B0F14_F18",
|
||||
"reason", "initialize_sub_3851100_active_table_before_mode0_object")
|
||||
case csharpCurrentSceneObjectListKind:
|
||||
body = s.buildCurrentSceneObjectListBodyForSession(ctx, session, charID, charName, selectedCharacter, hasSelectedCharacter)
|
||||
case csharpCurrentSceneOp9ActorDisplayKind:
|
||||
objectKey := currentSceneActorObjectKey(charID)
|
||||
body = buildCurrentSceneOp9ActorDisplayBodyInContext(
|
||||
objectKey,
|
||||
selectedCharacter,
|
||||
hasSelectedCharacter,
|
||||
charName,
|
||||
currentSceneObjectContext,
|
||||
)
|
||||
op9Name := charName
|
||||
if op9Name == "" && hasSelectedCharacter {
|
||||
op9Name = selectedCharacter.Name
|
||||
}
|
||||
s.logGameEvent(session, "game-upper-scene-op9-current-actor-display-send",
|
||||
"char_id", charID,
|
||||
"msg_id", packet.msgID,
|
||||
"scene_object_key", objectKey,
|
||||
"scene_value", currentSceneOp9StableSceneValue,
|
||||
"body_len", len(body),
|
||||
"record_kind", currentSceneOp9ActorDisplayKind,
|
||||
"name_len", len(rosterNameBytes(op9Name)),
|
||||
"slot_count", 0,
|
||||
"follow_mode", 0)
|
||||
case "title_book":
|
||||
// Send real title book lists from inventory (list 100) instead of empty fixture.
|
||||
characterID := strconv.FormatUint(uint64(session.selectedCharacterID), 10)
|
||||
tbCtx, tbCancel := context.WithTimeout(context.Background(), createWriteTimeout)
|
||||
repos, _ := s.repositoryGroup()
|
||||
if repos.Inventory != nil {
|
||||
_ = s.sendAllCurrentTitleBookLists(session, tbCtx, repos, characterID)
|
||||
}
|
||||
tbCancel()
|
||||
// Still send the empty fixture for categories the client expects at init.
|
||||
body = buildCSharpTitleBookBody(packet.occurrence)
|
||||
case "daily_schedule":
|
||||
body = buildCSharpDailyScheduleBody(packet.occurrence)
|
||||
}
|
||||
if err := s.sendCSharpSelectInitPacket(session, packet, body); err != nil {
|
||||
return err
|
||||
}
|
||||
if packet.kind == csharpCurrentSceneObjectListKind {
|
||||
if err := s.sendSelectedSceneUserInfo23Mode1Only(session, "select_character_after_current_object_list"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if packet.kind == "select_ack" {
|
||||
if !returnSelectTownReentryPending(session) {
|
||||
// Cold selection needs these caches before its client-owned progress
|
||||
// boundary. Return-select publishes them after the immediate scene
|
||||
// commit so the previous town cannot become visible first.
|
||||
if err := s.sendCollectboxInitializationStates(ctx, session); err != nil {
|
||||
s.logGameEvent(session, "game-collectbox-init-status-deferred",
|
||||
"char_id", charID,
|
||||
"reason", err)
|
||||
}
|
||||
if err := s.sendCurrentDungeonPermissionSnapshot(ctx, session, "select_character_after_ack_before_worldmap_scene"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Current EXE sub_11DBE40 consumes class0/op1378 raw u32 before
|
||||
// the selected scene is finalized. Send the durable per-character
|
||||
// story-summary level immediately after the select ACK, before the
|
||||
// tutorial/town route can emit any scene packet.
|
||||
//
|
||||
// 这里下发的是**持久化值本身**,不做任何"补齐到角色等级"的推进:
|
||||
// 0 表示"还没接受过任何摘要",把它推到角色等级会让低等级段
|
||||
// (例如等级 1 的新角色)的摘要永远不再播放。
|
||||
if err := s.sendCurrentStoryDigestLastLevel(session, selectedCharacter, hasSelectedCharacter, "select_character_after_ack_before_scene"); err != nil {
|
||||
return err
|
||||
}
|
||||
startsInTutorial := selectedCharacterStartsInTutorial(selectedCharacter, hasSelectedCharacter)
|
||||
if session.channelReconnect {
|
||||
clearReturnSelectTownReentry(session)
|
||||
s.logGameEvent(session, "game-channel-reconnect-normal-select-route-skipped",
|
||||
"char_id", charID,
|
||||
"target_channel", session.channel.ID,
|
||||
"reason", "fresh_target_channel_town_route_runs_after_op4_bootstrap")
|
||||
} else if startsInTutorial {
|
||||
clearReturnSelectTownReentry(session)
|
||||
s.logGameEvent(session, "game-upper-current-scene-transition-op24-deferred",
|
||||
"source", "select_character_after_ack",
|
||||
"char_id", charID,
|
||||
"msg_id", currentSceneTransitionMsgID,
|
||||
"tutorial_completed", false,
|
||||
"reason", "tutorial_pending_first_scene_is_dungeon_not_town")
|
||||
if err := s.sendEnterSelectDungeonState(session, "select_character_after_ack_15", false, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.sendCurrentSelectPreviewObjectState(ctx, session, charID, charName, selectedCharacter, hasSelectedCharacter, "select_character_after_scene_gate_op15"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s.armCurrentInitialTownRoute(session, charID)
|
||||
if returnSelectTownReentryPending(session) {
|
||||
if err := s.resumeCurrentInitialTownRouteAfterReturnSelect(session); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s.logGameEvent(session, "game-upper-select-character-tutorial-preview-skipped",
|
||||
"source", "select_character_after_ack",
|
||||
"char_id", charID,
|
||||
"tutorial_completed", hasPersistedDungeonTutorialCompletion(selectedCharacter),
|
||||
"tutorial_route_index", currentSelectAckPage1RouteIndex,
|
||||
"reason", "current_exe_select_ack_switches_to_page1_and_arms_client_requested_progress36_town_route")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if session.channelReconnect {
|
||||
return s.sendCurrentChannelReconnectTownEntry(session)
|
||||
}
|
||||
session.sceneBootstrapTailDeferred = true
|
||||
session.sceneBootstrapTailSent = false
|
||||
resetCurrentDeferredSelectSceneTailProgress(session)
|
||||
s.logGameEvent(session, "game-select-scene-tail-deferred",
|
||||
"source", "select_character_wait_for_current_op29",
|
||||
"char_id", session.selectedCharacterID,
|
||||
"reason", "preview_eviction_then_final_actor_binding_before_op27_and_full_state_after_op29",
|
||||
"deferred_sequence", "validated_op16_op9_kind3_preview_remove_then_op5_final_mode0_actor_binding_mode1_op27_op28_op29_full_mode1_mode3")
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user