Project_WL/AgentScripts/WL813x_Probe.cs

222 lines
13 KiB
C#

// PD 지시 #813 · 발주서 WL-813x — 핫픽스 검증 프로브 (에디트 모드 전용 · Play 0 · 로그인 0)
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// unity command run_script --file AgentScripts/WL813x_Probe.cs --entry WL813x_Probe.Edit --timeout 300
//
// 무엇을 재는가
// A. 결함 재현·수정 — NavMesh 밖 NavMeshAgent 에 대한 `isStopped = false`(= Resume) 가
// Q3 D-1 / Q2 D-4 의 문구를 실제로 뱉는지, 그리고 MobActor.cs:339 에 들어간 가드
// (`if (agent.isOnNavMesh)`)가 그 예외를 0으로 만드는지를 같은 에이전트로 A/B 측정한다.
// B. MobNavRecovery — SO 값 로드 · Active · 에디트 모드 ForceTick 예외 0 · 러너 오브젝트 0
// C. AutoCombatOnEnter — SO 값 로드 · Active · 에디트 모드 ForceTick 예외 0 · ServerInfo 오염 0
// D. 원본 소스 가드 문자열 확인(MobActor.cs 스킬 케이스)
//
// 게임 수치는 전부 SO 에서 읽는다(C45). 프로브가 만드는 임시 오브젝트는 HideAndDontSave + DestroyImmediate.
public static class WL813x_Probe
{
const string kOut = "AgentScripts/staging/WL813x/EDIT_PROBE.txt";
const string kMobActor = "Assets/Script/Character/Mob/MobActor.cs";
static System.Collections.Generic.List<string> s_log = new System.Collections.Generic.List<string>();
static bool s_hooked;
static void OnLog(string cond, string stack, UnityEngine.LogType t)
{
if (t == UnityEngine.LogType.Error || t == UnityEngine.LogType.Exception || t == UnityEngine.LogType.Assert)
s_log.Add(t + " | " + cond);
}
static void Hook() { if (!s_hooked) { UnityEngine.Application.logMessageReceived += OnLog; s_hooked = true; } }
static void Unhook() { if (s_hooked) { UnityEngine.Application.logMessageReceived -= OnLog; s_hooked = false; } }
static int Count() { return s_log.Count; }
static void Clear() { s_log.Clear(); }
public static string Edit()
{
var sb = new System.Text.StringBuilder();
sb.Append("# WL-813x 핫픽스 프로브 (에디트 모드)\n");
sb.Append("unity=").Append(UnityEngine.Application.unityVersion)
.Append(" isPlaying=").Append(UnityEngine.Application.isPlaying)
.Append(" utc=").Append(System.DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")).Append("\n\n");
SectionA(sb);
SectionB(sb);
SectionC(sb);
SectionD(sb);
var dir = System.IO.Path.GetDirectoryName(kOut);
if (!System.IO.Directory.Exists(dir)) System.IO.Directory.CreateDirectory(dir);
System.IO.File.WriteAllText(kOut, sb.ToString(), new System.Text.UTF8Encoding(false));
return sb.ToString();
}
// ─────────────────────────────────────────────────────────────────────────
// A. 결함 재현 → 가드 효과 (같은 에이전트로 A/B)
// ─────────────────────────────────────────────────────────────────────────
static void SectionA(System.Text.StringBuilder sb)
{
sb.Append("## A. Resume 예외 재현·가드 (NavMesh 밖 에이전트)\n");
UnityEngine.GameObject go = null;
try
{
go = new UnityEngine.GameObject("[WL813x] probe agent");
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
var agent = go.AddComponent<UnityEngine.AI.NavMeshAgent>();
sb.Append("agent.enabled=").Append(agent.enabled)
.Append(" isOnNavMesh=").Append(agent.isOnNavMesh).Append("\n");
// A-1 원본 패턴(가드 없음) = MobActor.cs:339 수정 전
Hook(); Clear();
try { agent.isStopped = false; } catch (System.Exception e) { s_log.Add("THROW | " + e.Message); }
int a1 = Count();
string a1msg = a1 > 0 ? s_log[0] : "";
Unhook();
// A-2 수정 후 패턴(가드) = 현재 MobActor.cs:339
Hook(); Clear();
try { if (agent.isOnNavMesh) agent.isStopped = false; } catch (System.Exception e) { s_log.Add("THROW | " + e.Message); }
int a2 = Count();
Unhook();
sb.Append("A-1 가드 없음 : 에러 ").Append(a1).Append("건 ").Append(a1msg).Append("\n");
sb.Append("A-2 가드 있음 : 에러 ").Append(a2).Append("건\n");
bool qaMatch = a1msg.Contains("Resume") && a1msg.Contains("NavMesh");
sb.Append("판정 : ").Append(a1 >= 1 && a2 == 0 ? "PASS" : "FAIL")
.Append(" (QA 문구 일치=").Append(qaMatch).Append(")\n\n");
}
catch (System.Exception e) { Unhook(); sb.Append("A 실패 : ").Append(e.GetType().Name).Append(" ").Append(e.Message).Append("\n\n"); }
finally { if (go != null) UnityEngine.Object.DestroyImmediate(go); TryClearConsole(); }
}
// ─────────────────────────────────────────────────────────────────────────
// B. MobNavRecovery
// ─────────────────────────────────────────────────────────────────────────
static void SectionB(System.Text.StringBuilder sb)
{
sb.Append("## B. MobNavRecovery (SO · Active · 에디트 모드 안전)\n");
try
{
var cfg = WL.Combat.Core.WLCombatCoreSettings.Instance;
sb.Append("SO=").Append(cfg == null ? "null" : cfg.name).Append("\n");
if (cfg != null)
sb.Append(" mobNavRecoveryEnabled=").Append(cfg.mobNavRecoveryEnabled)
.Append(" grace=").Append(cfg.mobNavRecoveryGraceSeconds)
.Append(" rescan=").Append(cfg.mobNavRecoveryRescanSeconds)
.Append(" check=").Append(cfg.mobNavRecoveryCheckSeconds)
.Append(" near=").Append(cfg.mobNavRecoveryNearRadius)
.Append(" far=").Append(cfg.mobNavRecoveryFarRadius).Append("\n");
sb.Append("Active=").Append(WL.Combat.Nav.MobNavRecovery.Active).Append("\n");
WL.Combat.Nav.MobNavRecovery.ResetDiagnostics();
Hook(); Clear();
WL.Combat.Nav.MobNavRecovery.ForceTick();
WL.Combat.Nav.MobNavRecovery.ForceTick();
int errs = Count();
Unhook();
sb.Append("ForceTick x2 : 에러 ").Append(errs)
.Append("건 · scans=").Append(WL.Combat.Nav.MobNavRecovery.Scans)
.Append(" anomalies=").Append(WL.Combat.Nav.MobNavRecovery.AnomaliesSeen)
.Append(" repaired=").Append(WL.Combat.Nav.MobNavRecovery.Repaired)
.Append(" failed=").Append(WL.Combat.Nav.MobNavRecovery.RepairFailed).Append("\n");
int runners = CountByName("[WL813x] MobNavRecoveryRunner");
sb.Append("에디트 모드 러너 오브젝트=").Append(runners).Append(" (기대 0)\n");
sb.Append("판정 : ").Append(errs == 0 && runners == 0 ? "PASS" : "FAIL").Append("\n\n");
}
catch (System.Exception e) { Unhook(); sb.Append("B 실패 : ").Append(e.GetType().Name).Append(" ").Append(e.Message).Append("\n\n"); }
}
// ─────────────────────────────────────────────────────────────────────────
// C. AutoCombatOnEnter
// ─────────────────────────────────────────────────────────────────────────
static void SectionC(System.Text.StringBuilder sb)
{
sb.Append("## C. AutoCombatOnEnter (SO · Active · 에디트 모드 안전)\n");
try
{
var cfg = WL.Combat.Core.WLCombatCoreSettings.Instance;
if (cfg != null)
sb.Append(" autoCombatOnEnter=").Append(cfg.autoCombatOnEnter)
.Append(" inLobby=").Append(cfg.autoCombatInLobby)
.Append(" poll=").Append(cfg.autoCombatPollSeconds)
.Append(" iconRetry=").Append(cfg.autoCombatIconRetrySeconds).Append("\n");
sb.Append("Active=").Append(WL.Combat.Auto.AutoCombatOnEnter.Active).Append("\n");
int serverBefore = CountByName("ServerInfo");
WL.Combat.Auto.AutoCombatOnEnter.ResetDiagnostics();
Hook(); Clear();
bool autoRead = WL.Combat.Auto.AutoCombatOnEnter.IsAutoOn; // 에디트 모드에서는 무조건 false + 오염 0
WL.Combat.Auto.AutoCombatOnEnter.ForceTick();
WL.Combat.Auto.AutoCombatOnEnter.ForceTick();
int errs = Count();
Unhook();
int serverAfter = CountByName("ServerInfo");
sb.Append("IsAutoOn(에디트)=").Append(autoRead).Append(" (기대 false · ServerInfo 생성 금지)\n");
sb.Append("ForceTick x2 : 에러 ").Append(errs)
.Append("건 · boundaries=").Append(WL.Combat.Auto.AutoCombatOnEnter.Boundaries)
.Append(" applied=").Append(WL.Combat.Auto.AutoCombatOnEnter.Applied)
.Append(" alreadyOn=").Append(WL.Combat.Auto.AutoCombatOnEnter.AlreadyOn)
.Append(" skippedLobby=").Append(WL.Combat.Auto.AutoCombatOnEnter.SkippedLobby).Append("\n");
sb.Append("ServerInfo 오브젝트 ").Append(serverBefore).Append(" → ").Append(serverAfter).Append(" (기대 동일)\n");
int runners = CountByName("[WL813x] AutoCombatOnEnterRunner");
sb.Append("에디트 모드 러너 오브젝트=").Append(runners).Append(" (기대 0)\n");
sb.Append("판정 : ").Append(errs == 0 && !autoRead && serverBefore == serverAfter && runners == 0 ? "PASS" : "FAIL").Append("\n");
sb.Append("🔴 인게임 실값(진입 후 auto=1 · 킬수)은 Play 로그인이 필요해 이 프로브 범위 밖 = 「미확인」. QA 재현 절차는 완료보고 참조.\n\n");
}
catch (System.Exception e) { Unhook(); sb.Append("C 실패 : ").Append(e.GetType().Name).Append(" ").Append(e.Message).Append("\n\n"); }
}
// ─────────────────────────────────────────────────────────────────────────
// D. 원본 소스 가드 확인
// ─────────────────────────────────────────────────────────────────────────
static void SectionD(System.Text.StringBuilder sb)
{
sb.Append("## D. 원본 가드 확인\n");
try
{
var lines = System.IO.File.ReadAllLines(kMobActor);
int guarded = 0, bare = 0, skillLine = -1;
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].Contains("isStopped = false"))
{
if (lines[i].Contains("isOnNavMesh")) guarded++; else bare++;
}
if (lines[i].Contains("case eMobStatus.Skill")) skillLine = i + 1;
}
sb.Append(kMobActor).Append(" : isStopped=false 가드 ").Append(guarded)
.Append("곳 · 무가드 ").Append(bare).Append("곳 · case eMobStatus.Skill = ").Append(skillLine).Append("줄\n");
sb.Append("판정 : ").Append(bare == 0 ? "PASS" : "FAIL").Append("\n");
}
catch (System.Exception e) { sb.Append("D 실패 : ").Append(e.GetType().Name).Append(" ").Append(e.Message).Append("\n"); }
}
// ─────────────────────────────────────────────────────────────────────────
static int CountByName(string name)
{
int n = 0;
var all = UnityEngine.Object.FindObjectsByType<UnityEngine.GameObject>(
UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None);
for (int i = 0; i < all.Length; i++) if (all[i].name == name) n++;
return n;
}
static void TryClearConsole()
{
// A-1 은 의도적으로 에러 1건을 만든다 — 뒤따르는 콘솔 판정(조직 C39)을 오염시키지 않도록 지운다.
try
{
var t = System.Type.GetType("UnityEditor.LogEntries,UnityEditor");
if (t == null) return;
var m = t.GetMethod("Clear", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
if (m != null) m.Invoke(null, null);
}
catch { }
}
}