// ───────────────────────────────────────────────────────────────────────────── // WLErrorGuardSettings.cs — 인게임 에러 모달 차단 · 투사체 null 타깃 가드 값의 단일 출처(SOT · C45) // // PD 지시 #813 · 발주서 WL-813x2 §1-2/§1-3 (2026-09-09) · 근거 = Q7 D-35 // // 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLErrorGuardSettings.asset // → Resources.Load("WL/WLErrorGuardSettings") 로 1회 로드 후 캐시(기존 WL 설정 SO 관례). // 에셋이 없거나 enabled = false 면 Enabled == false → 훅 무접촉 · 투사체 가드 무동작 = 원본 동작 100%(C8 롤백 1순위). // // 🔴 발주서는 이 값들을 `WLCombatCoreSettings.asset` 에 넣으라고 했으나, 그 클래스는 // `Assets/WL/Combat/Core/**` 에 있고 완료 기준 ⓑ 가 **Core 0 줄**을 요구한다. // → 충돌을 피해 같은 Resources/WL 폴더에 **신규 SO** 로 분리했다(보고서에 기재). // // 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것(Actor/ProjectileBase 가 Assembly-CSharp 에 있다). // ───────────────────────────────────────────────────────────────────────────── using UnityEngine; namespace WL.Combat.Diagnostics { [CreateAssetMenu(fileName = "WLErrorGuardSettings", menuName = "WL/Error Guard Settings", order = 360)] public sealed class WLErrorGuardSettings : ScriptableObject { public const string ResourcesPath = "WL/WLErrorGuardSettings"; static WLErrorGuardSettings s_cached; static bool s_lookupDone; /// 설정 에셋(1회 로드 후 캐시). 없으면 null. public static WLErrorGuardSettings Instance { get { if (!s_lookupDone) { s_cached = Resources.Load(ResourcesPath); s_lookupDone = true; } return s_cached; } } /// 진단·A/B 전용 런타임 스위치. Play 종료 시 도메인 리로드로 자동 false. public static bool RuntimeDisabled; /// 기능이 살아 있는가 = 에셋 존재 · enabled · 런타임 스위치 off. public static bool Enabled { get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; } } /// 테스트용: 캐시를 비워 다음 접근 때 다시 Resources.Load 한다. public static void ClearCache() { s_cached = null; s_lookupDone = false; } // ─────────────────────────────────────────── 전체 스위치 [Header("전체 스위치")] [Tooltip("끄면 에러 훅 무접촉 · 투사체 가드 무동작 = 원본 100%. 롤백 1순위.")] public bool enabled = true; // ─────────────────────────────────────────── 시연용 모달 차단 (§1-3) [Header("시연용 에러 모달 차단 (RunErrorModalGuard · Q7 D-35)")] [Tooltip("런 중 Unity Error/Exception 이 나도 원본 ErrorLogHookManager 의 인게임 파란 모달을 띄우지 않는다. " + "구현 = 훅 컴포넌트를 런 동안만 enabled=false 로 내려 OnDisable 이 Application.logMessageReceived 구독을 스스로 해제하게 한다" + "(원본 파일 0 줄 수정). 런이 끝나면 다시 enabled=true 로 되돌린다.")] public bool suppressErrorModalInRun = true; [Tooltip("1 = 런 중(RunDirector.IsRunning)에만 차단 · 0 = Play 내내 차단. 시연 중 로비 오류까지 막으려면 0.")] public bool suppressOnlyWhileRunning = true; [Tooltip("WL-813o2(Q9 D-42) — 런이 끝난 뒤(RunPhase.Ended)에도 훅을 내린 채 둔다. " + "결과창과 [다음 런] 대기 구간까지 모달 0 · 다음 RunStarted(Phase=Zones)에 복원된다. 끄면 813x2 원래 동작.")] public bool suppressUntilNextRunStart = true; [Tooltip("차단한(그리고 차단하지 않은) Error/Exception 을 파일로 남긴다. 끄면 카운터만 센다.")] public bool writeErrorFile = true; [Tooltip("로그 파일 폴더(프로젝트 루트 기준 상대 · 에디터). 빌드에서는 persistentDataPath 아래에 같은 이름으로 만든다.")] public string errorFileDir = "Screenshots_WL/WL813x2"; [Tooltip("스택트레이스도 같이 남긴다(발생원 추적용).")] public bool includeStack = true; [Tooltip("한 세션에 남길 최대 건수(로그 폭주 방지). 초과분은 카운터만 올라간다.")] public int maxEntriesPerSession = 200; [Tooltip("훅 상태 폴링 주기(초 · unscaled). 이 주기마다 런 진행 상태만 비교하고 바뀔 때만 손댄다(GC 0).")] public float pollSeconds = 0.25f; [Tooltip("훅 컴포넌트를 못 찾았을 때의 최대 재탐색 횟수(폴링 기준). 초과하면 탐색을 멈춘다(GC 0).")] public int hookSearchTries = 40; // ─────────────────────────────────────────── 투사체 null 타깃 가드 (§1-2) [Header("투사체 null 타깃 가드 (WLProjectileStart · D-35 원인)")] [Tooltip("ProjectileBase.Set 의 시작 위치 스위치에서 target 이 null 이면 shooter 위치로 대체한다. " + "끄면 원본 그대로(= target 을 그대로 역참조 → NullReferenceException).")] public bool projectileNullTargetGuard = true; [Tooltip("가드가 실제로 null 을 잡았을 때 Debug.Log 1줄을 남긴다(Error 아님 = 모달 유발 0). 시연 빌드에서는 0 권장.")] public bool projectileGuardLog = false; } }