// WL-816m 프로브 — 던전 결과창(던전 이름 + 획득 골드) 실측 (에디트 모드 · Play 0 · 로그인 0). // unity command run_script --file AgentScripts/WL816m_Probe.cs --entry WL816m_Probe.SaveAsset // unity command run_script --file AgentScripts/WL816m_Probe.cs --entry WL816m_Probe.RunAll // unity command run_script --file AgentScripts/WL816m_Probe.cs --entry WL816m_Probe.Capture // 산출물: /AgentScripts/WL816m_PROBE.txt · Screenshots_WL/WL816m/*.png // 🔴 NewGameUI.prefab 은 LoadPrefabContents 로 열고 저장하지 않는다(UnloadPrefabContents). // 🔴 PlayerPrefs 는 건드리지 않는다 — 에디트 모드라 WLIslandBridge 는 설치되지 않는다(전후 값 실측 대조). using System; using System.IO; using System.Text; using UnityEngine; using UnityEditor; using WL.UI; using WL.Island; public static class WL816m_Probe { const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; static StringBuilder _o; static int _pass, _fail; static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } } 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; } // ═════════════════════════════════════════════════════════════════════ // ① SO 에 새 필드를 직렬화한다(에셋 YAML 에 값이 보이도록 · C45) // ═════════════════════════════════════════════════════════════════════ public static string SaveAsset() { var g = WLStageUiSettings.Instance; if (g == null) return "WLStageUiSettings 에셋 없음"; EditorUtility.SetDirty(g); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return "저장 — 던전모드=" + g.dungeonModeEnabled + " 제목=\"" + g.dungeonClearTitleFormat + "\" 라벨=\"" + g.dungeonLabelFormat + "\" 골드행=" + g.dungeonGoldRowEnabled + " \"" + g.rowDungeonGoldLabel + "\" \"" + g.rowDungeonGoldFormat + "\""; } // ═════════════════════════════════════════════════════════════════════ // ② 실측 // ═════════════════════════════════════════════════════════════════════ public static string RunAll() { _o = new StringBuilder(); _pass = _fail = 0; _o.AppendLine("# WL816m Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)"); _o.AppendLine("playMode=" + Application.isPlaying); var cfg = WLIslandSettings.Instance; string prefsKey = cfg != null ? cfg.pendingPrefsKey : "WL_Island_PendingGold"; int prefsBefore = PlayerPrefs.GetInt(prefsKey, -1); var savedCurrent = WLDungeonSceneSetup.Current; GameObject prefabRoot = null; try { Step0_Settings(cfg); prefabRoot = PrefabUtility.LoadPrefabContents(kPrefab); BuildRig(prefabRoot); Step1_Detect(cfg); Step2_Gold(cfg); Step3_Result(cfg, prefabRoot); Step4_Hud(cfg, prefabRoot); Step5_StageUnchanged(prefabRoot); Step6_Rollback(cfg, prefabRoot); } catch (Exception e) { _fail++; _o.AppendLine(" [FAIL] 예외 " + e.GetType().Name + " : " + e.Message); _o.AppendLine(e.StackTrace); } finally { try { StageUiBridge.Teardown(); RunHud.Teardown(); RunResultUI.Teardown(); } catch { } DestroyRig(); WLDungeonSceneSetup.Current = savedCurrent; if (prefabRoot != null) PrefabUtility.UnloadPrefabContents(prefabRoot); } int prefsAfter = PlayerPrefs.GetInt(prefsKey, -1); H("⑦ PlayerPrefs 무변경 (섬 축은 에디트 모드에 설치되지 않는다)"); N("키 \"" + prefsKey + "\" 전=" + prefsBefore + " 후=" + prefsAfter); Chk(prefsBefore == prefsAfter, "보류 보상 PlayerPrefs 무변경"); _o.AppendLine(); _o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " (pass=" + _pass + " fail=" + _fail + ")"); string path = Path.Combine(Path.Combine(Root, "AgentScripts"), "WL816m_PROBE.txt"); File.WriteAllText(path, _o.ToString(), new UTF8Encoding(true)); return _o.ToString(); } static void Step0_Settings(WLIslandSettings cfg) { H("① 설정 실값 (SO · 하드코딩 0)"); var g = WLStageUiSettings.Instance; Chk(g != null, "WLStageUiSettings 에셋 로드"); if (g == null) return; N("던전모드=" + g.dungeonModeEnabled + " · 제목 \"" + g.dungeonClearTitleFormat + "\" · HUD \"" + g.dungeonLabelFormat + "\" · 대체이름 \"" + g.dungeonNameFallbackFormat + "\""); N("골드행=" + g.dungeonGoldRowEnabled + " · 라벨 \"" + g.rowDungeonGoldLabel + "\" · 값 \"" + g.rowDungeonGoldFormat + "\""); N("스테이지 제목(불변) \"" + g.stageClearTitleFormat + "\" · 스테이지 HUD(불변) \"" + g.stageLabelFormat + "\""); Chk(cfg != null && cfg.dungeons != null && cfg.dungeons.Length >= 3, "던전 표 3행 이상"); if (cfg == null || cfg.dungeons == null) return; for (int i = 0; i < cfg.dungeons.Length; i++) { var d = cfg.dungeons[i]; int id = StageIdOf(d.stageIndex); N("던전 표[" + i + "] id=" + d.id + " \"" + d.displayName + "\" stageIndex=" + d.stageIndex + " → 스테이지 표 id=" + id + "(지금 화면에 나오던 숫자) · goldMultiplier=" + WLIslandBridge.MultiplierOf(d.stageIndex).ToString("F2")); } N("보상 값 — clearBonus=" + cfg.goldClearBonus + " perKill=" + cfg.goldPerKill + " bossBonus=" + cfg.goldBossBonus + " 배수적용=" + cfg.applyGoldMultiplier + " rewardEnabled=" + cfg.rewardEnabled); } static int StageIdOf(int index) { var t = WL.Combat.Stage.WLStageTable.Instance; if (t == null) return -1; var def = t.Get(index); return def != null ? def.id : -1; } static void Step1_Detect(WLIslandSettings cfg) { H("② 던전 판정 = WLDungeonSceneSetup.Current 하나뿐"); WLDungeonSceneSetup.Current = null; Chk(StageUiBridge.CurrentDungeon == null, "Current=null → 던전 아님(#815 스테이지 모드)"); Chk(StageUiBridge.DungeonNameOf(null) == null, "이름 null"); WLDungeonSceneSetup.Current = cfg.dungeons[0]; var d1 = StageUiBridge.CurrentDungeon; Chk(d1 != null, "Current=던전1 행 → 던전으로 판정"); N("이름 = \"" + StageUiBridge.DungeonNameOf(d1) + "\""); Chk(StageUiBridge.DungeonNameOf(d1) == cfg.dungeons[0].displayName, "이름 = 표의 displayName"); // displayName 이 비었을 때의 대체 이름 string saved = cfg.dungeons[0].displayName; cfg.dungeons[0].displayName = ""; string fb = StageUiBridge.DungeonNameOf(StageUiBridge.CurrentDungeon); cfg.dungeons[0].displayName = saved; N("displayName 빈 경우 대체 이름 = \"" + fb + "\""); Chk(fb == "던전 " + cfg.dungeons[0].id, "대체 이름 = dungeonNameFallbackFormat + id"); } static void Step2_Gold(WLIslandSettings cfg) { H("③ 획득 골드 = 섬 축 WLIslandBridge.ComputeGold 재사용(식 1곳)"); var r1 = Cleared(cfg.dungeons[0].stageIndex, 3, false); int g1 = StageUiBridge.DungeonGoldOf(in r1); N("던전 1 (index " + cfg.dungeons[0].stageIndex + " · 처치 3) → " + g1 + " / 섬 축 직접 = " + WLIslandBridge.ComputeGold(cfg, in r1)); Chk(g1 == 96, "던전 1 = 96 (816g §5 실측과 같음)"); var r3 = Cleared(cfg.dungeons[2].stageIndex, 3, false); int g3 = StageUiBridge.DungeonGoldOf(in r3); N("던전 3 (index " + cfg.dungeons[2].stageIndex + " · 처치 3) → " + g3); Chk(g3 == 192, "던전 3 = 192 (816i2 §3 실측과 같음)"); Chk(g1 == WLIslandBridge.ComputeGold(cfg, in r1) && g3 == WLIslandBridge.ComputeGold(cfg, in r3), "UI 표시값 == 섬 축 지급 계산값(두 곳 일치)"); } static WL.Combat.Stage.StageClearResult Cleared(int index, int killed, bool isBoss) { return new WL.Combat.Stage.StageClearResult { index = index, stageId = StageIdOf(index), win = true, failReason = WL.Combat.Stage.StageFailReason.None, elapsedSec = 34.2f, timeLimit = 60f, killed = killed, total = killed, remaining = 0, retries = 0, isLastStage = false, isBoss = isBoss, time = Time.unscaledTime, frame = Time.frameCount, }; } // ── 목업 캔버스(815d 와 같은 방식) — TitleText·PhaseText 를 읽으려면 노드가 있어야 한다 ── static GameObject _rig; static Camera _cam; static RectTransform _crt; static ReviveDialog _revive; static void BuildRig(GameObject prefabRoot) { _rig = new GameObject("WL816m_Rig"); _rig.hideFlags = HideFlags.HideAndDontSave; var camGo = new GameObject("Cam"); camGo.transform.SetParent(_rig.transform, false); _cam = camGo.AddComponent(); _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.renderMode = RenderMode.ScreenSpaceCamera; canvas.worldCamera = _cam; canvas.planeDistance = 500f; var scaler = canvasGo.AddComponent(); scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1080f, 1920f); scaler.matchWidthOrHeight = 0.5f; canvasGo.AddComponent(); _crt = (RectTransform)canvasGo.transform; _crt.sizeDelta = new Vector2(1080f, 1920f); var ingame = NewRect(_crt, "IngameUIs"); var wlHud = NewRect(ingame, "WL_HUD"); 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); RunHud.Bind(_crt); RunResultUI.Bind(_crt); var dlgGo = new GameObject("WL_ReviveDialog", typeof(RectTransform)); dlgGo.layer = 5; dlgGo.transform.SetParent(wlHud, false); _revive = dlgGo.AddComponent(); _revive.SetFont(font); _revive.Initialize(); } static void DestroyRig() { if (_rig != null) UnityEngine.Object.DestroyImmediate(_rig); _rig = null; _cam = null; _crt = null; _revive = null; } static void Step3_Result(WLIslandSettings cfg, GameObject prefabRoot) { H("④ 결과창 — 던전 1 / 던전 3 (StageEvents 실제 Dispatch 경로)"); // 던전 1 WLDungeonSceneSetup.Current = cfg.dungeons[0]; int idx1 = cfg.dungeons[0].stageIndex; WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseCleared(idx1, StageIdOf(idx1), 34.2f, 60f, 3, 3, 0, false, false); N("제목 \"" + RunResultUI.TitleText + "\" · 행 " + RunResultUI.VisibleRows + "개 · 던전=" + RunResultUI.IsDungeonResult + " \"" + RunResultUI.DungeonName + "\" 골드=" + RunResultUI.DungeonGold); N("본문 ⤵\n" + Indent(RunResultUI.BodyText)); Chk(RunResultUI.TitleText == "던전 1 클리어!", "제목 = \"던전 1 클리어!\" (지금까지는 \"스테이지 11 클리어!\")"); Chk(RunResultUI.BodyText.Contains("획득 골드 : +96"), "행 = \"획득 골드 : +96\""); Chk(RunResultUI.VisibleRows == 7, "행 6 + 획득 골드 1 = 7"); // 던전 3 RunResultUI.Hide(); WLDungeonSceneSetup.Current = cfg.dungeons[2]; int idx3 = cfg.dungeons[2].stageIndex; WLStageUiProbeHooks.RaiseEntered(idx3, StageIdOf(idx3), 3, 60f, Vector3.zero, 13f, 1.5f, 0); StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseCleared(idx3, StageIdOf(idx3), 41.5f, 60f, 3, 3, 0, false, false); N("제목 \"" + RunResultUI.TitleText + "\" · 골드=" + RunResultUI.DungeonGold); Chk(RunResultUI.TitleText == "던전 3 클리어!", "제목 = \"던전 3 클리어!\""); Chk(RunResultUI.BodyText.Contains("획득 골드 : +192"), "행 = \"획득 골드 : +192\""); } static string Indent(string s) { if (string.IsNullOrEmpty(s)) return " (빈 본문)"; var lines = s.Replace("\r", "").Split('\n'); var sb = new StringBuilder(); for (int i = 0; i < lines.Length; i++) if (lines[i].Length > 0) sb.AppendLine(" " + lines[i]); return sb.ToString().TrimEnd(); } static void Step4_Hud(WLIslandSettings cfg, GameObject prefabRoot) { H("⑤ HUD 단계 라벨 — 던전이면 「던전 N」"); WLDungeonSceneSetup.Current = cfg.dungeons[0]; int idx1 = cfg.dungeons[0].stageIndex; WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); N("던전 1 → 단계 \"" + RunHud.PhaseText + "\" · 남은 적 \"" + RunHud.EnemyText + "\""); Chk(RunHud.PhaseText == "던전 1", "HUD 라벨 = \"던전 1\" (지금까지는 \"스테이지 11\")"); WLDungeonSceneSetup.Current = cfg.dungeons[2]; int idx3 = cfg.dungeons[2].stageIndex; WLStageUiProbeHooks.RaiseStarted(idx3, StageIdOf(idx3), 3, 60f, 0); N("던전 3 → 단계 \"" + RunHud.PhaseText + "\""); Chk(RunHud.PhaseText == "던전 3", "HUD 라벨 = \"던전 3\""); int w0 = RunHud.StageLabelWrites; WLStageUiProbeHooks.RaiseStarted(idx3, StageIdOf(idx3), 3, 60f, 0); Chk(RunHud.StageLabelWrites == w0, "같은 이름 재입력 시 쓰기 0회(GC 0)"); } static void Step5_StageUnchanged(GameObject prefabRoot) { H("⑥ #815 스테이지 모드 무변경 (Current = null)"); WLDungeonSceneSetup.Current = null; RunResultUI.Hide(); WLStageUiProbeHooks.RaiseEntered(2, 3, 12, 60f, Vector3.zero, 14f, 1.5f, 0); WLStageUiProbeHooks.RaiseEnemyCount(2, 5, 12); WLStageUiProbeHooks.RaiseTime(2, 8f, 60f); N("단계 \"" + RunHud.PhaseText + "\" · 시간 \"" + RunHud.TimerText + "\" · 남은 적 \"" + RunHud.EnemyText + "\""); Chk(RunHud.PhaseText == "스테이지 3", "HUD = \"스테이지 3\" 그대로"); StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseCleared(2, 3, 34.2f, 60f, 12, 12, 0, false, false); N("제목 \"" + RunResultUI.TitleText + "\" · 행 " + RunResultUI.VisibleRows + "개 · 던전=" + RunResultUI.IsDungeonResult); N("본문 ⤵\n" + Indent(RunResultUI.BodyText)); Chk(RunResultUI.TitleText == "스테이지 3 클리어!", "제목 = \"스테이지 3 클리어!\" 그대로"); Chk(RunResultUI.VisibleRows == 6, "행 6개 그대로(획득 골드 없음)"); Chk(!RunResultUI.BodyText.Contains("획득 골드"), "본문에 「획득 골드」 0건"); Chk(!RunResultUI.IsDungeonResult && RunResultUI.DungeonGold == -1, "던전 문맥 없음"); // 구 경로(4인자 = 816m 이전 동작)와 같은 본문인가 — 진짜 before/after 대조 string after = RunResultUI.BodyText; var r = Cleared(2, 12, false); RunResultUI.Hide(); RunResultUI.SetStageMode(true); RunResultUI.ShowStage(in r, 0, 0, 0); // 816m 이전과 같은 진입점 RunResultUI.BuildBody(WLRunUiSettings.Instance); N("구 경로 본문 ⤵\n" + Indent(RunResultUI.BodyText)); Chk(RunResultUI.BodyText == after, "구 4인자 경로(=816m 이전) == 새 경로 본문 문자열 동일"); } static void Step6_Rollback(WLIslandSettings cfg, GameObject prefabRoot) { H("⑦ C8 롤백 — dungeonModeEnabled = 0 이면 던전도 지금까지 그대로"); var g = WLStageUiSettings.Instance; bool saved = g.dungeonModeEnabled; try { g.dungeonModeEnabled = false; WLDungeonSceneSetup.Current = cfg.dungeons[0]; int idx1 = cfg.dungeons[0].stageIndex; RunResultUI.Hide(); WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseCleared(idx1, StageIdOf(idx1), 34.2f, 60f, 3, 3, 0, false, false); N("롤백 제목 \"" + RunResultUI.TitleText + "\" · HUD \"" + RunHud.PhaseText + "\" · 행 " + RunResultUI.VisibleRows); Chk(RunResultUI.TitleText == "스테이지 11 클리어!", "롤백 = \"스테이지 11 클리어!\"(PD 가 본 화면)"); Chk(RunHud.PhaseText == "스테이지 11", "롤백 HUD = \"스테이지 11\""); Chk(RunResultUI.VisibleRows == 6, "롤백 행 6개"); } finally { g.dungeonModeEnabled = saved; } } // ═════════════════════════════════════════════════════════════════════ // ③ 캡처 — 1080×1920 목업(실 노드 · 실 값 · RenderTexture · 815d 와 같은 방식) // ═════════════════════════════════════════════════════════════════════ public static string Capture() { var sb = new StringBuilder(); string dir = Path.Combine(Path.Combine(Root, "Screenshots_WL"), "WL816m"); Directory.CreateDirectory(dir); var cfg = WLIslandSettings.Instance; var savedCurrent = WLDungeonSceneSetup.Current; GameObject prefabRoot = null; try { prefabRoot = PrefabUtility.LoadPrefabContents(kPrefab); BuildRig(prefabRoot); var cam = _cam; var revive = _revive; var rt = new RenderTexture(1080, 1920, 24, RenderTextureFormat.ARGB32); cam.targetTexture = rt; int idx1 = cfg.dungeons[0].stageIndex, idx3 = cfg.dungeons[2].stageIndex; // ⓒ HUD — 던전 1 WLDungeonSceneSetup.Current = cfg.dungeons[0]; WLStageUiProbeHooks.RaiseEntered(idx1, StageIdOf(idx1), 3, 60f, Vector3.zero, 13f, 1.5f, 0); WLStageUiProbeHooks.RaiseEnemyCount(idx1, 3, 3); WLStageUiProbeHooks.RaiseTime(idx1, 42f, 60f); RunHud.ApplyLayout(); RunResultUI.Hide(); revive.HideNow(); sb.AppendLine("ⓒ HUD 던전1 \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\""); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "c_hud_dungeon1.png"))); // ⓐ 던전 1 클리어 StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseCleared(idx1, StageIdOf(idx1), 34.2f, 60f, 3, 3, 0, false, false); RunResultUI.ApplyLayout(); sb.AppendLine("ⓐ 던전1 \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows + " 골드=" + RunResultUI.DungeonGold); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "a_dungeon1_clear.png"))); // ⓑ 던전 3 클리어 RunResultUI.Hide(); WLDungeonSceneSetup.Current = cfg.dungeons[2]; WLStageUiProbeHooks.RaiseEntered(idx3, StageIdOf(idx3), 3, 60f, Vector3.zero, 13f, 1.5f, 0); StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseCleared(idx3, StageIdOf(idx3), 41.5f, 60f, 3, 3, 0, false, false); RunResultUI.ApplyLayout(); sb.AppendLine("ⓑ 던전3 \"" + RunResultUI.TitleText + "\" 골드=" + RunResultUI.DungeonGold); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "b_dungeon3_clear.png"))); // ⓓ 스테이지 모드 대조 — 새 경로(Current=null) RunResultUI.Hide(); WLDungeonSceneSetup.Current = null; WLStageUiProbeHooks.RaiseEntered(2, 3, 12, 60f, Vector3.zero, 14f, 1.5f, 0); WLStageUiProbeHooks.RaiseEnemyCount(2, 5, 12); WLStageUiProbeHooks.RaiseTime(2, 8f, 60f); RunHud.ApplyLayout(); sb.AppendLine("ⓓ HUD 스테이지 \"" + RunHud.PhaseText + "\" \"" + RunHud.TimerText + "\" \"" + RunHud.EnemyText + "\""); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d1_hud_stage_new.png"))); StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseCleared(2, 3, 34.2f, 60f, 12, 12, 0, false, false); RunResultUI.ApplyLayout(); sb.AppendLine("ⓓ 스테이지 클리어(새 경로) \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d1_stage_clear_new.png"))); // ⓓ 대조군 — 816m 이전과 같은 4인자 진입점 RunResultUI.Hide(); RunHud.WriteStageLabel(3); RunHud.ApplyLayout(); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d2_hud_stage_legacy.png"))); var r = Cleared(2, 12, false); RunResultUI.SetStageMode(true); RunResultUI.ShowStage(in r, 0, 0, 0); RunResultUI.ApplyLayout(); sb.AppendLine("ⓓ 스테이지 클리어(구 4인자) \"" + RunResultUI.TitleText + "\" 행=" + RunResultUI.VisibleRows); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "d2_stage_clear_legacy.png"))); // ⓔ 실패 팝업 (ReviveDialog.cs 0줄 변경) RunResultUI.Hide(); StageUiBridge.ProbeSetInStage(true); WLStageUiProbeHooks.RaiseFailed(2, 3, WL.Combat.Stage.StageFailReason.TimeLimit, 4, 12, 60f, 0); revive.ApplyLayout(); sb.AppendLine("ⓔ 실패 \"" + revive.LastMessage.Replace("\n", " / ") + "\" 버튼=[" + revive.PrimaryButtonText + "] [" + revive.SecondaryButtonText + "]"); sb.AppendLine(Shot(cam, rt, Path.Combine(dir, "e_fail.png"))); cam.targetTexture = null; UnityEngine.Object.DestroyImmediate(rt); sb.AppendLine(); sb.AppendLine("픽셀 대조 (MD5) — 새 경로 vs 816m 이전 경로"); sb.AppendLine(" HUD " + SameFile(Path.Combine(dir, "d1_hud_stage_new.png"), Path.Combine(dir, "d2_hud_stage_legacy.png"))); sb.AppendLine(" 결과창 " + SameFile(Path.Combine(dir, "d1_stage_clear_new.png"), Path.Combine(dir, "d2_stage_clear_legacy.png"))); } catch (Exception e) { sb.AppendLine("예외 " + e.GetType().Name + " : " + e.Message + "\n" + e.StackTrace); } finally { try { StageUiBridge.Teardown(); RunHud.Teardown(); RunResultUI.Teardown(); } catch { } DestroyRig(); WLDungeonSceneSetup.Current = savedCurrent; if (prefabRoot != null) PrefabUtility.UnloadPrefabContents(prefabRoot); } string path = Path.Combine(Path.Combine(Root, "AgentScripts"), "WL816m_CAPTURE.txt"); File.WriteAllText(path, sb.ToString(), new UTF8Encoding(true)); return sb.ToString(); } static string SameFile(string a, string b) { if (!File.Exists(a) || !File.Exists(b)) return "파일 없음"; string ha = Md5(a), hb = Md5(b); return (ha == hb ? "동일 " : "다름 ") + ha.Substring(0, 12) + " / " + hb.Substring(0, 12); } static string Md5(string path) { using (var md5 = System.Security.Cryptography.MD5.Create()) using (var fs = File.OpenRead(path)) return BitConverter.ToString(md5.ComputeHash(fs)).Replace("-", "").ToLowerInvariant(); } 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 + ")"; } }