Project_WL/AgentScripts/WL815d_Probe.cs

715 lines
42 KiB
C#
Raw 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-815d 프로브 — 스테이지 UI 실측 (에디트 모드 · Play 0 · 로그인 0).
// unity command run_script --file AgentScripts/WL815d_Probe.cs --entry WL815d_Probe.RunAll
// unity command run_script --file AgentScripts/WL815d_Probe.cs --entry WL815d_Probe.Capture
// 산출물: <worktree>/AgentScripts/WL815d_PROBE.txt · Screenshots_WL/WL815d/*.png
// 🔴 NewGameUI.prefab 은 LoadPrefabContents 로 열고 **저장하지 않는다**(UnloadPrefabContents(false)).
using System;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEditor;
using TMPro;
using WL.UI;
using WL.Combat.Survival;
public static class WL815d_Probe
{
const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
static StringBuilder _o;
static int _pass, _fail;
static GameObject _root;
static long _prefabLen;
static DateTime _prefabTime;
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
static void N(string s) { _o.AppendLine(" " + s); }
static bool Chk(bool ok, string what)
{
if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); }
else { _fail++; _o.AppendLine(" [FAIL] " + what); }
return ok;
}
static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } }
public static string RunAll()
{
_o = new StringBuilder();
_pass = _fail = 0;
_o.AppendLine("# WL815d Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
_o.AppendLine("playMode=" + Application.isPlaying);
bool savedStageUiDisabled = WLStageUiSettings.RuntimeDisabled;
try
{
Step0_Assets();
Step1_BarrierHook();
Step2_Nodes();
Step3_HudThreeItems();
Step4_Menus();
Step5_Clear();
Step6_Fail();
Step7_Counters();
Step8_Gc();
Step9_C8();
}
catch (Exception e)
{
_fail++;
_o.AppendLine(" [FAIL] 예외 " + e.GetType().Name + " : " + e.Message);
_o.AppendLine(e.StackTrace);
}
finally
{
WLStageUiSettings.RuntimeDisabled = savedStageUiDisabled;
Cleanup();
}
_o.AppendLine();
_o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " (pass=" + _pass + " fail=" + _fail + ")");
string path = Path.Combine(Path.Combine(Root, "AgentScripts"), "WL815d_PROBE.txt");
File.WriteAllText(path, _o.ToString(), new UTF8Encoding(false));
return _o.ToString() + "\n(파일: " + path + ")";
}
// ─────────────────────────────────────────────── ⓪ 에셋 · SO 값
static void Step0_Assets()
{
H("⓪ 에셋 · SO 값 (전부 C45)");
var pf = new FileInfo(Path.Combine(Root, kPrefab.Replace('/', Path.DirectorySeparatorChar)));
_prefabLen = pf.Length; _prefabTime = pf.LastWriteTimeUtc;
N("원본 프리팹 기준선: " + kPrefab + " " + _prefabLen + "B · " + _prefabTime.ToString("yyyy-MM-dd HH:mm:ss") + "Z");
WLStageUiSettings.ClearCache();
var g = WLStageUiSettings.Instance;
Chk(g != null, "Resources.Load(\"" + WLStageUiSettings.ResourcesPath + "\") — NEW 에셋");
if (g == null) return;
Chk(WLStageUiSettings.Enabled, "WLStageUiSettings.Enabled = " + WLStageUiSettings.Enabled);
N("HUD: stageLabel=\"" + g.stageLabelFormat + "\" enemy=\"" + g.enemyLabelFormat + "\" warn=" +
g.timerWarnSeconds + "s countdown=\"" + g.countdownText + "\" 존게이지숨김=" + g.hideZoneGaugeInStage);
N("메뉴: hide=" + g.hideMenusInStage + " 목록 " + (g.stageHidePaths != null ? g.stageHidePaths.Length : 0) +
"개 · 유지 표기 " + (g.stageKeepPaths != null ? g.stageKeepPaths.Length : 0) + "개 · 복원=" + g.restoreMenusOnExit +
" · 룩토글유지=" + g.keepLookToggleButtons);
N("결과: 제목=\"" + g.stageClearTitleFormat + "\" 행 " + (g.stageResultRows != null ? g.stageResultRows.Length : 0) +
"개 [" + string.Join(" · ", Array.ConvertAll(g.stageResultRows, r => r.ToString())) + "] · 버튼 [" +
g.nextStageText + "] [" + g.toLobbyText + "] · 배지=\"" + g.noHitBadgeText + "\"");
N("실패: [" + g.failRetryText + "] [" + g.failLobbyText + "] · 사망=\"" + g.failDeathMessage.Replace("\n", " / ") +
"\" · 시간초과=\"" + g.failTimeoutMessage.Replace("\n", " / ") + "\" · 결과창억제=" + g.suppressResultOnFail);
N("로비: lobbyMode=" + g.lobbyMode + " titleScene=\"" + g.titleSceneName + "\"");
Chk(Mathf.Approximately(g.timerWarnSeconds, 10f), "타이머 경고 = 마지막 10 s(PD 60 s 규칙) — 실측 " + g.timerWarnSeconds);
var c = WL.Combat.Stage.WLStageSettings.Instance;
Chk(c != null, "815b WLStageSettings 존재");
if (c != null)
{
Chk(c.autoEnterOnMapEnter, "F-27 autoEnterOnMapEnter = 1 (맵 진입 = 스테이지 1) — 실측 " + c.autoEnterOnMapEnter);
Chk(!c.bindReviveOkToRetry, "bindReviveOkToRetry = 0 (815d 2버튼 팝업이 대신한다) — 실측 " + c.bindReviveOkToRetry);
Chk(c.toLobbyCallsReturnToLobby, "toLobbyCallsReturnToLobby = 1 (로비 ⓐ안) — 실측 " + c.toLobbyCallsReturnToLobby);
Chk(c.useBarrierHook && c.clearBarrierOnExit, "장벽 훅 사용 on/off = " + c.useBarrierHook + "/" + c.clearBarrierOnExit);
N("기타: mirrorRunEnded=" + c.mirrorRunEnded + " mirrorRunHudEvents=" + c.mirrorRunHudEvents +
" clearHoldScale=" + c.clearHoldScale + " wrapToFirstStage=" + c.wrapToFirstStage);
}
var sv = WLSurvivalSettings.Instance;
if (sv != null) Chk(Mathf.Approximately(sv.reviveWatchdogSeconds, 0f),
"자동 부활 0 (reviveWatchdogSeconds) — 실측 " + sv.reviveWatchdogSeconds);
var bs = WL.Combat.Stage.WLStageBarrierSettings.Instance;
if (bs != null)
{
N("815c 장벽: style=" + bs.style + "(0 None/1 Ring/2 Props/3 Fire) enabled=" + bs.enabled +
" showVisual=" + bs.showVisual + " blockEnabled=" + bs.blockEnabled);
Chk((int)bs.style == 2, "F-26 style = 2(Props · 울타리) — 실측 " + (int)bs.style);
}
}
// ─────────────────────────────────────────────── ① 장벽 훅 2줄
static void Step1_BarrierHook()
{
H("① 장벽 훅 — StageBarrierHook.Set/Clear = StageBarrier.Set/Clear");
N(WL.Combat.Stage.StageBarrierBinding.Bind());
Chk(WL.Combat.Stage.StageBarrierHook.HasSet, "StageBarrierHook.Set 대입됨");
Chk(WL.Combat.Stage.StageBarrierHook.HasClear, "StageBarrierHook.Clear 대입됨");
WL.Combat.Stage.StageBarrierHook.ResetDiagnostics();
int set0 = WL.Combat.Stage.StageBarrier.SetCount, clr0 = WL.Combat.Stage.StageBarrier.ClearCount;
var center = new Vector3(115f, 0.4f, -22f);
WL.Combat.Stage.StageBarrierHook.RaiseSet(center, 14f);
Chk(WL.Combat.Stage.StageBarrierHook.SetCalls == 1, "훅 Set 호출 1 — 실측 " + WL.Combat.Stage.StageBarrierHook.SetCalls);
Chk(WL.Combat.Stage.StageBarrier.SetCount == set0 + 1,
"815c StageBarrier.Set 실제 실행 +1 — 실측 " + (WL.Combat.Stage.StageBarrier.SetCount - set0));
Chk(WL.Combat.Stage.StageBarrier.Active, "장벽 활성 = " + WL.Combat.Stage.StageBarrier.Active +
" 중심 " + WL.Combat.Stage.StageBarrier.Center.ToString("F1") + " r=" + WL.Combat.Stage.StageBarrier.Radius.ToString("F1"));
WL.Combat.Stage.StageBarrierHook.RaiseClear();
Chk(WL.Combat.Stage.StageBarrier.ClearCount == clr0 + 1,
"815c StageBarrier.Clear 실제 실행 +1 — 실측 " + (WL.Combat.Stage.StageBarrier.ClearCount - clr0));
Chk(!WL.Combat.Stage.StageBarrier.Active, "장벽 해제 = " + (!WL.Combat.Stage.StageBarrier.Active));
Chk(WL.Combat.Stage.StageBarrierHook.Faults == 0, "훅 예외 0 — 실측 " + WL.Combat.Stage.StageBarrierHook.Faults);
// 실입장 경로(815b StageDirector) 가 훅을 부르는가
WL.Combat.Stage.StageBarrierHook.ResetDiagnostics();
WL.Combat.Stage.StageDirector.ResetAll();
bool entered = false;
try { entered = WL.Combat.Stage.StageDirector.Enter(0, "815d probe"); }
catch (Exception e) { N("Enter 예외(에디트 모드) " + e.GetType().Name + " : " + e.Message); }
N("StageDirector.Enter(0) = " + entered + " · 상태 " + WL.Combat.Stage.StageDirector.State +
" · 고정 수 " + WL.Combat.Stage.StageDirector.TotalEnemies + " · 제한 " + WL.Combat.Stage.StageDirector.TimeLimit + "s");
Chk(WL.Combat.Stage.StageBarrierHook.SetCalls >= 1,
"입장 시 훅 Set 호출 ≥1 — 실측 " + WL.Combat.Stage.StageBarrierHook.SetCalls +
" 중심 " + WL.Combat.Stage.StageBarrierHook.LastCenter.ToString("F1") +
" r=" + WL.Combat.Stage.StageBarrierHook.LastRadius.ToString("F1"));
WL.Combat.Stage.StageDirector.ToLobby();
Chk(WL.Combat.Stage.StageBarrierHook.ClearCalls >= 1,
"이탈 시 훅 Clear 호출 ≥1 — 실측 " + WL.Combat.Stage.StageBarrierHook.ClearCalls);
// C8 ① — 훅을 떼면 no-op
N(WL.Combat.Stage.StageBarrierBinding.Unbind());
int s1 = WL.Combat.Stage.StageBarrier.SetCount;
WL.Combat.Stage.StageBarrierHook.RaiseSet(center, 14f);
Chk(WL.Combat.Stage.StageBarrier.SetCount == s1, "Unbind 뒤 815c 실행 0 = no-op(C8 ①)");
N(WL.Combat.Stage.StageBarrierBinding.Bind());
WL.Combat.Stage.StageBarrier.Clear();
N(WL.Combat.Stage.StageBarrierBinding.Dump());
}
// ─────────────────────────────────────────────── ② 노드
static void Step2_Nodes()
{
H("② 노드 — NewGameUI 프리팹(저장 0)에 HUD·결과·실패 팝업 조립");
var root = PrefabUtility.LoadPrefabContents(kPrefab);
_root = root;
Chk(root != null, "LoadPrefabContents(" + kPrefab + ")");
if (root == null) return;
StageUiBridge.SetUiRootForProbe(root.transform);
RunHud.Subscribe(true);
RunResultUI.Subscribe(true);
StageUiBridge.Subscribe(true);
N("Bind HUD : " + RunHud.Bind(root.transform));
N("Bind 결과 : " + RunResultUI.Bind(root.transform));
Chk(RunHud.Bound && RunResultUI.Bound, "813q 노드 조립(WL_RunHud · WL_RunResult · HideFlags.DontSave)");
Chk(StageUiBridge.Subscribed, "StageUiBridge 구독 on");
Chk(WL.Combat.Stage.StageEvents.StageEntered.Count >= 1 && WL.Combat.Stage.StageEvents.StageCleared.Count >= 1 &&
WL.Combat.Stage.StageEvents.StageFailed.Count >= 1 && WL.Combat.Stage.StageEvents.StageExited.Count >= 1,
"StageEvents 구독 배선 — Entered " + WL.Combat.Stage.StageEvents.StageEntered.Count +
" Countdown " + WL.Combat.Stage.StageEvents.CountdownTick.Count +
" Started " + WL.Combat.Stage.StageEvents.StageStarted.Count +
" Enemy " + WL.Combat.Stage.StageEvents.EnemyCountChanged.Count +
" Time " + WL.Combat.Stage.StageEvents.TimeChanged.Count +
" Cleared " + WL.Combat.Stage.StageEvents.StageCleared.Count +
" Failed " + WL.Combat.Stage.StageEvents.StageFailed.Count +
" Exited " + WL.Combat.Stage.StageEvents.StageExited.Count);
var dlg = FindRevive(root.transform);
Chk(dlg != null, "813z ReviveDialog 컴포넌트 발견(IngameUIs/WL_HUD/WL_ReviveDialog)");
if (dlg != null) N("ReviveDialog init: " + dlg.Initialize());
Chk(ReviveDialog.Active != null, "ReviveDialog.Active 세팅");
}
static ReviveDialog FindRevive(Transform root)
{
var all = root.GetComponentsInChildren<ReviveDialog>(true);
return all != null && all.Length > 0 ? all[0] : null;
}
// ─────────────────────────────────────────────── ③ 상단 3요소
static void Step3_HudThreeItems()
{
H("③ 상단 3요소 — 스테이지 N · 남은 시간 mm:ss · 남은 적 M/T (+ 카운트다운)");
RunHud.ResetState();
StageUiBridge.ResetState();
WLStageUiProbeHooks.RaiseEntered(0, 1, 8, 60f, new Vector3(115f, 0.4f, -22f), 14f, 1.5f, 0);
Chk(StageUiBridge.EnteredSeen == 1, "StageEntered 수신 1");
Chk(RunHud.StageMode, "RunHud 스테이지 모드 on");
Chk(RunHud.PhaseText == "스테이지 1", "① 단계 = \"스테이지 1\" — 실측 \"" + RunHud.PhaseText + "\"");
Chk(RunHud.TimerText == "01:00", "② 타이머 = 01:00(제한 60 s) — 실측 \"" + RunHud.TimerText + "\"");
Chk(RunHud.EnemyText == "남은 적 8 / 8", "③ 남은 적 = \"남은 적 8 / 8\" — 실측 \"" + RunHud.EnemyText + "\"");
Chk(!RunHud.ZoneGaugeVisible, "존 게이지 접힘(기준서 §F-2) — 실측 활성=" + RunHud.ZoneGaugeVisible);
WLStageUiProbeHooks.RaiseCountdown(0, 1.5f, 1.5f);
Chk(RunHud.NoticeVisible && RunHud.NoticeText == "준비",
"카운트다운 = \"준비\" 표시 — 실측 보임=" + RunHud.NoticeVisible + " \"" + RunHud.NoticeText + "\"");
WLStageUiProbeHooks.RaiseStarted(0, 1, 8, 60f, 0);
Chk(!RunHud.NoticeVisible, "전투 시작 = 카운트다운 줄 접힘 — 실측 " + RunHud.NoticeVisible);
WLStageUiProbeHooks.RaiseEnemyCount(0, 5, 8);
Chk(RunHud.EnemyText == "남은 적 5 / 8", "처치 3 → \"남은 적 5 / 8\" — 실측 \"" + RunHud.EnemyText + "\"");
WLStageUiProbeHooks.RaiseTime(0, 42f, 60f);
Chk(RunHud.TimerText == "00:42" && !RunHud.TimerWarn, "42 s → 00:42 · 평상색 — 실측 \"" + RunHud.TimerText +
"\" warn=" + RunHud.TimerWarn);
WLStageUiProbeHooks.RaiseTime(0, 9.4f, 60f);
Chk(RunHud.TimerText == "00:10" && RunHud.TimerWarn,
"마지막 10 s → 붉은색(경고) — 실측 \"" + RunHud.TimerText + "\" warn=" + RunHud.TimerWarn +
" color=#" + ColorUtility.ToHtmlStringRGB(RunHud.TimerColor));
WLStageUiProbeHooks.RaiseEnemyCount(0, 2, 8);
Chk(RunHud.EnemyText == "남은 적 2 / 8", "남은 적 2 → 강조색 — 실측 \"" + RunHud.EnemyText + "\"");
// 815b 병행 발행(RunStarted/RunTick)이 스테이지 표시를 덮지 않는가
int ignored0 = RunHud.StageEventsIgnored;
WLRunUiProbeHooks.RaiseRunStarted(1, 60f, 1, "stage mirror");
WLRunUiProbeHooks.RaiseRunTick(18f, 42f, 0, 6, 8, 0, 1, WL.Combat.Run.RunPhase.Zones, 1);
Chk(RunHud.StageEventsIgnored - ignored0 == 2, "병행 RunEvents 2건 무시 — 실측 " + (RunHud.StageEventsIgnored - ignored0));
Chk(RunHud.PhaseText == "스테이지 1" && RunHud.EnemyText == "남은 적 2 / 8" && RunHud.TimerText == "00:10",
"🔴 병행 발행이 상단 3요소를 덮지 않는다 — \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\"");
N("HUD 배치: " + RunHud.ApplyLayout());
N(RunHud.Dump());
}
// ─────────────────────────────────────────────── ④ 메뉴 숨김 / 복원
static void Step4_Menus()
{
H("④ 불필요 메뉴 숨김 · 복원 (§J ① · 원본 프리팹 diff 0)");
var g = WLStageUiSettings.Instance;
if (_root == null || g == null) { Chk(false, "프리팹 루트 없음"); return; }
var paths = g.stageHidePaths;
var found = new Transform[paths.Length];
var saved = new bool[paths.Length];
int missing = 0;
for (int i = 0; i < paths.Length; i++)
{
found[i] = WLVignetteUtil.FindUiPath(_root.transform, paths[i]);
if (found[i] == null) { missing++; continue; }
saved[i] = found[i].gameObject.activeSelf; // 프리팹 저장 상태
}
Chk(missing == 0, "숨김 목록 " + paths.Length + "개 전부 실존(경로 해석 성공) — 못 찾음 " + missing);
// 🔴 프리팹 **저장 상태**로는 인게임 화면을 재현할 수 없다(IngameUIs 는 저장 시 꺼져 있다).
// 실기에서 켜져 있는 상태를 만든 뒤 숨김 → 복원 왕복을 재는 것이 유일한 실측이다.
for (int i = 0; i < paths.Length; i++)
if (found[i] != null && !found[i].gameObject.activeSelf) found[i].gameObject.SetActive(true);
N("숨김: " + StageUiBridge.ApplyMenuHide(true));
var mid = new bool[paths.Length];
for (int i = 0; i < paths.Length; i++) mid[i] = found[i] != null && found[i].gameObject.activeSelf;
// 🔴 재보장 — 원본 `NewGameUI.SetUI_byGameMode` 가 스테이지 중에 같은 노드를 다시 켜는 경우
int reOn = 0;
for (int i = 0; i < paths.Length && reOn < 3; i++)
if (found[i] != null) { found[i].gameObject.SetActive(true); reOn++; }
int hit = StageUiBridge.ReassertHide();
int stillOn = 0;
for (int i = 0; i < paths.Length; i++) if (found[i] != null && found[i].gameObject.activeSelf) stillOn++;
Chk(hit == reOn && stillOn == 0, "원본이 다시 켠 " + reOn + "건을 재보장으로 도로 끔 — 실측 " + hit +
"건 · 남은 on " + stillOn + " (주기 " + g.menuReassertSeconds + "s)");
Chk(StageUiBridge.ReassertHide() == 0, "이미 꺼져 있으면 쓰기 0회(GC 0)");
N("복원: " + StageUiBridge.ApplyMenuHide(false));
int restored = 0, hidOk = 0;
_o.AppendLine(" | # | 경로(NewGameUI 루트 기준) | 실제 노드 경로 | 프리팹 저장 | 스테이지 중 | 이탈 뒤 |");
_o.AppendLine(" |---|---|---|---|---|---|");
for (int i = 0; i < paths.Length; i++)
{
bool after = found[i] != null && found[i].gameObject.activeSelf;
if (found[i] != null)
{
if (!mid[i]) hidOk++;
if (after) restored++;
}
_o.AppendLine(" | " + i + " | `" + paths[i] + "` | " + (found[i] != null ? "`" + FullPath(found[i], _root.transform) + "`" : "**없음**") +
" | " + (found[i] != null ? (saved[i] ? "on" : "off") : "-") +
" | " + (found[i] != null ? (mid[i] ? "on" : "**off**") : "-") +
" | " + (found[i] != null ? (after ? "**on**" : "off") : "-") + " |");
}
Chk(hidOk == paths.Length - missing, "스테이지 중 전부 SetActive(false) — " + hidOk + "/" + (paths.Length - missing));
Chk(restored == paths.Length - missing, "이탈 시 전부 켜진 값으로 복원 — " + restored + "/" + (paths.Length - missing));
// 프리팹 저장 상태로 되돌린다(미리보기 사본이지만 뒤 단계에 영향 0)
for (int i = 0; i < paths.Length; i++)
if (found[i] != null && found[i].gameObject.activeSelf != saved[i]) found[i].gameObject.SetActive(saved[i]);
// 유지 대상은 건드리지 않았는가
int keptOk = 0, keptMissing = 0;
for (int i = 0; i < g.stageKeepPaths.Length; i++)
{
var t = WLVignetteUtil.FindUiPath(_root.transform, g.stageKeepPaths[i]);
if (t == null) { keptMissing++; N(" (유지) " + g.stageKeepPaths[i] + " : 없음"); continue; }
keptOk++;
N(" (유지) " + g.stageKeepPaths[i] + " activeSelf=" + t.gameObject.activeSelf);
}
Chk(keptMissing == 0, "유지 목록 " + g.stageKeepPaths.Length + "개 전부 실존 — 없음 " + keptMissing);
N("유지 확인 " + keptOk + "개 (조이스틱·스킬 패드 BattleUI · HP/MP MyInfoUI · 813 WL HUD 요소)");
// 🔴 원본 프리팹 **파일**이 바뀌지 않았는가(LoadPrefabContents 는 메모리 사본이라 dirty 플래그는 뜬다 —
// 실제 판정은 디스크 파일의 길이·수정시각이다. 저장은 하지 않는다 = UnloadPrefabContents).
var fi = new FileInfo(Path.Combine(Root, kPrefab.Replace('/', Path.DirectorySeparatorChar)));
Chk(fi.Length == _prefabLen && fi.LastWriteTimeUtc == _prefabTime,
"원본 NewGameUI.prefab 파일 무변경 — 길이 " + fi.Length + "B(=" + _prefabLen + ") 수정시각 " +
fi.LastWriteTimeUtc.ToString("HH:mm:ss") + "(=" + _prefabTime.ToString("HH:mm:ss") + ")");
N("(참고) 미리보기 사본 dirty=" + EditorUtility.IsDirty(_root) +
" — 런타임 노드는 HideFlags.DontSave 라 저장해도 굽히지 않는다(813y 규약)");
}
// ─────────────────────────────────────────────── ⑤ 클리어 화면
static void Step5_Clear()
{
H("⑤ 클리어 화면 — StageCleared → 813q RunResultUI (기준서 §F-1)");
var g = WLStageUiSettings.Instance;
RunResultUI.ResetState();
StageUiBridge.ResetState();
StageUiBridge.ProbeSetInStage(true);
// 피격 3회 · 연쇄 5 를 스테이지 구간 동안 실측
WLStageUiProbeHooks.RaiseDamaged(null); // victim 이 PC 가 아니면 세지 않는다
Chk(StageUiBridge.Hits == 0, "Damaged(victim=null)는 피격으로 세지 않는다 — 실측 " + StageUiBridge.Hits);
StageUiBridge.ProbeNoteHit(); StageUiBridge.ProbeNoteHit(); StageUiBridge.ProbeNoteHit();
WLStageUiProbeHooks.RaiseChainEnded(5, 2, 1.2f);
WLStageUiProbeHooks.RaiseChainEnded(3, 1, 0.8f);
Chk(StageUiBridge.Hits == 3, "피격 3회 집계 — 실측 " + StageUiBridge.Hits);
Chk(StageUiBridge.MaxChain == 5, "최대 연쇄 = 5(3 은 갱신 안 함) — 실측 " + StageUiBridge.MaxChain);
WLStageUiProbeHooks.RaiseCleared(0, 1, 34.2f, 60f, 8, 8, 1, false, false);
Chk(StageUiBridge.ClearedSeen == 1, "StageCleared 수신 1");
Chk(RunResultUI.IsOpen, "결과 화면 표시 — 실측 open=" + RunResultUI.IsOpen);
Chk(RunResultUI.StageMode, "결과창 스테이지 모드 on");
Chk(RunResultUI.TitleText == "스테이지 1 클리어!", "제목 = \"스테이지 1 클리어!\" — 실측 \"" + RunResultUI.TitleText + "\"");
N("본문 =\n" + Indent(RunResultUI.BodyText));
Chk(RunResultUI.VisibleRows == g.stageResultRows.Length,
"행 " + g.stageResultRows.Length + "개 — 실측 " + RunResultUI.VisibleRows);
Chk(!RunResultUI.BadgeVisible, "피격 3회 → 무피격 배지 없음 — 실측 " + RunResultUI.BadgeVisible);
N("배치: " + RunResultUI.ApplyLayout());
// 병행 RunEnded(Win) 이 결과창을 두 번 띄우지 않는가
int shows0 = RunResultUI.ShownCount;
var mirror = new WL.Combat.Run.RunResult { outcome = WL.Combat.Run.RunOutcome.Win, runIndex = 1, totalSec = 34.2f, kills = 8 };
WLRunUiProbeHooks.RaiseRunEnded(in mirror);
Chk(RunResultUI.ShownCount == shows0, "🔴 병행 RunEnded(Win) 로 결과창 중복 표시 0 — 실측 +" +
(RunResultUI.ShownCount - shows0) + " · 억제 " + RunResultUI.SuppressedRunEndedCount + "회");
Chk(RunResultUI.TitleText == "스테이지 1 클리어!", "제목이 「런 클리어!」로 덮이지 않았다 — \"" + RunResultUI.TitleText + "\"");
// 무피격 클리어 → 배지
StageUiBridge.ResetState();
StageUiBridge.ProbeSetInStage(true);
WLStageUiProbeHooks.RaiseCleared(1, 2, 21.5f, 60f, 10, 10, 0, false, false);
Chk(RunResultUI.BadgeVisible && RunResultUI.BadgeText == "무피격 클리어!",
"피격 0회 → 무피격 배지 — 실측 " + RunResultUI.BadgeVisible + " \"" + RunResultUI.BadgeText + "\"");
// 마지막 스테이지 → [처음부터]
WLStageUiProbeHooks.RaiseCleared(9, 10, 55.0f, 60f, 15, 15, 2, true, true);
Chk(RunResultUI.TitleText == "전 스테이지 클리어!", "마지막 스테이지 제목 — 실측 \"" + RunResultUI.TitleText + "\"");
N("마지막 스테이지 배치: " + RunResultUI.ApplyLayout());
N("본문(보스 스테이지) =\n" + Indent(RunResultUI.BodyText));
}
// ─────────────────────────────────────────────── ⑥ 실패 팝업
static void Step6_Fail()
{
H("⑥ 실패 팝업 — StageFailed → 813z ReviveDialog 2버튼 (기준서 §E)");
var dlg = ReviveDialog.Active;
if (dlg == null) { Chk(false, "ReviveDialog.Active 없음"); return; }
RunResultUI.ResetState();
StageUiBridge.ResetState();
StageUiBridge.ProbeSetInStage(true);
WLStageUiProbeHooks.RaiseFailed(0, 1, WL.Combat.Stage.StageFailReason.TimeLimit, 3, 8, 60f, 0);
Chk(StageUiBridge.FailedSeen == 1, "StageFailed 수신 1");
Chk(dlg.IsOpen, "실패 팝업 표시 — 실측 open=" + dlg.IsOpen);
Chk(ReviveDialog.StageMode, "ReviveDialog 스테이지 모드 on");
Chk(!dlg.UsedCommonPopup, "공용 Popup 미사용(2버튼 불가) — 실측 " + dlg.UsedCommonPopup);
N("배치: " + dlg.ApplyLayout());
Chk(dlg.SecondaryVisible, "두 번째 버튼 노출 — 실측 " + dlg.SecondaryVisible);
Chk(dlg.PrimaryButtonText == "다시 도전" && dlg.SecondaryButtonText == "로비로",
"버튼 = [다시 도전] [로비로] — 실측 [" + dlg.PrimaryButtonText + "] [" + dlg.SecondaryButtonText + "]");
Chk(dlg.LastMessage.Contains("시간 초과") && dlg.LastMessage.Contains("3"),
"시간 초과 문구 + 남은 적 3 — 실측 \"" + dlg.LastMessage.Replace("\n", " / ") + "\"");
// 🔴 813z 3층 방어 — 전용 Canvas 는 붙었지만 **부모 계층이 비활성인 동안 Unity 가 overrideSorting 을 되돌린다**
// (814d §4 실측). 그래서 에디트 모드의 SortingOrder 게터는 1 이다. SO 값과 재보장 횟수로 확인한다.
var sv2 = WLSurvivalSettings.Instance;
Chk(sv2 != null && sv2.reviveSortingOrder == 60 && dlg.TopmostCount >= 1,
"813z 3층 방어 유지 — SO sortingOrder " + (sv2 != null ? sv2.reviveSortingOrder : -1) +
" · 최상단 재보장 " + dlg.TopmostCount + "회 · 실측 게터=" + dlg.SortingOrder +
"(부모 계층 비활성 → Unity 가 overrideSorting 되돌림 · 814d §4)");
Chk(RunResultUI.SuppressedRunEndedCount == 0 && !RunResultUI.IsOpen, "실패 시점에 결과창 0");
// 병행 RunEnded(Timeout) 억제
var mirror = new WL.Combat.Run.RunResult { outcome = WL.Combat.Run.RunOutcome.Timeout, runIndex = 1, totalSec = 60f, kills = 5 };
WLRunUiProbeHooks.RaiseRunEnded(in mirror);
Chk(!RunResultUI.IsOpen && RunResultUI.SuppressedRunEndedCount == 1,
"🔴 병행 RunEnded(Timeout) 억제 → 결과창 안 뜸 — 실측 open=" + RunResultUI.IsOpen +
" 억제 " + RunResultUI.SuppressedRunEndedCount + "회");
// 버튼 → StageDirector
// 🔴 순서 주의: 실기 순서는 「입장 → 실패 → 클릭」이다. 프로브도 같은 순서로 태운다
// (입장(StageEntered)이 실패 팝업 모드를 지우기 때문 — 이것이 정상 동작이다).
WL.Combat.Stage.StageDirector.ResetAll();
try { WL.Combat.Stage.StageDirector.Enter(0, "815d fail probe"); } catch (Exception e) { N("Enter 예외 " + e.GetType().Name); }
StageUiBridge.ProbeSetInStage(true);
// 사망 문구
WLStageUiProbeHooks.RaiseFailed(0, 1, WL.Combat.Stage.StageFailReason.PlayerDied, 4, 8, 22f, 1);
Chk(dlg.LastMessage.Contains("쓰러졌"), "사망 문구 구분 — 실측 \"" + dlg.LastMessage.Replace("\n", " / ") + "\"");
Chk(ReviveDialog.StageMode, "사망 실패도 2버튼 모드 — 실측 " + ReviveDialog.StageMode);
int retry0 = WL.Combat.Stage.StageDirector.RetryCount;
N("클릭 [다시 도전] : " + dlg.ProbeClickPrimary());
Chk(WL.Combat.Stage.StageDirector.RetryCount == retry0 + 1,
"[다시 도전] → StageDirector.Retry() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.RetryCount - retry0));
Chk(!dlg.IsOpen, "누르면 팝업 닫힘 — 실측 open=" + dlg.IsOpen);
WLStageUiProbeHooks.RaiseFailed(0, 1, WL.Combat.Stage.StageFailReason.PlayerDied, 4, 8, 22f, 2);
int lobby0 = WL.Combat.Stage.StageDirector.LobbyCount;
N("클릭 [로비로] : " + dlg.ProbeClickSecondary());
Chk(WL.Combat.Stage.StageDirector.LobbyCount == lobby0 + 1,
"[로비로] → StageDirector.ToLobby() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.LobbyCount - lobby0));
N(dlg.Dump());
}
// ─────────────────────────────────────────────── ⑦ 결과창 버튼
static void Step7_Counters()
{
H("⑦ 클리어 화면 버튼 → StageDirector.Next() / ToLobby()");
StageUiBridge.ResetState();
StageUiBridge.ProbeSetInStage(true);
WL.Combat.Stage.StageDirector.ResetAll();
try { WL.Combat.Stage.StageDirector.Enter(0, "815d btn probe"); } catch (Exception e) { N("Enter 예외 " + e.GetType().Name); }
WLStageUiProbeHooks.RaiseCleared(0, 1, 30f, 60f, 8, 8, 0, false, false);
int next0 = WL.Combat.Stage.StageDirector.NextCount;
N("클릭 [다음 스테이지] : " + RunResultUI.OnNextRun());
Chk(WL.Combat.Stage.StageDirector.NextCount == next0 + 1,
"[다음 스테이지] → StageDirector.Next() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.NextCount - next0));
Chk(!RunResultUI.IsOpen, "누르면 결과창 닫힘");
WLStageUiProbeHooks.RaiseCleared(0, 1, 30f, 60f, 8, 8, 0, false, false);
int lobby0 = WL.Combat.Stage.StageDirector.LobbyCount;
N("클릭 [로비로] : " + RunResultUI.OnClose());
Chk(WL.Combat.Stage.StageDirector.LobbyCount == lobby0 + 1,
"[로비로] → StageDirector.ToLobby() 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.LobbyCount - lobby0));
N("로비 동작: " + StageUiBridge.LastLog);
// 마지막 스테이지 → [처음부터] = Enter(0)
WLStageUiProbeHooks.RaiseCleared(9, 10, 50f, 60f, 15, 15, 0, true, true);
int enter0 = WL.Combat.Stage.StageDirector.EnterCount;
N("클릭 [처음부터] : " + RunResultUI.OnNextRun());
Chk(WL.Combat.Stage.StageDirector.EnterCount == enter0 + 1,
"[처음부터] → StageDirector.Enter(0) 1회 — 실측 +" + (WL.Combat.Stage.StageDirector.EnterCount - enter0));
// 이탈 → 스테이지 모드 해제 + 메뉴 복원
WLStageUiProbeHooks.RaiseExited(0, true, "probe lobby");
Chk(!RunHud.StageMode, "이탈 → RunHud 스테이지 모드 off");
Chk(!RunResultUI.StageMode, "이탈 → 결과창 스테이지 모드 off");
Chk(!ReviveDialog.StageMode, "이탈 → 실패 팝업 모드 off(813z 원래 1버튼)");
N(StageUiBridge.Dump());
}
// ─────────────────────────────────────────────── ⑧ GC
static void Step8_Gc()
{
H("⑧ GC — 같은 값 100틱 = 표시 쓰기 0회");
StageUiBridge.ResetState();
WLStageUiProbeHooks.RaiseEntered(0, 3, 12, 60f, Vector3.zero, 14f, 1.5f, 0);
WLStageUiProbeHooks.RaiseStarted(0, 3, 12, 60f, 0);
WLStageUiProbeHooks.RaiseTime(0, 30f, 60f);
WLStageUiProbeHooks.RaiseEnemyCount(0, 7, 12);
int t0 = RunHud.TimerWrites, e0 = RunHud.EnemyWrites, p0 = RunHud.StageLabelWrites;
long m0 = GC.GetTotalMemory(false);
for (int i = 0; i < 100; i++)
{
WLStageUiProbeHooks.RaiseTime(0, 30f, 60f);
WLStageUiProbeHooks.RaiseEnemyCount(0, 7, 12);
}
long m1 = GC.GetTotalMemory(false);
Chk(RunHud.TimerWrites == t0, "같은 초 100틱 → 타이머 쓰기 0회 — 실측 +" + (RunHud.TimerWrites - t0));
Chk(RunHud.EnemyWrites == e0, "같은 수 100틱 → 남은 적 쓰기 0회 — 실측 +" + (RunHud.EnemyWrites - e0));
Chk(RunHud.StageLabelWrites == p0, "같은 스테이지 → 라벨 쓰기 0회 — 실측 +" + (RunHud.StageLabelWrites - p0));
N("ΔGetTotalMemory = " + (m1 - m0) + " B (에디터 TMP 문자열 포함 · 빌드에는 없는 줄)");
WLStageUiProbeHooks.RaiseExited(0, false, "gc probe end");
}
// ─────────────────────────────────────────────── ⑨ C8
static void Step9_C8()
{
H("⑨ C8 — WLStageUiSettings 를 끄면 813q/813z 가 원래대로");
StageUiBridge.ResetState();
RunHud.ResetState();
RunResultUI.ResetState();
WLStageUiSettings.RuntimeDisabled = true;
Chk(!WLStageUiSettings.Enabled, "RuntimeDisabled → Enabled=false");
WLStageUiProbeHooks.RaiseEntered(0, 4, 15, 60f, Vector3.zero, 14f, 1.5f, 0);
Chk(!RunHud.StageMode, "축 off → 스테이지 모드 0 — 실측 " + RunHud.StageMode);
Chk(StageUiBridge.HiddenCount == 0, "축 off → 메뉴 숨김 0 — 실측 " + StageUiBridge.HiddenCount);
WLStageUiProbeHooks.RaiseCleared(0, 4, 12f, 60f, 15, 15, 0, false, false);
Chk(!RunResultUI.StageMode, "축 off → 결과창 스테이지 모드 0");
WLStageUiProbeHooks.RaiseFailed(0, 4, WL.Combat.Stage.StageFailReason.TimeLimit, 5, 15, 60f, 0);
Chk(!ReviveDialog.StageMode, "축 off → 실패 팝업 2버튼 0(813z 원래 부활 팝업)");
// 813q 런 축은 그대로 돈다
Chk(WLRunUiSettings.Enabled, "813q WLRunUiSettings.Enabled 불변 = " + WLRunUiSettings.Enabled);
WLRunUiProbeHooks.RaiseRunStarted(2, 240f, 4, "c8 probe");
Chk(RunHud.TimerText == "04:00" && RunHud.PhaseText == "존 1",
"813q 런 HUD 원래대로 — 실측 \"" + RunHud.TimerText + "\" \"" + RunHud.PhaseText + "\"");
WLStageUiSettings.RuntimeDisabled = false;
Chk(WLStageUiSettings.Enabled, "복구 → Enabled=true");
}
/// <summary>root 기준 전체 경로(실제로 어느 노드를 잡았는지 실측 표기).</summary>
static string FullPath(Transform t, Transform root)
{
var sb = new StringBuilder(t.name);
var p = t.parent;
while (p != null && p != root) { sb.Insert(0, p.name + "/"); p = p.parent; }
return sb.ToString();
}
static string Indent(string s)
{
if (string.IsNullOrEmpty(s)) return " (없음)";
return " " + s.TrimEnd('\n').Replace("\n", "\n ");
}
static void Cleanup()
{
try
{
StageUiBridge.Teardown();
RunHud.Teardown();
RunResultUI.Teardown();
WL.Combat.Stage.StageDirector.ResetAll();
WL.Combat.Stage.StageBarrier.Clear();
}
catch (Exception e) { _o.AppendLine(" (정리 예외 " + e.GetType().Name + ")"); }
if (_root != null) { PrefabUtility.UnloadPrefabContents(_root); _root = null; }
}
// ═════════════════════════════════════════════════════════════════════
// 캡처 3장 — 1080×1920 목업(실 노드 · 실 값 · RenderTexture 렌더)
// ═════════════════════════════════════════════════════════════════════
public static string Capture()
{
var sb = new StringBuilder();
string dir = Path.Combine(Path.Combine(Root, "Screenshots_WL"), "WL815d");
Directory.CreateDirectory(dir);
GameObject rig = null, prefabRoot = null;
try
{
prefabRoot = PrefabUtility.LoadPrefabContents(kPrefab);
var revive = FindRevive(prefabRoot.transform);
rig = new GameObject("WL815d_CaptureRig");
rig.hideFlags = HideFlags.HideAndDontSave;
var camGo = new GameObject("Cam");
camGo.transform.SetParent(rig.transform, false);
var cam = camGo.AddComponent<Camera>();
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0.09f, 0.10f, 0.12f, 1f);
cam.orthographic = true;
cam.orthographicSize = 960f;
cam.nearClipPlane = 0.1f; cam.farClipPlane = 5000f;
camGo.transform.position = new Vector3(0f, 0f, -1000f);
var canvasGo = new GameObject("Canvas", typeof(RectTransform));
canvasGo.transform.SetParent(rig.transform, false);
canvasGo.layer = 5;
var canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = cam;
canvas.planeDistance = 500f;
var scaler = canvasGo.AddComponent<UnityEngine.UI.CanvasScaler>();
scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1080f, 1920f);
scaler.matchWidthOrHeight = 0.5f;
canvasGo.AddComponent<UnityEngine.UI.GraphicRaycaster>();
var crt = (RectTransform)canvasGo.transform;
crt.sizeDelta = new Vector2(1080f, 1920f);
// 813q/813z 가 찾는 부모 경로(IngameUIs/WL_HUD)를 목업 캔버스 안에 그대로 만든다
var ingame = NewRect(crt, "IngameUIs");
var wlHud = NewRect(ingame, "WL_HUD");
// 폰트는 원본 UI 에서 빌린다(한글 글리프)
var font = RunHud.BorrowFont(prefabRoot.transform);
RunHud.SetFont(font); RunResultUI.SetFont(font);
StageUiBridge.SetUiRootForProbe(prefabRoot.transform);
RunHud.Subscribe(true); RunResultUI.Subscribe(true); StageUiBridge.Subscribe(true);
sb.AppendLine("HUD : " + RunHud.Bind(crt));
sb.AppendLine("결과 : " + RunResultUI.Bind(crt));
// 실패 팝업은 목업 캔버스 안에 새 인스턴스로 만든다(원본 프리팹 무수정)
var dlgGo = new GameObject("WL_ReviveDialog", typeof(RectTransform));
dlgGo.layer = 5;
dlgGo.transform.SetParent(wlHud, false);
revive = dlgGo.AddComponent<ReviveDialog>();
revive.SetFont(font);
sb.AppendLine("팝업 : " + revive.Initialize());
var rt = new RenderTexture(1080, 1920, 24, RenderTextureFormat.ARGB32);
cam.targetTexture = rt;
// ① HUD (스테이지 3요소)
WLStageUiProbeHooks.RaiseEntered(0, 3, 12, 60f, new Vector3(147f, 2.6f, 30f), 14f, 1.5f, 0);
WLStageUiProbeHooks.RaiseStarted(0, 3, 12, 60f, 0);
WLStageUiProbeHooks.RaiseEnemyCount(0, 5, 12);
WLStageUiProbeHooks.RaiseTime(0, 8f, 60f);
RunHud.ApplyLayout();
RunResultUI.Hide();
if (revive != null) revive.HideNow();
sb.AppendLine("① HUD \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\"");
sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "01_hud_stage.png")));
// ② 클리어 화면
StageUiBridge.ProbeSetInStage(true);
StageUiBridge.ProbeNoteHit();
WLStageUiProbeHooks.RaiseChainEnded(6, 2, 1.4f);
WLStageUiProbeHooks.RaiseCleared(0, 3, 34.2f, 60f, 12, 12, 1, false, false);
RunResultUI.ApplyLayout();
sb.AppendLine("② 클리어 \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows +
" 배지=" + RunResultUI.BadgeVisible);
sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "02_clear.png")));
// ③ 실패 팝업
RunResultUI.Hide();
StageUiBridge.ProbeSetInStage(true);
WLStageUiProbeHooks.RaiseFailed(0, 3, WL.Combat.Stage.StageFailReason.TimeLimit, 4, 12, 60f, 0);
if (revive != null) revive.ApplyLayout();
sb.AppendLine("③ 실패 \"" + (revive != null ? revive.LastMessage.Replace("\n", " / ") : "-") + "\" 버튼=[" +
(revive != null ? revive.PrimaryButtonText : "") + "] [" +
(revive != null ? revive.SecondaryButtonText : "") + "]");
sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "03_fail.png")));
cam.targetTexture = null;
UnityEngine.Object.DestroyImmediate(rt);
}
catch (Exception e) { sb.AppendLine("예외 " + e.GetType().Name + " : " + e.Message + "\n" + e.StackTrace); }
finally
{
try { StageUiBridge.Teardown(); RunHud.Teardown(); RunResultUI.Teardown(); } catch { }
if (rig != null) UnityEngine.Object.DestroyImmediate(rig);
if (prefabRoot != null) PrefabUtility.UnloadPrefabContents(prefabRoot);
}
return sb.ToString();
}
static RectTransform NewRect(RectTransform parent, string name)
{
var go = new GameObject(name, typeof(RectTransform));
go.layer = 5;
var rt = (RectTransform)go.transform;
rt.SetParent(parent, false);
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
rt.localScale = Vector3.one;
return rt;
}
static string Shot(Camera cam, RenderTexture rt, string path)
{
Canvas.ForceUpdateCanvases();
cam.Render();
var prev = RenderTexture.active;
RenderTexture.active = rt;
var tex = new Texture2D(rt.width, rt.height, TextureFormat.RGBA32, false);
tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
tex.Apply();
RenderTexture.active = prev;
var png = tex.EncodeToPNG();
UnityEngine.Object.DestroyImmediate(tex);
File.WriteAllBytes(path, png);
return " → " + path + " (" + png.Length + " B · " + rt.width + "×" + rt.height + ")";
}
}