Project_WL/AgentScripts/WL813c3_Probe.cs

377 lines
21 KiB
C#
Raw Normal View History

// WL-813c3 프로브 — Q7 결함 D-26 「계기는 맞고 화면은 틀리다」 실측(에디트 모드 · Play 0 · 로그인 0 · #813).
// unity command run_script --file AgentScripts/WL813c3_Probe.cs --entry WL813c3_Probe.RunAll
// 산출물: <worktree>/AgentScripts/WL813c3_PROBE.txt
//
// 🔴 프리팹 저장 0 — LoadPrefabContents 로 열고 finally 에서 UnloadPrefabContents. SaveAsPrefabAsset 호출 없음.
// 파일 크기·수정시각을 전후로 찍어 "저장 0" 을 증거로 남긴다.
// 🔴 SO 값은 메모리에서만 바꾸고 finally 에서 원복(AssetDatabase.SaveAssets 0).
// 🔴 Gameplay 는 읽기만. Play 진입 0.
// 🔴 핵심 계측 = **fillAmount 가 아니라 실제로 생성되는 메쉬 폭**.
// uGUI Image.OnPopulateMesh(VertexHelper) 를 리플렉션으로 직접 불러 정점 x 범위를 잰다.
// sprite 가 없으면 Graphic.OnPopulateMesh(사각형 전체) 로 빠지므로 fillAmount 와 무관하게 폭 비율 1.000 이 나온다.
// = D-26 「화면 만피」 의 기계적 증거.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
using WL.UI;
public static class WL813c3_Probe
{
const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
const string kHudPath = "IngameUIs/WL_HUD";
const string kBarName = "WL_BossHpBar";
static StringBuilder _o;
static int _pass, _fail;
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; } }
static string OutPath(string n) { return Path.Combine(Path.Combine(Root, "AgentScripts"), n); }
// ── 메쉬 실측 ────────────────────────────────────────────────────────────
static MethodInfo _opm;
/// <summary>Image 가 실제로 만드는 정점의 x 폭 / rect 폭. sprite 없으면 언제나 1.000 이 나온다.</summary>
static float MeshWidthRatio(Image img, out int vertexCount, out string note)
{
vertexCount = 0; note = "";
if (img == null) { note = "Image 없음"; return -1f; }
if (_opm == null)
{
_opm = typeof(Image).GetMethod("OnPopulateMesh",
BindingFlags.Instance | BindingFlags.NonPublic,
null, new Type[] { typeof(VertexHelper) }, null);
if (_opm == null)
_opm = typeof(Graphic).GetMethod("OnPopulateMesh",
BindingFlags.Instance | BindingFlags.NonPublic,
null, new Type[] { typeof(VertexHelper) }, null);
}
if (_opm == null) { note = "OnPopulateMesh 리플렉션 실패"; return -1f; }
var vh = new VertexHelper();
try { _opm.Invoke(img, new object[] { vh }); }
catch (Exception e) { note = "invoke 실패: " + e.GetBaseException().Message; vh.Dispose(); return -1f; }
vertexCount = vh.currentVertCount;
if (vertexCount == 0) { vh.Dispose(); note = "정점 0"; return 0f; }
float minX = float.MaxValue, maxX = float.MinValue;
var v = new UIVertex();
for (int i = 0; i < vertexCount; i++)
{
vh.PopulateUIVertex(ref v, i);
if (v.position.x < minX) minX = v.position.x;
if (v.position.x > maxX) maxX = v.position.x;
}
vh.Dispose();
float rectW = img.rectTransform.rect.width;
if (rectW <= 0f) { note = "rect 폭 0"; return -1f; }
note = "verts=" + vertexCount + " x[" + minX.ToString("F1") + "," + maxX.ToString("F1") + "] rectW=" + rectW.ToString("F1");
return (maxX - minX) / rectW;
}
// ── 노드 전수 덤프 ───────────────────────────────────────────────────────
static void DumpTree(Transform t, int depth)
{
var sb = new StringBuilder();
sb.Append(new string(' ', 4 + depth * 2)).Append("- ").Append(t.name);
sb.Append(" active=").Append(t.gameObject.activeSelf).Append("/").Append(t.gameObject.activeInHierarchy);
sb.Append(" parent=").Append(t.parent != null ? t.parent.name : "(없음)");
var img = t.GetComponent<Image>();
if (img != null)
{
sb.Append(" [Image type=").Append(img.type)
.Append(" method=").Append(img.fillMethod)
.Append(" origin=").Append(img.fillOrigin)
.Append(" amount=").Append(img.fillAmount.ToString("F3"))
.Append(" sprite=").Append(img.sprite != null ? img.sprite.name : "🔴없음")
.Append(" enabled=").Append(img.enabled)
.Append(" color=#").Append(ColorUtility.ToHtmlStringRGBA(img.color))
.Append(" raycast=").Append(img.raycastTarget).Append("]");
int vc; string note;
float r = MeshWidthRatio(img, out vc, out note);
sb.Append(" 메쉬폭비=").Append(r < 0f ? "?" : r.ToString("F3")).Append(" (").Append(note).Append(")");
}
var cg = t.GetComponent<CanvasGroup>();
if (cg != null) sb.Append(" [CanvasGroup alpha=").Append(cg.alpha.ToString("F2")).Append("]");
var cv = t.GetComponent<Canvas>();
if (cv != null) sb.Append(" [Canvas order=").Append(cv.sortingOrder).Append(" override=").Append(cv.overrideSorting)
.Append(" enabled=").Append(cv.enabled).Append("]");
var tmp = t.GetComponent<TMPro.TextMeshProUGUI>();
if (tmp != null) sb.Append(" [TMP \"").Append(tmp.text).Append("\" fs=").Append(tmp.fontSize.ToString("F1")).Append("]");
var bhb = t.GetComponent<BossHpBar>();
if (bhb != null) sb.Append(" [BossHpBar]");
_o.AppendLine(sb.ToString());
for (int i = 0; i < t.childCount; i++) DumpTree(t.GetChild(i), depth + 1);
}
static void DumpBarNodes(GameObject root, string title)
{
H(title);
var all = root.GetComponentsInChildren<BossHpBar>(true);
N("BossHpBar 인스턴스 = " + all.Length + "벌");
foreach (var b in all)
{
// 부모 사슬 + 캔버스
var chain = new List<string>();
var p = b.transform;
Canvas nearest = null;
while (p != null)
{
chain.Add(p.name);
if (nearest == null) { var c = p.GetComponent<Canvas>(); if (c != null) nearest = c; }
p = p.parent;
}
chain.Reverse();
N("경로 = " + string.Join("/", chain.ToArray()) +
" · 소속 캔버스 = " + (nearest != null ? nearest.name + "(order=" + nearest.sortingOrder +
" override=" + nearest.overrideSorting + " enabled=" + nearest.enabled + " mode=" + nearest.renderMode + ")" : "🔴없음"));
DumpTree(b.transform, 0);
}
}
// ── 본체 ─────────────────────────────────────────────────────────────────
public static string RunAll()
{
_o = new StringBuilder();
_pass = _fail = 0;
_o.AppendLine("# WL813c3 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
" (edit mode · Play " + Application.isPlaying + ")");
var boss = WLBossUiSettings.Instance;
var hudS = WLHudLayoutSettings.Instance;
if (boss == null || hudS == null)
{
_o.AppendLine("🔴 설정 에셋 없음 — boss=" + (boss != null) + " hud=" + (hudS != null));
return Flush();
}
// SO 스냅샷(원복용)
bool sForce = boss.bossBarForceFilledFill, sWhite = boss.bossBarFillWhiteFallback;
bool sBgTick = boss.bossBarSpriteOnBgAndTicks, sDedup = boss.bossBarDedupInstances, sZero = boss.bossBarHideOnZeroHp;
string abs = Path.Combine(Root, kPrefab.Replace('/', Path.DirectorySeparatorChar));
var fi0 = new FileInfo(abs);
long len0 = fi0.Length; DateTime mt0 = fi0.LastWriteTimeUtc;
N("프리팹 전 = " + len0 + " B · " + mt0.ToString("yyyy-MM-dd HH:mm:ss.fff") + " UTC");
GameObject root = null;
BossHpBar bar = null;
GameObject dupGo = null;
try
{
root = PrefabUtility.LoadPrefabContents(kPrefab);
var hud = root.transform.Find(kHudPath);
if (hud == null) { _o.AppendLine("🔴 " + kHudPath + " 없음"); return Flush(); }
var barT = hud.Find(kBarName);
if (barT == null) { _o.AppendLine("🔴 " + kBarName + " 없음"); return Flush(); }
bar = barT.GetComponent<BossHpBar>();
if (bar == null) { _o.AppendLine("🔴 BossHpBar 컴포넌트 없음"); return Flush(); }
// ── ① 저장된 상태 그대로 (수리 전) ──────────────────────────────
DumpBarNodes(root, "① 프리팹에 저장된 그대로 (수리 전 · Initialize 호출 0)");
var fillT = barT.Find("Bar/Fill");
var fillImg = fillT != null ? fillT.GetComponent<Image>() : null;
Chk(fillImg != null, "Bar/Fill Image 존재");
Chk(fillImg != null && fillImg.sprite == null,
"🔴 D-26 원인 재현: 저장된 Fill 의 sprite = 없음 (BossHpBar.cs:499 가 AddComponent 만 했다)");
Chk(fillImg != null && fillImg.type == Image.Type.Filled,
"저장된 Fill 의 type = Filled (후보 ③ 「type 이 Filled 가 아니다」는 반증됨)");
Chk(root.GetComponentsInChildren<BossHpBar>(true).Length == 1,
"바 노드 1벌뿐 (후보 ① 「2벌」 반증)");
// ── ② 메쉬 실측 (수리 전 · fillAmount 0.368 = Q7 캡처 그 값) ────
H("② 메쉬 실측 (수리 전) — fillAmount 를 Q7 D-26 그 값 0.368 로 두고 실제 정점을 만든다");
if (fillImg != null)
{
fillImg.fillAmount = 0.368f;
int vc; string note;
float r = MeshWidthRatio(fillImg, out vc, out note);
N("fillAmount=0.368 → 메쉬폭비 = " + r.ToString("F3") + " (" + note + ")");
Chk(r > 0.99f,
"🔴 D-26 기계적 증거: sprite 가 없어 fillAmount 0.368 인데 메쉬는 폭비 " + r.ToString("F3") + " = 만피 사각형");
}
// ── ③ 수리 = 런타임과 같은 경로(Initialize) ─────────────────────
H("③ 수리 — BossHpBar.Initialize() (런타임 OnEnable 과 같은 경로)");
boss.bossBarForceFilledFill = true; boss.bossBarFillWhiteFallback = true;
string init = bar.Initialize();
N("Initialize → " + init);
Chk(fillImg != null && fillImg.sprite != null,
"고침: Fill.sprite = " + (fillImg != null && fillImg.sprite != null ? fillImg.sprite.name : "(없음)"));
Chk(fillImg != null && fillImg.type == Image.Type.Filled &&
fillImg.fillMethod == Image.FillMethod.Horizontal &&
fillImg.fillOrigin == (int)Image.OriginHorizontal.Left,
"고침: type=Filled · method=Horizontal · origin=Left 강제");
// ── ④ 메쉬 실측 (수리 후) ───────────────────────────────────────
H("④ 메쉬 실측 (수리 후) — 같은 fillAmount 0.368");
if (fillImg != null)
{
fillImg.fillAmount = 0.368f;
int vc; string note;
float r = MeshWidthRatio(fillImg, out vc, out note);
N("fillAmount=0.368 → 메쉬폭비 = " + r.ToString("F3") + " (" + note + ")");
Chk(Mathf.Abs(r - 0.368f) < 0.01f,
"화면 추종: 메쉬폭비 " + r.ToString("F3") + " ≈ fillAmount 0.368 (오차 " +
Mathf.Abs(r - 0.368f).ToString("F4") + ")");
}
// ── ⑤ 발주서 §1-2 프로브: 가짜 HP 1800 → 869 → 화면 노드 fill 0.483 ──
H("⑤ 가짜 HP 1800 → 869 (발주서 §1-2) — 화면 노드가 따라오는가");
BossHpBar.DebugFallbackMaxHp = 1800f;
BossHpBar.DebugFakeMaxHp = 1800f;
BossHpBar.DebugFakeHp = 1800f;
N(BossHpBar.RaiseFakeBossSpawned(1800f));
bar.PollBoss(true);
N("스폰 직후 ratio=" + bar.LastRatio.ToString("F3") + " fillAmount=" +
(fillImg != null ? fillImg.fillAmount.ToString("F3") : "-"));
BossHpBar.DebugFakeHp = 869f;
bar.PollBoss(true);
float expect = 869f / 1800f;
int vc2; string note2;
float r2 = MeshWidthRatio(fillImg, out vc2, out note2);
N("HP 869/1800 → LastRatio=" + bar.LastRatio.ToString("F3") +
" · fillAmount=" + (fillImg != null ? fillImg.fillAmount.ToString("F3") : "-") +
" · 메쉬폭비=" + r2.ToString("F3") + " (" + note2 + ")");
Chk(Mathf.Abs(bar.LastRatio - expect) < 0.002f, "계기 LastRatio = " + expect.ToString("F3"));
Chk(fillImg != null && Mathf.Abs(fillImg.fillAmount - expect) < 0.002f, "fillAmount = " + expect.ToString("F3"));
Chk(Mathf.Abs(r2 - expect) < 0.01f,
"🔴 화면 노드 메쉬폭비 = " + r2.ToString("F3") + " (발주서 §1-2 목표 0.483)");
Chk(bar.Visible, "바가 보이는 상태(CanvasGroup alpha > 0)");
// 3칸 구분선(눈금)이 fill 위에 그대로 얹혀 있는가
var ticks = barT.Find("Bar/Ticks");
int tickN = ticks != null ? ticks.childCount : 0;
N("눈금 " + tickN + "개 · Ticks 형제 순서 = " + (ticks != null ? ticks.GetSiblingIndex() : -1) +
" / Fill = " + (fillT != null ? fillT.GetSiblingIndex() : -1));
Chk(tickN == 2 && ticks != null && fillT != null && ticks.GetSiblingIndex() > fillT.GetSiblingIndex(),
"3칸 = fill 위에 얹힌 구분선 2개(Fill 뒤 형제) — 별도 세그먼트 이미지 아님");
// ── ⑥ C8 롤백 재현 ──────────────────────────────────────────────
H("⑥ C8 롤백 — bossBarForceFilledFill = 0 → 813c2 상태(화면 만피)로 복귀");
boss.bossBarForceFilledFill = false;
if (fillImg != null) fillImg.sprite = null; // 저장된 프리팹과 같은 조건
N("EnsureFillRenderable → " + bar.EnsureFillRenderable());
if (fillImg != null) fillImg.fillAmount = 0.483f;
int vc3; string note3;
float r3 = MeshWidthRatio(fillImg, out vc3, out note3);
N("롤백 상태 fillAmount=0.483 → 메쉬폭비 = " + r3.ToString("F3") + " (" + note3 + ")");
Chk(r3 > 0.99f, "롤백 시 D-26 재현(메쉬폭비 " + r3.ToString("F3") + ") = 스위치 하나로 되돌아간다(C8)");
boss.bossBarForceFilledFill = true;
N("복원 → " + bar.EnsureFillRenderable());
// ── ⑦ HP 0 숨김 ────────────────────────────────────────────────
H("⑦ 보스 HP 0 → 숨김 예약");
int zero0 = bar.ZeroHpHideCount;
BossHpBar.DebugFakeHp = 0f;
bar.PollBoss(true);
N("HP 0 → ratio=" + bar.LastRatio.ToString("F3") + " · ZeroHpHideCount " + zero0 + "→" + bar.ZeroHpHideCount);
Chk(bar.LastRatio <= 0.0001f, "HP 0 → ratio 0");
Chk(bar.ZeroHpHideCount > zero0, "HP 0 숨김 예약 1회 (bossBarHideDelaySec " + hudS.bossBarHideDelaySec + "s 뒤)");
boss.bossBarHideOnZeroHp = false;
int zero1 = bar.ZeroHpHideCount;
bar.PollBoss(true);
Chk(bar.ZeroHpHideCount == zero1, "C8: bossBarHideOnZeroHp=0 → 예약 생략");
boss.bossBarHideOnZeroHp = sZero;
// ── ⑧ 중복 노드 차단 ───────────────────────────────────────────
H("⑧ 중복 노드 차단 — 같은 캔버스에 바를 하나 더 만들어 본다");
dupGo = new GameObject(kBarName + "_DUP", typeof(RectTransform));
dupGo.layer = barT.gameObject.layer;
dupGo.hideFlags = HideFlags.HideAndDontSave;
((RectTransform)dupGo.transform).SetParent(hud, false);
var dup = dupGo.AddComponent<BossHpBar>();
string dupInit = dup.Initialize();
N("중복 Initialize → " + dupInit);
N("원본 BarInstanceCount=" + bar.BarInstanceCount + " · 중복 숨김=" + dup.DedupHidden +
" · 중복 Visible=" + dup.Visible);
Chk(dup.BarInstanceCount >= 2, "중복 감지 " + dup.BarInstanceCount + "벌");
Chk(!dup.Visible, "중복 바는 숨김(alpha 0)");
dup.Subscribe(false);
boss.bossBarDedupInstances = false;
N("C8: bossBarDedupInstances=0 → " + dup.DedupInstances());
boss.bossBarDedupInstances = sDedup;
// ── ⑨ GC ───────────────────────────────────────────────────────
H("⑨ GC — 폴링 20,000 회 할당");
BossHpBar.DebugFakeHp = 900f;
bar.PollBoss(true);
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long g0 = GC.GetTotalMemory(true);
for (int i = 0; i < 20000; i++) bar.PollBoss(true);
long g1 = GC.GetTotalMemory(false);
N("GC " + g0 + " → " + g1 + " (Δ " + (g1 - g0) + " B)");
Chk(g1 - g0 <= 0, "폴링 20,000회 GC 증가 0 B (Δ " + (g1 - g0) + ")");
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long e0 = GC.GetTotalMemory(true);
for (int i = 0; i < 20000; i++) bar.EnsureFillRenderable();
long e1 = GC.GetTotalMemory(false);
N("EnsureFillRenderable 20,000회: GC " + e0 + " → " + e1 + " (Δ " + (e1 - e0) + " B · 문자열 반환 포함)");
// ── ⑩ 수리 후 노드 전수 재덤프 ─────────────────────────────────
BossHpBar.DebugFakeHp = 869f;
bar.PollBoss(true);
DumpBarNodes(root, "⑩ 수리 후 노드 전수 (HP 869/1800)");
N(bar.Dump());
}
finally
{
try { if (dupGo != null) UnityEngine.Object.DestroyImmediate(dupGo); } catch { }
try { if (bar != null) bar.Subscribe(false); } catch { }
BossHpBar.ClearFakeHp();
BossHpBar.DebugFallbackMaxHp = 100f;
BossHpBar.RunBossHpRatio = -1f; BossHpBar.RunBossSeen = false; BossHpBar.RunResetCount = 0;
boss.bossBarForceFilledFill = sForce; boss.bossBarFillWhiteFallback = sWhite;
boss.bossBarSpriteOnBgAndTicks = sBgTick; boss.bossBarDedupInstances = sDedup;
boss.bossBarHideOnZeroHp = sZero;
try { if (root != null) PrefabUtility.UnloadPrefabContents(root); } catch { }
}
// ── ⑪ 저장 0 · 구독 0 ───────────────────────────────────────────────
H("⑪ 뒷정리 — 프리팹 저장 0 · 구독 0");
var fi1 = new FileInfo(abs); fi1.Refresh();
N("프리팹 후 = " + fi1.Length + " B · " + fi1.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss.fff") + " UTC");
Chk(fi1.Length == len0 && fi1.LastWriteTimeUtc == mt0, "프리팹 파일 무변동 = 저장 0");
N("CombatEvents Spawned=" + WL.Combat.Core.CombatEvents.Spawned.Count +
" HitConfirmed=" + WL.Combat.Core.CombatEvents.HitConfirmed.Count +
" Killed=" + WL.Combat.Core.CombatEvents.Killed.Count +
" · RunStarted=" + WL.Combat.Run.RunEvents.RunStarted.Count);
N("SO 원복 = force:" + boss.bossBarForceFilledFill + " white:" + boss.bossBarFillWhiteFallback +
" bgTick:" + boss.bossBarSpriteOnBgAndTicks + " dedup:" + boss.bossBarDedupInstances +
" hp0:" + boss.bossBarHideOnZeroHp);
N("DebugFakeHp=" + BossHpBar.DebugFakeHp + " DebugFakeMaxHp=" + BossHpBar.DebugFakeMaxHp +
" DebugFallbackMaxHp=" + BossHpBar.DebugFallbackMaxHp);
return Flush();
}
static string Flush()
{
_o.AppendLine();
_o.AppendLine("== 합계 PASS " + _pass + " / FAIL " + _fail + " ==");
string p = OutPath("WL813c3_PROBE.txt");
try { File.WriteAllText(p, _o.ToString(), new UTF8Encoding(true)); } catch { }
return _o.ToString() + "\n(파일: " + p + ")";
}
}