Project_WL/AgentScripts/WL813q2_Probe.cs

701 lines
38 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WL-813q2 프로브 — 결과 화면 핫픽스 실측(에디트 모드 · Play 0 · #813).
// run_script --file AgentScripts/WL813q2_Probe.cs --entry WL813q2_Probe.<Entry>
// Overlap · Watchdog · Reset · Rows · Gc · All
// 로그는 <worktree>/Logs/WL813q2_*.txt (Assets 밖 · 미추적).
//
// 🔴 에디트 모드에서 `Time.timeScale` 에 쓰지 않는다(ProjectSettings/TimeManager.asset 오염 · 811b-fix).
// DeathFlow.HoldWorld 도 `Application.isPlaying` 가드가 있어 에디트 모드에선 대입하지 않는다.
// 🔴 씬·프리팹을 저장하지 않는다. 만든 노드는 finally 에서 전부 DestroyImmediate.
// 🔴 SO 값은 메모리에서만 바꾸고 finally 에서 원복한다(AssetDatabase.SaveAssets 호출 0).
// 🔴 Gameplay 는 읽기·구독·공개 API 호출만 한다(파일 수정 0). `RunDirector.s_phase` 는 리플렉션 **읽기/쓰기**로
// "런 중/런 밖"만 흉내 낸다(런 시작·종료 부작용을 피하려고 StartRun/EndRun 을 쓰지 않는다).
using System;
using System.IO;
using System.Reflection;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using WL.Combat.Core;
using WL.Combat.Run;
using WL.Combat.Survival;
using WL.UI;
public static class WL813q2_Probe
{
const BindingFlags NP = BindingFlags.Instance | BindingFlags.NonPublic;
const BindingFlags SP = BindingFlags.Static | BindingFlags.NonPublic;
static int s_pass, s_fail;
// ─────────────────────────────────────────────────────────── 공통
static string LogDir
{
get
{
string d = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "Logs");
if (!Directory.Exists(d)) Directory.CreateDirectory(d);
return d;
}
}
static string Write(string name, string body)
{
string p = Path.Combine(LogDir, name);
File.WriteAllText(p, body, new UTF8Encoding(false));
return p;
}
static string Chk(StringBuilder sb, bool ok, string what)
{
if (ok) s_pass++; else s_fail++;
sb.AppendLine((ok ? " PASS " : " **FAIL** ") + what);
return what;
}
/// <summary>세로 1080×1920 기준 합성 캔버스 + `IngameUIs/WL_HUD` 대역(SO 경로와 같게).</summary>
static Transform BuildUiRoot(out GameObject root)
{
root = new GameObject("__WL813q2_Canvas", typeof(RectTransform));
root.hideFlags = HideFlags.DontSave;
var canvas = root.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = root.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
root.AddComponent<GraphicRaycaster>();
var ing = new GameObject("IngameUIs", typeof(RectTransform)).GetComponent<RectTransform>();
ing.gameObject.hideFlags = HideFlags.DontSave;
ing.SetParent(root.transform, false);
var hud = new GameObject("WL_HUD", typeof(RectTransform)).GetComponent<RectTransform>();
hud.gameObject.hideFlags = HideFlags.DontSave;
hud.SetParent(ing, false);
hud.anchorMin = hud.anchorMax = new Vector2(0.5f, 0.5f);
hud.pivot = new Vector2(0.5f, 0.5f);
hud.sizeDelta = new Vector2(1080f, 1920f);
// 결과 패널이 형제 맨 뒤/맨 앞으로 움직이는 것을 볼 수 있게 미끼 형제 2개
for (int i = 0; i < 2; i++)
{
var d = new GameObject("Decoy" + i, typeof(RectTransform)).GetComponent<RectTransform>();
d.gameObject.hideFlags = HideFlags.DontSave;
d.SetParent(hud, false);
}
return root.transform;
}
/// <summary>813z 프로브와 같은 가짜 메인 PC(사망 상태).</summary>
static PCActor MakePC()
{
var go = new GameObject("__WL813q2_PC");
go.hideFlags = HideFlags.DontSave;
var pc = go.AddComponent<PCActor>();
pc.m_Role = eRole.PC; pc.m_SubRole = eSubRol.None;
var stat = new ActorStatInfo(eRole.PC);
stat.Set_Stat(eStat.MaxHP, 1000d); stat.Set_Stat(eStat.HP, 0d);
typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat);
typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, false);
typeof(Actor).GetField("DeadStatus", NP).SetValue(pc, true);
return pc;
}
static void SetPhase(RunPhase p)
{
typeof(RunDirector).GetField("s_phase", SP).SetValue(null, p);
}
static RunPhase GetPhase()
{
return (RunPhase)typeof(RunDirector).GetField("s_phase", SP).GetValue(null);
}
static void OnReq(in ReviveRequestEvent e) { s_reqSeen++; }
static int s_reqSeen;
static void Kill(GameObject go) { if (go != null) UnityEngine.Object.DestroyImmediate(go); }
static RunResult FakeResult(RunOutcome outcome, float total, float bossClear, int kills, int bossKills)
{
return new RunResult
{
outcome = outcome, runIndex = 1, totalSec = total, runSeconds = 240f,
zoneClearSec = new float[] { 40f, -1f }, zoneKills = new int[] { 24, 11 },
zoneCount = 2, zonesCleared = 1,
bossGateSec = -1f, bossStartSec = -1f, bossClearSec = bossClear,
kills = kills, bossKills = bossKills, maxChain = 7,
maxDamage = 1234.4, maxDamageBurst = 8888.5,
levelFrom = 11, levelTo = 12,
lootByGrade = new int[] { 0, 5, 2, 0, 0, 0, 0, 0, 0, 0 }, lootTotal = 7, gold = 777,
deaths = 1, potionsUsed = 2, skillCasts = 9,
time = Time.unscaledTime, frame = Time.frameCount,
};
}
// ────────────────────────────────────────── ① D-18 결과 위 부활 겹침
public static string Overlap()
{
s_pass = s_fail = 0;
var sb = new StringBuilder();
sb.AppendLine("=== WL-813q2 ① D-18 결과 화면 위 부활 팝업 겹침 (에디트 모드 · Play 0) ===");
var ui = WLRunUiSettings.Instance;
var sv = WLSurvivalSettings.Instance;
if (ui == null || sv == null) { string b = sb.AppendLine("**FAIL** 설정 에셋 없음").ToString(); return b + "\n→ " + Write("WL813q2_overlap.txt", b); }
GameObject root = null; PCActor pc = null;
var phase0 = GetPhase();
int ov0 = WLSurvivalSettings.RuntimeOverride;
var ovr0 = DeathFlow.ReviveOverride;
bool skip0 = ui.skipRevivePopupAfterRunEnd, dying0 = ui.resultDeferWhileDying, lower0 = ui.resultLowerWhileRevive;
sb.AppendLine("근거: Q5 캡처 PD2_07 — 제목 \"시간 종료\"(= 결과가 **먼저** 떴다) 위에 부활 팝업이 겹쳐 4행이 가려졌다.");
sb.AppendLine("실측 전제: 813z 부활 팝업 sortingOrder = " + sv.reviveSortingOrder +
" · 결과 화면 SO sortingOrder = " + ui.resultSortingOrder + "(부활 중 " + ui.resultSortingOrderWhileRevive + ")");
sb.AppendLine();
try
{
WLSurvivalSettings.RuntimeOverride = 1;
DeathFlow.ReviveOverride = (p, r) => { };
DeathFlow.ReviveRequested.Add(OnReq);
s_reqSeen = 0;
var uiRoot = BuildUiRoot(out root);
sb.AppendLine("Bind: " + RunResultUI.Bind(uiRoot));
RunResultUI.Subscribe(true);
RunResultUI.ResetState();
DeathFlow.ResetDiagnostics();
pc = MakePC();
Chk(sb, RunResultUI.Bound, "결과 화면 노드 조립됨");
Chk(sb, RunResultUI.SortingOrder == ui.resultSortingOrder,
"전용 Canvas sortingOrder = " + RunResultUI.SortingOrder + " (SO " + ui.resultSortingOrder +
") < 부활 " + sv.reviveSortingOrder);
// ── A. 캡처와 같은 순서: RunEnded(결과 표시) → 그 뒤 사망 ────────
sb.AppendLine();
sb.AppendLine("A. 캡처 순서 재현 — RunEnded → 결과 표시 → **그 뒤** 사망(런 밖)");
SetPhase(RunPhase.Zones);
var r = FakeResult(RunOutcome.Timeout, 241f, -1f, 29, 0);
SetPhase(RunPhase.Ended); // EndRun 과 같은 상태(IsRunning=false)
WLRunUiProbeHooks.RaiseRunEnded(in r);
Chk(sb, RunResultUI.IsOpen, "RunEnded 로 결과 화면이 떴다(open=" + RunResultUI.IsOpen + " 제목 \"" + RunResultUI.TitleText + "\")");
Chk(sb, !RunDirector.IsRunning, "런 밖 상태(IsRunning=" + RunDirector.IsRunning + ")");
float t = 1000f;
DeathFlow.Tick(t);
DeathFlow.OnPCDied(pc); // ← 결과가 떠 있는 상태에서 사망
sb.AppendLine(" 사망 직후: DeathFlow=" + DeathFlow.State + " · 즉시부활 호출 " + RunResultUI.SkipPopupReviveCount +
"회 · " + RunResultUI.LastDeathLog);
Chk(sb, RunResultUI.SkipPopupReviveCount >= 1, "런 밖 사망 → 팝업 생략 즉시 부활을 불렀다(skipRevivePopupAfterRunEnd=1)");
int reqBefore = s_reqSeen;
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f); // 연출 끝 → acceptEarly 로 바로 부활
sb.AppendLine(" 연출 끝: DeathFlow=" + DeathFlow.State + " · ReviveRequested " + (s_reqSeen - reqBefore) +
"회(팝업이 1프레임 안에 닫힌다) · Revived=" + RunResultUI.RevivedSeen);
Chk(sb, DeathFlow.State == DeathState.None, "부활 완료 — DeathFlow.State=None(부활 대기 0 = 팝업이 남지 않는다)");
Chk(sb, DeathFlow.ReviveCount >= 1, "DeathFlow.ReviveCount=" + DeathFlow.ReviveCount);
Chk(sb, RunResultUI.IsOpen && !RunResultUI.Lowered,
"결과 화면은 그대로 · 재정렬 없음(open=" + RunResultUI.IsOpen + " lowered=" + RunResultUI.Lowered + ")");
// ── B. 스위치를 끄면 813q 동작(팝업이 뜬다) → 재정렬로 막는다 ──
sb.AppendLine();
sb.AppendLine("B. skipRevivePopupAfterRunEnd=0 (C8 롤백) — 팝업이 뜨는 경우 결과를 부활 **뒤로 재정렬**");
ui.skipRevivePopupAfterRunEnd = false;
DeathFlow.ResetDiagnostics();
RunResultUI.ResetState();
SetPhase(RunPhase.Ended);
WLRunUiProbeHooks.RaiseRunEnded(in r);
Chk(sb, RunResultUI.IsOpen, "결과 화면 재표시(open=" + RunResultUI.IsOpen + ")");
int sib0 = RunResultUI.SiblingIndex;
t = 2000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
sb.AppendLine(" 사망 후: " + RunResultUI.LastDeathLog);
Chk(sb, RunResultUI.SkipPopupReviveCount == 0, "즉시 부활 호출 0(스위치 off = 813q 동작)");
Chk(sb, RunResultUI.Lowered && RunResultUI.SortingOrder == ui.resultSortingOrderWhileRevive,
"결과를 부활 뒤로 재정렬 sortingOrder " + ui.resultSortingOrder + " → " + RunResultUI.SortingOrder +
" (부활 " + sv.reviveSortingOrder + " 아래)");
Chk(sb, RunResultUI.SiblingIndex == 0 && sib0 != 0,
"형제 순서 " + sib0 + " → " + RunResultUI.SiblingIndex + " (SetAsFirstSibling)");
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f); // AwaitingRevive
Chk(sb, DeathFlow.State == DeathState.AwaitingRevive, "부활 대기 상태(DeathFlow=" + DeathFlow.State + ")");
Chk(sb, RunResultUI.ReviveBusy, "「부활 우선」 판정 ON — 이유 = " + RunResultUI.ReviveBusyReason);
DeathFlow.Revive();
Chk(sb, !RunResultUI.Lowered && RunResultUI.SortingOrder == ui.resultSortingOrder,
"부활 완료 → 결과 화면 복귀 sortingOrder=" + RunResultUI.SortingOrder + " 형제=" + RunResultUI.SiblingIndex);
// ── C. 813q 원래 판정(ReviveDialog.Busy)만으로는 못 잡는다는 증명 ──
sb.AppendLine();
sb.AppendLine("C. 왜 813q 판정이 못 잡았나 — RunEnded 시점에는 아무도 죽지 않아 Busy=false 였다");
DeathFlow.ResetDiagnostics();
RunResultUI.ResetState();
SetPhase(RunPhase.Ended);
Chk(sb, !ReviveDialog.Busy && DeathFlow.State == DeathState.None,
"RunEnded 직전 실측: ReviveDialog.Busy=" + ReviveDialog.Busy + " · DeathFlow=" + DeathFlow.State +
" → 813q 의 미룸 조건은 어느 쪽으로도 걸리지 않는다");
ui.skipRevivePopupAfterRunEnd = true;
// ── D. 반대 순서(사망 먼저 → RunEnded)는 813q2 에서도 미뤄진다 ──
sb.AppendLine();
sb.AppendLine("D. 반대 순서 — 사망(런 중) → RunEnded → 부활 뒤에 결과가 뜬다");
RunResultUI.ResetState();
DeathFlow.ResetDiagnostics();
SetPhase(RunPhase.Zones);
t = 3000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
Chk(sb, DeathFlow.State == DeathState.Dying, "사망 연출 중(DeathFlow=" + DeathFlow.State + ")");
Chk(sb, RunResultUI.ReviveBusy, "「부활 우선」 ON — " + RunResultUI.ReviveBusyReason);
SetPhase(RunPhase.Ended);
WLRunUiProbeHooks.RaiseRunEnded(in r);
Chk(sb, !RunResultUI.IsOpen && RunResultUI.Pending,
"결과 화면 미룸(open=" + RunResultUI.IsOpen + " pending=" + RunResultUI.Pending +
" defer=" + RunResultUI.DeferredCount + ")");
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f);
DeathFlow.Revive();
RunResultUI.Tick(Time.unscaledTime);
Chk(sb, RunResultUI.IsOpen && !RunResultUI.Pending,
"부활 뒤 결과 화면 표시(open=" + RunResultUI.IsOpen + " shows=" + RunResultUI.ShownCount + ")");
sb.AppendLine();
sb.AppendLine(RunResultUI.Dump());
}
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
finally
{
ui.skipRevivePopupAfterRunEnd = skip0; ui.resultDeferWhileDying = dying0; ui.resultLowerWhileRevive = lower0;
DeathFlow.ReviveRequested.Remove(OnReq);
DeathFlow.ReviveOverride = ovr0;
DeathFlow.Abort("probe cleanup");
WLSurvivalSettings.RuntimeOverride = ov0;
RunResultUI.Teardown();
Kill(pc != null ? pc.gameObject : null);
Kill(root);
SetPhase(phase0);
}
sb.AppendLine();
sb.AppendLine("① 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
string body = sb.ToString();
return body + "\n→ " + Write("WL813q2_overlap.txt", body);
}
// ────────────────────────────────────── ② 워치독 자동 부활 경로(813z)
public static string Watchdog()
{
s_pass = s_fail = 0;
var sb = new StringBuilder();
sb.AppendLine("=== WL-813q2 ② 워치독 자동 부활 경로에서도 「부활 우선」이 걸리는가 (813z §1) ===");
var ui = WLRunUiSettings.Instance;
var sv = WLSurvivalSettings.Instance;
if (ui == null || sv == null) { string b = sb.AppendLine("**FAIL** 설정 에셋 없음").ToString(); return b + "\n→ " + Write("WL813q2_watchdog.txt", b); }
GameObject root = null; PCActor pc = null;
var phase0 = GetPhase();
int ov0 = WLSurvivalSettings.RuntimeOverride;
var ovr0 = DeathFlow.ReviveOverride;
bool dying0 = ui.resultDeferWhileDying;
try
{
WLSurvivalSettings.RuntimeOverride = 1;
DeathFlow.ReviveOverride = (p, r) => { };
DeathFlow.ReviveRequested.Add(OnReq);
s_reqSeen = 0;
var uiRoot = BuildUiRoot(out root);
RunResultUI.Bind(uiRoot);
RunResultUI.Subscribe(true);
RunResultUI.ResetState();
DeathFlow.ResetDiagnostics();
pc = MakePC();
sb.AppendLine("워치독 SO 실측: reviveWatchdogSeconds=" + sv.reviveWatchdogSeconds.ToString("F1") + "s");
SetPhase(RunPhase.Zones);
float t = 5000f;
DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
float req = t + sv.deathDisplaySeconds + 0.01f;
DeathFlow.Tick(req);
Chk(sb, DeathFlow.State == DeathState.AwaitingRevive, "부활 대기(DeathFlow=" + DeathFlow.State + ")");
// 런 종료가 부활 대기 중에 온다 → 결과는 미뤄진다(813q 는 Busy 로만 봤다)
SetPhase(RunPhase.Ended);
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 31, 0);
WLRunUiProbeHooks.RaiseRunEnded(in r);
Chk(sb, !RunResultUI.IsOpen && RunResultUI.Pending,
"결과 미룸 — 이유 = " + RunResultUI.ReviveBusyReason + " (ReviveDialog.Busy=" + ReviveDialog.Busy +
" 이라 813q 판정만으로는 못 막는다)");
// 워치독 시각 직전/직후
DeathFlow.Tick(req + sv.reviveWatchdogSeconds - 0.1f);
RunResultUI.Tick(Time.unscaledTime);
Chk(sb, !RunResultUI.IsOpen, "워치독 직전까지 계속 미룸(defer=" + RunResultUI.DeferredCount + ")");
DeathFlow.Tick(req + sv.reviveWatchdogSeconds + 0.02f);
sb.AppendLine(" 워치독: WatchdogCount=" + DeathFlow.WatchdogCount + " 대기 " + DeathFlow.LastWatchdogSeconds.ToString("F2") +
"s · auto 부활=" + DeathFlow.AutoReviveCount + " · RunResultUI.Revived=" + RunResultUI.RevivedSeen);
Chk(sb, DeathFlow.WatchdogCount == 1 && DeathFlow.State == DeathState.None, "워치독 자동 부활 1회 · 상태 None");
Chk(sb, RunResultUI.RevivedSeen >= 1, "RunResultUI 가 Revived 를 받았다(워치독 경로 포함)");
RunResultUI.Tick(Time.unscaledTime);
Chk(sb, RunResultUI.IsOpen && !RunResultUI.Pending, "워치독 부활 뒤 결과 화면 표시(shows=" + RunResultUI.ShownCount + ")");
// C8 — resultDeferWhileDying=0 이면 813q 동작(미룸 없음)
sb.AppendLine();
sb.AppendLine("C8 롤백 — resultDeferWhileDying=0 (813q 동작 복귀)");
ui.resultDeferWhileDying = false;
RunResultUI.ResetState(); DeathFlow.ResetDiagnostics();
SetPhase(RunPhase.Zones);
t = 6000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
DeathFlow.Tick(t + sv.deathDisplaySeconds + 0.01f);
Chk(sb, DeathFlow.State == DeathState.AwaitingRevive && !RunResultUI.ReviveBusy,
"DeathFlow=" + DeathFlow.State + " 인데 부활우선=" + RunResultUI.ReviveBusy + " (스위치 off = 813q 판정만)");
DeathFlow.Revive();
}
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
finally
{
ui.resultDeferWhileDying = dying0;
DeathFlow.ReviveRequested.Remove(OnReq);
DeathFlow.ReviveOverride = ovr0;
DeathFlow.Abort("probe cleanup");
WLSurvivalSettings.RuntimeOverride = ov0;
RunResultUI.Teardown();
Kill(pc != null ? pc.gameObject : null);
Kill(root);
SetPhase(phase0);
}
sb.AppendLine();
sb.AppendLine("② 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
string body2 = sb.ToString();
return body2 + "\n→ " + Write("WL813q2_watchdog.txt", body2);
}
// ─────────────────────────────── ③ D-23 「다음 런」 뒤 상태 리셋
public static string Reset()
{
s_pass = s_fail = 0;
var sb = new StringBuilder();
sb.AppendLine("=== WL-813q2 ③ D-23 「다음 런」 뒤 타이머 색 · 보스 바 · 배너 리셋 ===");
var ui = WLRunUiSettings.Instance;
if (ui == null) { string b = sb.AppendLine("**FAIL** WLRunUiSettings 없음").ToString(); return b + "\n→ " + Write("WL813q2_reset.txt", b); }
GameObject root = null, barGo = null, bannerGo = null;
bool reset0 = ui.timerResetColorOnRunStart;
var phase0 = GetPhase();
try
{
var uiRoot = BuildUiRoot(out root);
sb.AppendLine("RunHud Bind: " + RunHud.Bind(uiRoot));
RunHud.Subscribe(true);
RunHud.ResetState();
string warnHex = ColorUtility.ToHtmlStringRGB(ui.timerWarnColor);
string normHex = ColorUtility.ToHtmlStringRGB(ui.timerColor);
sb.AppendLine("SO 색 실측: 평상 #" + normHex + " · 경고 #" + warnHex + " (경고 구간 " + ui.timerWarnSeconds + "s)");
// 런 1 — 시작 → 마지막 30 s 경고색
WLRunUiProbeHooks.RaiseRunStarted(1, 240f, 2, "probe");
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == normHex,
"런 1 시작 타이머 색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) + " = 평상색 · 표시 \"" + RunHud.TimerText + "\"");
WLRunUiProbeHooks.RaiseRunTick(215f, 25f, 1, 3, 14, 1, 2, RunPhase.Zones, 1);
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == warnHex && RunHud.TimerWarn,
"남은 25 s → 경고색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) + " · 표시 \"" + RunHud.TimerText + "\"");
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 29, 0);
WLRunUiProbeHooks.RaiseRunEnded(in r);
// 런 2 (= 「다음 런」) — 색이 평상색으로 돌아와야 한다
WLRunUiProbeHooks.RaiseRunStarted(2, 240f, 2, "restart");
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == normHex,
"🔴 D-23 — 다음 런 시작 타이머 색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) +
" = 평상색 · 표시 \"" + RunHud.TimerText + "\" (색 쓰기 " + RunHud.TimerColorWrites + "회)");
bool zonesZero = true;
for (int i = 0; i < RunHud.ZoneCells; i++) if (RunHud.ZoneFill(i) > 0.0005f) zonesZero = false;
Chk(sb, zonesZero && RunHud.ZoneCells == 2, "존 게이지 리셋 — 칸 " + RunHud.ZoneCells + "개 전부 fill 0");
// C8 — 스위치를 끄면 813q 버그가 그대로 재현된다(= 이 스위치가 원인 지점이다)
sb.AppendLine();
sb.AppendLine("C8 롤백 — timerResetColorOnRunStart=0 (813q 동작 = D-23 재현)");
ui.timerResetColorOnRunStart = false;
WLRunUiProbeHooks.RaiseRunTick(215f, 25f, 1, 3, 14, 1, 2, RunPhase.Zones, 2);
WLRunUiProbeHooks.RaiseRunStarted(3, 240f, 2, "restart");
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == warnHex,
"스위치 off → 이전 런의 경고색 #" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) +
" 이 그대로 남는다 = D-23 원인 지점 확정");
ui.timerResetColorOnRunStart = true;
WLRunUiProbeHooks.RaiseRunStarted(4, 240f, 2, "restart");
Chk(sb, ColorUtility.ToHtmlStringRGB(RunHud.TimerColor) == normHex, "스위치 on 복귀 → 평상색");
// ── 보스 HP 바
sb.AppendLine();
sb.AppendLine("보스 HP 바 — 이전 런의 바가 남는가");
barGo = new GameObject("__WL813q2_BossBar", typeof(RectTransform));
barGo.hideFlags = HideFlags.DontSave;
barGo.transform.SetParent(root.transform, false);
var bar = barGo.AddComponent<BossHpBar>();
sb.AppendLine(" " + bar.Initialize());
sb.AppendLine(" " + bar.Bind(null, "테스트 보스", 200f)); // 합성(actor=null) 경로
Chk(sb, bar.Visible && bar.LastRatio > 0.99f, "바 표시 · ratio=" + bar.LastRatio.ToString("F3"));
bar.SetRatio(0.37f);
Chk(sb, Mathf.Abs(BossHpBar.RunBossHpRatio - 0.37f) < 0.001f,
"런 단위 보스 HP 실측값 = " + (BossHpBar.RunBossHpRatio * 100f).ToString("F1") + "% (결과 화면이 읽는 값)");
int reset1 = BossHpBar.RunResetCount;
WLRunUiProbeHooks.RaiseRunStarted(5, 240f, 2, "restart");
sb.AppendLine(" RunStarted 뒤: 리셋 " + (BossHpBar.RunResetCount - reset1) + "회 · visible=" + bar.Visible +
" · 런 HP=" + BossHpBar.RunBossHpRatio.ToString("F2") + " · 늦은구독 스캔=" + bar.LateScanCount);
Chk(sb, BossHpBar.RunResetCount == reset1 + 1, "RunStarted 를 받아 리셋했다");
Chk(sb, !bar.Visible, "🔴 D-23 — 이전 런의 보스 바가 사라졌다(visible=" + bar.Visible + ")");
Chk(sb, BossHpBar.RunBossHpRatio < 0f, "런 단위 보스 HP 값도 비워졌다(" + BossHpBar.RunBossHpRatio.ToString("F2") + ")");
ui.bossResetOnRunStart = false;
bar.Bind(null, "테스트 보스", 200f);
WLRunUiProbeHooks.RaiseRunStarted(6, 240f, 2, "restart");
Chk(sb, bar.Visible, "C8 롤백 bossResetOnRunStart=0 → 813q 동작(바가 남는다 · visible=" + bar.Visible + ")");
ui.bossResetOnRunStart = true;
// ── 보스 배너(재트리거 가드)
sb.AppendLine();
sb.AppendLine("보스 등장 배너 — 재트리거 가드가 다음 런에서 풀리는가");
bannerGo = new GameObject("__WL813q2_BossBanner", typeof(RectTransform));
bannerGo.hideFlags = HideFlags.DontSave;
bannerGo.transform.SetParent(root.transform, false);
var banner = bannerGo.AddComponent<BossBanner>();
sb.AppendLine(" " + banner.Initialize());
sb.AppendLine(" " + banner.Trigger("테스트 보스", true));
int show1 = banner.ShowCount;
sb.AppendLine(" 같은 런에서 재트리거: " + banner.Trigger("테스트 보스", true));
Chk(sb, banner.ShowCount == show1, "같은 런에서는 가드가 막는다(shows=" + banner.ShowCount + ")");
WLRunUiProbeHooks.RaiseRunStarted(7, 240f, 2, "restart");
Chk(sb, !banner.Visible, "다음 런 시작 → 배너 숨김(alpha=" + banner.BannerAlpha.ToString("F2") + ")");
sb.AppendLine(" 다음 런에서 재트리거: " + banner.Trigger("테스트 보스", true));
Chk(sb, banner.ShowCount == show1 + 1, "다음 런에서는 다시 뜬다 = 가드 해제됨(shows=" + banner.ShowCount + ")");
sb.AppendLine();
sb.AppendLine(RunHud.Dump());
}
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
finally
{
ui.timerResetColorOnRunStart = reset0;
ui.bossResetOnRunStart = true;
BossHpBar.RunBossHpRatio = -1f; BossHpBar.RunBossSeen = false;
RunHud.Teardown();
Kill(barGo); Kill(bannerGo); Kill(root);
SetPhase(phase0);
}
sb.AppendLine();
sb.AppendLine("③ 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
string body3 = sb.ToString();
return body3 + "\n→ " + Write("WL813q2_reset.txt", body3);
}
// ─────────────────────────────── ④ 결과 화면 행 보강(보스 미처치 · 엘리트)
public static string Rows()
{
s_pass = s_fail = 0;
var sb = new StringBuilder();
sb.AppendLine("=== WL-813q2 ④ 결과 화면 행 — 보스 미처치 문구 · 엘리트 처치 수 ===");
var ui = WLRunUiSettings.Instance;
if (ui == null) { string b = sb.AppendLine("**FAIL** WLRunUiSettings 없음").ToString(); return b + "\n→ " + Write("WL813q2_rows.txt", b); }
GameObject root = null, barGo = null;
var phase0 = GetPhase();
try
{
var uiRoot = BuildUiRoot(out root);
RunResultUI.Bind(uiRoot);
RunResultUI.Subscribe(true);
RunResultUI.ResetState();
BossHpBar.RunBossHpRatio = -1f; BossHpBar.RunBossSeen = false; // 앞 항목의 잔값 제거
sb.AppendLine("SO 행 배열 = " + ui.resultRows.Length + "개 · 엘리트 행 포함=" + ui.HasRow(WLRunResultRow.EliteKills));
Chk(sb, ui.HasRow(WLRunResultRow.EliteKills), "엘리트 처치 행이 SO 배열에 있다");
// 엘리트 집계 — 런 중에만 센다
SetPhase(RunPhase.Zones);
WLRunUiProbeHooks.RaiseRunStarted(1, 240f, 2, "probe");
for (int i = 0; i < 3; i++) WLRunUiProbeHooks.RaiseKilled(eSubRol.Elite);
for (int i = 0; i < 5; i++) WLRunUiProbeHooks.RaiseKilled(eSubRol.None);
WLRunUiProbeHooks.RaiseKilled(eSubRol.Boss);
Chk(sb, RunResultUI.EliteKills == 3, "런 중 엘리트 처치 " + RunResultUI.EliteKills + "건(일반 5 · 보스 1 은 세지 않는다)");
SetPhase(RunPhase.Ended);
WLRunUiProbeHooks.RaiseKilled(eSubRol.Elite); // 런 밖 처치는 세지 않는다
Chk(sb, RunResultUI.EliteKills == 3, "런 밖 엘리트 처치는 무시(" + RunResultUI.EliteKills + ")");
// 보스 미처치 + HP 를 모르는 경우
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 29, 0);
WLRunUiProbeHooks.RaiseRunEnded(in r);
string boss1 = RunResultUI.RowValueText(WLRunResultRow.BossClear);
string elite1 = RunResultUI.RowValueText(WLRunResultRow.EliteKills);
sb.AppendLine(" 보스 처치 행 = \"" + boss1 + "\" · 엘리트 처치 행 = \"" + elite1 + "\"");
Chk(sb, boss1 == ui.resultBossNotClearedText, "보스 미처치(HP 모름) → \"" + boss1 + "\"");
Chk(sb, elite1 == "3", "엘리트 처치 행 = \"" + elite1 + "\" (RunEnded 순간 스냅샷)");
// 보스 미처치 + 813c 바가 남긴 HP 실측값
barGo = new GameObject("__WL813q2_BossBar2", typeof(RectTransform));
barGo.hideFlags = HideFlags.DontSave;
barGo.transform.SetParent(root.transform, false);
var bar = barGo.AddComponent<BossHpBar>();
bar.Initialize();
bar.Bind(null, "테스트 보스", 200f);
bar.SetRatio(0.436f);
RunResultUI.ResetState();
WLRunUiProbeHooks.RaiseRunEnded(in r);
string boss2 = RunResultUI.RowValueText(WLRunResultRow.BossClear);
sb.AppendLine(" 바 실측 HP 43.6% 일 때 보스 처치 행 = \"" + boss2 + "\"");
Chk(sb, boss2.Contains("44"), "보스 미처치 + HP 남음 44 % 표기 — \"" + boss2 + "\"");
// 보스 처치한 런은 시간이 그대로 나온다
var r2 = FakeResult(RunOutcome.Win, 154f, 154f, 99, 1);
RunResultUI.ResetState();
WLRunUiProbeHooks.RaiseRunEnded(in r2);
string boss3 = RunResultUI.RowValueText(WLRunResultRow.BossClear);
Chk(sb, boss3 == "02:34", "보스 처치 런 = \"" + boss3 + "\" (mm:ss 그대로 · 813q 동작 불변)");
// C8 — UI 집계·UI HP 를 끄면 이전 동작
ui.resultBossHpFromUi = false; ui.resultCountElitesInUi = false;
RunResultUI.ResetState();
WLRunUiProbeHooks.RaiseRunEnded(in r);
Chk(sb, RunResultUI.RowValueText(WLRunResultRow.BossClear) == ui.resultBossNotClearedText &&
RunResultUI.RowValueText(WLRunResultRow.EliteKills) == "0",
"C8 롤백 — 보스 행 \"" + RunResultUI.RowValueText(WLRunResultRow.BossClear) + "\" · 엘리트 행 \"" +
RunResultUI.RowValueText(WLRunResultRow.EliteKills) + "\"");
ui.resultBossHpFromUi = true; ui.resultCountElitesInUi = true;
sb.AppendLine();
sb.AppendLine(RunResultUI.Dump());
}
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
finally
{
ui.resultBossHpFromUi = true; ui.resultCountElitesInUi = true;
RunResultUI.Teardown();
Kill(barGo); Kill(root);
SetPhase(phase0);
}
sb.AppendLine();
sb.AppendLine("④ 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
string body4 = sb.ToString();
return body4 + "\n→ " + Write("WL813q2_rows.txt", body4);
}
// ────────────────────────────────────────────────── ⑤ GC 0 · C8
public static string Gc()
{
s_pass = s_fail = 0;
var sb = new StringBuilder();
sb.AppendLine("=== WL-813q2 ⑤ GC 0 · C8 ===");
var ui = WLRunUiSettings.Instance;
if (ui == null) { string b = sb.AppendLine("**FAIL** WLRunUiSettings 없음").ToString(); return b + "\n→ " + Write("WL813q2_gc.txt", b); }
GameObject root = null;
var phase0 = GetPhase();
try
{
var uiRoot = BuildUiRoot(out root);
RunHud.Bind(uiRoot); RunHud.Subscribe(true); RunHud.ResetState();
RunResultUI.Bind(uiRoot); RunResultUI.Subscribe(true); RunResultUI.ResetState();
SetPhase(RunPhase.Zones);
WLRunUiProbeHooks.RaiseRunStarted(1, 240f, 2, "probe");
WLRunUiProbeHooks.RaiseRunTick(60f, 180f, 0, 5, 24, 0, 2, RunPhase.Zones, 1);
// 같은 초 100틱 = 표시 쓰기 0회 · 델타 0 B
GC.Collect(); GC.WaitForPendingFinalizers();
long m0 = GC.GetTotalMemory(true);
int tw0 = RunHud.TimerWrites, cw0 = RunHud.TimerColorWrites;
for (int i = 0; i < 100; i++) WLRunUiProbeHooks.RaiseRunTick(60f, 180f, 0, 5, 24, 0, 2, RunPhase.Zones, 1);
long m1 = GC.GetTotalMemory(false);
Chk(sb, RunHud.TimerWrites == tw0 && RunHud.TimerColorWrites == cw0,
"같은 초 100틱 — 타이머 쓰기 " + (RunHud.TimerWrites - tw0) + "회 · 색 쓰기 " + (RunHud.TimerColorWrites - cw0) + "회");
Chk(sb, m1 - m0 == 0, "Δ GetTotalMemory = " + (m1 - m0) + " B");
// Killed 100건(엘리트 집계 경로) — 문자열 0
GC.Collect(); GC.WaitForPendingFinalizers();
long k0 = GC.GetTotalMemory(true);
for (int i = 0; i < 100; i++) WLRunUiProbeHooks.RaiseKilled(i % 2 == 0 ? eSubRol.Elite : eSubRol.None);
long k1 = GC.GetTotalMemory(false);
Chk(sb, k1 - k0 == 0, "Killed 100건(엘리트 50) Δ = " + (k1 - k0) + " B · 집계=" + RunResultUI.EliteKills);
// 재정렬 100회 왕복 — 노드 재생성 0(Canvas·Raycaster 는 1개씩 재사용)
// 🔴 재정렬 함수는 진단 문자열을 돌려주므로 그 자체는 할당이 있다. 다만 **매 프레임 경로가 아니다**
// (사망 1회 · 부활 1회). 여기서는 "노드를 다시 만들지 않는다"만 PASS 기준으로 둔다.
long l0 = GC.GetTotalMemory(true);
for (int i = 0; i < 100; i++) { RunResultUI.LowerBehindRevive(); RunResultUI.RaiseToFront(); }
long l1 = GC.GetTotalMemory(false);
var resRoot = root.transform.Find("IngameUIs/WL_HUD/WL_RunResult");
int canvasN = resRoot != null ? resRoot.GetComponents<Canvas>().Length : -1;
int rayN = resRoot != null ? resRoot.GetComponents<GraphicRaycaster>().Length : -1;
Chk(sb, RunResultUI.LowerCount == 100 && RunResultUI.RaiseCount == 100, "재정렬 왕복 100회(내림 " +
RunResultUI.LowerCount + " / 복귀 " + RunResultUI.RaiseCount + ") · Δ = " + (l1 - l0) + " B(진단 문자열분)");
Chk(sb, canvasN == 1 && rayN == 1, "노드 재생성 0 — Canvas " + canvasN + "개 · GraphicRaycaster " + rayN + "개");
// C8 — 전체 off
sb.AppendLine();
WLRunUiSettings.RuntimeDisabled = true;
RunResultUI.ResetState();
var r = FakeResult(RunOutcome.Timeout, 240f, -1f, 29, 0);
SetPhase(RunPhase.Ended);
WLRunUiProbeHooks.RaiseRunEnded(in r);
RunResultUI.Tick(Time.unscaledTime);
Chk(sb, !RunResultUI.IsOpen, "C8 RuntimeDisabled — 결과 화면 표시 0(open=" + RunResultUI.IsOpen + ")");
Chk(sb, !RunResultUI.ReviveBusy, "C8 중 부활우선 판정도 무동작(" + RunResultUI.ReviveBusyReason + ")");
WLRunUiSettings.RuntimeDisabled = false;
Chk(sb, WLRunUiSettings.Enabled, "C8 복구 — Enabled=" + WLRunUiSettings.Enabled);
}
catch (Exception ex) { s_fail++; sb.AppendLine("**FAIL** 예외 " + ex.GetType().Name + " — " + ex.Message + "\n" + ex.StackTrace); }
finally
{
WLRunUiSettings.RuntimeDisabled = false;
RunHud.Teardown(); RunResultUI.Teardown();
Kill(root);
SetPhase(phase0);
}
sb.AppendLine();
sb.AppendLine("⑤ 결과 = " + s_pass + " PASS / " + s_fail + " FAIL");
string body5 = sb.ToString();
return body5 + "\n→ " + Write("WL813q2_gc.txt", body5);
}
// ───────────────────────────────────────────────────────── 전체
public static string All()
{
var sb = new StringBuilder();
int pass = 0, fail = 0;
string[] parts = new string[5];
parts[0] = Overlap(); pass += s_pass; fail += s_fail;
parts[1] = Watchdog(); pass += s_pass; fail += s_fail;
parts[2] = Reset(); pass += s_pass; fail += s_fail;
parts[3] = Rows(); pass += s_pass; fail += s_fail;
parts[4] = Gc(); pass += s_pass; fail += s_fail;
for (int i = 0; i < parts.Length; i++) sb.AppendLine(parts[i]).AppendLine();
sb.AppendLine("======================================================");
sb.AppendLine("WL-813q2 전체 = " + pass + " PASS / " + fail + " FAIL");
string body = sb.ToString();
return "WL-813q2 전체 = " + pass + " PASS / " + fail + " FAIL\n→ " + Write("WL813q2_all.txt", body);
}
}