Project_WL/AgentScripts/WL813m3_Probe.cs

601 lines
33 KiB
C#
Raw Permalink Normal View History

// WL813m3_Probe.cs — #813m3(사망 원인 진단 · 배율 미적용 경로 가드 · 빈 머티리얼 가드) 에디트 모드 검증 프로브
//
// 실행: unity command run_script --project-path <wt> --file AgentScripts/WL813m3_Probe.cs --entry WL813m3_Probe.RunAll
// 결과: Console + AgentScripts/WL813m3_PROBE.txt(커밋) + Screenshots_WL/WL813m3/probe_<HHmmss>.txt
//
// ⓐ 에셋 실측(WLSurvivalSettings 신규 4필드 · WLNullMaterialSettings 12필드)
// ⓑ DeathDiag 링버퍼 16(20건 기록 → 마지막 16건만 남는가 · 순서)
// ⓒ 사망 파일 생성(Screenshots_WL/WL813m3/death_*.txt) + 콘솔 1줄
// ⓓ 배율 적용 표(Survival.TakeDamage 경유 = HookHpLoss 실측 · DeathDiag via=Scaled)
// ⓔ 배율 미적용 경로 감지·환급(훅 없이 HP 를 깎으면 DamageGuard 가 잡아 (1배율) 만큼 되돌리는가)
// ⓕ 훅을 거친 감소는 오검출 0
// ⓖ NullMaterialGuard — 빈 슬롯 렌더러 비활성 · 정상 렌더러 무변경 · 캐시(2회차 무동작)
// ⓗ 🔴 Q9 §6 의 5종 프리팹 실측(디스크 원본 · 읽기만) — 어느 렌더러의 어느 슬롯이 null 인가 = 아트 수정 목록
// ⓘ GC 0(Record ×200k · Poll ×200k · Inspect 캐시 히트 ×200k)
// ⓙ C8(스위치 off → 전부 무동작 · 러너 0)
// ⓚ 정리(임시 오브젝트 0 · MyPC 원복 · timeScale 원복 · 씬 dirty 0 · 에셋 캐시 원복 · 프리팹 무수정)
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using WL.Combat.Survival;
using WL.Combat.Reaction;
public static class WL813m3_Probe
{
const string OutDir = "Screenshots_WL/WL813m3";
const string OutCommit = "AgentScripts/WL813m3_PROBE.txt";
const BindingFlags NP = BindingFlags.NonPublic | BindingFlags.Instance;
const BindingFlags NS = BindingFlags.NonPublic | BindingFlags.Static;
static WLSurvivalSettings s_clone;
static WLNullMaterialSettings s_nmClone;
// Q9 §6 실측 경로 5종(런타임 클론 이름 → 디스크 프리팹)
static readonly string[] Q9Prefabs =
{
"Assets/Res_Addr/Effect/Effect_Hit_10501.prefab",
"Assets/Res_Addr/Effect/Effect_Hit_AnubisSkill2.prefab",
"Assets/Res_Addr/Effect/Effect_Poison.prefab",
"Assets/Res_Addr/SelectSkill/Select_WindFairy.prefab",
"Assets/Res_Addr/Effect/Mob/Hit_P_Batty_Meele.prefab",
};
public static object RunAll()
{
var sb = new StringBuilder();
sb.AppendLine("# WL813m3 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0)");
int fail = 0;
float ts0 = Time.timeScale;
var pc0 = MyValue.MyPC;
bool dirty0 = UnityEngine.SceneManagement.SceneManager.GetActiveScene().isDirty;
try { fail += Asset(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓐ 예외 " + ex); }
try { fail += Ring(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓑ 예외 " + ex); }
try { fail += DeathFile(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓒ 예외 " + ex); }
try { fail += HookTable(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓓ 예외 " + ex); }
try { fail += GuardDetect(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓔⓕ 예외 " + ex); }
try { fail += NullMat(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓖ 예외 " + ex); }
try { fail += Q9Scan(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓗ 예외 " + ex); }
try { fail += Gc(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓘ 예외 " + ex); }
try { fail += C8(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓙ 예외 " + ex); }
// 정리
RestoreAssets();
MyValue.MyPC = pc0;
Time.timeScale = ts0;
try { fail += Cleanup(sb, ts0, dirty0); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⓚ 예외 " + ex); }
string result = fail == 0 ? "RESULT PASS" : ("RESULT FAIL " + fail);
sb.AppendLine(); sb.AppendLine(result);
string body = sb.ToString();
Write(body);
Debug.Log(body);
return result;
}
// ───────────────────────────────── ⓐ 에셋
static int Asset(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓐ 에셋 실측");
var st = WLSurvivalSettings.Instance;
bool a1 = st != null;
sb.AppendLine(P(a1) + " WLSurvivalSettings 로드 " + (a1 ? "OK" : "null")); if (!a1) return f + 1;
bool a2 = st.deathDiagEnabled && st.deathDiagDir == "Screenshots_WL/WL813m3" && st.unscaledDamageGuardEnabled && !st.unscaledDamageGuardLog;
sb.AppendLine(P(a2) + " 신규 4필드 deathDiagEnabled=" + st.deathDiagEnabled + " dir=" + st.deathDiagDir
+ " guard=" + st.unscaledDamageGuardEnabled + " guardLog=" + st.unscaledDamageGuardLog); if (!a2) f++;
bool a3 = Math.Abs(st.mobDamageMultiplier - 0.1f) < 1e-4 && Math.Abs(st.bossDamageMultiplier - 0.15f) < 1e-4;
sb.AppendLine(P(a3) + " 재사용 배율(기존 필드) mob=" + st.mobDamageMultiplier + " boss=" + st.bossDamageMultiplier + " (F-16 값)"); if (!a3) f++;
var nm = WLNullMaterialSettings.Instance;
bool a4 = nm != null;
sb.AppendLine(P(a4) + " WLNullMaterialSettings 로드 " + (a4 ? "OK" : "null")); if (!a4) return f + 1;
bool a5 = nm.enabled && nm.action == NullMaterialAction.DisableRenderer && Math.Abs(nm.sweepIntervalSeconds - 0.5f) < 1e-4
&& nm.maxInspectPerSweep == 24 && nm.inspectCacheMax == 4096 && nm.sweepIngameEffects && nm.sweepIngameObjs
&& nm.sweepSkillSpectacle && nm.sweepMainPc && nm.pathSampleMax == 16 && !nm.treatEmptyArrayAsNull && !nm.verboseLog;
sb.AppendLine(P(a5) + " NullMaterial 12필드 enabled=" + nm.enabled + " action=" + nm.action + " interval=" + nm.sweepIntervalSeconds
+ " maxPerSweep=" + nm.maxInspectPerSweep + " roots(fx/objs/spectacle/pc)=" + nm.sweepIngameEffects + "/" + nm.sweepIngameObjs
+ "/" + nm.sweepSkillSpectacle + "/" + nm.sweepMainPc); if (!a5) f++;
return f;
}
// ───────────────────────────────── ⓑ 링버퍼 16
static int Ring(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓑ DeathDiag 링버퍼 16");
DeathDiag.ResetDiagnostics();
for (int i = 1; i <= 20; i++)
DeathDiag.Record(i % 4 == 0 ? DamageVia.Unscaled : DamageVia.Scaled, null, i, 0.1d, i, 310d - i, 310d);
bool b1 = DeathDiag.RecordCount == 20;
sb.AppendLine(P(b1) + " 기록 20건 · RecordCount=" + DeathDiag.RecordCount); if (!b1) f++;
// 링 16칸에는 5~20번만 남아야 한다
var seen = new List<double>();
for (int i = 0; i < DeathDiag.Capacity; i++) seen.Add(DeathDiag.Get(i).raw);
seen.Sort();
bool b2 = seen.Count == 16 && Math.Abs(seen[0] - 5d) < 1e-6 && Math.Abs(seen[15] - 20d) < 1e-6;
sb.AppendLine(P(b2) + " 마지막 16건만 보존(raw " + seen[0] + "~" + seen[15] + " · 1~4 소멸)"); if (!b2) f++;
bool b3 = DeathDiag.UnscaledCount == 5 && Math.Abs(DeathDiag.UnscaledHpTotal - (4 + 8 + 12 + 16 + 20)) < 1e-6;
sb.AppendLine(P(b3) + " Unscaled 누계 " + DeathDiag.UnscaledCount + "건 " + DeathDiag.UnscaledHpTotal + " HP"); if (!b3) f++;
return f;
}
// ───────────────────────────────── ⓒ 사망 파일
static int DeathFile(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓒ 사망 파일 · 콘솔 1줄");
int before = DeathDiag.FileCount;
DeathDiag.DumpForProbe(7);
bool c1 = DeathDiag.FileCount == before + 1 && !string.IsNullOrEmpty(DeathDiag.LastFilePath) && File.Exists(DeathDiag.LastFilePath);
sb.AppendLine(P(c1) + " 파일 생성 " + DeathDiag.LastFilePath); if (!c1) { return f + 1; }
string txt = File.ReadAllText(DeathDiag.LastFilePath);
int lines = txt.Split('\n').Length;
bool c2 = txt.Contains("death #7") && txt.Contains("UNSCALED") && txt.Contains("Scaled") && lines >= 20;
sb.AppendLine(P(c2) + " 내용 " + lines + "줄 · death #7 · via 열(Scaled/UNSCALED) 포함"); if (!c2) f++;
bool c3 = DeathDiag.LastSummary.StartsWith("[WL813m3] death #7") && !DeathDiag.LastSummary.Contains("\n");
sb.AppendLine(P(c3) + " 콘솔 1줄 = " + DeathDiag.LastSummary); if (!c3) f++;
// verboseLog 와 무관해야 한다
var st = UseClone(); if (st == null) return f;
bool v0 = st.verboseLog; st.verboseLog = false;
int before2 = DeathDiag.FileCount;
DeathDiag.DumpForProbe(8);
bool c4 = DeathDiag.FileCount == before2 + 1 && DeathDiag.LastSummary.Contains("death #8");
sb.AppendLine(P(c4) + " verboseLog=0 에서도 파일·1줄 생성"); if (!c4) f++;
st.verboseLog = v0;
return f;
}
// ───────────────────────────────── ⓓ 배율 적용 표(훅 경유)
static int HookTable(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓓ 배율 적용 표(PCActor.Get_Damage 훅 경유 · HookHpLoss 실측)");
var st = UseClone(); if (st == null) { sb.AppendLine("[FAIL] SO 클론 실패"); return f + 1; }
st.enabled = true; WLSurvivalSettings.RuntimeOverride = 1;
st.takeMobDamage = true; st.takeBossDamage = true;
st.mobDamageMultiplier = 0.1f; st.bossDamageMultiplier = 0.15f;
st.unscaledDamageGuardEnabled = false; // 이 절은 훅만 본다
var pc = MakePC("d_pc", 310, 310);
var mob = MakeBeater("d_mob", eSubRol.None);
var boss = MakeBeater("d_boss", eSubRol.Boss);
var pc0 = MyValue.MyPC; MyValue.MyPC = pc;
try
{
Survival.ResetDiagnostics(); DeathDiag.ResetDiagnostics();
sb.AppendLine("| 가해자 | 원 피해 | 배율 | 훅 후 HP 감소(실측) | DeathDiag via |");
sb.AppendLine("|---|---|---|---|---|");
double h0 = Survival.HookHpLoss;
var d = Dmg(mob, 100);
Survival.TakeDamage(pc, d, ApplyToPc(pc));
double lost1 = Survival.HookHpLoss - h0;
var r1 = DeathDiag.Get(0);
bool d1 = Math.Abs(lost1 - 10d) < 1e-3 && r1.via == DamageVia.Scaled && Math.Abs(r1.mult - 0.1d) < 1e-6;
sb.AppendLine("| 잡몹 | 100 | ×0.10 | " + lost1.ToString("F1") + " | " + r1.via + " |");
sb.AppendLine(P(d1) + " 잡몹 배율 적용 + 누계 반영"); if (!d1) f++;
h0 = Survival.HookHpLoss;
d = Dmg(boss, 100);
Survival.TakeDamage(pc, d, ApplyToPc(pc));
double lost2 = Survival.HookHpLoss - h0;
var r2 = DeathDiag.Get(1);
bool d2 = Math.Abs(lost2 - 15d) < 1e-3 && r2.via == DamageVia.Scaled && r2.attackerBoss;
sb.AppendLine("| 보스 | 100 | ×0.15 | " + lost2.ToString("F1") + " | " + r2.via + "(boss) |");
sb.AppendLine(P(d2) + " 보스 배율 적용 + 가해자 종류 기록"); if (!d2) f++;
st.takeMobDamage = false;
h0 = Survival.HookHpLoss;
d = Dmg(mob, 100);
Survival.TakeDamage(pc, d, NoDamage());
double lost3 = Survival.HookHpLoss - h0;
var r3 = DeathDiag.Get(2);
bool d3 = Math.Abs(lost3) < 1e-6 && r3.via == DamageVia.SwitchOff;
sb.AppendLine("| 잡몹(스위치 off) | 100 | — | " + lost3.ToString("F1") + " | " + r3.via + " |");
sb.AppendLine(P(d3) + " 스위치 off = 원본 무적 그대로 · via=SwitchOff"); if (!d3) f++;
st.takeMobDamage = true;
Survival.SetReviveInvincible(pc, 5f); Survival.Tick(0f);
h0 = Survival.HookHpLoss;
d = Dmg(mob, 100);
Survival.TakeDamage(pc, d, NoDamage());
var r4 = DeathDiag.Get(3);
bool d4 = Math.Abs(Survival.HookHpLoss - h0) < 1e-6 && r4.via == DamageVia.ReviveInv && Survival.BlockedReviveInvincible == 1;
sb.AppendLine("| 잡몹(부활 무적 창) | 100 | — | 0.0 | " + r4.via + " |");
sb.AppendLine(P(d4) + " 부활 무적 창 = 피해 0 · via=ReviveInv"); if (!d4) f++;
Survival.SetReviveInvincible(null, 0f);
}
finally { MyValue.MyPC = pc0; Kill(pc, mob, boss); }
return f;
}
// ───────────────────────────────── ⓔⓕ 배율 미적용 경로 감지·환급
static int GuardDetect(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓔⓕ 배율 미적용 경로 감지 · 환급(도트/Cal_Damage 직접 호출 재현)");
var st = UseClone(); if (st == null) return f + 1;
st.enabled = true; WLSurvivalSettings.RuntimeOverride = 1;
st.unscaledDamageGuardEnabled = true; st.unscaledDamageGuardLog = false;
st.mobDamageMultiplier = 0.1f;
var pc = MakePC("g_pc", 310, 200);
var pc0 = MyValue.MyPC; MyValue.MyPC = pc;
try
{
DamageGuard.ResetDiagnostics(); DeathDiag.ResetDiagnostics();
DamageGuard.Poll(); // 기준선 무장
bool e0 = DamageGuard.DetectedCount == 0 && DamageGuard.PollCount == 1;
sb.AppendLine(P(e0) + " 첫 폴링 = 기준선만 잡는다(감지 0)"); if (!e0) f++;
// 훅을 거치지 않고 HP 40 감소 = Actor.Co_DotDamage:1536 → Cal_Damage 재현
pc.Get_StatInfo().Set_Stat(false, eStat.HP, 40d);
DamageGuard.Poll();
double hp = pc.Get_HP();
bool e1 = DamageGuard.DetectedCount == 1 && Math.Abs(DamageGuard.LastDetected - 40d) < 1e-3;
sb.AppendLine(P(e1) + " 감지 1건 · 미적용 피해 " + DamageGuard.LastDetected.ToString("F1")); if (!e1) f++;
bool e2 = Math.Abs(DamageGuard.LastRefund - 36d) < 1e-3 && Math.Abs(hp - 196d) < 1e-3;
sb.AppendLine(P(e2) + " 환급 40×(10.10)=36 → HP 160 → " + hp.ToString("F1") + " (순 피해 4 = 40×0.10)"); if (!e2) f++;
var rec = DeathDiag.Get(0);
bool e3 = rec.via == DamageVia.Unscaled && Math.Abs(rec.raw - 40d) < 1e-3;
sb.AppendLine(P(e3) + " DeathDiag via=Unscaled 기록 raw=" + rec.raw.ToString("F1")); if (!e3) f++;
// ⓕ 훅을 거친 감소는 오검출 0
int det0 = DamageGuard.DetectedCount;
double h0 = Survival.HookHpLoss;
pc.Get_StatInfo().Set_Stat(false, eStat.HP, 30d);
Survival.HookHpLoss = h0 + 30d; // 훅이 책임진 30
DamageGuard.Poll();
bool e4 = DamageGuard.DetectedCount == det0 && Math.Abs(pc.Get_HP() - 166d) < 1e-3;
sb.AppendLine(P(e4) + " 훅 경유 30 감소 → 감지 " + (DamageGuard.DetectedCount - det0) + "건(기대 0) · HP " + pc.Get_HP().ToString("F1")); if (!e4) f++;
// 회복(부활·물약)은 기준선만 갱신
pc.Get_StatInfo().Set_Stat(true, eStat.HP, 100d);
DamageGuard.Poll();
bool e5 = DamageGuard.DetectedCount == det0;
sb.AppendLine(P(e5) + " 회복(+100) → 감지 0 · HP " + pc.Get_HP().ToString("F1")); if (!e5) f++;
// 부활/런 경계 → 기준선 재설정
int rb0 = DamageGuard.RebaseCount;
Survival.SetReviveInvincible(pc, 1f);
bool e6 = DamageGuard.RebaseCount == rb0 + 1;
sb.AppendLine(P(e6) + " SetReviveInvincible → Rebase " + (DamageGuard.RebaseCount - rb0) + "회"); if (!e6) f++;
Survival.SetReviveInvincible(null, 0f);
}
finally { MyValue.MyPC = pc0; Kill(pc); Survival.HookHpLoss = 0d; }
return f;
}
// ───────────────────────────────── ⓖ NullMaterialGuard
static int NullMat(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓖ NullMaterialGuard — 빈 슬롯 렌더러 비활성");
var nm = UseNmClone(); if (nm == null) { sb.AppendLine("[FAIL] NM SO 클론 실패"); return f + 1; }
nm.enabled = true; WLNullMaterialSettings.RuntimeOverride = 1;
nm.action = NullMaterialAction.DisableRenderer; nm.treatEmptyArrayAsNull = false;
GameObject bad = null, good = null;
Material mat = null;
try
{
NullMaterialGuard.ResetDiagnostics();
bad = GameObject.CreatePrimitive(PrimitiveType.Quad); bad.name = "__WL813m3_bad";
var badChild = GameObject.CreatePrimitive(PrimitiveType.Quad); badChild.name = "Pos";
badChild.transform.SetParent(bad.transform, false);
var rBad = badChild.GetComponent<MeshRenderer>();
rBad.sharedMaterials = new Material[] { null }; // Q9 실측 = slot 0 이 null
var rBadRoot = bad.GetComponent<MeshRenderer>();
mat = new Material(Shader.Find("Sprites/Default")); mat.hideFlags = HideFlags.HideAndDontSave;
rBadRoot.sharedMaterial = mat; // 루트는 정상
good = GameObject.CreatePrimitive(PrimitiveType.Quad); good.name = "__WL813m3_good";
good.GetComponent<MeshRenderer>().sharedMaterial = mat;
NullMaterialGuard.Inspect(bad);
NullMaterialGuard.Inspect(good);
bool g1 = NullMaterialGuard.NullMaterialRenderers == 1 && NullMaterialGuard.DisabledRenderers == 1 && NullMaterialGuard.OffendingObjects == 1;
sb.AppendLine(P(g1) + " 결함 렌더러 " + NullMaterialGuard.NullMaterialRenderers + " · 비활성 " + NullMaterialGuard.DisabledRenderers
+ " · 결함 오브젝트 " + NullMaterialGuard.OffendingObjects); if (!g1) f++;
bool g2 = !rBad.enabled && rBadRoot.enabled && good.GetComponent<MeshRenderer>().enabled;
sb.AppendLine(P(g2) + " 빈 슬롯 렌더러만 enabled=false(정상 렌더러 2개 무변경)"); if (!g2) f++;
bool g3 = NullMaterialGuard.LastPath.EndsWith("/Pos") && NullMaterialGuard.Samples.Count == 1;
sb.AppendLine(P(g3) + " 경로 표본 = " + NullMaterialGuard.LastPath); if (!g3) f++;
int insp0 = NullMaterialGuard.InspectedObjects;
NullMaterialGuard.Inspect(bad); NullMaterialGuard.Inspect(good);
bool g4 = NullMaterialGuard.InspectedObjects == insp0 && NullMaterialGuard.CacheCount == 2;
sb.AppendLine(P(g4) + " 캐시 = 2회차 재검사 0(스폰 1회 검사 · 캐시 " + NullMaterialGuard.CacheCount + ")"); if (!g4) f++;
// MeasureOnly = 계측만
NullMaterialGuard.ResetDiagnostics();
nm.action = NullMaterialAction.MeasureOnly;
rBad.enabled = true;
NullMaterialGuard.Inspect(bad);
bool g5 = NullMaterialGuard.NullMaterialRenderers == 1 && NullMaterialGuard.DisabledRenderers == 0 && rBad.enabled;
sb.AppendLine(P(g5) + " MeasureOnly = 계측 1 · 비활성 0(A/B 비교용)"); if (!g5) f++;
nm.action = NullMaterialAction.DisableRenderer;
// 러너는 Play 밖에서 만들어지지 않는다
bool g6 = !NullMaterialGuardRunner.Exists;
sb.AppendLine(P(g6) + " 에디트 모드 러너 생성 0"); if (!g6) f++;
}
finally { Kill(bad, good); if (mat != null) UnityEngine.Object.DestroyImmediate(mat); }
return f;
}
// ───────────────────────────────── ⓗ Q9 §6 5종 프리팹 실측(읽기만)
static int Q9Scan(StringBuilder sb)
{
sb.AppendLine(); sb.AppendLine("## ⓗ Q9 §6 5종 프리팹 실측(디스크 원본 · 수정 0) = 아트 수정 목록");
sb.AppendLine("| # | 프리팹 | 렌더러 수 | 🔴 빈 슬롯 렌더러(경로/슬롯) |");
sb.AppendLine("|---|---|---|---|");
int found = 0, missing = 0;
var rends = new List<Renderer>();
var mats = new List<Material>();
for (int i = 0; i < Q9Prefabs.Length; i++)
{
var go = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(Q9Prefabs[i]);
if (go == null) { missing++; sb.AppendLine("| " + (i + 1) + " | " + Q9Prefabs[i] + " | — | ❌ 디스크에 없음 |"); continue; }
rends.Clear(); go.GetComponentsInChildren<Renderer>(true, rends);
var bad = new StringBuilder();
int badCount = 0;
for (int k = 0; k < rends.Count; k++)
{
mats.Clear(); rends[k].GetSharedMaterials(mats);
for (int s = 0; s < mats.Count; s++)
{
if (mats[s] != null) continue;
badCount++;
if (bad.Length > 0) bad.Append(" · ");
bad.Append(PathIn(go.transform, rends[k].transform)).Append("[slot ").Append(s).Append(']');
}
if (mats.Count == 0) { badCount++; if (bad.Length > 0) bad.Append(" · "); bad.Append(PathIn(go.transform, rends[k].transform)).Append("[슬롯 0개]"); }
}
if (badCount > 0) found++;
sb.AppendLine("| " + (i + 1) + " | " + Q9Prefabs[i] + " | " + rends.Count + " | " + (badCount > 0 ? "🔴 " + badCount + "건 — " + bad : "없음") + " |");
}
sb.AppendLine("→ 5종 중 **" + found + "종**에서 빈 머티리얼 슬롯 실측(디스크 부재 " + missing + "종). 근본 수리 = 아트/이펙트 발주(프리팹 슬롯 채우기 또는 그 렌더러 삭제).");
return 0; // 실측 보고 — PASS/FAIL 판정 대상 아님
}
// ───────────────────────────────── ⓘ GC 0
static int Gc(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓘ GC 0(핫 경로)");
var st = UseClone(); if (st == null) return f + 1;
st.enabled = true; WLSurvivalSettings.RuntimeOverride = 1; st.unscaledDamageGuardEnabled = true;
var pc = MakePC("gc_pc", 310, 310);
var pc0 = MyValue.MyPC; MyValue.MyPC = pc;
var bad = GameObject.CreatePrimitive(PrimitiveType.Quad); bad.name = "__WL813m3_gc";
try
{
DeathDiag.ResetDiagnostics(); DamageGuard.ResetDiagnostics(); NullMaterialGuard.ResetDiagnostics();
NullMaterialGuard.Inspect(bad); // 캐시 채우기
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long m0 = Mono(); long t0 = GC.GetTotalMemory(false); int c0 = GC.CollectionCount(0);
for (int i = 0; i < 200000; i++) DeathDiag.Record(DamageVia.Scaled, null, 1d, 0.1d, 1d, 300d, 310d);
long m1 = Mono(); long t1 = GC.GetTotalMemory(false); int c1 = GC.CollectionCount(0);
bool i1 = (m1 - m0) <= 0 && (t1 - t0) <= 4096 && c1 == c0;
sb.AppendLine(P(i1) + " DeathDiag.Record ×200k · mono Δ" + (m1 - m0) + " B · total Δ" + (t1 - t0) + " B · gen0 " + (c1 - c0)); if (!i1) f++;
DamageGuard.Poll(); // 무장
long m2 = Mono(); long t2 = GC.GetTotalMemory(false); int c2 = GC.CollectionCount(0);
for (int i = 0; i < 200000; i++) DamageGuard.Poll();
long m3 = Mono(); long t3 = GC.GetTotalMemory(false); int c3 = GC.CollectionCount(0);
bool i2 = (m3 - m2) <= 0 && (t3 - t2) <= 4096 && c3 == c2 && DamageGuard.DetectedCount == 0;
sb.AppendLine(P(i2) + " DamageGuard.Poll ×200k(변화 없음) · mono Δ" + (m3 - m2) + " B · total Δ" + (t3 - t2) + " B · gen0 " + (c3 - c2)); if (!i2) f++;
long m4 = Mono(); long t4 = GC.GetTotalMemory(false); int c4 = GC.CollectionCount(0);
for (int i = 0; i < 200000; i++) NullMaterialGuard.Inspect(bad);
long m5 = Mono(); long t5 = GC.GetTotalMemory(false); int c5 = GC.CollectionCount(0);
bool i3 = (m5 - m4) <= 0 && (t5 - t4) <= 4096 && c5 == c4;
sb.AppendLine(P(i3) + " NullMaterialGuard.Inspect ×200k(캐시 히트) · mono Δ" + (m5 - m4) + " B · total Δ" + (t5 - t4) + " B · gen0 " + (c5 - c4)); if (!i3) f++;
}
finally { MyValue.MyPC = pc0; Kill(pc, bad); }
return f;
}
// ───────────────────────────────── ⓙ C8
static int C8(StringBuilder sb)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓙ C8 롤백");
var st = UseClone(); if (st == null) return f + 1;
var pc = MakePC("c8_pc", 310, 200);
var pc0 = MyValue.MyPC; MyValue.MyPC = pc;
try
{
// ① 생존 축 자체 off
WLSurvivalSettings.RuntimeOverride = -1;
DamageGuard.ResetDiagnostics(); DeathDiag.ResetDiagnostics();
DamageGuard.Poll(); pc.Get_StatInfo().Set_Stat(false, eStat.HP, 50d); DamageGuard.Poll();
bool j1 = !DamageGuard.Active && DamageGuard.DetectedCount == 0 && Math.Abs(pc.Get_HP() - 150d) < 1e-3;
sb.AppendLine(P(j1) + " SO off(RuntimeOverride=-1) → 가드 무동작 · HP " + pc.Get_HP().ToString("F1") + "(환급 0)"); if (!j1) f++;
// ② 생존 축은 on 인데 가드 스위치만 off
WLSurvivalSettings.RuntimeOverride = 1; st.enabled = true; st.unscaledDamageGuardEnabled = false;
DamageGuard.ResetDiagnostics();
DamageGuard.Poll(); pc.Get_StatInfo().Set_Stat(false, eStat.HP, 50d); DamageGuard.Poll();
bool j2 = !DamageGuard.Active && DamageGuard.DetectedCount == 0 && Math.Abs(pc.Get_HP() - 100d) < 1e-3;
sb.AppendLine(P(j2) + " unscaledDamageGuardEnabled=0 → 무동작 · HP " + pc.Get_HP().ToString("F1")); if (!j2) f++;
st.unscaledDamageGuardEnabled = true;
// ③ 사망 로그 off
st.deathDiagEnabled = false;
DeathDiag.ResetDiagnostics();
DeathDiag.Record(DamageVia.Scaled, null, 1d, 1d, 1d, 1d, 1d);
int fc = DeathDiag.FileCount; DeathDiag.DumpForProbe(99);
bool j3 = DeathDiag.RecordCount == 0 && DeathDiag.FileCount == fc;
sb.AppendLine(P(j3) + " deathDiagEnabled=0 → 기록 0 · 파일 0"); if (!j3) f++;
st.deathDiagEnabled = true;
// ④ 빈 머티리얼 가드 off
var nm = UseNmClone();
WLNullMaterialSettings.RuntimeOverride = -1;
NullMaterialGuard.ResetDiagnostics();
var bad = GameObject.CreatePrimitive(PrimitiveType.Quad); bad.name = "__WL813m3_c8";
bad.GetComponent<MeshRenderer>().sharedMaterials = new Material[] { null };
NullMaterialGuard.Inspect(bad); NullMaterialGuard.Tick(999f);
bool j4 = !WLNullMaterialSettings.Enabled && NullMaterialGuard.InspectedObjects == 0 && NullMaterialGuard.SweepCount == 0
&& bad.GetComponent<MeshRenderer>().enabled && !NullMaterialGuardRunner.Exists;
sb.AppendLine(P(j4) + " NullMaterial off → 검사 0 · 스윕 0 · 렌더러 무변경 · 러너 0"); if (!j4) f++;
Kill(bad);
WLNullMaterialSettings.RuntimeOverride = 0;
}
finally { MyValue.MyPC = pc0; Kill(pc); WLSurvivalSettings.RuntimeOverride = 0; }
return f;
}
// ───────────────────────────────── ⓚ 정리
static int Cleanup(StringBuilder sb, float ts0, bool dirty0)
{
int f = 0;
sb.AppendLine(); sb.AppendLine("## ⓚ 정리");
int leftovers = 0;
foreach (var go in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None))
if (go != null && go.name.StartsWith("__WL813m3")) leftovers++;
var st = WLSurvivalSettings.Instance;
var nm = WLNullMaterialSettings.Instance;
bool assetBack = st != null && st.deathDiagEnabled && st.unscaledDamageGuardEnabled
&& Math.Abs(st.mobDamageMultiplier - 0.1f) < 1e-4
&& nm != null && nm.enabled && nm.action == NullMaterialAction.DisableRenderer;
bool dirty = UnityEngine.SceneManagement.SceneManager.GetActiveScene().isDirty;
bool ok = leftovers == 0 && assetBack && Math.Abs(Time.timeScale - ts0) < 1e-6 && MyValue.MyPC == null
&& !SurvivalRunner.Exists && !NullMaterialGuardRunner.Exists && dirty == dirty0
&& WLSurvivalSettings.RuntimeOverride == 0 && WLNullMaterialSettings.RuntimeOverride == 0;
sb.AppendLine(P(ok) + " 임시 오브젝트 " + leftovers + " · 에셋 원복 " + assetBack + " · timeScale " + Time.timeScale
+ " · MyPC=null " + (MyValue.MyPC == null) + " · 러너 0 " + (!SurvivalRunner.Exists && !NullMaterialGuardRunner.Exists)
+ " · 씬 dirty " + dirty + "(시작 " + dirty0 + ")");
if (!ok) f++;
return f;
}
// ───────────────────────────────── helpers
static string P(bool ok) { return ok ? "[PASS]" : "[FAIL]"; }
static long Mono() { return UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(); }
static WLSurvivalSettings UseClone()
{
var orig = WLSurvivalSettings.Instance;
if (orig == null) return null;
if (s_clone == null)
{
s_clone = UnityEngine.Object.Instantiate(orig);
s_clone.hideFlags = HideFlags.HideAndDontSave; s_clone.name = "WLSurvivalSettings(probe clone)";
}
typeof(WLSurvivalSettings).GetField("s_cached", NS).SetValue(null, s_clone);
typeof(WLSurvivalSettings).GetField("s_lookupDone", NS).SetValue(null, true);
return s_clone;
}
static WLNullMaterialSettings UseNmClone()
{
var orig = WLNullMaterialSettings.Instance;
if (orig == null) return null;
if (s_nmClone == null)
{
s_nmClone = UnityEngine.Object.Instantiate(orig);
s_nmClone.hideFlags = HideFlags.HideAndDontSave; s_nmClone.name = "WLNullMaterialSettings(probe clone)";
}
typeof(WLNullMaterialSettings).GetField("s_cached", NS).SetValue(null, s_nmClone);
typeof(WLNullMaterialSettings).GetField("s_lookupDone", NS).SetValue(null, true);
return s_nmClone;
}
static void RestoreAssets()
{
WLSurvivalSettings.ClearCache(); WLSurvivalSettings.RuntimeOverride = 0;
WLNullMaterialSettings.ClearCache(); WLNullMaterialSettings.RuntimeOverride = 0;
if (s_clone != null) { UnityEngine.Object.DestroyImmediate(s_clone); s_clone = null; }
if (s_nmClone != null) { UnityEngine.Object.DestroyImmediate(s_nmClone); s_nmClone = null; }
}
static PCActor MakePC(string name, double maxHp, double hp)
{
var go = new GameObject("__WL813m3_" + name);
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, maxHp); stat.Set_Stat(eStat.HP, hp);
typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat);
typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, false);
return pc;
}
static Actor MakeBeater(string name, eSubRol sub)
{
var go = new GameObject("__WL813m3_" + name);
var a = go.AddComponent<Actor>();
a.m_Role = eRole.Mob; a.m_SubRole = sub;
typeof(Actor).GetField("m_Enemy", NP).SetValue(a, true);
return a;
}
static DamageInfo Dmg(Actor beater, double dmg)
{
var d = new DamageInfo();
d.Beater = beater; d.Damage = dmg;
return d;
}
/// <summary>가짜 base.Get_Damage — 넘어온 (배율 적용된) 피해만큼 PC HP 를 실제로 깎는다.</summary>
static Action<DamageInfo> ApplyToPc(PCActor pc)
{
return d => { if (d != null) pc.Get_StatInfo().Set_Stat(false, eStat.HP, d.Damage); };
}
/// <summary>가짜 base.Get_Damage — 원본 무적 경로(피해 0).</summary>
static Action<DamageInfo> NoDamage() { return d => { }; }
static void Kill(params UnityEngine.Object[] objs)
{
foreach (var o in objs)
{
if (o == null) continue;
var c = o as Component;
UnityEngine.Object.DestroyImmediate(c != null ? c.gameObject : o);
}
}
static string PathIn(Transform root, Transform t)
{
if (t == root) return t.name;
var sb = new StringBuilder(t.name);
var p = t.parent;
while (p != null && p != root) { sb.Insert(0, '/'); sb.Insert(0, p.name); p = p.parent; }
sb.Insert(0, '/'); sb.Insert(0, root.name);
return sb.ToString();
}
static void Write(string body)
{
try
{
string root = Path.GetDirectoryName(Application.dataPath);
string dir = Path.Combine(root, OutDir);
Directory.CreateDirectory(dir);
File.WriteAllText(Path.Combine(dir, "probe_" + DateTime.Now.ToString("HHmmss") + ".txt"), body);
File.WriteAllText(Path.Combine(root, OutCommit), body);
}
catch (Exception ex) { Debug.Log("[WL813m3] probe 파일 기록 실패: " + ex.Message); }
}
}