240 lines
12 KiB
C#
240 lines
12 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
|
// RunErrorModalGuard.cs — 런 중 인게임 에러 모달 차단 + WL 에러 로그 파일 (Q7 D-35)
|
|
//
|
|
// PD 지시 #813 · 발주서 WL-813x2 §1-3 (2026-09-09)
|
|
//
|
|
// ■ 사슬(813x·813z 실측)
|
|
// Unity Error/Exception → `Assets/Script/Util/ErrorLogHookManager.cs:26 HandleLog`
|
|
// → `:33 Popup.Ins.Set(ePopupType.One, 174, null, null, logString)` = 화면 중앙 파란 모달.
|
|
// Q7 R1 t≈181 s(보스 HP 54 %)에 이 모달이 보스전을 덮었다(캡처 R1_14).
|
|
//
|
|
// ■ 왜 "앞단 구독"만으로는 못 막나 — 실측
|
|
// `Application.logMessageReceived` 는 **취소·소비가 불가능한 멀티캐스트 델리게이트**다.
|
|
// 우리가 먼저 구독해도(SubsystemRegistration 등) 원본 핸들러는 그대로 뒤이어 호출된다.
|
|
// → 유일하게 원본 파일 0 줄로 막는 방법 = **훅 컴포넌트를 내려서 그쪽이 스스로 구독을 끊게 하는 것**.
|
|
// `ErrorLogHookManager.OnDisable()` 이 `Application.logMessageReceived -= HandleLog` 를 부른다(:21-24).
|
|
// 런이 끝나면 `enabled = true` → `OnEnable()` 이 다시 구독한다. 원본 무수정 · 가역.
|
|
//
|
|
// ■ 대신 우리가 같은 이벤트를 구독해 파일로 남긴다(모달 대신 로그).
|
|
// Screenshots_WL/WL813x2/errors_<HHmmss>.txt (never_commit 경로 · 스택 포함)
|
|
//
|
|
// 🔴 이 파일은 절대 Debug.LogError/LogException 을 쓰지 않는다(813z 규칙 — 그 자체가 모달이 된다).
|
|
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
|
|
namespace WL.Combat.Diagnostics
|
|
{
|
|
/// <summary>런 중 원본 에러 모달을 내리고, 대신 WL 로그 파일에 남긴다.</summary>
|
|
public static class RunErrorModalGuard
|
|
{
|
|
// ─────────────────────────────────────────── 실측 카운터(프로브가 읽는다)
|
|
/// <summary>우리 핸들러가 받은 모든 로그 건수(타입 무관 · 구독이 살아 있는지의 증거).</summary>
|
|
public static int SeenAnyCount;
|
|
/// <summary>우리 핸들러가 받은 Error/Exception/Assert 건수.</summary>
|
|
public static int CapturedCount;
|
|
/// <summary>그중 훅이 내려가 있어 모달이 뜨지 않은 건수.</summary>
|
|
public static int SuppressedCount;
|
|
/// <summary>파일에 실제로 기록한 건수.</summary>
|
|
public static int WrittenCount;
|
|
/// <summary>우리가 훅을 내려둔 상태인가.</summary>
|
|
public static bool HookDisabledByUs;
|
|
/// <summary>훅 컴포넌트를 찾았는가.</summary>
|
|
public static bool HookFound { get { return s_hook != null; } }
|
|
/// <summary>훅 컴포넌트의 현재 enabled(못 찾았으면 false).</summary>
|
|
public static bool HookEnabled { get { return s_hook != null && s_hook.enabled; } }
|
|
/// <summary>이번 세션의 로그 파일 경로(아직 안 만들었으면 "").</summary>
|
|
public static string LastFilePath = "";
|
|
/// <summary>마지막으로 받은 로그 본문(진단용).</summary>
|
|
public static string LastLog = "";
|
|
/// <summary>구독 상태.</summary>
|
|
public static bool Subscribed { get { return s_subscribed; } }
|
|
|
|
/// <summary>프로브·A/B 전용 강제 상태. 0 = SO 판정 · 1 = 무조건 차단 · -1 = 무조건 해제.</summary>
|
|
public static int ForceState;
|
|
|
|
// ─────────────────────────────────────────── 내부
|
|
static bool s_subscribed;
|
|
static ErrorLogHookManager s_hook;
|
|
static int s_searchTries;
|
|
static float s_pollLeft;
|
|
static readonly StringBuilder s_sb = new StringBuilder(512);
|
|
|
|
static WLErrorGuardSettings Cfg { get { return WLErrorGuardSettings.Instance; } }
|
|
|
|
// ─────────────────────────────────────────── 부팅
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
static void Boot()
|
|
{
|
|
if (!WLErrorGuardSettings.Enabled) return; // 꺼져 있으면 구독 0 · 오브젝트 0(C8)
|
|
Subscribe();
|
|
RunErrorModalGuardRunner.Ensure();
|
|
}
|
|
|
|
/// <summary>로그 이벤트 구독(중복 안전).</summary>
|
|
public static void Subscribe()
|
|
{
|
|
if (s_subscribed) return;
|
|
Application.logMessageReceived += OnLog;
|
|
s_subscribed = true;
|
|
}
|
|
|
|
/// <summary>구독 해제 + 훅 원복.</summary>
|
|
public static void Unsubscribe()
|
|
{
|
|
if (s_subscribed)
|
|
{
|
|
Application.logMessageReceived -= OnLog;
|
|
s_subscribed = false;
|
|
}
|
|
Restore();
|
|
}
|
|
|
|
/// <summary>테스트 초기화(프로브 전용).</summary>
|
|
public static void ResetCounters()
|
|
{
|
|
SeenAnyCount = CapturedCount = SuppressedCount = WrittenCount = 0;
|
|
LastFilePath = "";
|
|
LastLog = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 테스트 전용: 훅 인스턴스를 직접 주입한다.
|
|
/// 에디트 모드 프로브의 임시 훅은 HideAndDontSave 라 FindFirstObjectByType 이 찾지 못하기 때문이다
|
|
/// (실물 훅은 `ErrorLogHookManager.cs:8` 이 hideFlags 없이 만드는 평범한 GameObject 라 탐색된다).
|
|
/// </summary>
|
|
public static void SetHookForTest(ErrorLogHookManager hook) { s_hook = hook; s_searchTries = 0; }
|
|
|
|
/// <summary>훅 캐시·탐색 횟수까지 비운다(프로브 전용 · 임시 훅을 갈아끼울 때).</summary>
|
|
public static void ResetHookCache()
|
|
{
|
|
Restore();
|
|
s_hook = null;
|
|
s_searchTries = 0;
|
|
s_pollLeft = 0f;
|
|
}
|
|
|
|
// ─────────────────────────────────────────── 판정
|
|
/// <summary>지금 모달을 막아야 하는가.</summary>
|
|
public static bool ShouldSuppress()
|
|
{
|
|
if (ForceState > 0) return true;
|
|
if (ForceState < 0) return false;
|
|
var cfg = Cfg;
|
|
if (cfg == null || !cfg.enabled || WLErrorGuardSettings.RuntimeDisabled) return false;
|
|
if (!cfg.suppressErrorModalInRun) return false;
|
|
if (!cfg.suppressOnlyWhileRunning) return true;
|
|
return WL.Combat.Run.RunDirector.IsRunning;
|
|
}
|
|
|
|
// ─────────────────────────────────────────── 틱(러너가 부른다 · 프로브도 직접 부를 수 있다)
|
|
public static void Tick(float unscaledDt)
|
|
{
|
|
var cfg = Cfg;
|
|
if (cfg == null || !cfg.enabled || WLErrorGuardSettings.RuntimeDisabled) { Restore(); return; }
|
|
|
|
s_pollLeft -= unscaledDt;
|
|
if (s_pollLeft > 0f) return;
|
|
s_pollLeft = cfg.pollSeconds > 0.01f ? cfg.pollSeconds : 0.25f;
|
|
|
|
Apply(ShouldSuppress(), cfg);
|
|
}
|
|
|
|
/// <summary>훅 상태를 목표 상태로 맞춘다(바뀔 때만 손댄다 · GC 0).</summary>
|
|
public static void Apply(bool suppress, WLErrorGuardSettings cfg)
|
|
{
|
|
var hook = FindHook(cfg);
|
|
if (hook == null) return;
|
|
|
|
if (suppress)
|
|
{
|
|
if (hook.enabled) { hook.enabled = false; HookDisabledByUs = true; } // OnDisable 이 구독 해제
|
|
}
|
|
else if (HookDisabledByUs)
|
|
{
|
|
if (!hook.enabled) hook.enabled = true; // OnEnable 이 재구독
|
|
HookDisabledByUs = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>우리가 내려둔 훅을 원상복구한다.</summary>
|
|
public static void Restore()
|
|
{
|
|
if (!HookDisabledByUs) return;
|
|
if (s_hook != null && !s_hook.enabled) s_hook.enabled = true;
|
|
HookDisabledByUs = false;
|
|
}
|
|
|
|
static ErrorLogHookManager FindHook(WLErrorGuardSettings cfg)
|
|
{
|
|
if (s_hook != null) return s_hook;
|
|
int max = cfg != null && cfg.hookSearchTries > 0 ? cfg.hookSearchTries : 40;
|
|
if (s_searchTries >= max) return null; // 탐색 폭주 방지(GC 0)
|
|
s_searchTries++;
|
|
s_hook = UnityEngine.Object.FindFirstObjectByType<ErrorLogHookManager>(FindObjectsInactive.Include);
|
|
return s_hook;
|
|
}
|
|
|
|
// ─────────────────────────────────────────── 로그 수신
|
|
static void OnLog(string logString, string stackTrace, LogType type)
|
|
{
|
|
SeenAnyCount++;
|
|
if (type != LogType.Error && type != LogType.Exception && type != LogType.Assert) return;
|
|
|
|
var cfg = Cfg;
|
|
if (cfg == null || !cfg.enabled) return;
|
|
|
|
CapturedCount++;
|
|
bool suppressed = HookDisabledByUs;
|
|
if (suppressed) SuppressedCount++;
|
|
LastLog = logString;
|
|
|
|
if (!cfg.writeErrorFile) return;
|
|
if (cfg.maxEntriesPerSession > 0 && WrittenCount >= cfg.maxEntriesPerSession) return;
|
|
WriteEntry(cfg, type, logString, stackTrace, suppressed);
|
|
}
|
|
|
|
static void WriteEntry(WLErrorGuardSettings cfg, LogType type, string logString, string stackTrace, bool suppressed)
|
|
{
|
|
try
|
|
{
|
|
string dir = LogDir(cfg);
|
|
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
|
if (string.IsNullOrEmpty(LastFilePath))
|
|
LastFilePath = Path.Combine(dir, "errors_" + DateTime.Now.ToString("HHmmss") + ".txt");
|
|
|
|
s_sb.Length = 0;
|
|
s_sb.Append(DateTime.Now.ToString("HH:mm:ss")).Append(" [").Append(type.ToString()).Append("] ")
|
|
.Append(suppressed ? "모달차단" : "모달표시")
|
|
.Append(" · 런 ").Append(WL.Combat.Run.RunDirector.Phase.ToString())
|
|
.Append(" t=").Append(WL.Combat.Run.RunDirector.Elapsed.ToString("F0")).Append("s")
|
|
.Append('\n').Append(logString).Append('\n');
|
|
if (cfg.includeStack && !string.IsNullOrEmpty(stackTrace))
|
|
{
|
|
s_sb.Append(stackTrace);
|
|
if (stackTrace[stackTrace.Length - 1] != '\n') s_sb.Append('\n');
|
|
}
|
|
s_sb.Append("--------\n");
|
|
|
|
File.AppendAllText(LastFilePath, s_sb.ToString());
|
|
WrittenCount++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LastLog = "[WL813x2] 로그 파일 기록 실패: " + ex.Message; // 🔴 Debug.LogError 금지(813z)
|
|
}
|
|
}
|
|
|
|
static string LogDir(WLErrorGuardSettings cfg)
|
|
{
|
|
string sub = cfg != null && !string.IsNullOrEmpty(cfg.errorFileDir) ? cfg.errorFileDir : "Screenshots_WL/WL813x2";
|
|
string root = Application.isEditor ? Path.GetDirectoryName(Application.dataPath) : Application.persistentDataPath;
|
|
return string.IsNullOrEmpty(root) ? sub : Path.Combine(root, sub);
|
|
}
|
|
}
|
|
}
|