From a2c5a63babe195cdbb9c1c39da6ea606d406c0c5 Mon Sep 17 00:00:00 2001 From: swrring Date: Wed, 9 Sep 2026 09:23:06 +0900 Subject: [PATCH] =?UTF-8?q?[WL-813z]=20=ED=95=AB=ED=94=BD=EC=8A=A4:=20?= =?UTF-8?q?=EB=B6=80=ED=99=9C=20=EC=9B=8C=EC=B9=98=EB=8F=85=20=C2=B7=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=20=EC=B5=9C=EC=83=81=EB=8B=A8=20=C2=B7=20WL?= =?UTF-8?q?=5Fbtn=5Fok=20=C2=B7=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=ED=81=B4?= =?UTF-8?q?=EB=9E=A8=ED=94=84=20(#813)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AgentScripts/WL813z_Probe.cs | 554 ++++++++++++++++++ .../Resources/WL/WLSurvivalSettings.asset | 7 + Assets/WL/Combat/Survival/DeathFlow.cs | 25 +- .../WL/Combat/Survival/WLSurvivalSettings.cs | 34 ++ Assets/WL/UI/Scripts/LootToast.cs | 48 +- Assets/WL/UI/Scripts/ReviveDialog.cs | 174 +++++- .../Resources/WL/WLCombatTextSettings.asset | 4 + Assets/WL/UI/Settings/WLCombatTextSettings.cs | 18 + 8 files changed, 855 insertions(+), 9 deletions(-) create mode 100644 AgentScripts/WL813z_Probe.cs diff --git a/AgentScripts/WL813z_Probe.cs b/AgentScripts/WL813z_Probe.cs new file mode 100644 index 000000000..b839611aa --- /dev/null +++ b/AgentScripts/WL813z_Probe.cs @@ -0,0 +1,554 @@ +// WL-813z 프로브 — Q4-a 핫픽스 실측(에디트 모드 · Play 0 · #813). +// run_script --file AgentScripts/WL813z_Probe.cs --entry WL813z_Probe. +// Watchdog · Topmost · Toast · All +// 로그는 /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**"; } + + /// 세로 1080×1920 기준(참조 1920×1080 Expand)의 합성 캔버스 + Safe Area 부모. + 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.renderMode = RenderMode.ScreenSpaceOverlay; + var scaler = root.AddComponent(); + scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; + scaler.referenceResolution = new Vector2(1920f, 1080f); // 원본 NewGameUI 와 같은 설정(802b 실측 1.7778) + scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.Expand; + root.AddComponent(); + + 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(); + 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(); + rt.gameObject.hideFlags = HideFlags.DontSave; + rt.SetParent(parent, false); + return rt; + } + + /// 813y 프로브와 같은 가짜 메인 PC(사망 상태). 원본 회복 경로는 ReviveOverride 로 막는다. + static PCActor MakePC() + { + var go = new GameObject("__WL813z_PC"); + go.hideFlags = HideFlags.DontSave; + var pc = go.AddComponent(); + 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(); + + 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(); + var gr = dlgRt.GetComponent(); + var cg = dlgRt.GetComponent(); + 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(true)) + { + if (t.name == okName) found = t; + if (t.name == "btn_ok") old = t; + } + bool dd = found != null && found.GetComponent