374 lines
22 KiB
C#
374 lines
22 KiB
C#
|
|
// WL813x2_Probe.cs — #813x2(D-35 NullReference 모달) 에디트 모드 검증 프로브
|
||
|
|
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 디스크 에셋/씬 무수정(런타임 클론 SO 를 리플렉션으로 주입 · 임시 오브젝트 즉시 파괴)
|
||
|
|
//
|
||
|
|
// 상주 에디터: unity command run_script --project-path <wt> --file AgentScripts/WL813x2_Probe.cs --entry WL813x2_Probe.RunAll
|
||
|
|
// 결과: Console + AgentScripts/WL813x2_PROBE.txt(커밋) + Screenshots_WL/WL813x2/probe_<HHmmss>.txt (RESULT PASS / RESULT FAIL n)
|
||
|
|
//
|
||
|
|
// 검사(발주서 WL-813x2 §1-2/§1-3/§1-4)
|
||
|
|
// ① 에셋 실측: 디스크 WLErrorGuardSettings 10값 · Enabled
|
||
|
|
// ② 원인 재현 + 가드: WLProjectileStart.TargetPos — target null → shooter 대체 · 둘 다 null → 현재 유지 · target 정상 → 원본과 동일 식
|
||
|
|
// ③ C8(투사체): projectileNullTargetGuard=0 → 원본 그대로 NullReferenceException (= D-35 원인 재현)
|
||
|
|
// ④ 구독 왕복: Subscribe() 후 Debug.Log(Info) 1줄 → SeenAnyCount +1 (콘솔 error 0 — 일부러 에러를 내지 않는다)
|
||
|
|
// ⑤ 가짜 예외 → 모달 0 · 로그 파일 1건(스택 포함 · 「모달차단」 표기)
|
||
|
|
// ⑥ 훅 토글: 임시 ErrorLogHookManager → Apply(true) enabled=false · Apply(false) 원복 · OnEnable/OnDisable 존재
|
||
|
|
// ⑦ 스위치 판정: 런 중에만/항상/off 3분기
|
||
|
|
// ⑧ GC 0: Tick 200회 + TargetPos 200회
|
||
|
|
// ⑨ C8(전체): enabled=0 → 판정 false · 훅 무접촉
|
||
|
|
// ⑩ 정리(구독 해제 · 임시 오브젝트 파괴 · 캐시 복구 · 씬 dirty 0)
|
||
|
|
//
|
||
|
|
// 에디트 모드 한계: ErrorLogHookManager 는 비활성 오브젝트에 붙인다(Awake 의 DontDestroyOnLoad 가 에디트 모드에서 돌지 않게).
|
||
|
|
// 따라서 enabled=false 가 실제로 OnDisable → logMessageReceived 구독 해제를 부르는 것은 **Play 필요 = 「미확인」**.
|
||
|
|
// (Unity 계약상 활성 오브젝트의 컴포넌트 enabled 토글은 OnDisable/OnEnable 을 부른다 — 메서드 존재는 ⑥ 에서 실측한다.)
|
||
|
|
|
||
|
|
using System;
|
||
|
|
using System.IO;
|
||
|
|
using System.Reflection;
|
||
|
|
using System.Text;
|
||
|
|
using UnityEngine;
|
||
|
|
using WL.Combat.Diagnostics;
|
||
|
|
|
||
|
|
public static class WL813x2_Probe
|
||
|
|
{
|
||
|
|
const string OutDir = "Screenshots_WL/WL813x2";
|
||
|
|
const string OutCommit = "AgentScripts/WL813x2_PROBE.txt";
|
||
|
|
const BindingFlags SNP = BindingFlags.NonPublic | BindingFlags.Static;
|
||
|
|
|
||
|
|
static WLErrorGuardSettings s_clone;
|
||
|
|
static GameObject s_tmpHookGo, s_tmpActorGo;
|
||
|
|
|
||
|
|
public static object RunAll()
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
sb.AppendLine("# WL813x2 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0)");
|
||
|
|
int fail = 0;
|
||
|
|
bool dirty0 = UnityEngine.SceneManagement.SceneManager.GetActiveScene().isDirty;
|
||
|
|
|
||
|
|
try { fail += Asset(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ① 예외 " + ex); }
|
||
|
|
try { fail += ProjGuard(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ② 예외 " + ex); }
|
||
|
|
try { fail += ProjC8(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ③ 예외 " + ex); }
|
||
|
|
try { fail += Subscribe(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ④ 예외 " + ex); }
|
||
|
|
try { fail += FakeException(sb); }catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑤ 예외 " + ex); }
|
||
|
|
try { fail += HookToggle(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑥ 예외 " + ex); }
|
||
|
|
try { fail += SwitchLogic(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑦ 예외 " + ex); }
|
||
|
|
try { fail += Gc(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑧ 예외 " + ex); }
|
||
|
|
try { fail += C8(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑨ 예외 " + ex); }
|
||
|
|
try { fail += Cleanup(sb, dirty0); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑩ 예외 " + ex); }
|
||
|
|
|
||
|
|
sb.AppendLine(fail == 0 ? "RESULT PASS" : "RESULT FAIL " + fail);
|
||
|
|
var s = sb.ToString();
|
||
|
|
Debug.Log(s);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
string root = Path.GetDirectoryName(Application.dataPath);
|
||
|
|
Directory.CreateDirectory(Path.Combine(root, OutDir));
|
||
|
|
File.WriteAllText(Path.Combine(root, OutDir + "/probe_" + DateTime.Now.ToString("HHmmss") + ".txt"), s);
|
||
|
|
File.WriteAllText(Path.Combine(root, OutCommit), s);
|
||
|
|
}
|
||
|
|
catch (Exception ex) { Debug.Log("[WL813x2] 프로브 파일 기록 실패 " + ex.Message); }
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── 유틸
|
||
|
|
static int Chk(StringBuilder sb, bool ok, string label, string got)
|
||
|
|
{
|
||
|
|
sb.AppendLine((ok ? "[PASS] " : "[FAIL] ") + label + " — " + got);
|
||
|
|
return ok ? 0 : 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>디스크 에셋을 건드리지 않고 클론 SO 를 캐시에 주입한다.</summary>
|
||
|
|
static WLErrorGuardSettings Inject()
|
||
|
|
{
|
||
|
|
var src = WLErrorGuardSettings.Instance;
|
||
|
|
if (src == null) return null;
|
||
|
|
if (s_clone == null) s_clone = UnityEngine.Object.Instantiate(src);
|
||
|
|
var t = typeof(WLErrorGuardSettings);
|
||
|
|
t.GetField("s_cached", SNP).SetValue(null, s_clone);
|
||
|
|
t.GetField("s_lookupDone", SNP).SetValue(null, true);
|
||
|
|
return s_clone;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void RestoreCache()
|
||
|
|
{
|
||
|
|
var t = typeof(WLErrorGuardSettings);
|
||
|
|
t.GetField("s_cached", SNP).SetValue(null, null);
|
||
|
|
t.GetField("s_lookupDone", SNP).SetValue(null, false);
|
||
|
|
WLErrorGuardSettings.RuntimeDisabled = false;
|
||
|
|
RunErrorModalGuard.ForceState = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static Actor NewActor(Vector3 pos)
|
||
|
|
{
|
||
|
|
if (s_tmpActorGo == null)
|
||
|
|
{
|
||
|
|
s_tmpActorGo = new GameObject("[WL813x2] probe actor root");
|
||
|
|
s_tmpActorGo.hideFlags = HideFlags.HideAndDontSave;
|
||
|
|
s_tmpActorGo.SetActive(false); // Awake 를 돌리지 않는다
|
||
|
|
}
|
||
|
|
var go = new GameObject("a");
|
||
|
|
go.hideFlags = HideFlags.HideAndDontSave;
|
||
|
|
go.transform.SetParent(s_tmpActorGo.transform);
|
||
|
|
go.transform.position = pos;
|
||
|
|
return go.AddComponent<Actor>();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ① 에셋
|
||
|
|
static int Asset(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
var cfg = WLErrorGuardSettings.Instance;
|
||
|
|
f += Chk(sb, cfg != null, "① 에셋 로드", cfg != null ? "WL/WLErrorGuardSettings 존재" : "null(Resources 경로 확인)");
|
||
|
|
if (cfg == null) return f;
|
||
|
|
f += Chk(sb, WLErrorGuardSettings.Enabled, "① Enabled", "enabled=" + cfg.enabled + " RuntimeDisabled=" + WLErrorGuardSettings.RuntimeDisabled);
|
||
|
|
f += Chk(sb, cfg.suppressErrorModalInRun, "① suppressErrorModalInRun 기본 1", cfg.suppressErrorModalInRun.ToString());
|
||
|
|
f += Chk(sb, cfg.suppressOnlyWhileRunning, "① suppressOnlyWhileRunning 기본 1", cfg.suppressOnlyWhileRunning.ToString());
|
||
|
|
f += Chk(sb, cfg.writeErrorFile, "① writeErrorFile 기본 1", cfg.writeErrorFile.ToString());
|
||
|
|
f += Chk(sb, cfg.includeStack, "① includeStack 기본 1", cfg.includeStack.ToString());
|
||
|
|
f += Chk(sb, cfg.projectileNullTargetGuard, "① projectileNullTargetGuard 기본 1", cfg.projectileNullTargetGuard.ToString());
|
||
|
|
f += Chk(sb, !cfg.projectileGuardLog, "① projectileGuardLog 기본 0", cfg.projectileGuardLog.ToString());
|
||
|
|
sb.AppendLine(" 값: errorFileDir=" + cfg.errorFileDir + " · maxEntriesPerSession=" + cfg.maxEntriesPerSession +
|
||
|
|
" · pollSeconds=" + cfg.pollSeconds + " · hookSearchTries=" + cfg.hookSearchTries);
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ② 투사체 가드
|
||
|
|
static int ProjGuard(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
Inject();
|
||
|
|
WLProjectileStart.ResetCounters();
|
||
|
|
|
||
|
|
var cur = new Vector3(7f, 8f, 9f);
|
||
|
|
|
||
|
|
// (a) target null · shooter 살아 있음 → shooter 위치
|
||
|
|
var shooter = NewActor(new Vector3(1f, 0f, 2f));
|
||
|
|
var d1 = new ProjectileData { shooter = shooter, target = null, eStartPos = eProjectileStartPos.Target };
|
||
|
|
Vector3 p1 = WLProjectileStart.TargetPos(d1, cur);
|
||
|
|
f += Chk(sb, p1 == shooter.Get_CenterPositionFoward() && WLProjectileStart.NullTargetCount == 1,
|
||
|
|
"② target null → shooter 대체 · 예외 0", "pos=" + p1 + " null누적=" + WLProjectileStart.NullTargetCount);
|
||
|
|
|
||
|
|
// (b) target·shooter 모두 null → 현재 위치 유지
|
||
|
|
var d2 = new ProjectileData { shooter = null, target = null, eStartPos = eProjectileStartPos.TargetCenter };
|
||
|
|
Vector3 p2 = WLProjectileStart.TargetPos(d2, cur);
|
||
|
|
f += Chk(sb, p2 == cur && WLProjectileStart.NoShooterCount == 1,
|
||
|
|
"② target·shooter null → 현재 위치 유지", "pos=" + p2 + " noShooter=" + WLProjectileStart.NoShooterCount);
|
||
|
|
|
||
|
|
// (c) target 정상 → 원본과 완전히 동일한 식
|
||
|
|
var target = NewActor(new Vector3(-3f, 1f, 4f));
|
||
|
|
var d3 = new ProjectileData { shooter = shooter, target = target, eStartPos = eProjectileStartPos.Target };
|
||
|
|
int n3 = WLProjectileStart.NullTargetCount; // (a)+(b) 로 2
|
||
|
|
Vector3 p3 = WLProjectileStart.TargetPos(d3, cur);
|
||
|
|
f += Chk(sb, p3 == target.Get_CenterPositionFoward() && WLProjectileStart.NullTargetCount == n3,
|
||
|
|
"② target 정상 → 원본 식과 동일 · 카운터 불변", "pos=" + p3 + " 원본=" + target.Get_CenterPositionFoward() + " null누적=" + n3 + "→" + WLProjectileStart.NullTargetCount);
|
||
|
|
|
||
|
|
// (d) 파괴된 Actor(가짜 null · 813y) 도 잡는가
|
||
|
|
var doomed = NewActor(new Vector3(5f, 0f, 5f));
|
||
|
|
var d4 = new ProjectileData { shooter = shooter, target = doomed, eStartPos = eProjectileStartPos.Target };
|
||
|
|
int n4 = WLProjectileStart.NullTargetCount;
|
||
|
|
UnityEngine.Object.DestroyImmediate(doomed.gameObject);
|
||
|
|
Vector3 p4 = WLProjectileStart.TargetPos(d4, cur);
|
||
|
|
f += Chk(sb, p4 == shooter.Get_CenterPositionFoward() && WLProjectileStart.NullTargetCount == n4 + 1,
|
||
|
|
"② 파괴된 target(가짜 null) 도 == 로 걸림", "pos=" + p4 + " null누적=" + n4 + "→" + WLProjectileStart.NullTargetCount);
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ③ C8(투사체) = D-35 원인 재현
|
||
|
|
static int ProjC8(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
var cfg = Inject();
|
||
|
|
if (cfg == null) return Chk(sb, false, "③ C8", "SO 없음");
|
||
|
|
cfg.projectileNullTargetGuard = false;
|
||
|
|
bool threw = false;
|
||
|
|
string kind = "";
|
||
|
|
try { WLProjectileStart.TargetPos(new ProjectileData { shooter = null, target = null }, Vector3.zero); }
|
||
|
|
catch (NullReferenceException) { threw = true; kind = "NullReferenceException"; }
|
||
|
|
catch (Exception ex) { threw = true; kind = ex.GetType().Name; }
|
||
|
|
cfg.projectileNullTargetGuard = true;
|
||
|
|
f += Chk(sb, threw && kind == "NullReferenceException",
|
||
|
|
"③ 가드 off → 원본 동작(NRE) 재현 = D-35 원인 확정", kind == "" ? "예외 없음" : kind);
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ④ 구독 왕복
|
||
|
|
static int Subscribe(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
Inject();
|
||
|
|
RunErrorModalGuard.ResetCounters();
|
||
|
|
RunErrorModalGuard.Subscribe();
|
||
|
|
int before = RunErrorModalGuard.SeenAnyCount;
|
||
|
|
Debug.Log("[WL813x2] 프로브 구독 확인용 정보 로그(에러 아님)");
|
||
|
|
f += Chk(sb, RunErrorModalGuard.Subscribed && RunErrorModalGuard.SeenAnyCount > before,
|
||
|
|
"④ logMessageReceived 구독 살아 있음(Info 1줄 · 콘솔 error 0)",
|
||
|
|
"Subscribed=" + RunErrorModalGuard.Subscribed + " SeenAny=" + before + "→" + RunErrorModalGuard.SeenAnyCount);
|
||
|
|
f += Chk(sb, RunErrorModalGuard.CapturedCount == 0, "④ Info 는 Error 로 세지 않는다", "Captured=" + RunErrorModalGuard.CapturedCount);
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ⑤ 가짜 예외 → 모달 0 · 파일 1건
|
||
|
|
static int FakeException(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
var cfg = Inject();
|
||
|
|
RunErrorModalGuard.ResetCounters();
|
||
|
|
|
||
|
|
// 실제 콘솔 에러를 내지 않고(=콘솔 error 0 유지) 핸들러 본체를 그대로 호출한다.
|
||
|
|
var onLog = typeof(RunErrorModalGuard).GetMethod("OnLog", SNP);
|
||
|
|
if (onLog == null) return Chk(sb, false, "⑤ OnLog 리플렉션", "메서드 없음");
|
||
|
|
|
||
|
|
// 훅이 내려간 상태(런 중)로 가정 — 공개 정적 필드에 직접 세팅
|
||
|
|
typeof(RunErrorModalGuard).GetField("HookDisabledByUs").SetValue(null, true);
|
||
|
|
|
||
|
|
string msg = "NullReferenceException: Object reference not set to an instance of an object";
|
||
|
|
string stk = " at ProjectileBase.Set (ProjectileData _data) in Assets/Script/Character/Projectile/ProjectileBase.cs:233";
|
||
|
|
onLog.Invoke(null, new object[] { msg, stk, LogType.Exception });
|
||
|
|
|
||
|
|
bool wrote = RunErrorModalGuard.WrittenCount == 1 && !string.IsNullOrEmpty(RunErrorModalGuard.LastFilePath) && File.Exists(RunErrorModalGuard.LastFilePath);
|
||
|
|
string body = wrote ? File.ReadAllText(RunErrorModalGuard.LastFilePath) : "";
|
||
|
|
f += Chk(sb, RunErrorModalGuard.CapturedCount == 1 && RunErrorModalGuard.SuppressedCount == 1,
|
||
|
|
"⑤ 예외 1건 수신 · 모달차단으로 집계", "Captured=" + RunErrorModalGuard.CapturedCount + " Suppressed=" + RunErrorModalGuard.SuppressedCount);
|
||
|
|
f += Chk(sb, wrote, "⑤ WL 로그 파일 1건 생성", wrote ? RunErrorModalGuard.LastFilePath : "미생성");
|
||
|
|
f += Chk(sb, body.Contains(msg) && body.Contains("모달차단"), "⑤ 본문 = 문구 + 「모달차단」", wrote ? body.Length + "byte" : "-");
|
||
|
|
f += Chk(sb, cfg == null || !cfg.includeStack || body.Contains("ProjectileBase.cs:233"), "⑤ 스택 포함", wrote ? "포함" : "-");
|
||
|
|
|
||
|
|
typeof(RunErrorModalGuard).GetField("HookDisabledByUs").SetValue(null, false);
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ⑥ 훅 토글
|
||
|
|
static int HookToggle(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
Inject();
|
||
|
|
var cfg = WLErrorGuardSettings.Instance;
|
||
|
|
|
||
|
|
// 비활성 오브젝트에 붙인다(Awake/DontDestroyOnLoad 가 에디트 모드에서 돌지 않게).
|
||
|
|
s_tmpHookGo = new GameObject("[WL813x2] probe ErrorLogHookManager");
|
||
|
|
s_tmpHookGo.hideFlags = HideFlags.HideAndDontSave;
|
||
|
|
s_tmpHookGo.SetActive(false);
|
||
|
|
var hook = s_tmpHookGo.AddComponent<ErrorLogHookManager>();
|
||
|
|
|
||
|
|
var t = typeof(ErrorLogHookManager);
|
||
|
|
bool hasOnEnable = t.GetMethod("OnEnable", BindingFlags.NonPublic | BindingFlags.Instance) != null;
|
||
|
|
bool hasOnDisable = t.GetMethod("OnDisable", BindingFlags.NonPublic | BindingFlags.Instance) != null;
|
||
|
|
f += Chk(sb, hasOnEnable && hasOnDisable,
|
||
|
|
"⑥ 원본 훅에 OnEnable/OnDisable 존재(= enabled 토글로 구독 왕복)", "OnEnable=" + hasOnEnable + " OnDisable=" + hasOnDisable);
|
||
|
|
|
||
|
|
// 훅이 아예 없을 때 무해한가(탐색 실패 경로)
|
||
|
|
RunErrorModalGuard.ResetHookCache();
|
||
|
|
RunErrorModalGuard.SetHookForTest(null);
|
||
|
|
RunErrorModalGuard.Apply(true, cfg);
|
||
|
|
f += Chk(sb, !RunErrorModalGuard.HookFound && !RunErrorModalGuard.HookDisabledByUs,
|
||
|
|
"⑥ 훅 미발견 시 무해(예외 0 · 상태 불변)", "HookFound=" + RunErrorModalGuard.HookFound);
|
||
|
|
|
||
|
|
// 임시 훅은 HideAndDontSave 라 FindFirstObjectByType 이 못 찾는다 → 직접 주입해 토글만 실측
|
||
|
|
RunErrorModalGuard.SetHookForTest(hook);
|
||
|
|
RunErrorModalGuard.Apply(true, cfg);
|
||
|
|
f += Chk(sb, !hook.enabled && RunErrorModalGuard.HookDisabledByUs,
|
||
|
|
"⑥ Apply(true) → 훅 enabled=false (원본 파일 0줄)", "enabled=" + hook.enabled + " byUs=" + RunErrorModalGuard.HookDisabledByUs);
|
||
|
|
|
||
|
|
RunErrorModalGuard.Apply(false, cfg);
|
||
|
|
f += Chk(sb, hook.enabled && !RunErrorModalGuard.HookDisabledByUs,
|
||
|
|
"⑥ Apply(false) → 훅 원복", "enabled=" + hook.enabled + " byUs=" + RunErrorModalGuard.HookDisabledByUs);
|
||
|
|
|
||
|
|
RunErrorModalGuard.Apply(true, cfg);
|
||
|
|
RunErrorModalGuard.Restore();
|
||
|
|
f += Chk(sb, hook.enabled && !RunErrorModalGuard.HookDisabledByUs, "⑥ Restore() 안전망", "enabled=" + hook.enabled);
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ⑦ 스위치 판정
|
||
|
|
static int SwitchLogic(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
var cfg = Inject();
|
||
|
|
if (cfg == null) return Chk(sb, false, "⑦ 스위치", "SO 없음");
|
||
|
|
|
||
|
|
cfg.suppressErrorModalInRun = true; cfg.suppressOnlyWhileRunning = true;
|
||
|
|
bool running = WL.Combat.Run.RunDirector.IsRunning;
|
||
|
|
f += Chk(sb, RunErrorModalGuard.ShouldSuppress() == running,
|
||
|
|
"⑦ 런 중에만 차단(에디트 모드 = 런 아님)", "IsRunning=" + running + " Phase=" + WL.Combat.Run.RunDirector.Phase + " → " + RunErrorModalGuard.ShouldSuppress());
|
||
|
|
|
||
|
|
cfg.suppressOnlyWhileRunning = false;
|
||
|
|
f += Chk(sb, RunErrorModalGuard.ShouldSuppress(), "⑦ suppressOnlyWhileRunning=0 → 항상 차단", RunErrorModalGuard.ShouldSuppress().ToString());
|
||
|
|
|
||
|
|
cfg.suppressErrorModalInRun = false;
|
||
|
|
f += Chk(sb, !RunErrorModalGuard.ShouldSuppress(), "⑦ suppressErrorModalInRun=0 → 원본 모달", RunErrorModalGuard.ShouldSuppress().ToString());
|
||
|
|
|
||
|
|
cfg.suppressErrorModalInRun = true; cfg.suppressOnlyWhileRunning = true;
|
||
|
|
RunErrorModalGuard.ForceState = 1;
|
||
|
|
f += Chk(sb, RunErrorModalGuard.ShouldSuppress(), "⑦ ForceState=1 강제 차단", "true");
|
||
|
|
RunErrorModalGuard.ForceState = -1;
|
||
|
|
f += Chk(sb, !RunErrorModalGuard.ShouldSuppress(), "⑦ ForceState=-1 강제 해제", "false");
|
||
|
|
RunErrorModalGuard.ForceState = 0;
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ⑧ GC 0
|
||
|
|
static int Gc(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
var cfg = Inject();
|
||
|
|
var cur = new Vector3(1f, 2f, 3f);
|
||
|
|
var shooter = NewActor(Vector3.zero);
|
||
|
|
var d = new ProjectileData { shooter = shooter, target = null, eStartPos = eProjectileStartPos.Target };
|
||
|
|
|
||
|
|
for (int i = 0; i < 20; i++) { RunErrorModalGuard.Tick(0.001f); WLProjectileStart.TargetPos(d, cur); } // 워밍업
|
||
|
|
GC.Collect(); GC.WaitForPendingFinalizers();
|
||
|
|
long m0 = GC.GetTotalMemory(false);
|
||
|
|
for (int i = 0; i < 200; i++) { RunErrorModalGuard.Tick(0.001f); WLProjectileStart.TargetPos(d, cur); }
|
||
|
|
long m1 = GC.GetTotalMemory(false);
|
||
|
|
long d1 = m1 - m0;
|
||
|
|
f += Chk(sb, d1 <= 4096, "⑧ GC 0(Tick 200 + TargetPos 200 · 허용 4 KB)", d1 + " byte");
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ⑨ C8 전체
|
||
|
|
static int C8(StringBuilder sb)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
var cfg = Inject();
|
||
|
|
if (cfg == null) return Chk(sb, false, "⑨ C8", "SO 없음");
|
||
|
|
|
||
|
|
cfg.enabled = false;
|
||
|
|
f += Chk(sb, !RunErrorModalGuard.ShouldSuppress(), "⑨ enabled=0 → 차단 판정 false", "false");
|
||
|
|
|
||
|
|
var hook = s_tmpHookGo != null ? s_tmpHookGo.GetComponent<ErrorLogHookManager>() : null;
|
||
|
|
if (hook != null)
|
||
|
|
{
|
||
|
|
hook.enabled = true;
|
||
|
|
RunErrorModalGuard.Tick(999f);
|
||
|
|
f += Chk(sb, hook.enabled && !RunErrorModalGuard.HookDisabledByUs, "⑨ enabled=0 → 훅 무접촉", "hook.enabled=" + hook.enabled);
|
||
|
|
}
|
||
|
|
|
||
|
|
WLErrorGuardSettings.RuntimeDisabled = true;
|
||
|
|
cfg.enabled = true;
|
||
|
|
f += Chk(sb, !RunErrorModalGuard.ShouldSuppress(), "⑨ RuntimeDisabled → 차단 판정 false", "false");
|
||
|
|
WLErrorGuardSettings.RuntimeDisabled = false;
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─────────────────────────────────────────── ⑩ 정리
|
||
|
|
static int Cleanup(StringBuilder sb, bool dirty0)
|
||
|
|
{
|
||
|
|
int f = 0;
|
||
|
|
RunErrorModalGuard.Unsubscribe();
|
||
|
|
RunErrorModalGuard.ResetHookCache();
|
||
|
|
RunErrorModalGuard.ResetCounters();
|
||
|
|
WLProjectileStart.ResetCounters();
|
||
|
|
|
||
|
|
if (s_tmpHookGo != null) { UnityEngine.Object.DestroyImmediate(s_tmpHookGo); s_tmpHookGo = null; }
|
||
|
|
if (s_tmpActorGo != null) { UnityEngine.Object.DestroyImmediate(s_tmpActorGo); s_tmpActorGo = null; }
|
||
|
|
if (s_clone != null) { UnityEngine.Object.DestroyImmediate(s_clone); s_clone = null; }
|
||
|
|
RestoreCache();
|
||
|
|
|
||
|
|
bool dirty = UnityEngine.SceneManagement.SceneManager.GetActiveScene().isDirty;
|
||
|
|
f += Chk(sb, !RunErrorModalGuard.Subscribed, "⑩ 구독 해제", RunErrorModalGuard.Subscribed.ToString());
|
||
|
|
f += Chk(sb, dirty == dirty0, "⑩ 씬 dirty 불변", dirty0 + "→" + dirty);
|
||
|
|
f += Chk(sb, WLErrorGuardSettings.Instance != null && WLErrorGuardSettings.Instance.enabled, "⑩ 디스크 SO 복구", "enabled=1");
|
||
|
|
var live = UnityEngine.Object.FindObjectsByType<ErrorLogHookManager>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||
|
|
f += Chk(sb, live.Length == 0, "⑩ 임시 훅 잔존 0", live.Length.ToString());
|
||
|
|
return f;
|
||
|
|
}
|
||
|
|
}
|