Project_WL/AgentScripts/WL816m_Probe.cs

505 lines
26 KiB
C#
Raw Normal View History

// 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
// 산출물: <worktree>/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<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>();
_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<ReviveDialog>();
_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 + ")";
}
}