601 lines
33 KiB
C#
601 lines
33 KiB
C#
|
|
// 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×(1−0.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); }
|
|||
|
|
}
|
|||
|
|
}
|