Project_WL/AgentScripts/WL813k_Probe.cs

302 lines
17 KiB
C#
Raw Normal View History

// WL813k_Probe.cs — #813k 레벨업 폭발 검증 프로브 (에디트 모드 전용 · Play 없음 · 에셋 무수정)
//
// unity command run_script --file AgentScripts/WL813k_Probe.cs --entry WL813k_Probe.Run
// 결과: AgentScripts/WL813k_PROBE.txt + Console
//
// 원본·코어·설정 에셋 필드를 수정하지 않는다(런타임 static 스위치와 EffectBudget 카운터만 쓴다).
// 스폰한 이펙트는 DestroyImmediate 로 즉시 정리하고, 씬은 저장하지 않는다.
using System;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using WL.Combat.Core;
using WL.Combat.Growth;
public static class WL813k_Probe
{
const string OutPath = "AgentScripts/WL813k_PROBE.txt";
static StringBuilder sb;
public static object Run()
{
sb = new StringBuilder();
L("# WL-813k 레벨업 폭발 프로브 — " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
" · Unity " + Application.unityVersion + " · isPlaying=" + Application.isPlaying);
L("");
Section1_Dump();
Section2_Fake();
Section3_BudgetThrottle();
Section4_Recovery();
Section5_Gc();
Section6_Rollback();
Section7_Cleanup();
try { Directory.CreateDirectory("AgentScripts"); File.WriteAllText(OutPath, sb.ToString(), new UTF8Encoding(true)); }
catch (Exception ex) { Debug.LogException(ex); }
Debug.Log(sb.ToString());
return sb.ToString();
}
static void L(string s) { sb.AppendLine(s); }
// ── ① 설정 에셋 로드 · 프리팹 실존 ──────────────────────────────────────
static void Section1_Dump()
{
L("## ① 설정 에셋 · 프리팹");
var g = WLGrowthSettings.Instance;
var c = WLCombatCoreSettings.Instance;
L("core asset = " + (c != null ? "found" : "NULL") + " · WLCombatCoreSettings.Enabled = " + WLCombatCoreSettings.Enabled);
L("growth asset= " + (g != null ? "found" : "NULL") + " · WLGrowthSettings.Enabled = " + WLGrowthSettings.Enabled +
" · GrowthEvents.Enabled = " + GrowthEvents.Enabled);
if (g == null) { L("!! 에셋 로드 실패 — 이후 검증 불가"); return; }
L("burstPrefab = " + (g.burstPrefab != null ? g.burstPrefab.name : "NULL") +
" · path = " + (g.burstPrefab != null ? UnityEditor.AssetDatabase.GetAssetPath(g.burstPrefab) : "-"));
if (g.burstPrefab != null)
L(" ParticleSystem = " + g.burstPrefab.GetComponentsInChildren<ParticleSystem>(true).Length +
" · Renderer = " + g.burstPrefab.GetComponentsInChildren<Renderer>(true).Length);
L(JsonUtility.ToJson(g, true));
L("stat 3종 = " + g.statA + " / " + g.statB + " / " + g.statC + " (enum int " + (int)g.statA + "/" + (int)g.statB + "/" + (int)g.statC + ")");
L("EffectBudget.Heavy limit = " + EffectBudget.Limit(EffectBudgetKind.Heavy) + " · count = " + EffectBudget.Count(EffectBudgetKind.Heavy));
L("");
}
// ── ② 가짜 LevelUp 발행 → 이펙트 스폰 ───────────────────────────────────
static void Section2_Fake()
{
L("## ② 가짜 LevelUp 발행 → 이펙트 스폰");
LevelUpBurst.EnsureSubscribed();
GrowthEvents.ResetDiagnostics();
LevelUpBurst.ResetDiagnostics();
L("subscribed = " + LevelUpBurst.Subscribed + " · LevelUp.Count = " + GrowthEvents.LevelUp.Count);
var data = MakeFakeData(6); // Lv 6 → 7 로 오르는 상황
L("fake SD_PC: Lv=" + (int)data.Lv + " · Get_Point()=" + data.Get_Point() + " · Get_UsePoint()=" + data.Get_UsePoint());
GrowthEvents.RaiseLevelUp(data, 7);
L("Raised=" + GrowthEvents.LevelUp.Raised + " Dispatched=" + GrowthEvents.LevelUp.Dispatched +
" TotalRaised=" + GrowthEvents.TotalRaised + " last=" + GrowthEvents.LastEvent);
L("payload: prev→new = " + GrowthEvents.LastPrevLevel + "→" + GrowthEvents.LastLevel +
" · levelsGained=" + GrowthEvents.LastLevelsGained + " · statPointTotal=" + GrowthEvents.LastStatPoint);
L("core forward: CombatEvents.LevelUp.Raised=" + CombatEvents.LevelUp.Raised + " last=" + CombatEvents.LastEvent);
var go = LevelUpBurst.LastBurstInstance;
L("burst instance = " + (go != null ? go.name : "NULL") +
" · pos = " + (go != null ? go.transform.position.ToString("F2") : "-") +
" · 수명(측정) = " + LevelUpBurst.LastLifetime.ToString("F2") + " s");
if (go != null)
L(" 스폰된 ParticleSystem = " + go.GetComponentsInChildren<ParticleSystem>(true).Length +
" · Heavy 슬롯 count = " + EffectBudget.Count(EffectBudgetKind.Heavy));
L("counters: Handled=" + LevelUpBurst.Handled + " BurstSpawned=" + LevelUpBurst.BurstSpawned +
" SkippedBudget=" + LevelUpBurst.SkippedByBudget + " SkippedThrottle=" + LevelUpBurst.SkippedByThrottle +
" SkippedNoPrefab=" + LevelUpBurst.SkippedNoPrefab);
L("HP/MP 전회복: pc=null(에디트 모드 · 월드 PC 없음) → healHp=" + LevelUpBurst.LastHealHp + " healMp=" + LevelUpBurst.LastHealMp +
" (실제 Heal 호출은 Play 필요 — 계산식은 §④에서 검증)");
LevelUpBurst.DespawnLast();
L("정리 후 Heavy count = " + EffectBudget.Count(EffectBudgetKind.Heavy) + " · instance = " + (LevelUpBurst.LastBurstInstance != null));
L("");
}
// ── ③ 상한(EffectBudget) · 연속 레벨업 스로틀 ───────────────────────────
static void Section3_BudgetThrottle()
{
L("## ③ 이펙트 상한 · 연속 레벨업 스로틀");
var g = WLGrowthSettings.Instance;
var data = MakeFakeData(10);
// (a) 상한 초과 = 이펙트만 스킵
LevelUpBurst.ResetDiagnostics();
int limit = EffectBudget.Limit(EffectBudgetKind.Heavy);
int taken = 0;
while (EffectBudget.Count(EffectBudgetKind.Heavy) < limit && taken < 64) { EffectBudget.TryAcquire(EffectBudgetKind.Heavy); taken++; }
L("(a) Heavy 슬롯 선점 " + taken + "/" + limit + " → count=" + EffectBudget.Count(EffectBudgetKind.Heavy));
GrowthEvents.RaiseLevelUp(data, 11);
L(" BurstSpawned=" + LevelUpBurst.BurstSpawned + " SkippedBudget=" + LevelUpBurst.SkippedByBudget +
" Handled=" + LevelUpBurst.Handled + " note=" + LevelUpBurst.LastNote);
for (int i = 0; i < taken; i++) EffectBudget.Release(EffectBudgetKind.Heavy);
L(" 슬롯 반납 후 count=" + EffectBudget.Count(EffectBudgetKind.Heavy));
// (b) 연속 레벨업(같은 프레임 5회) = 이펙트 1회 + 나머지 스로틀
LevelUpBurst.ResetDiagnostics();
GrowthEvents.ResetDiagnostics();
for (int i = 0; i < 5; i++) GrowthEvents.RaiseLevelUp(data, 12 + i);
L("(b) 연속 5회 (interval=" + g.burstMinIntervalSeconds + "s): Handled=" + LevelUpBurst.Handled +
" BurstSpawned=" + LevelUpBurst.BurstSpawned + " SkippedThrottle=" + LevelUpBurst.SkippedByThrottle);
L(" levelsGained(마지막)=" + GrowthEvents.LastLevelsGained + " · Raised=" + GrowthEvents.LevelUp.Raised +
" Dispatched=" + GrowthEvents.LevelUp.Dispatched);
LevelUpBurst.DespawnLast();
L(" 정리 후 Heavy count=" + EffectBudget.Count(EffectBudgetKind.Heavy));
L("");
}
// ── ④ HP/MP 전회복 계산식 ───────────────────────────────────────────────
static void Section4_Recovery()
{
L("## ④ HP/MP 전회복 계산식 (LevelUpBurst.ComputeRecovery · 순수 함수)");
var g = WLGrowthSettings.Instance;
L("설정 비율: hpRecoverRate=" + g.hpRecoverRate + " mpRecoverRate=" + g.mpRecoverRate + " showRecoverHud=" + g.showRecoverHud);
L("| HP/MaxHP | MP/MaxMP | 비율 | healHP | healMP | 결과 HP | 결과 MP |");
Rec(1d, 1000d, 5d, 200d, g.hpRecoverRate, g.mpRecoverRate);
Rec(370d, 1000d, 120d, 200d, g.hpRecoverRate, g.mpRecoverRate);
Rec(1000d, 1000d, 200d, 200d, g.hpRecoverRate, g.mpRecoverRate);
Rec(0d, 1000d, 0d, 200d, g.hpRecoverRate, g.mpRecoverRate);
Rec(370d, 1000d, 120d, 200d, 0.5f, 0.5f);
Rec(370d, 1000d, 120d, 200d, 0f, 0f);
L("");
}
static void Rec(double hp, double maxHp, double mp, double maxMp, float hpRate, float mpRate)
{
double hHp, hMp;
LevelUpBurst.ComputeRecovery(hp, maxHp, mp, maxMp, hpRate, mpRate, out hHp, out hMp);
L("| " + hp + "/" + maxHp + " | " + mp + "/" + maxMp + " | " + hpRate + "/" + mpRate +
" | +" + hHp + " | +" + hMp + " | " + (hp + hHp) + " | " + (mp + hMp) + " |");
}
// ── ⑤ GC ────────────────────────────────────────────────────────────────
static void Section5_Gc()
{
L("## ⑤ GC (구독자 1 = LevelUpBurst · 스로틀 구간)");
var data = MakeFakeData(30);
GrowthEvents.RaiseLevelUp(data, 31); // 예열 = 첫 스폰(할당 있음)을 측정 밖으로
LevelUpBurst.DespawnLast();
long b0 = Warm();
for (int i = 0; i < 1000; i++) GrowthEvents.RaiseLevelUp(null, 40 + i);
long a0 = GC.GetTotalMemory(false);
L("(a) RaiseLevelUp(data=null) × 1000 → GC 델타 = " + (a0 - b0) + " B (회당 " + ((a0 - b0) / 1000.0).ToString("F2") + " B)");
long b1 = Warm();
for (int i = 0; i < 1000; i++) GrowthEvents.RaiseLevelUp(data, 40 + i);
long a1 = GC.GetTotalMemory(false);
L("(b) RaiseLevelUp(data=SD_PC) × 1000 → GC 델타 = " + (a1 - b1) + " B (회당 " + ((a1 - b1) / 1000.0).ToString("F2") + " B)");
L(" ※ (b)(a) 는 페이로드의 statPointTotal 이 부르는 **원본** SD_PC.Get_Point() → Get_UsePoint() 의 LINQ(Select/Sum) 할당이다.");
long b2 = Warm();
for (int i = 0; i < 1000; i++) data.Get_Point();
long a2 = GC.GetTotalMemory(false);
L("(c) 원본 SD_PC.Get_Point() × 1000 (이벤트 없이) → GC 델타 = " + (a2 - b2) + " B (회당 " + ((a2 - b2) / 1000.0).ToString("F2") + " B)");
L(" BurstSpawned=" + LevelUpBurst.BurstSpawned + " SkippedThrottle=" + LevelUpBurst.SkippedByThrottle + "(측정 구간은 전부 스로틀)");
LevelUpBurst.DespawnLast();
L("");
}
static long Warm()
{
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
return GC.GetTotalMemory(true);
}
/// <summary>GC 재측정 — 측정법의 민감도(대조군)까지 같이 낸다. 단독 실행 entry.</summary>
public static object Gc2()
{
sb = new StringBuilder();
L("# WL-813k GC 재측정 — " + DateTime.Now.ToString("HH:mm:ss"));
LevelUpBurst.EnsureSubscribed();
var data = MakeFakeData(30);
GrowthEvents.RaiseLevelUp(data, 31); LevelUpBurst.DespawnLast(); // 예열(첫 스폰을 측정 밖으로)
Meas("대조군 new object() × 1000", () => { object o = null; for (int i = 0; i < 1000; i++) o = new object(); GC.KeepAlive(o); });
Meas("대조군 원본 SD_PC.Get_Point() × 1000 (LINQ Select/Sum)", () => { int s = 0; for (int i = 0; i < 1000; i++) s += data.Get_Point(); GC.KeepAlive(s); });
Meas("RaiseLevelUp(null) × 1000", () => { for (int i = 0; i < 1000; i++) GrowthEvents.RaiseLevelUp(null, 40 + i); });
Meas("RaiseLevelUp(SD_PC) × 1000", () => { for (int i = 0; i < 1000; i++) GrowthEvents.RaiseLevelUp(data, 40 + i); });
L("BurstSpawned=" + LevelUpBurst.BurstSpawned + " SkippedThrottle=" + LevelUpBurst.SkippedByThrottle);
LevelUpBurst.DespawnLast(); LevelUpBurst.Unsubscribe();
L("Heavy count = " + EffectBudget.Count(EffectBudgetKind.Heavy) + " · 구독 해제 = " + (GrowthEvents.LevelUp.Count == 0));
try { File.AppendAllText(OutPath, sb.ToString(), new UTF8Encoding(false)); } catch (Exception ex) { Debug.LogException(ex); }
Debug.Log(sb.ToString());
return sb.ToString();
}
static void Meas(string name, Action act)
{
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long g0 = GC.GetTotalMemory(true);
long m0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
act();
long g1 = GC.GetTotalMemory(false);
long m1 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
L(" " + name + " → GC.GetTotalMemory 델타 = " + (g1 - g0) + " B · Profiler.MonoUsed 델타 = " + (m1 - m0) + " B");
}
// ── ⑥ C8 롤백 ───────────────────────────────────────────────────────────
static void Section6_Rollback()
{
L("## ⑥ C8 롤백 (원본 100% 복귀)");
var data = MakeFakeData(50);
GrowthEvents.ResetDiagnostics(); LevelUpBurst.ResetDiagnostics();
// (a) 성장 스위치 off
WLGrowthSettings.RuntimeDisabled = true;
GrowthEvents.RaiseLevelUp(data, 51);
L("(a) WLGrowthSettings.RuntimeDisabled=true → Enabled=" + GrowthEvents.Enabled +
" Raised=" + GrowthEvents.LevelUp.Raised + " TotalRaised=" + GrowthEvents.TotalRaised +
" Handled=" + LevelUpBurst.Handled + " BurstSpawned=" + LevelUpBurst.BurstSpawned);
WLGrowthSettings.RuntimeDisabled = false;
// (b) 코어 스위치 off
WLCombatCoreSettings.RuntimeDisabled = true;
GrowthEvents.RaiseLevelUp(data, 52);
L("(b) WLCombatCoreSettings.RuntimeDisabled=true → Enabled=" + GrowthEvents.Enabled +
" Raised=" + GrowthEvents.LevelUp.Raised + " TotalRaised=" + GrowthEvents.TotalRaised +
" Handled=" + LevelUpBurst.Handled + " BurstSpawned=" + LevelUpBurst.BurstSpawned);
WLCombatCoreSettings.RuntimeDisabled = false;
// (c) 설정 에셋 없음 (Resources 로드 결과를 null 로 강제 = 에셋 삭제와 같은 경로)
var fCached = typeof(WLGrowthSettings).GetField("s_cached", BindingFlags.NonPublic | BindingFlags.Static);
var fDone = typeof(WLGrowthSettings).GetField("s_lookupDone", BindingFlags.NonPublic | BindingFlags.Static);
if (fCached != null && fDone != null)
{
fCached.SetValue(null, null); fDone.SetValue(null, true);
GrowthEvents.RaiseLevelUp(data, 53);
L("(c) 에셋 없음(Instance=null) → WLGrowthSettings.Enabled=" + WLGrowthSettings.Enabled +
" GrowthEvents.Enabled=" + GrowthEvents.Enabled +
" Raised=" + GrowthEvents.LevelUp.Raised + " TotalRaised=" + GrowthEvents.TotalRaised +
" Handled=" + LevelUpBurst.Handled + " BurstSpawned=" + LevelUpBurst.BurstSpawned);
WLGrowthSettings.ClearCache();
L(" ClearCache 복구 → Instance=" + (WLGrowthSettings.Instance != null ? "found" : "NULL") + " Enabled=" + WLGrowthSettings.Enabled);
}
else L("(c) 리플렉션 실패 — 미검증");
// (d) 스위치 복구 후 정상 동작 확인
LevelUpBurst.ResetDiagnostics(); GrowthEvents.ResetDiagnostics();
GrowthEvents.RaiseLevelUp(data, 54);
L("(d) 복구 후 → Raised=" + GrowthEvents.LevelUp.Raised + " Handled=" + LevelUpBurst.Handled +
" BurstSpawned=" + LevelUpBurst.BurstSpawned + " instance=" + (LevelUpBurst.LastBurstInstance != null));
LevelUpBurst.DespawnLast();
L("");
}
// ── ⑦ 정리 ──────────────────────────────────────────────────────────────
static void Section7_Cleanup()
{
L("## ⑦ 정리");
LevelUpBurst.DespawnLast();
LevelUpBurst.Unsubscribe();
WLGrowthSettings.RuntimeDisabled = false;
WLCombatCoreSettings.RuntimeDisabled = false;
L("구독 해제 = " + (GrowthEvents.LevelUp.Count == 0) + " · Heavy count = " + EffectBudget.Count(EffectBudgetKind.Heavy) +
" · Acquired/Denied = " + EffectBudget.AcquiredCount + "/" + EffectBudget.DeniedCount);
var sc = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene();
L("활성 씬 = " + (string.IsNullOrEmpty(sc.name) ? "(none)" : sc.name) + " · dirty = " + sc.isDirty + " (저장하지 않는다)");
var stray = UnityEngine.Object.FindObjectsByType<ParticleSystem>(FindObjectsSortMode.None);
int fx = 0;
for (int i = 0; i < stray.Length; i++) if (stray[i] != null && stray[i].transform.root.name.StartsWith("FX_FireLevelUp")) fx++;
L("잔존 FX_FireLevelUp 파티클 = " + fx);
L("");
}
// ── 헬퍼 ────────────────────────────────────────────────────────────────
static SD_PC MakeFakeData(int lv)
{
var d = new SD_PC();
d.Lv = lv; d.Lv.RandomizeCryptoKey();
return d;
}
}