#pragma once namespace dnf90_first_login_recovery { constexpr long kMaxFirstLoginRecoveryAttempts = 2; inline bool IsFirstLoginServerProgressMessage(int messageType) { return messageType == 1; // ENUM_CMDPACKET_LOGIN } inline bool IsFirstLoginClientProgressMessage(int messageType) { switch (messageType) { case 171: // ENUM_CMDPACKET_CLIENT_SPEC_STATISTIC case 476: // ENUM_CMDPACKET_COMPATIBILITY_INDEX case 1320: // ENUM_CMDPACKET_LOG_DOMAIN_CONNECT case 1262: // ENUM_CMDPACKET_LOG_CONNECT_PROCESS case 593: // ENUM_CMDPACKET_SECURITY_STATUS case 8: // ENUM_CMDPACKET_GET_USERINFO return true; default: return false; } } // The auto-connect timer can run inside the native startup message pump. // Opening a socket while the packet processor is uninitialized loses LOGIN: // native 02261E30 returns 80000001, but 03456304 still consumes the packet. constexpr unsigned int kPacketProcessorPollMs = 50; constexpr unsigned long long kPacketProcessorWaitLimitMs = 30000; enum class PacketProcessorReadiness { NotReady, Ready, ReadFailed }; enum class AutoConnectAction { Ignore, Wait, Connect, TimedOut, ReadFailed }; struct AutoConnectReadiness { long generation = 0; unsigned long long startedAt = 0; bool pending = false; bool waited = false; void Begin(long newGeneration, unsigned long long now) { generation = newGeneration; startedAt = now; pending = true; waited = false; } void Cancel() { pending = false; } AutoConnectAction Poll(long currentGeneration, unsigned long long now, PacketProcessorReadiness readiness) { if (!pending || generation != currentGeneration) { return AutoConnectAction::Ignore; } if (readiness == PacketProcessorReadiness::ReadFailed) { pending = false; return AutoConnectAction::ReadFailed; } if (readiness == PacketProcessorReadiness::Ready) { pending = false; // Claim exactly one connect for this directory. return AutoConnectAction::Connect; } waited = true; if (now - startedAt >= kPacketProcessorWaitLimitMs) { pending = false; return AutoConnectAction::TimedOut; } return AutoConnectAction::Wait; } }; struct WatchdogState { long watchdogGeneration; long currentGeneration; bool active; bool serverDataSeen; bool clientDataSeen; long recoveryCount; }; inline bool ShouldRecover(const WatchdogState& state) { // Native ChannelConnect success is only a transport candidate. The first // login is healthy once the client emits any proved post-login progress. // serverDataSeen is diagnostic only: a stale native socket can prevent the // receive hook from observing LOGIN even when the server already sent it. return state.active && state.recoveryCount < kMaxFirstLoginRecoveryAttempts && state.watchdogGeneration > 0 && state.currentGeneration == state.watchdogGeneration && !state.clientDataSeen; } inline bool ShouldRecoverAfterRejectedRetry( long initialSuccessGeneration, long retryGeneration, bool clientDataSeen, long recoveryCount, bool retryConnected) { return initialSuccessGeneration > 0 && retryGeneration > initialSuccessGeneration && !clientDataSeen && recoveryCount < kMaxFirstLoginRecoveryAttempts && !retryConnected; } inline bool ShouldForceReleaseStaleSocket( bool residentResetSucceeded, bool socketPresent) { return residentResetSucceeded && socketPresent; } // A proved login must not permanently disable recovery after disconnect. // Only a rejected later directory connect with no live transport may reset; // a successful connect or a still-owned socket always remains native-owned. inline bool ShouldRecoverDisconnectedRetry(long initialGeneration, long retryGeneration, bool clientProgress, bool connected, bool transportReadable, bool socketAbsent, long attempts) { return initialGeneration > 0 && retryGeneration > initialGeneration && clientProgress && !connected && transportReadable && socketAbsent && attempts < kMaxFirstLoginRecoveryAttempts; } } // namespace dnf90_first_login_recovery