Project_WL/AgentScripts/WL813k_Probe.cs

302 lines
17 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;
}
}