174 lines
9.5 KiB
C#
174 lines
9.5 KiB
C#
|
|
// WL811bfix_Probe.cs — #811b-fix(코어 리뷰 FIX 6 + isKill 판정) 검증 프로브
|
||
|
|
// 에디트 모드 전용 · Play 불필요 · 에셋/씬 무수정(임시 오브젝트는 즉시 파괴)
|
||
|
|
//
|
||
|
|
// 상주 에디터: unity command run_script --file AgentScripts/WL811bfix_Probe.cs --entry WL811bfix_Probe.RunAll
|
||
|
|
// 헤드리스 : 이 파일을 Assets/<무언가>/Editor/ 로 복사 후
|
||
|
|
// Unity -batchmode -nographics -quit -projectPath <이 worktree> -executeMethod WL811bfix_Probe.RunAll
|
||
|
|
// 결과: Console + Screenshots_WL/WL811bfix/probe_<HHmmss>.txt (RESULT PASS / RESULT FAIL n)
|
||
|
|
//
|
||
|
|
// 검사(발주서 WL-811b-fix ⓒ)
|
||
|
|
// ① FIX-1 스로틀 경로 100회 GC 증가 0 B (Set(r, info) 문자열 생성이 verboseLog 안으로 들어갔는가)
|
||
|
|
// ② QA D-1 isKill 판정 4케이스 — 살아 있음(전이 플래그) · 죽음 · 시체 재타격 · 사망통지만(StatTest)
|
||
|
|
// ③ FIX-4 TryHandleHitStop(seconds ≤ 0) → false (기존 코루틴 폴백 유지)
|
||
|
|
//
|
||
|
|
// 에디트 모드 한계: Applied 경로는 틱 러너를 만들며 DontDestroyOnLoad 를 부르는데 이건 Play 전용이라
|
||
|
|
// 에디트 모드에서 예외가 난다(런타임에는 정상). 그래서 s_lastHitStopStart 를 리플렉션으로 세워
|
||
|
|
// "직전에 히트스톱이 발동한 상태" 를 만든 뒤 스로틀 경로만 측정한다(오브젝트 생성 0).
|
||
|
|
|
||
|
|
using System;
|
||
|
|
using System.IO;
|
||
|
|
using System.Reflection;
|
||
|
|
using System.Text;
|
||
|
|
using UnityEngine;
|
||
|
|
using WL.Combat.Core;
|
||
|
|
|
||
|
|
public static class WL811bfix_Probe
|
||
|
|
{
|
||
|
|
const string OutDir = "Screenshots_WL/WL811bfix";
|
||
|
|
|
||
|
|
public static object RunAll()
|
||
|
|
{
|
||
|
|
var sb = new StringBuilder();
|
||
|
|
sb.AppendLine("# WL811b-fix Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode)");
|
||
|
|
int fail = 0;
|
||
|
|
try { fail += ThrottleGc(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ① 예외 " + ex); }
|
||
|
|
try { fail += IsKill(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ② 예외 " + ex); }
|
||
|
|
try { fail += InvalidHitStop(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ③ 예외 " + ex); }
|
||
|
|
sb.AppendLine(fail == 0 ? "RESULT PASS" : "RESULT FAIL " + fail);
|
||
|
|
var s = sb.ToString();
|
||
|
|
Debug.Log(s);
|
||
|
|
try
|
||
|
|
{
|
||
|
|
Directory.CreateDirectory(OutDir);
|
||
|
|
File.WriteAllText(Path.Combine(OutDir, "probe_" + DateTime.Now.ToString("HHmmss") + ".txt"), s);
|
||
|
|
}
|
||
|
|
catch { /* 출력 저장 실패는 판정에 영향 없음 */ }
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── ① FIX-1: 스로틀 경로 할당 0 -------------------------------------------------
|
||
|
|
static int ThrottleGc(StringBuilder sb)
|
||
|
|
{
|
||
|
|
var cfg = WLCombatCoreSettings.Instance;
|
||
|
|
sb.AppendLine("asset=" + (cfg != null ? "found" : "NULL")
|
||
|
|
+ " coreEnabled=" + WLCombatCoreSettings.Enabled + " arbiterEnabled=" + TimeScaleArbiter.Enabled
|
||
|
|
+ " hitStopPerSecondMax=" + (cfg != null ? cfg.hitStopPerSecondMax.ToString("F2") : "?")
|
||
|
|
+ " verboseLog=" + (cfg != null && cfg.verboseLog));
|
||
|
|
if (!TimeScaleArbiter.Enabled) { sb.AppendLine("[FAIL] ① 중재자 비활성 — 측정 불가"); return 1; }
|
||
|
|
|
||
|
|
// Time.timeScale 은 읽기만 한다 — 에디트 모드에서 대입하면 ProjectSettings/TimeManager.asset 에 그대로 적힌다(실측 사고).
|
||
|
|
TimeScaleArbiter.ResetDiagnostics();
|
||
|
|
if (!ArmThrottle()) { sb.AppendLine("[FAIL] ① s_lastHitStopStart 리플렉션 실패 — 스로틀 상태를 만들 수 없음"); return 1; }
|
||
|
|
var rThrottled = TimeScaleArbiter.Request(TimeLayer.HitStop, 0.05f, 0.05f); // 워밍업(JIT) · 기대 Throttled
|
||
|
|
for (int i = 0; i < 20; i++) TimeScaleArbiter.Request(TimeLayer.HitStop, 0.05f, 0.05f);
|
||
|
|
|
||
|
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||
|
|
long a0 = Alloc(), m0 = GC.GetTotalMemory(false), p0 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
||
|
|
for (int i = 0; i < 100; i++) TimeScaleArbiter.Request(TimeLayer.HitStop, 0.05f, 0.05f);
|
||
|
|
long a1 = Alloc(), m1 = GC.GetTotalMemory(false), p1 = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
||
|
|
|
||
|
|
int throttled = TimeScaleArbiter.ThrottledCount;
|
||
|
|
var lastInfo = TimeScaleArbiter.LastInfo; // 읽는 순간에만 조립된다(측정 뒤에 읽는다)
|
||
|
|
Cleanup("__WLTimeScaleArbiter");
|
||
|
|
|
||
|
|
long alloc = a1 - a0;
|
||
|
|
bool ok = rThrottled == ArbiterResult.Throttled && throttled >= 121 && alloc == 0L;
|
||
|
|
sb.AppendLine((ok ? "[PASS]" : "[FAIL]") + " ① 스로틀 100회: result=" + rThrottled
|
||
|
|
+ " throttledCount=" + throttled + " (기대 121)"
|
||
|
|
+ " · GC.GetAllocatedBytesForCurrentThread 증가=" + alloc + " B (기대 0)"
|
||
|
|
+ " · GetTotalMemory 증가=" + (m1 - m0) + " B · MonoUsedSize 증가=" + (p1 - p0) + " B"
|
||
|
|
+ " · LastInfo=\"" + lastInfo + "\"");
|
||
|
|
return ok ? 0 : 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
static long Alloc()
|
||
|
|
{
|
||
|
|
try { return GC.GetAllocatedBytesForCurrentThread(); }
|
||
|
|
catch (Exception) { return GC.GetTotalMemory(false); }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>"방금 히트스톱이 발동했다" 상태를 만든다 — 이후 Request 는 전부 스로틀 경로(오브젝트 생성 0).</summary>
|
||
|
|
static bool ArmThrottle()
|
||
|
|
{
|
||
|
|
var f = typeof(TimeScaleArbiter).GetField("s_lastHitStopStart", BindingFlags.NonPublic | BindingFlags.Static);
|
||
|
|
if (f == null) return false;
|
||
|
|
f.SetValue(null, Time.unscaledTime);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── ② QA D-1: isKill 판정 --------------------------------------------------------
|
||
|
|
static int IsKill(StringBuilder sb)
|
||
|
|
{
|
||
|
|
var go = new GameObject("__WL811bfix_victim");
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var actor = go.AddComponent<Actor>();
|
||
|
|
var fDead = typeof(Actor).GetField("DeadStatus", BindingFlags.NonPublic | BindingFlags.Instance);
|
||
|
|
if (fDead == null) { sb.AppendLine("[FAIL] ② Actor.DeadStatus 리플렉션 실패"); return 1; }
|
||
|
|
|
||
|
|
// 공유 DamageInfo 가 앞 피격자의 사망으로 이미 오염된 상태(QA D-1 원인 재현)
|
||
|
|
var stale = new DamageInfo(); stale.IsDeadByThisDamage = true;
|
||
|
|
var clean = new DamageInfo(); clean.IsDeadByThisDamage = false;
|
||
|
|
|
||
|
|
CombatEvents.ResetDiagnostics();
|
||
|
|
|
||
|
|
fDead.SetValue(actor, false);
|
||
|
|
CombatEvents.RaiseHitConfirmed(actor, stale);
|
||
|
|
bool a = !CombatEvents.LastIsKill; // A 살아 있는 피격자 + 전이 플래그 → false
|
||
|
|
|
||
|
|
fDead.SetValue(actor, true);
|
||
|
|
CombatEvents.RaiseKilled(actor);
|
||
|
|
CombatEvents.RaiseHitConfirmed(actor, clean);
|
||
|
|
bool b = CombatEvents.LastIsKill; // B 실제 사망(플래그는 false) → true
|
||
|
|
|
||
|
|
CombatEvents.RaiseHitConfirmed(actor, stale);
|
||
|
|
bool c = !CombatEvents.LastIsKill; // C 같은 프레임 시체 재타격 → false
|
||
|
|
|
||
|
|
fDead.SetValue(actor, false);
|
||
|
|
CombatEvents.RaiseKilled(actor);
|
||
|
|
CombatEvents.RaiseHitConfirmed(actor, stale);
|
||
|
|
bool d = !CombatEvents.LastIsKill; // D 사망통지만 있고 실제 생존(StatTest) → false
|
||
|
|
|
||
|
|
int killed = CombatEvents.Killed.Raised, kills = CombatEvents.KillConfirmedCount;
|
||
|
|
bool e = kills == 1; // E 실제 사망 1건 = isKill 1건
|
||
|
|
bool ok = a && b && c && d && e;
|
||
|
|
sb.AppendLine((ok ? "[PASS]" : "[FAIL]") + " ② isKill: A살아있음+전이플래그=" + (a ? "false✔" : "true✘")
|
||
|
|
+ " B사망=" + (b ? "true✔" : "false✘") + " C시체재타격=" + (c ? "false✔" : "true✘")
|
||
|
|
+ " D사망통지만=" + (d ? "false✔" : "true✘")
|
||
|
|
+ " · HitConfirmed=" + CombatEvents.HitConfirmed.Raised + " Killed=" + killed + " isKillTrue=" + kills + " (기대 1)");
|
||
|
|
return ok ? 0 : 1;
|
||
|
|
}
|
||
|
|
finally { UnityEngine.Object.DestroyImmediate(go); CombatEvents.ResetDiagnostics(); }
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── ③ FIX-4: Invalid 요청은 코어가 처리하지 않았다고 답한다 ------------------------
|
||
|
|
static int InvalidHitStop(StringBuilder sb)
|
||
|
|
{
|
||
|
|
var st = WL.Feel.WLHitFeel.Settings;
|
||
|
|
if (st == null) { sb.AppendLine("[FAIL] ③ WLHitFeelSettings 에셋 없음 — 측정 불가"); return 1; }
|
||
|
|
TimeScaleArbiter.ResetDiagnostics();
|
||
|
|
ArmThrottle(); // 에디트 모드에서 Applied(러너 생성) 를 피한다 — 정상 요청은 Throttled 로 받는다
|
||
|
|
|
||
|
|
bool zero = TimeScaleArbiter.TryHandleHitStop(st, 0f);
|
||
|
|
var rZero = TimeScaleArbiter.LastResult;
|
||
|
|
bool neg = TimeScaleArbiter.TryHandleHitStop(st, -1f);
|
||
|
|
bool valid = TimeScaleArbiter.TryHandleHitStop(st, 0.04f); // 코어가 처리한 요청은 그대로 true
|
||
|
|
var rValid = TimeScaleArbiter.LastResult;
|
||
|
|
|
||
|
|
TimeScaleArbiter.ReleaseAll();
|
||
|
|
Cleanup("__WLTimeScaleArbiter");
|
||
|
|
|
||
|
|
bool ok = !zero && !neg && valid && rZero == ArbiterResult.Invalid && rValid == ArbiterResult.Throttled;
|
||
|
|
sb.AppendLine((ok ? "[PASS]" : "[FAIL]") + " ③ TryHandleHitStop: seconds=0 → " + zero + "(기대 false · " + rZero + ")"
|
||
|
|
+ " · seconds=-1 → " + neg + "(기대 false) · seconds=0.04 → " + valid + "(기대 true · " + rValid + ")"
|
||
|
|
+ " · hitStopMode=" + st.hitStopMode);
|
||
|
|
return ok ? 0 : 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void Cleanup(string name)
|
||
|
|
{
|
||
|
|
var go = GameObject.Find(name);
|
||
|
|
if (go != null) UnityEngine.Object.DestroyImmediate(go);
|
||
|
|
}
|
||
|
|
}
|