Project_WL/AgentScripts/WL813z_Probe.cs

555 lines
32 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-813z 프로브 — Q4-a 핫픽스 실측(에디트 모드 · Play 0 · #813).
// run_script --file AgentScripts/WL813z_Probe.cs --entry WL813z_Probe.<Entry>
// Watchdog · Topmost · Toast · All
// 로그는 <worktree>/Logs/WL813z_*.txt (Assets 밖 · 미추적).
//
// 🔴 에디트 모드에서 `Time.timeScale` 에 **쓰지 않는다** — 쓰면 ProjectSettings/TimeManager.asset 이 더러워진다
// (811b-fix 실측 · DeathFlow.HoldWorld 도 같은 이유로 `Application.isPlaying` 가드를 둔다).
// 세계 정지 복원은 ① `DeathFlow.WorldHeld` 전이 ② `MyValue.Get_GameSpeed()` 실측값 ③ `Time.timeScale` 읽기로 증명한다.
// 🔴 씬·프리팹을 저장하지 않는다. 만든 노드는 finally 에서 전부 DestroyImmediate.
// 🔴 SO 값은 메모리에서만 바꾸고 finally 에서 원복한다(AssetDatabase.SaveAssets 호출 0).
using System;
using System.IO;
using System.Reflection;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using WL.Combat.Survival;
using WL.UI;
public static class WL813z_Probe
{
const BindingFlags NP = BindingFlags.Instance | BindingFlags.NonPublic;
// ─────────────────────────────────────────────────────────── 공통
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 Ok(bool b) { return b ? "PASS" : "**FAIL**"; }
/// <summary>세로 1080×1920 기준(참조 1920×1080 Expand)의 합성 캔버스 + Safe Area 부모.</summary>
static RectTransform BuildCanvas(out GameObject root, out Canvas canvas, out float unitsPerPx)
{
root = new GameObject("__WL813z_Canvas", typeof(RectTransform));
root.hideFlags = HideFlags.DontSave;
canvas = root.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = root.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f); // 원본 NewGameUI 와 같은 설정(802b 실측 1.7778)
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand;
root.AddComponent<GraphicRaycaster>();
var s = WLCombatTextSettings.Instance;
unitsPerPx = s != null ? s.UnitsPerPx(canvas) : 1f;
// WL_HUD 대역(SafeAreaFitter 아래 = Safe Area) — 배치가 화면 크기에 흔들리지 않게 크기를 못박는다.
var hud = new GameObject("WL_HUD", typeof(RectTransform)).GetComponent<RectTransform>();
hud.gameObject.hideFlags = HideFlags.DontSave;
hud.SetParent(root.transform, false);
hud.anchorMin = hud.anchorMax = new Vector2(0.5f, 0.5f);
hud.pivot = new Vector2(0.5f, 0.5f);
hud.anchoredPosition = Vector2.zero;
hud.sizeDelta = new Vector2(1080f * unitsPerPx, 1920f * unitsPerPx);
return hud;
}
static RectTransform Decoy(RectTransform parent, string n)
{
var rt = new GameObject(n, typeof(RectTransform)).GetComponent<RectTransform>();
rt.gameObject.hideFlags = HideFlags.DontSave;
rt.SetParent(parent, false);
return rt;
}
/// <summary>813y 프로브와 같은 가짜 메인 PC(사망 상태). 원본 회복 경로는 ReviveOverride 로 막는다.</summary>
static PCActor MakePC()
{
var go = new GameObject("__WL813z_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 OnReq(in ReviveRequestEvent e) { s_reqSeen++; }
static int s_reqSeen;
static void Kill(GameObject go) { if (go != null) UnityEngine.Object.DestroyImmediate(go); }
// ─────────────────────────────────────────────── ① D-3 부활 워치독
public static string Watchdog()
{
var sb = new StringBuilder();
sb.AppendLine("=== WL-813z ① D-3 부활 워치독 (에디트 모드 · Play 0) ===");
var st = WLSurvivalSettings.Instance;
if (st == null) { var b0 = sb.AppendLine("**FAIL** WLSurvivalSettings.asset 없음").ToString(); return b0 + "\n→ " + Write("WL813z_watchdog.txt", b0); }
float ts0 = Time.timeScale;
int ov0 = WLSurvivalSettings.RuntimeOverride;
float wd0 = st.reviveWatchdogSeconds; bool ig0 = st.reviveWatchdogIgnoresDisable; bool lg0 = st.reviveWatchdogLog;
float to0 = st.autoReviveTimeoutSeconds; bool an0 = st.autoReviveWhenNoListener; bool hw0 = st.holdWorldWhileReviving;
var ovr0 = DeathFlow.ReviveOverride;
int overrideCalls = 0;
PCActor pc = null;
sb.AppendLine("에셋 값 실측: reviveWatchdogSeconds=" + wd0.ToString("F1") + "s · 무시disable=" + ig0 +
" · log=" + lg0 + " · autoReviveTimeoutSeconds=" + to0.ToString("F1") + "s · holdWorld=" + hw0);
sb.AppendLine("세계 복원값 실측: MyValue.Get_GameSpeed() = " + MyValue.Get_GameSpeed().ToString("F2") +
" · 시작 Time.timeScale = " + ts0.ToString("F2") +
" (에디트 모드에선 DeathFlow.HoldWorld 가 isPlaying 가드로 0 을 쓰지 않는다 = TimeManager.asset 무변경)");
sb.AppendLine();
try
{
WLSurvivalSettings.RuntimeOverride = 1;
DeathFlow.ReviveOverride = (p, r) => { overrideCalls++; };
s_reqSeen = 0;
DeathFlow.ReviveRequested.Add(OnReq); // 구독자 1 = autoReviveWhenNoListener 무력화(813y 와 같은 조건)
pc = MakePC();
float disp = st.deathDisplaySeconds;
// ── A. 기본 20 s 워치독
st.reviveWatchdogSeconds = 20f; st.reviveWatchdogIgnoresDisable = true; st.reviveWatchdogLog = true;
DeathFlow.ResetDiagnostics();
float t = 1000f;
DeathFlow.Tick(t);
DeathFlow.OnPCDied(pc);
sb.AppendLine("[A] 20 s 워치독 (정상 · 스위치 ON)");
sb.AppendLine(" 사망 → state=" + DeathFlow.State + " deathCount=" + DeathFlow.DeathCount);
float req = t + disp + 0.01f;
DeathFlow.Tick(req);
sb.AppendLine(" +" + disp.ToString("F2") + "s → state=" + DeathFlow.State + " · ReviveRequested 수신=" + s_reqSeen +
" · 구독자=" + DeathFlow.ReviveRequested.Count + " · WorldHeld=" + DeathFlow.WorldHeld);
DeathFlow.Tick(req + 19.9f);
sb.AppendLine(" +19.9s(팝업이 에러 모달에 가려 확인 못 누른 상태) → state=" + DeathFlow.State +
" 대기=" + DeathFlow.AwaitingSeconds.ToString("F2") + "s · 워치독=" + DeathFlow.WatchdogCount + "회 · " +
Ok(DeathFlow.State == DeathState.AwaitingRevive && DeathFlow.WatchdogCount == 0));
DeathFlow.Tick(req + 20.02f);
bool a = DeathFlow.State == DeathState.None && DeathFlow.WatchdogCount == 1 && DeathFlow.AutoReviveCount == 1
&& DeathFlow.ReviveCount == 1 && !DeathFlow.WorldHeld && overrideCalls == 1;
sb.AppendLine(" +20.02s → state=" + DeathFlow.State + " · 워치독=" + DeathFlow.WatchdogCount + "회(" +
DeathFlow.LastWatchdogSeconds.ToString("F2") + "s) · auto=" + DeathFlow.AutoReviveCount +
" · reviveCount=" + DeathFlow.ReviveCount + " · 부활실행=" + overrideCalls +
" · WorldHeld=" + DeathFlow.WorldHeld + " · timeScale=" + Time.timeScale.ToString("F2") + " · " + Ok(a));
sb.AppendLine();
// ── B. RuntimeDisabled 무관 (도중에 스위치가 꺼져도 워치독이 돈다)
DeathFlow.ResetDiagnostics(); overrideCalls = 0; s_reqSeen = 0;
t = 2000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
req = t + disp + 0.01f; DeathFlow.Tick(req);
WLSurvivalSettings.RuntimeOverride = -1; // = QA 의 RuntimeDisabled
sb.AppendLine("[B] 대기 중 RuntimeDisabled(-1) · reviveWatchdogIgnoresDisable=true");
sb.AppendLine(" Enabled=" + WLSurvivalSettings.Enabled + " state=" + DeathFlow.State);
DeathFlow.Tick(req + 20.02f);
bool bb = DeathFlow.State == DeathState.None && DeathFlow.WatchdogCount == 1 && DeathFlow.AbortCount == 0;
sb.AppendLine(" +20.02s → state=" + DeathFlow.State + " · 워치독=" + DeathFlow.WatchdogCount + "회 · abort=" +
DeathFlow.AbortCount + " · 부활실행=" + overrideCalls + " · WorldHeld=" + DeathFlow.WorldHeld + " · " + Ok(bb));
// ── B'. 무시disable=false 면 워치독 대신 Abort — 이때도 세계 정지는 풀린다(잠금 0)
st.reviveWatchdogIgnoresDisable = false;
DeathFlow.ResetDiagnostics(); overrideCalls = 0;
WLSurvivalSettings.RuntimeOverride = 1;
t = 3000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
req = t + disp + 0.01f; DeathFlow.Tick(req);
WLSurvivalSettings.RuntimeOverride = -1;
DeathFlow.Tick(req + 0.02f);
bool bp = DeathFlow.State == DeathState.None && DeathFlow.AbortCount == 1 && !DeathFlow.WorldHeld;
sb.AppendLine(" 무시disable=false → 대신 Abort(\"" + DeathFlow.LastAbortReason + "\") · state=" + DeathFlow.State +
" · WorldHeld=" + DeathFlow.WorldHeld + " (ReleaseWorld 통과 = 잠금 0) · " + Ok(bp));
st.reviveWatchdogIgnoresDisable = true;
WLSurvivalSettings.RuntimeOverride = 1;
sb.AppendLine();
// ── C. C8 롤백 — reviveWatchdogSeconds = 0 이면 워치독이 없다(813i 원래 동작 100%)
st.reviveWatchdogSeconds = 0f;
DeathFlow.ResetDiagnostics(); overrideCalls = 0;
t = 4000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
req = t + disp + 0.01f; DeathFlow.Tick(req);
DeathFlow.Tick(req + 999f);
bool c = DeathFlow.State == DeathState.AwaitingRevive && DeathFlow.WatchdogCount == 0;
sb.AppendLine("[C] C8 롤백 reviveWatchdogSeconds=0 → +999s 뒤에도 state=" + DeathFlow.State +
" · 워치독=" + DeathFlow.WatchdogCount + "회 · " + Ok(c) + " (= 813i 원래 동작)");
DeathFlow.Abort("probe cleanup");
sb.AppendLine();
// ── D. 정상 확인 버튼 경로가 워치독보다 먼저면 워치독은 안 돈다
st.reviveWatchdogSeconds = 20f;
DeathFlow.ResetDiagnostics(); overrideCalls = 0;
t = 5000f; DeathFlow.Tick(t); DeathFlow.OnPCDied(pc);
req = t + disp + 0.01f; DeathFlow.Tick(req);
DeathFlow.Tick(req + 3f);
bool revived = DeathFlow.Revive();
bool d = revived && DeathFlow.State == DeathState.None && DeathFlow.WatchdogCount == 0 && DeathFlow.AutoReviveCount == 0;
sb.AppendLine("[D] 3 s 뒤 확인 버튼(DeathFlow.Revive()) → " + revived + " · state=" + DeathFlow.State +
" · 워치독=" + DeathFlow.WatchdogCount + "회 · auto=" + DeathFlow.AutoReviveCount + " · " + Ok(d));
}
catch (Exception ex) { sb.AppendLine("**예외** " + ex); }
finally
{
DeathFlow.ReviveRequested.Remove(OnReq);
DeathFlow.Abort("probe finally");
DeathFlow.ReviveOverride = ovr0;
DeathFlow.ResetDiagnostics();
st.reviveWatchdogSeconds = wd0; st.reviveWatchdogIgnoresDisable = ig0; st.reviveWatchdogLog = lg0;
st.autoReviveTimeoutSeconds = to0; st.autoReviveWhenNoListener = an0; st.holdWorldWhileReviving = hw0;
WLSurvivalSettings.RuntimeOverride = ov0;
if (pc != null) Kill(pc.gameObject);
}
sb.AppendLine();
sb.AppendLine("정리: state=" + DeathFlow.State + " · WorldHeld=" + DeathFlow.WorldHeld +
" · 구독 Req=" + DeathFlow.ReviveRequested.Count +
" · timeScale " + ts0.ToString("F2") + " → " + Time.timeScale.ToString("F2") +
" (에디트 모드 쓰기 0) · SO 원복 완료");
var body = sb.ToString();
return body + "\n→ " + Write("WL813z_watchdog.txt", body);
}
// ─────────────────────────────────── ② 부활 팝업 최상단 보장 + WL_btn_ok
public static string Topmost()
{
var sb = new StringBuilder();
sb.AppendLine("=== WL-813z ② 부활 팝업 최상단 보장(D-3) + WL_btn_ok(D-9) ===");
var st = WLSurvivalSettings.Instance;
var us = WLSurvivalUiSettings.Instance;
if (st == null || us == null)
{
var b0 = sb.AppendLine("**FAIL** WLSurvivalSettings/WLSurvivalUiSettings 에셋 없음").ToString();
return b0 + "\n→ " + Write("WL813z_topmost.txt", b0);
}
bool tm0 = st.reviveTopmostEnabled; int so0 = st.reviveSortingOrder;
float iv0 = st.reviveTopmostIntervalSeconds; bool es0 = st.reviveEscalateOnHijack;
GameObject root = null;
try
{
Canvas canvas; float u;
var hud = BuildCanvas(out root, out canvas, out u);
sb.AppendLine("합성 캔버스: unitsPerPx=" + u.ToString("F4") + " · WL_HUD size=" + hud.sizeDelta +
" · 공용 팝업 프리팹 SortOrder_5 의 Canvas.m_SortingOrder = 5 (실측 · Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab:9159)");
sb.AppendLine("에셋 값: reviveTopmostEnabled=" + tm0 + " · reviveSortingOrder=" + so0 +
" · 재보장 주기=" + iv0.ToString("F2") + "s · 승격=" + es0);
sb.AppendLine();
Decoy(hud, "기존팝업_A");
var dlgRt = Decoy(hud, "WL_ReviveDialog");
Decoy(hud, "기존팝업_B");
var dlg = dlgRt.gameObject.AddComponent<ReviveDialog>();
sb.AppendLine("[A] 표시 순간 최상단 · 레이캐스트");
sb.AppendLine(" init → " + dlg.Initialize().Split('·')[0].Trim());
sb.AppendLine(" 표시 전: 맨뒤형제=" + dlg.IsLastSibling + " sortingOrder=" + dlg.SortingOrder);
string shown = dlg.ShowNow();
var cv = dlgRt.GetComponent<Canvas>();
var gr = dlgRt.GetComponent<GraphicRaycaster>();
var cg = dlgRt.GetComponent<CanvasGroup>();
bool a = dlg.IsLastSibling && cv != null && cv.overrideSorting && cv.sortingOrder == st.reviveSortingOrder
&& cv.sortingOrder > 5 && gr != null && gr.enabled && cg != null && cg.blocksRaycasts;
sb.AppendLine(" ShowNow → " + shown);
sb.AppendLine(" 맨뒤형제=" + dlg.IsLastSibling + " · overrideSorting=" + (cv != null && cv.overrideSorting) +
" · sortingOrder=" + (cv != null ? cv.sortingOrder : -1) + "(> 공용 5) · GraphicRaycaster=" +
(gr != null && gr.enabled) + " · CanvasGroup.blocksRaycasts=" + (cg != null && cg.blocksRaycasts) +
" · " + Ok(a));
sb.AppendLine();
sb.AppendLine("[B] 표시 중 " + iv0.ToString("F2") + "s 마다 최상단 재보장");
float now = Time.unscaledTime;
Decoy(hud, "나중에_뜬_에러팝업"); // 부활 팝업 위로 올라온다
bool before = dlg.IsLastSibling;
int tc0 = dlg.TopmostCount;
dlg.Tick(now + iv0 + 0.01f);
bool b = !before && dlg.IsLastSibling && dlg.TopmostCount > tc0;
sb.AppendLine(" 에러 팝업이 뒤에 붙음 → 맨뒤형제=" + before + " → Tick(+" + (iv0 + 0.01f).ToString("F2") +
"s) → 맨뒤형제=" + dlg.IsLastSibling + " · 재보장 " + tc0 + "→" + dlg.TopmostCount + "회 · " + Ok(b));
sb.AppendLine();
sb.AppendLine("[C] 재보장 루프 GC 0");
for (int i = 0; i < 20; i++) dlg.Tick(now + 100f + i * (iv0 + 0.01f)); // 워밍업(첫 AddComponent 배제)
long m0 = GC.GetTotalMemory(true);
int n = 600;
for (int i = 0; i < n; i++) dlg.Tick(now + 200f + i * (iv0 + 0.01f));
long m1 = GC.GetTotalMemory(false);
long d = m1 - m0;
sb.AppendLine(" Tick ×" + n + "(전부 재보장 실행) → GC.GetTotalMemory 델타 = " + d + " B · 재보장 누계 " +
dlg.TopmostCount + "회 · " + Ok(d <= 0));
sb.AppendLine();
sb.AppendLine("[D] D-9 런타임 버튼 이름");
var okName = ReviveDialog.OkButtonName;
Transform found = null, old = null;
foreach (var t in dlgRt.GetComponentsInChildren<Transform>(true))
{
if (t.name == okName) found = t;
if (t.name == "btn_ok") old = t;
}
bool dd = found != null && found.GetComponent<Button>() != null && old == null && okName == "WL_btn_ok";
sb.AppendLine(" ReviveDialog.OkButtonName=\"" + okName + "\" · 노드 존재=" + (found != null) +
" · Button=" + (found != null && found.GetComponent<Button>() != null) +
" · 옛 \"btn_ok\" 노드=" + (old != null ? old.name : "없음") + " · " + Ok(dd));
sb.AppendLine(" → QA 절차: `LightProbe.ClickButton [\"WL_btn_ok\"]` (원본 `NightmareResult/btn_ok` 와 이름 충돌 0)");
sb.AppendLine();
sb.AppendLine("[E] 공용 Popup 탈취 → 전용 팝업 승격");
dlg.HideNow();
sb.AppendLine(" " + dlg.ProbeSimulateCommonPopup("존 시작점에서 부활합니다.\n무료"));
sb.AppendLine(" (에디트 모드는 Popup.Ins=" + (Popup.Ins != null ? "있음" : "없음") + " → '공용 팝업이 사라졌다/덮였다'와 같은 조건)");
string r = dlg.ProbeCheckHijack();
bool e = dlg.HijackCount == 1 && dlg.Escalated && dlg.FallbackVisible && dlg.IsLastSibling && !dlg.UsedCommonPopup;
sb.AppendLine(" " + r + " · " + Ok(e));
sb.AppendLine();
sb.AppendLine("[F] C8 롤백");
st.reviveTopmostEnabled = false;
var dlg2Rt = Decoy(hud, "WL_ReviveDialog_C8");
var dlg2 = dlg2Rt.gameObject.AddComponent<ReviveDialog>();
dlg2.Initialize();
Decoy(hud, "C8_뒤형제");
dlg2.ShowNow();
bool f1 = dlg2Rt.GetComponent<Canvas>() == null && !dlg2.IsLastSibling;
sb.AppendLine(" reviveTopmostEnabled=0 → Canvas 부착=" + (dlg2Rt.GetComponent<Canvas>() != null) +
" · 맨뒤형제=" + dlg2.IsLastSibling + " · " + Ok(f1) + " (813y 동작 100%)");
st.reviveTopmostEnabled = tm0;
st.reviveEscalateOnHijack = false;
dlg2.HideNow();
dlg2.ProbeSimulateCommonPopup("존 시작점에서 부활합니다.");
int hj0 = dlg2.HijackCount;
dlg2.ProbeCheckHijack();
bool f2 = dlg2.HijackCount == hj0 && !dlg2.Escalated;
sb.AppendLine(" reviveEscalateOnHijack=0 → 탈취감지 " + hj0 + "→" + dlg2.HijackCount + " · 승격=" + dlg2.Escalated + " · " + Ok(f2));
st.reviveEscalateOnHijack = es0;
dlg2.HideNow();
sb.AppendLine();
// ── G. 공용 Popup 싱글턴을 가짜로 세워 D-3 사슬 전체를 재현한다.
// (에디트 모드엔 Popup·BackKeyMgr 인스턴스가 없다 — 둘 다 public static 필드라 직접 세운다.)
sb.AppendLine("[G] 공용 Popup 실물 재현 — 탈취 감지 / 남의 팝업 불간섭 / QA-4 닫힘");
var popup0 = Popup.Ins; var bk0 = BackKeyMgr.Ins;
GameObject fakeBk = null, fakePopupGo = null;
try
{
fakeBk = new GameObject("__WL813z_BackKeyMgr"); fakeBk.hideFlags = HideFlags.DontSave;
BackKeyMgr.Ins = fakeBk.AddComponent<BackKeyMgr>();
fakePopupGo = new GameObject("__WL813z_Popup"); fakePopupGo.hideFlags = HideFlags.DontSave;
var fp = fakePopupGo.AddComponent<Popup>();
var popBody = new GameObject("m_popup"); popBody.hideFlags = HideFlags.DontSave; popBody.transform.SetParent(fakePopupGo.transform, false);
var msgGo = new GameObject("label_msg", typeof(RectTransform)); msgGo.hideFlags = HideFlags.DontSave;
msgGo.transform.SetParent(popBody.transform, false);
fp.m_popup = popBody;
fp.label_msg = msgGo.AddComponent<TextMeshProUGUI>();
Popup.Ins = fp;
const string mine = "존 시작점에서 부활합니다.\n무료";
// G-1. 우리 본문이 그대로면 탈취 아님(오탐 0)
dlg.HideNow();
dlg.ProbeSimulateCommonPopup(mine);
popBody.SetActive(true); fp.label_msg.text = mine;
int hj = dlg.HijackCount;
dlg.ProbeCheckHijack();
bool g1 = dlg.HijackCount == hj && !dlg.Escalated && dlg.UsedCommonPopup;
sb.AppendLine(" G-1 본문 그대로 → 탈취감지 " + hj + "→" + dlg.HijackCount + " · 승격=" + dlg.Escalated + " · " + Ok(g1) + " (오탐 0)");
// G-2. QA-4 — 부활이 끝나면 공용 Popup 도 닫는다(우리 것일 때만)
int closed0 = dlg.CommonPopupClosed;
dlg.HideNow();
bool g2 = !popBody.activeSelf && dlg.CommonPopupClosed == closed0 + 1;
sb.AppendLine(" G-2 HideNow(부활 완료) → 공용 Popup active=" + popBody.activeSelf + " · 우리가 닫음 " +
closed0 + "→" + dlg.CommonPopupClosed + " · " + Ok(g2) + " (Q4-a QA-4 해소)");
// G-3. ErrorLogHookManager 가 같은 싱글턴을 덮어쓴 상태 = D-3 그 자체
dlg.ProbeSimulateCommonPopup(mine);
popBody.SetActive(true); fp.label_msg.text = mine;
fp.label_msg.text = "NullReferenceException: Object reference not set …"; // = ErrorLogHookManager.cs:33 이 덮어쓴 뒤
int hj2 = dlg.HijackCount;
dlg.ProbeCheckHijack();
bool g3 = dlg.HijackCount == hj2 + 1 && dlg.Escalated && dlg.FallbackVisible && dlg.IsLastSibling && !dlg.UsedCommonPopup;
sb.AppendLine(" G-3 에러 모달이 덮음(본문 교체 = m_ok 도 null 로 지워진 상태) → 탈취감지 " + hj2 + "→" + dlg.HijackCount +
" · 승격=" + dlg.Escalated + " · 전용 팝업 표시=" + dlg.FallbackVisible +
" · 맨뒤형제=" + dlg.IsLastSibling + " · sortingOrder=" + dlg.SortingOrder + " · " + Ok(g3));
// G-4. 남의 팝업은 닫지 않는다
int closed1 = dlg.CommonPopupClosed;
dlg.HideNow();
bool g4 = popBody.activeSelf && dlg.CommonPopupClosed == closed1;
sb.AppendLine(" G-4 남의(에러) 팝업 → HideNow 후에도 active=" + popBody.activeSelf + " · 닫은 수 " +
closed1 + "→" + dlg.CommonPopupClosed + " · " + Ok(g4) + " (불간섭)");
}
finally
{
Popup.Ins = popup0; BackKeyMgr.Ins = bk0;
Kill(fakePopupGo); Kill(fakeBk);
}
dlg.HideNow();
}
catch (Exception ex) { sb.AppendLine("**예외** " + ex); }
finally
{
st.reviveTopmostEnabled = tm0; st.reviveSortingOrder = so0;
st.reviveTopmostIntervalSeconds = iv0; st.reviveEscalateOnHijack = es0;
Kill(root);
}
sb.AppendLine("정리: 합성 노드 파기 · SO 원복 · 씬/프리팹 저장 0");
var body = sb.ToString();
return body + "\n→ " + Write("WL813z_topmost.txt", body);
}
// ─────────────────────────────────── ③ D-7 전리품 토스트 Safe Area 클램프
public static string Toast()
{
var sb = new StringBuilder();
sb.AppendLine("=== WL-813z ③ D-7 전리품 토스트 Safe Area 클램프 ===");
var s = WLCombatTextSettings.Instance;
if (s == null) { var b0 = sb.AppendLine("**FAIL** WLCombatTextSettings.asset 없음").ToString(); return b0 + "\n→ " + Write("WL813z_toast.txt", b0); }
bool cl0 = s.lootToastClampToSafeArea; float mw0 = s.lootToastMaxWidthPx;
float sp0 = s.lootToastSafePadPx; bool el0 = s.lootToastEllipsis;
GameObject root = null;
try
{
Canvas canvas; float u;
var hud = BuildCanvas(out root, out canvas, out u);
var toastRt = Decoy(hud, "WL_LootToast");
var toast = toastRt.gameObject.AddComponent<LootToast>();
sb.AppendLine("합성 Safe Area: 폭 1080 px(=" + (1080f * u).ToString("F1") + " 유닛) · unitsPerPx=" + u.ToString("F4"));
sb.AppendLine("에셋 값: widthPx=" + s.lootToastWidthPx.ToString("F0") + " · rightMarginPx=" + s.lootToastRightMarginPx.ToString("F0") +
" · slideInPx=" + s.lootToastSlideInPx.ToString("F0") + " · panelPadXPx=" + s.lootToastPanelPadXPx.ToString("F0") +
" · centerOffsetPx=" + s.lootToastCenterOffsetPx.ToString("F0") + "(유지)");
sb.AppendLine();
// ── A. 클램프 OFF = Q4-a 가 본 상태를 그대로 재현
s.lootToastClampToSafeArea = false;
toast.Initialize();
sb.AppendLine("[A] 클램프 OFF(= 813y 현재 · D-7 재현)");
sb.AppendLine(" " + toast.LastClamp);
float overOff = s.lootToastSlideInPx + s.lootToastPanelPadXPx - s.lootToastRightMarginPx;
sb.AppendLine(" 등장 프레임 우측 침범 = slideIn " + s.lootToastSlideInPx.ToString("F0") + " + 패널여백 " +
s.lootToastPanelPadXPx.ToString("F0") + " 우여백 " + s.lootToastRightMarginPx.ToString("F0") +
" = **+" + overOff.ToString("F0") + " px**(Safe Area 밖 · " + s.lootToastSlideSeconds.ToString("F2") +
"s 동안) → Q4-a 캡처 23_arena_12 / 29_levelup_01 의 잘린 `[-]` 와 일치 · " + Ok(overOff > 0f));
sb.AppendLine();
// ── B. 클램프 ON
s.lootToastClampToSafeArea = true;
toast.Initialize();
sb.AppendLine("[B] 클램프 ON");
sb.AppendLine(" " + toast.LastClamp);
var lineRoot = toastRt.Find("Lines") as RectTransform;
var line0 = lineRoot != null ? lineRoot.Find("Line0") as RectTransform : null;
var label0 = line0 != null ? line0.Find("Label") as RectTransform : null;
float rightEdgeUnits = lineRoot != null ? lineRoot.anchoredPosition.x : 0f; // 부모 우측 기준(0 = Safe Area 우변)
float padUnits = (s.lootToastPanelEnabled ? s.lootToastPanelPadXPx : 0f) * u;
float slideMax = (float)typeof(LootToast).GetField("_slideMaxPx", NP).GetValue(toast);
float widthPx = (float)typeof(LootToast).GetField("_widthPx", NP).GetValue(toast);
float overOn = (rightEdgeUnits + slideMax * u + padUnits) / u; // > 0 이면 Safe Area 밖
bool b = overOn <= 0.001f && widthPx <= s.lootToastMaxWidthPx + 0.001f;
sb.AppendLine(" 줄 뿌리 우측 = " + (rightEdgeUnits / u).ToString("F1") + " px(0 = Safe Area 우변) · 슬라이드 상한 = " +
slideMax.ToString("F1") + " px · 패널 여백 " + s.lootToastPanelPadXPx.ToString("F0") + " px");
sb.AppendLine(" 등장 프레임 우측 침범 = **" + overOn.ToString("F1") + " px** (≤ 0 = Safe Area 안) · 실폭 " +
widthPx.ToString("F1") + " px ≤ 상한 " + s.lootToastMaxWidthPx.ToString("F0") + " px · " + Ok(b));
sb.AppendLine();
// ── C. 긴 이름 말줄임
sb.AppendLine("[C] 긴 아이템 이름 말줄임(…)");
var tmp = label0 != null ? label0.GetComponent<TextMeshProUGUI>() : null;
if (tmp == null) sb.AppendLine(" **FAIL** Line0/Label 없음");
else
{
string longName = "아주아주긴전설급아이템이름테스트문자열0123456789";
tmp.text = longName;
tmp.ForceMeshUpdate();
int shownChars = tmp.textInfo.characterCount;
// TMP 는 말줄임을 넣은 뒤 다음 칸에 0x03(End Of Text)을 넣는다(TMP_Text.cs:4067·4077 실측) —
// 그래서 마지막 칸이 아니라 **가시 구간 전체**에서 '…' 를 찾는다.
bool hasEllipsis = false; int ellipsisAt = -1;
for (int ci = 0; ci < shownChars; ci++)
if (tmp.textInfo.characterInfo[ci].character == '…') { hasEllipsis = true; ellipsisAt = ci; break; }
float lineW = tmp.textInfo.lineCount > 0 ? tmp.textInfo.lineInfo[0].width / u : 0f;
float rectW = tmp.rectTransform.rect.width / u;
bool c = tmp.textWrappingMode == TextWrappingModes.NoWrap && tmp.overflowMode == TextOverflowModes.Ellipsis
&& tmp.isTextTruncated && hasEllipsis && lineW <= rectW + 0.5f;
sb.AppendLine(" wrapping=" + tmp.textWrappingMode + " · overflow=" + tmp.overflowMode +
" · 원문 " + longName.Length + "자 → 표시 " + shownChars + "자 · isTextTruncated=" + tmp.isTextTruncated +
" · '…' 위치 " + ellipsisAt + " · 줄 폭 " + lineW.ToString("F1") + " px ≤ rect " +
rectW.ToString("F1") + " px · " + Ok(c));
tmp.text = "";
}
sb.AppendLine();
// ── D. C8 롤백
s.lootToastClampToSafeArea = false;
toast.Initialize();
float w2 = (float)typeof(LootToast).GetField("_widthPx", NP).GetValue(toast);
float sm2 = (float)typeof(LootToast).GetField("_slideMaxPx", NP).GetValue(toast);
bool d = Mathf.Approximately(w2, s.lootToastWidthPx) && Mathf.Approximately(sm2, s.lootToastSlideInPx);
sb.AppendLine("[D] C8 롤백 lootToastClampToSafeArea=0 → 폭 " + w2.ToString("F1") + " px(에셋 " +
s.lootToastWidthPx.ToString("F0") + ") · 슬라이드 " + sm2.ToString("F1") + " px(에셋 " +
s.lootToastSlideInPx.ToString("F0") + ") · " + Ok(d) + " (813y 배치 100%)");
toast.Subscribe(false);
}
catch (Exception ex) { sb.AppendLine("**예외** " + ex); }
finally
{
s.lootToastClampToSafeArea = cl0; s.lootToastMaxWidthPx = mw0;
s.lootToastSafePadPx = sp0; s.lootToastEllipsis = el0;
Kill(root);
}
sb.AppendLine("정리: 합성 노드 파기 · SO 원복 · Killed 구독 해제");
var body = sb.ToString();
return body + "\n→ " + Write("WL813z_toast.txt", body);
}
// ─────────────────────────────────────────────────────────── 전부
public static string All()
{
var sb = new StringBuilder();
sb.AppendLine("################ WL-813z 프로브 전체 ################");
sb.AppendLine(Watchdog()); sb.AppendLine();
sb.AppendLine(Topmost()); sb.AppendLine();
sb.AppendLine(Toast());
var body = sb.ToString();
return body + "\n→ " + Write("WL813z_all.txt", body);
}
}