Project_WL/AgentScripts/WL813h_Probe.cs

261 lines
14 KiB
C#
Raw 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.

// PD 지시 #813 · 발주서 WL-813h §1-4 — 보스 패턴 로더 · 페이즈 통지 에디트 모드 프로브 (읽기 전용)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기 · 첫 Roslyn 컴파일은 --timeout 60)
// unity command run_script --file AgentScripts/WL813h_Probe.cs --entry WL813h_Probe.Run --timeout 180
//
// 무엇을 확인하나 (발주서 §3 ⓒ ⓓ ⓔ)
// ① 로더 상태(JSON 행 수 · 파싱) 와 12행 임계 조회표
// ② Anubis(10006) 프리팹 인스턴스에 행 값이 적용되는지 — 적용 전/후 필드 덤프
// ③ 행 없음(가짜 ID) → 값 무변경(원본 유지)
// ④ BossPhase 통지 — 가짜 HP 100 → 70 → 40 % 에서 이벤트 3회 + 페이로드
// ⑤ 디스패치 GC 할당(1000회 Raise 전후 GC.GetTotalMemory 차)
// ⑥ C8 — RuntimeDisabled = true 면 로더/통지 무동작
//
// Play 하지 않는다. 프리팹은 LoadPrefabContents → (저장 없이) UnloadPrefabContents 라 에셋 무변경.
// 수치는 전부 JSON/프리팹에서 읽는다(C45). 프로브의 반복 횟수·가짜 HP 는 "측정 조건"이지 게임 데이터가 아니다.
public static class WL813h_Probe
{
const string kAnubisPrefab = "Assets/Res_Addr/Mobs/FieldBoss/Anubis.prefab";
const int kAnubisId = 10006;
const int kMissingId = 99999; // JSON 에 없는 ID (행 없음 경로 검증)
const int kGcIterations = 1000; // 측정 조건
const string kLog = "AgentScripts/WL813h_PROBE.txt"; // 발주서 §4 허용 경로(AgentScripts/WL813h_*)
static System.Text.StringBuilder s;
static int s_eventCount;
static string s_eventLog;
public static string Run()
{
s = new System.Text.StringBuilder();
s_eventCount = 0; s_eventLog = "";
L("===== WL-813h 보스 패턴 로더 · 페이즈 통지 프로브 =====");
L("playMode=" + UnityEngine.Application.isPlaying + " time=" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
WL.Combat.Boss.BossPatternTable.ClearCache();
WL.Combat.Boss.BossEvents.ResetDiagnostics();
Section1_Table();
Section2_Apply(); // ②③ 안에서 ④⑤(페이즈·GC)까지 같은 인스턴스로 이어서 돈다
Section4_Rollback();
L("");
L("===== 끝 =====");
return Flush();
}
// ───────────────────────────────────────── ① 로더 상태 + 임계 조회표
static void Section1_Table()
{
L("");
L("── ① 로더 상태");
L(" 코어 Enabled = " + WL.Combat.Core.WLCombatCoreSettings.Enabled);
L(" 로더 Enabled = " + WL.Combat.Boss.BossPatternTable.Enabled);
L(" RowCount = " + WL.Combat.Boss.BossPatternTable.RowCount);
L(" ParseFailed = " + WL.Combat.Boss.BossPatternTable.ParseFailed +
(WL.Combat.Boss.BossPatternTable.LoadError.Length > 0 ? (" (" + WL.Combat.Boss.BossPatternTable.LoadError + ")") : ""));
L("");
L("── ① 임계 조회표 (skill1 / skill2 = 해금 임계 · phases = 페이즈 임계)");
L(" ID 이름 skill1 skill2 phases 필드수");
var f = WL.Combat.Boss.BossPatternTable.File;
if (f == null || f.rows == null) { L(" (JSON 없음)"); return; }
for (int i = 0; i < f.rows.Count; i++)
{
var r = f.rows[i];
float t1, t2;
WL.Combat.Boss.BossPatternTable.GetSkillThresholds(r.id, out t1, out t2);
var th = WL.Combat.Boss.BossPatternTable.GetPhaseThresholds(r.id);
L(string.Format(" {0,-7} {1,-20} {2,-7} {3,-7} {4,-21} {5}",
r.id, r.name, t1.ToString("0.###"), t2.ToString("0.###"),
"[" + string.Join(", ", System.Array.ConvertAll(th, x => x.ToString("0.##"))) + "]",
r.fields != null ? r.fields.Count : 0));
}
}
// ───────────────────────────────────────── ② 행 적용 · ③ 행 없음
static void Section2_Apply()
{
L("");
L("── ② Anubis(10006) 프리팹 인스턴스에 행 적용 (LoadPrefabContents · 저장 없음)");
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kAnubisPrefab);
if (root == null) { L(" [에러] 프리팹 로드 실패: " + kAnubisPrefab); return; }
try
{
var boss = root.GetComponentInChildren<BossMobActor>(true);
if (boss == null) { L(" [에러] BossMobActor 컴포넌트 없음"); return; }
L(" 대상 = " + boss.GetType().Name + " on '" + boss.gameObject.name + "'");
var before = Dump(boss);
int applied = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId);
var after = Dump(boss);
L(" ApplyById(10006) → 적용 필드 " + applied + "개 · 미지원 키 " + WL.Combat.Boss.BossPatternTable.LastUnknownKeys + "개");
L("");
L(" 필드 프리팹값 → 적용후 (변화)");
foreach (var kv in before)
{
string a = after.ContainsKey(kv.Key) ? after[kv.Key] : "?";
L(string.Format(" {0,-24} {1,-9} → {2,-9} {3}", kv.Key, kv.Value, a, kv.Value == a ? "" : "<<< 변경"));
}
L("");
L("── ③ 행 없음(ID " + kMissingId + ") → 값 유지 검증");
int reverted = WL.Combat.Boss.BossPatternTable.Revert(boss, kAnubisId);
L(" Revert(10006) = " + reverted + "개 되돌림 (프리팹 원본 값으로 복구)");
var baseline = Dump(boss);
int applied2 = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kMissingId);
var afterMissing = Dump(boss);
int changed = 0;
foreach (var kv in baseline) if (!afterMissing.ContainsKey(kv.Key) || afterMissing[kv.Key] != kv.Value) changed++;
L(" ApplyById(" + kMissingId + ") = " + applied2 + " (1 = 무동작) · 변경된 필드 " + changed + "개");
L(" 판정 = " + (applied2 == -1 && changed == 0 ? "PASS (행 없으면 프리팹 값 그대로)" : "FAIL"));
// 페이즈 검사는 같은 인스턴스로 이어서 한다.
s_probeBoss = boss;
Section3_PhaseInner();
}
finally
{
s_probeBoss = null;
UnityEditor.PrefabUtility.UnloadPrefabContents(root); // 저장하지 않으므로 프리팹 에셋 무변경
}
}
static BossMobActor s_probeBoss;
// ───────────────────────────────────────── ④ 페이즈 통지 · ⑤ GC
static void Section3_PhaseInner()
{
var boss = s_probeBoss;
L("");
L("── ④ BossPhase 통지 (가짜 HP 100 → 70 → 40 %)");
WL.Combat.Core.CombatHandler<WL.Combat.Boss.BossPhaseEvent> h = OnPhase;
WL.Combat.Boss.BossEvents.BossPhase.Add(h);
WL.Combat.Boss.BossEvents.ResetDiagnostics();
s_eventCount = 0; s_eventLog = "";
try
{
WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId); // 스폰 = 페이즈 1
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 1.00f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.85f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.70f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.55f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.40f);
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.90f); // 회복 — 되돌아가지 않아야 한다
WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.05f);
L(s_eventLog);
L(" 이벤트 " + s_eventCount + "회 · Raised=" + WL.Combat.Boss.BossEvents.BossPhase.Raised +
" Dispatched=" + WL.Combat.Boss.BossEvents.BossPhase.Dispatched +
" 최종 페이즈=" + WL.Combat.Boss.BossPatternTable.CurrentPhase(boss));
L(" 판정 = " + (s_eventCount == 3 ? "PASS (100→70→40 · 3회 · 회복 시 되돌림 없음)" : "FAIL (기대 3회)"));
L("");
L("── ④ 스킬 해금 임계 (원본 0.9 / 0.7 · 행이 값을 주면 그 값)");
bool c1 = false, c2 = false;
float[] hps = { 1.00f, 0.95f, 0.90f, 0.80f, 0.70f, 0.50f };
for (int i = 0; i < hps.Length; i++)
{
WL.Combat.Boss.BossPatternTable.ApplySkillGatesById(boss, kAnubisId, hps[i], ref c1, ref c2);
L(string.Format(" HP {0,5:P0} → CanUseSkill1={1,-5} CanUseSkill2={2}", hps[i], c1, c2));
}
L("");
L("── ⑤ 디스패치 GC (Raise " + kGcIterations + "회 · 구독자는 카운터만 = 로그 문자열 할당 배제)");
WL.Combat.Boss.BossEvents.BossPhase.Remove(h);
WL.Combat.Core.CombatHandler<WL.Combat.Boss.BossPhaseEvent> hc = OnPhaseCount;
WL.Combat.Boss.BossEvents.BossPhase.Add(hc);
s_gcCount = 0;
System.GC.Collect();
long m0 = System.GC.GetTotalMemory(true);
for (int i = 0; i < kGcIterations; i++)
WL.Combat.Boss.BossEvents.RaiseBossPhase(boss, kAnubisId, 1, 2, 3, 0.7f, 0.7f, false);
long m1 = System.GC.GetTotalMemory(false);
WL.Combat.Boss.BossEvents.BossPhase.Remove(hc);
L(" 구독자 1명 · 수신 " + s_gcCount + "회 · 할당 델타 = " + (m1 - m0) + " byte (회당 " +
((m1 - m0) / (double)kGcIterations).ToString("0.##") + " byte)");
}
finally { WL.Combat.Boss.BossEvents.BossPhase.Remove(h); }
}
static int s_gcCount;
/// GC 측정용 — 할당이 전혀 없는 구독자(문자열 조립 없음).
static void OnPhaseCount(in WL.Combat.Boss.BossPhaseEvent e) { s_gcCount++; }
static void OnPhase(in WL.Combat.Boss.BossPhaseEvent e)
{
s_eventCount++;
s_eventLog += string.Format(" #{0} phase {1}→{2}/{3} id={4} hp={5:P0} 임계={6:0.##} spawn={7}\n",
s_eventCount, e.prevPhase, e.phase, e.phaseCount, e.monsterId, e.hpPercent, e.threshold, e.isSpawn);
}
// ───────────────────────────────────────── ⑥ C8 롤백
static void Section4_Rollback()
{
L("");
L("── ⑥ C8 롤백 (RuntimeDisabled = true → 로더·통지 무동작)");
var root = UnityEditor.PrefabUtility.LoadPrefabContents(kAnubisPrefab);
try
{
var boss = root.GetComponentInChildren<BossMobActor>(true);
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = true;
WL.Combat.Boss.BossEvents.ResetDiagnostics();
var before = Dump(boss);
int applied = WL.Combat.Boss.BossPatternTable.ApplyById(boss, kAnubisId);
var after = Dump(boss);
int changed = 0;
foreach (var kv in before) if (after[kv.Key] != kv.Value) changed++;
bool c1 = false, c2 = false;
WL.Combat.Boss.BossPatternTable.ApplySkillGatesById(boss, kAnubisId, 0.10f, ref c1, ref c2);
int ph = WL.Combat.Boss.BossPatternTable.UpdatePhaseById(boss, kAnubisId, 0.10f);
L(" 로더 Enabled=" + WL.Combat.Boss.BossPatternTable.Enabled + " · ApplyById=" + applied +
" · 변경 필드=" + changed + " · 게이트(HP 10%)=" + c1 + "/" + c2 +
" · UpdatePhase=" + ph + " · 이벤트=" + WL.Combat.Boss.BossEvents.TotalRaised);
L(" 판정 = " + (applied == -1 && changed == 0 && !c1 && !c2 && ph == 0 &&
WL.Combat.Boss.BossEvents.TotalRaised == 0
? "PASS (프리팹 값 · 원본 게이트 · 통지 0)" : "FAIL"));
}
finally
{
UnityEditor.PrefabUtility.UnloadPrefabContents(root);
WL.Combat.Core.WLCombatCoreSettings.RuntimeDisabled = false;
WL.Combat.Boss.BossPatternTable.ClearCache();
}
}
// ───────────────────────────────────────── 유틸
static System.Collections.Generic.Dictionary<string, string> Dump(BossMobActor boss)
{
var d = new System.Collections.Generic.Dictionary<string, string>();
var fields = boss.GetType().GetFields(System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.DeclaredOnly);
for (int i = 0; i < fields.Length; i++)
{
var t = fields[i].FieldType;
if (t == typeof(float) || t == typeof(int) || t == typeof(bool) || t == typeof(string))
d[fields[i].Name] = System.Convert.ToString(fields[i].GetValue(boss));
}
return d;
}
static void L(string line) { s.Append(line).Append("\n"); }
static string Flush()
{
var text = s.ToString();
try
{
var dir = System.IO.Path.GetDirectoryName(kLog);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(kLog, text, new System.Text.UTF8Encoding(false));
}
catch (System.Exception ex) { text += "\n[로그 저장 실패] " + ex.Message; }
return text;
}
}