import { DEV } from 'cc/env'; import { UIID } from '../game/ConfigRes'; import { PfDiagnostics } from './PfDiagnostics'; /** * 战斗诊断:仅上报高置信度真 bug(胜利未结算等)。 * 预警波次卡住类日志仅 DEV 本地,不上传平台。 */ export class BattleDiagnostics { private static _pauseSinceMs = 0; private static _pauseReason = ''; private static _lastStuckLogMs = 0; private static _warnTrackSinceMs = 0; private static _lastWarnStuckLogMs = 0; /** 真卡死判定:pause 持续超过此时间才考虑上报 */ private static readonly STUCK_PAUSE_MS = 45000; /** 同局同 tag 上传间隔 */ private static readonly STUCK_LOG_COOLDOWN_MS = 600000; /** 结算流程中其它 pause 不报;waveWin|panels=none 本身就是要抓的 bug */ private static readonly _SETTLEMENT_PAUSE = new Set([ 'BattleFail', 'BattleRevive', ]); private static readonly _INIT_PAUSE = new Set([ 'battleInit', 'unknown', ]); private static readonly _INTENTIONAL_PAUSE = new Set([ 'TetriDoc', 'adRecoverHp', 'useShovel', 'mechanismEdit', 'SkillSelect', 'TurnTable', 'BattlePause', 'BattleRevive', 'BattleFail', 'newMechanism', 'videoAd', ]); /** 仅这些 reason + panels=none 才上传 STUCK_PAUSE(已有 orphan heal 的不报) */ private static readonly _STUCK_UPLOAD_REASONS = new Set([ 'waveWarn', 'waveWin', ]); static resetBattle(): void { this._pauseSinceMs = 0; this._pauseReason = ''; this._warnTrackSinceMs = 0; this._lastStuckLogMs = 0; this._lastWarnStuckLogMs = 0; } static onPause(reason = 'unknown'): void { this._pauseSinceMs = Date.now(); this._pauseReason = reason; } static getPauseReason(): string { return this._pauseReason; } static onTetriDocPauseHeal(): void {} static onWaveWarnPauseHeal(): void {} static onSkillSelectPauseHeal(): void {} static onTurnTablePauseHeal(): void {} static onResumeSuccess(): void { this._pauseSinceMs = 0; this._pauseReason = ''; } static onResumeBlocked(_blocker: string): void {} static onWarnStart(_wave: number, _fixLeft: number): void { this._warnTrackSinceMs = Date.now(); } static onWarnFinished(_wave: number): void { this._warnTrackSinceMs = 0; } static onWarnForceFinish(_wave: number, _warnMs: number): void { this._warnTrackSinceMs = 0; } static tick(battle: { IsGamePause?: boolean; IsGameOver?: boolean; IsWarnTimeActive?: boolean; IsEdit?: boolean; CurentWaveNum?: number; tetriWarnTimeFix?: number; } | null | undefined): void { if (!battle || battle !== gg.game?.CurentBattle || battle.IsGameOver) return; const now = Date.now(); this._tickWaveWarn(battle, now); if (battle.IsGamePause && this._pauseSinceMs > 0 && !battle.IsWarnTimeActive) { const pausedMs = now - this._pauseSinceMs; if ( pausedMs >= this.STUCK_PAUSE_MS && now - this._lastStuckLogMs >= this.STUCK_LOG_COOLDOWN_MS && this._shouldReportStuckPause(this._pauseReason) ) { this._lastStuckLogMs = now; PfDiagnostics.write( 'BATTLE', 'STUCK_PAUSE', `ms=${pausedMs}|reason=${this._pauseReason}|${this._panelFlags()}`, this.STUCK_LOG_COOLDOWN_MS, 'error', ); } } } private static _tickWaveWarn( battle: { IsWarnTimeActive?: boolean; IsGamePause?: boolean; CurentWaveNum?: number; tetriWarnTimeFix?: number; }, now: number, ): void { if (!battle.IsWarnTimeActive) { this._warnTrackSinceMs = 0; return; } if (this._warnTrackSinceMs <= 0) { this._warnTrackSinceMs = now; } const warnMs = now - this._warnTrackSinceMs; const hasUiModal = this._hasUiModal(); const fixLeft = battle.tetriWarnTimeFix ?? 0; const wave = battle.CurentWaveNum ?? 0; // 预警倒计时结束后未开波:仅本地记录,不上传平台(避免 panels=warn 类日志刷屏) if ( fixLeft <= 0 && !hasUiModal && warnMs >= 8000 && now - this._lastWarnStuckLogMs >= this.STUCK_LOG_COOLDOWN_MS ) { this._lastWarnStuckLogMs = now; if (DEV) { console.warn( '[BattleDiag][local]', `WAVE_WARN_STUCK wave=${wave}|fix=${fixLeft}|pause=${battle.IsGamePause ? 1 : 0}|ms=${warnMs}|${this._panelFlags()}`, ); } } } private static _shouldReportStuckPause(reason: string): boolean { if (!this._STUCK_UPLOAD_REASONS.has(reason)) return false; if (this._hasUiModal()) return false; const b = gg.game?.CurentBattle; if (b?.IsEdit) return false; if (this._SETTLEMENT_PAUSE.has(reason)) return false; if (this._INIT_PAUSE.has(reason)) return false; if (this._INTENTIONAL_PAUSE.has(reason)) return false; return true; } static getResumeBlocker(): string | null { if (gg.ui.panelIsShow(UIID.newMechanism)) return 'newMechanism'; if (gg.ui.panelIsShow(UIID.BattlePause)) return 'BattlePause'; if (gg.ui.panelIsShow(UIID.BattleFail)) return 'BattleFail'; if (gg.ui.panelIsShow(UIID.SkillSelect)) return 'SkillSelect'; if (gg.ui.panelIsOpening(UIID.SkillSelect)) return 'SkillSelect'; if (gg.ui.panelIsShow(UIID.TurnTable)) return 'TurnTable'; if (gg.ui.panelIsOpening(UIID.TurnTable)) return 'TurnTable'; if (gg.ui.panelIsShow(UIID.BattleRevive)) return 'BattleRevive'; if (gg.ui.panelIsShow(UIID.TetriDoc)) return 'TetriDoc'; const b = gg.game?.CurentBattle; if (b?.IsGachaModeChoujiang) return 'GachaModeChoujiang'; if (b?.IsEdit) return 'IsEdit'; if (b?.IsWarnTimeActive && b?.IsGamePause) return 'waveWarn'; return null; } private static _hasUiModal(): boolean { if (gg.ui.panelIsShow(UIID.newMechanism)) return true; if (gg.ui.panelIsShow(UIID.BattlePause)) return true; if (gg.ui.panelIsShow(UIID.BattleFail)) return true; if (gg.ui.panelIsShow(UIID.SkillSelect)) return true; if (gg.ui.panelIsOpening(UIID.SkillSelect)) return true; if (gg.ui.panelIsShow(UIID.TurnTable)) return true; if (gg.ui.panelIsOpening(UIID.TurnTable)) return true; if (gg.ui.panelIsShow(UIID.BattleRevive)) return true; if (gg.ui.panelIsShow(UIID.TetriDoc)) return true; if (gg.ui.panelIsOpening(UIID.TetriDoc)) return true; const b = gg.game?.CurentBattle; if (b?.IsGachaModeChoujiang) return true; if (b?.IsEdit) return true; return false; } private static _panelFlags(): string { const ids: Array<[string, number]> = [ ['SS', UIID.SkillSelect], ['TT', UIID.TurnTable], ['BP', UIID.BattlePause], ['BF', UIID.BattleFail], ['BR', UIID.BattleRevive], ['TD', UIID.TetriDoc], ]; const on = ids.filter(([, id]) => gg.ui.panelIsShow(id)).map(([k]) => k); const b = gg.game?.CurentBattle; const extra: string[] = []; if (b?.IsWarnTimeActive) extra.push('warn'); if (b?.IsGachaModeChoujiang) extra.push('gacha'); const parts = [...on, ...extra]; return parts.length ? `panels=${parts.join(',')}` : 'panels=none'; } }