// WL813w_Probe.cs — #813w(런 진행 가이드 · 보스 우선 타깃 · 리쉬 완화 · 보스 바 이름) 에디트 모드 검증 프로브 // 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴) // // 상주 에디터: unity command run_script --project-path --file AgentScripts/WL813w_Probe.cs --entry WL813w_Probe.RunAll --timeout 300 // 결과: Console + AgentScripts/WL813w_PROBE.txt (RESULT PASS / RESULT FAIL n) // // 검사 (발주서 WL-813w §1-5) // ① 에셋·SOT: WLRunGuideSettings 로드·값 전수 · WLBossArenaSettings 신규 3필드 // ② 앵커: 스포너 노드(MobControlMgr.SpawnerID) 실좌표 우선 · 노드가 없으면 SO 폴백 · WL_Nature.prefab 좌표 대조 // ③ 존 이동: 가짜 ZoneCleared(0) → 목적지 = 존2 앵커 · Travel → 도착(Hold) → 이탈 → 되돌림 // ④ 타깃 규칙: Travel 중 타깃 해제 · Hold 중 「앵커에서 guideLeashRadius 밖」만 무시(가까운 적은 유지) // ⑤ 보스: 가짜 BossGateOpened → 아레나 중심 · BossStarted(boss) → 타깃 고정 + 원본 락타깃 · 사망 → 해제 // ⑥ 피격: SimulateHit → 가이드 중단(조종 0) · 해제 → 재개 // ⑦ 리쉬 회복 판정(임시 SO): 부분 회복 → 쿨다운 → 쿨다운 뒤 다시 회복 · percent>=1 이면 만피(813d 레거시) // ⑧ 보스 바 이름: StripClone · ResolveBossLabel · MonsterList/Localization 실측 행 대조 // ⑨ GC: ForceTick 100회 0 B(대조군 = 의도적 할당) // ⑩ C8: WLRunGuideSettings.RuntimeDisabled → 아무 일도 하지 않는다(조종 해제 · 상태 불변) // // 에디트 모드 한계(보고서 「미확인」으로 올린다) // · NavMesh 가 없어 NavMeshAgent.SetDestination 실이동은 못 잰다 → Steer 는 agent 검사에서 되돌아간다. // 상태 전이·타깃 규칙은 PC transform 을 직접 옮겨 검증한다(원본 Get_position() 이 transform 이다). // · table_monsterlist/localtext 가 없어 「테이블 이름」 실경로는 JSON 행 대조로만 확인한다. // · 실제 보스 Heal 은 살아 있는 m_Stat 이 필요해(813d 미확인 ②와 같은 뿌리) 판정만 검증한다. using System; using System.IO; using System.Text; using UnityEngine; using WL.Combat.Boss; using WL.Combat.Core; using WL.Combat.Run; public static class WL813w_Probe { const string OutCommit = "AgentScripts/WL813w_PROBE.txt"; const string MonsterListJson = "Assets/ResWork/Table/Export/MonsterList.json"; const string LocalizationJson = "Assets/ResWork/Table/Export/Localization.json"; const string NaturePrefab = "Assets/Res_Addr/Map/WL_Nature.prefab"; static StringBuilder sb; static int s_fail; static GameObject s_pcGo, s_bossGo, s_mobFarGo, s_mobNearGo; static GameObject[] s_spawnerGo; static PCActor s_pc; static Actor s_boss, s_mobFar, s_mobNear; static PCActor s_prevMyPc; public static object RunAll() { sb = new StringBuilder(); s_fail = 0; sb.AppendLine("# WL813w Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)"); sb.AppendLine("playMode=" + Application.isPlaying); try { Setup(); Sec1_Asset(); Sec2_Anchor(); Sec3_ZoneMove(); Sec4_TargetRule(); Sec5_Boss(); Sec6_Hit(); Sec7_LeashHeal(); Sec8_BossName(); Sec9_GC(); Sec10_C8(); } catch (Exception ex) { L("[FAIL] 예외 — " + ex.GetType().Name + ": " + ex.Message); L(ex.StackTrace); s_fail++; } finally { Teardown(); } L(""); L(s_fail == 0 ? "RESULT PASS" : "RESULT FAIL " + s_fail); var text = sb.ToString(); try { File.WriteAllText(OutCommit, text, new UTF8Encoding(false)); } catch { } Debug.Log(text); return text; } // ───────────────────────────────────────── 유틸 static void L(string m) { sb.AppendLine(m); } static string P(bool ok) { if (!ok) s_fail++; return ok ? "[PASS]" : "[FAIL]"; } static long Alloc() { return GC.GetTotalMemory(false); } static string V(Vector3 v) { return "(" + v.x.ToString("F1") + "," + v.y.ToString("F1") + "," + v.z.ToString("F1") + ")"; } static float DXZ(Vector3 a, Vector3 b) { float dx = a.x - b.x, dz = a.z - b.z; return Mathf.Sqrt(dx * dx + dz * dz); } static Actor MakeActor(string name, eRole role, eSubRol sub, Vector3 pos, out GameObject go) { go = new GameObject("__WL813w_" + name); go.transform.position = pos; var a = go.AddComponent(); a.m_Role = role; a.m_SubRole = sub; return a; } static void Setup() { s_prevMyPc = MyValue.MyPC; s_pcGo = new GameObject("__WL813w_PC"); s_pcGo.transform.position = Vector3.zero; s_pc = s_pcGo.AddComponent(); s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None; // m_Enemy 기본 false → IsMainPC() true MyValue.MyPC = s_pc; s_boss = MakeActor("Anubis(Clone)", eRole.Mob, eSubRol.Boss, Vector3.zero, out s_bossGo); s_mobFar = MakeActor("mobFar", eRole.Mob, eSubRol.None, Vector3.zero, out s_mobFarGo); s_mobNear = MakeActor("mobNear", eRole.Mob, eSubRol.None, Vector3.zero, out s_mobNearGo); RunGuide.ResetDiagnostics(); WLRunGuideSettings.RuntimeDisabled = false; L(""); L("── ⓪ 준비"); L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true · MyValue.MyPC 등록"); L(" " + P(RunGuide.Active) + " RunGuide.Active(에셋 · enabled · 런 축)"); L(" Agent=" + (s_pc.Get_Agent() == null ? "없음(에디트 모드 · Steer 는 여기서 되돌아간다)" : "있음")); } static void Teardown() { try { RunGuide.StopGuide("probe teardown"); RunGuide.ResetDiagnostics(); WLRunGuideSettings.RuntimeDisabled = false; MyValue.MyPC = s_prevMyPc; Kill(s_pcGo, s_bossGo, s_mobFarGo, s_mobNearGo); if (s_spawnerGo != null) for (int i = 0; i < s_spawnerGo.Length; i++) Kill(s_spawnerGo[i]); s_spawnerGo = null; } catch { } } static void Kill(params GameObject[] gos) { for (int i = 0; i < gos.Length; i++) if (gos[i] != null) UnityEngine.Object.DestroyImmediate(gos[i]); } // ───────────────────────────────────────── ① 에셋 · SOT static void Sec1_Asset() { L(""); L("── ① 에셋 · SOT"); var c = WLRunGuideSettings.Instance; L(" " + P(c != null) + " Resources.Load(\"" + WLRunGuideSettings.ResourcesPath + "\")"); if (c == null) return; L(" enabled=" + c.enabled + " verboseLog=" + c.verboseLog + " pollSeconds=" + c.pollSeconds); L(" zoneGuideEnabled=" + c.zoneGuideEnabled + " guideOnRunStart=" + c.guideOnRunStart + " zoneArriveRadius=" + c.zoneArriveRadius + " zoneHoldRadius=" + c.zoneHoldRadius); L(" guideLeashRadius=" + c.guideLeashRadius + " guideStopDistance=" + c.guideStopDistance + " repathIntervalSeconds=" + c.repathIntervalSeconds); L(" resumeAfterHitSeconds=" + c.resumeAfterHitSeconds + " maxTravelSeconds=" + c.maxTravelSeconds); L(" bossGuideEnabled=" + c.bossGuideEnabled + " bossPriorityEnabled=" + c.bossPriorityEnabled + " bossArriveRadius=" + c.bossArriveRadius + " bossHoldRadius=" + c.bossHoldRadius + " bossLockTarget=" + c.bossLockTarget); L(" zoneAnchorFallbacks=" + (c.zoneAnchorFallbacks == null ? 0 : c.zoneAnchorFallbacks.Length) + "개"); var b = WLBossArenaSettings.Instance; L(" " + P(b != null) + " WLBossArenaSettings 로드"); if (b == null) return; L(" leashHealPercent=" + b.leashHealPercent + " leashHealCooldownSeconds=" + b.leashHealCooldownSeconds + " bossNameFromTable=" + b.bossNameFromTable + " (기존 resetHealsBoss=" + b.resetHealsBoss + " resetHealRate=" + b.resetHealRate + ")"); L(" " + P(b.leashHealPercent > 0f && b.leashHealPercent < 1f) + " 리쉬 부분 회복 구간(0 0) L(" " + P(!RunGuide.AnchorFromNode(0) && a0 == c.zoneAnchorFallbacks[0]) + " 폴백 값 일치"); // (b) 가짜 스포너 노드를 심고 다시 읽는다 = 노드 실좌표 우선 s_spawnerGo = new GameObject[n]; for (int i = 0; i < n; i++) { s_spawnerGo[i] = new GameObject("__WL813w_Spawner" + rs.zoneSpawnerIds[i]); s_spawnerGo[i].transform.position = new Vector3(1000f + i, 0f, 2000f + i); var m = s_spawnerGo[i].AddComponent(); m.SpawnerID = rs.zoneSpawnerIds[i]; } RunGuide.ResetDiagnostics(); Vector3 a1; bool got1 = RunGuide.TryGetAnchor(c, 1, out a1); L(" " + P(got1 && RunGuide.AnchorFromNode(1) && a1 == s_spawnerGo[1].transform.position) + " 존1 앵커 = " + V(a1) + " · 노드 실좌표 우선(스캔 " + RunGuide.AnchorScans + "회)"); // (c) WL_Nature.prefab 실좌표와 SO 폴백 대조 (원본 프리팹은 읽기만 한다) L(" 프리팹 실측 대조(" + NaturePrefab + "):"); for (int i = 0; i < n && i < (c.zoneAnchorFallbacks == null ? 0 : c.zoneAnchorFallbacks.Length); i++) { Vector3 pf; bool found = PrefabSpawnerPos(rs.zoneSpawnerIds[i], out pf); bool same = found && DXZ(pf, c.zoneAnchorFallbacks[i]) < 0.5f; L(" " + P(found && same) + " " + rs.zoneSpawnerIds[i] + " 프리팹 " + (found ? V(pf) : "없음") + " ↔ SO 폴백 " + V(c.zoneAnchorFallbacks[i])); } } /// WL_Nature.prefab 을 텍스트로 읽어 SpawnerID 노드의 월드 좌표를 구한다(프리팹 무수정). static bool PrefabSpawnerPos(int spawnerId, out Vector3 pos) { pos = Vector3.zero; try { var go = UnityEditor.AssetDatabase.LoadAssetAtPath(NaturePrefab); if (go == null) return false; var mgrs = go.GetComponentsInChildren(true); for (int i = 0; i < mgrs.Length; i++) if (mgrs[i].SpawnerID == spawnerId) { pos = mgrs[i].transform.position; return true; } } catch { } return false; } // ───────────────────────────────────────── ③ 존 이동 static void Sec3_ZoneMove() { L(""); L("── ③ 존 이동 (ZoneCleared → 다음 존 · 도착 · 이탈 복귀)"); var c = WLRunGuideSettings.Instance; RunGuide.ResetDiagnostics(); Vector3 anchor1; RunGuide.TryGetAnchor(c, 1, out anchor1); // 존0 클리어 → 존1 유도 RunGuide.BeginZone(1, "probe ZoneCleared(0)"); L(" " + P(RunGuide.DestKind == RunGuideDest.Zone && RunGuide.ZoneIndex == 1) + " 목적지 = 존1(Zone)"); L(" " + P(DXZ(RunGuide.Destination, anchor1) < 0.01f) + " 목적지 좌표 = " + V(RunGuide.Destination)); L(" " + P(RunGuide.State == RunGuideState.Travel) + " 상태 Travel · GuideBegun=" + RunGuide.GuideBegun); // 멀리 있을 때 = Travel 유지 s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 30f); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Travel) + " 30 m — Travel 유지(조종 시도 · Repaths=" + RunGuide.Repaths + ")"); // 도착 s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, c.zoneArriveRadius - 1f); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold && RunGuide.Arrived == 1) + " " + (c.zoneArriveRadius - 1f).ToString("F1") + " m — 도착 → Hold(자동전투 재개) · Arrived=" + RunGuide.Arrived); L(" " + P(!RunGuide.Steering) + " 도착 뒤 조종 해제(JoystickStatus=" + s_pc.JoystickStatus + ")"); // Hold 유지 범위 s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, c.zoneHoldRadius - 2f); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold && RunGuide.HoldPullbacks == 0) + " " + (c.zoneHoldRadius - 2f).ToString("F1") + " m — Hold 유지(끌어오지 않는다)"); // 이탈 → 되돌림 s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, c.zoneHoldRadius + 5f); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.HoldPullbacks == 1) + " " + (c.zoneHoldRadius + 5f).ToString("F1") + " m — 이탈 → 다시 유도 · HoldPullbacks=" + RunGuide.HoldPullbacks); L(" 로그=" + RunGuide.LastLog); } // ───────────────────────────────────────── ④ 타깃 규칙 static void Sec4_TargetRule() { L(""); L("── ④ 타깃 규칙 (Travel 전면 해제 · Hold 는 앵커에서 먼 것만 무시)"); var c = WLRunGuideSettings.Instance; RunGuide.ResetDiagnostics(); Vector3 anchor1; RunGuide.TryGetAnchor(c, 1, out anchor1); RunGuide.BeginZone(1, "probe target rule"); // Travel 중: 어떤 타깃이든 놓는다 s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 25f); s_mobNearGo.transform.position = anchor1 + new Vector3(0f, 0f, 24f); // PC 코앞이지만 이동 중 s_pc.Set_Target(s_mobNear); RunGuide.ForceTick(); L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == 1) + " Travel 중 타깃 해제(무시 " + RunGuide.FarTargetIgnored + " · 마지막=" + RunGuide.LastIgnoredTarget + ")"); // 도착 → Hold s_pcGo.transform.position = anchor1; RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold) + " 도착 → Hold"); // Hold: 앵커에서 먼 타깃 = 무시 int before = RunGuide.FarTargetIgnored; s_mobFarGo.transform.position = anchor1 + new Vector3(0f, 0f, c.guideLeashRadius + 8f); s_pc.Set_Target(s_mobFar); RunGuide.ForceTick(); L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == before + 1) + " 앵커 +" + (c.guideLeashRadius + 8f).ToString("F0") + " m 타깃 무시(Del_Target)"); // Hold: 앵커 가까이의 타깃 = 유지(원본 자동전투가 잡는다) before = RunGuide.FarTargetIgnored; s_mobNearGo.transform.position = anchor1 + new Vector3(0f, 0f, c.guideLeashRadius - 4f); s_pc.Set_Target(s_mobNear); RunGuide.ForceTick(); L(" " + P(s_pc.isTarget && RunGuide.FarTargetIgnored == before) + " 앵커 +" + (c.guideLeashRadius - 4f).ToString("F0") + " m 타깃 유지(원본 자동전투)"); s_pc.Del_Target(true); } // ───────────────────────────────────────── ⑤ 보스 static void Sec5_Boss() { L(""); L("── ⑤ 보스 (아레나 유도 · 우선 타깃 고정)"); var c = WLRunGuideSettings.Instance; RunGuide.ResetDiagnostics(); Vector3 center = BossArena.Center; RunGuide.BeginBoss("probe BossGateOpened"); L(" " + P(RunGuide.DestKind == RunGuideDest.Boss) + " 목적지 = 보스 아레나 " + V(RunGuide.Destination) + " (BossArena.Center=" + V(center) + ")"); // 보스 등장 → 아레나 밖에서 시작 s_bossGo.transform.position = center; RunGuide.SetBossForProbe(s_boss); s_pcGo.transform.position = center + new Vector3(0f, 0f, 25f); s_pc.Set_Target(s_mobFar); s_mobFarGo.transform.position = center + new Vector3(0f, 0f, 40f); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Travel) + " 25 m — Travel(아레나로 유도)"); L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_boss) && RunGuide.BossRetargets >= 1) + " 타깃 = 보스 고정(BossRetargets=" + RunGuide.BossRetargets + ")"); L(" " + P(!c.bossLockTarget || s_boss.m_IsLockTarget) + " 원본 락타깃 m_IsLockTarget=" + s_boss.m_IsLockTarget + "(BossLocks=" + RunGuide.BossLocks + ")"); // 잡몹으로 타깃이 새면 폴링이 되돌린다 s_pc.Set_Target(s_mobFar); RunGuide.ForceTick(); L(" " + P(ReferenceEquals(s_pc.Get_Target(), s_boss)) + " 잡몹 타깃 전환 → 다음 폴링에서 보스로 복귀"); // 아레나 도착 s_pcGo.transform.position = center + new Vector3(0f, 0f, c.bossArriveRadius - 2f); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Hold) + " " + (c.bossArriveRadius - 2f).ToString("F1") + " m — 도착 → Hold"); // 아레나 이탈(리쉬 발동선 전에) → 되돌림 var b = WLBossArenaSettings.Instance; float leashLine = b != null ? b.arenaRadius + b.playerLeaveMargin : 24f; s_pcGo.transform.position = center + new Vector3(0f, 0f, c.bossHoldRadius + 2f); RunGuide.ForceTick(); L(" " + P(RunGuide.State == RunGuideState.Travel && RunGuide.HoldPullbacks == 1) + " " + (c.bossHoldRadius + 2f).ToString("F1") + " m 이탈 → 되돌림(리쉬 발동선 " + leashLine.ToString("F0") + " m 前)"); L(" " + P(c.bossHoldRadius + 2f < leashLine) + " 되돌림 지점 < 리쉬 발동선 = 리셋이 아예 안 걸린다(D-11)"); // 보스 사망 → 락 해제 s_bossGo.SetActive(false); // IsDead() = !activeInHierarchy RunGuide.ForceTick(); L(" " + P(!s_boss.m_IsLockTarget) + " 보스 사망/비활성 → 락타깃 해제"); s_bossGo.SetActive(true); RunGuide.StopGuide("probe ⑤ 종료"); } // ───────────────────────────────────────── ⑥ 피격 응전 static void Sec6_Hit() { L(""); L("── ⑥ 피격 → 즉시 응전(가이드 일시 중단)"); var c = WLRunGuideSettings.Instance; RunGuide.ResetDiagnostics(); Vector3 anchor1; RunGuide.TryGetAnchor(c, 1, out anchor1); RunGuide.BeginZone(1, "probe hit"); s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 25f); s_mobFarGo.transform.position = anchor1 + new Vector3(0f, 0f, 40f); RunGuide.SimulateHit(); s_pc.Set_Target(s_mobFar); int before = RunGuide.FarTargetIgnored; RunGuide.ForceTick(); L(" " + P(s_pc.isTarget && RunGuide.FarTargetIgnored == before && !RunGuide.Steering) + " 피격 중 — 타깃 유지 · 조종 0(원본 자동전투가 응전) · HitSuspends=" + RunGuide.HitSuspends); L(" " + P(RunGuide.DestKind == RunGuideDest.Zone) + " 목적지는 유지(중단일 뿐 취소 아님)"); RunGuide.ClearHitSuspendForProbe(); RunGuide.ForceTick(); L(" " + P(!s_pc.isTarget && RunGuide.FarTargetIgnored == before + 1) + " 무피격 구간 복귀 → 가이드 재개"); RunGuide.StopGuide("probe ⑥ 종료"); } // ───────────────────────────────────────── ⑦ 리쉬 회복 static void Sec7_LeashHeal() { L(""); L("── ⑦ 리쉬 회복 판정 (임시 SO · 에셋 무수정)"); var tmp = ScriptableObject.CreateInstance(); try { tmp.resetHealsBoss = true; tmp.leashHealPercent = 0.15f; tmp.leashHealCooldownSeconds = 10f; BossArena.ResetLeashState(); int v1 = BossArena.EvaluateLeashHeal(tmp, 100f); int v2 = BossArena.EvaluateLeashHeal(tmp, 105f); int v3 = BossArena.EvaluateLeashHeal(tmp, 111f); L(" " + P(v1 == BossArena.kHealPartial) + " t=100 첫 리쉬 → 부분 회복(+15 %)"); L(" " + P(v2 == BossArena.kHealCooldown) + " t=105 (5 s 뒤) → 쿨다운으로 건너뜀 = 만피 복구 없음"); L(" " + P(v3 == BossArena.kHealPartial) + " t=111 (11 s 뒤) → 다시 부분 회복 · 최대 1회/10 s"); tmp.leashHealPercent = 1f; BossArena.ResetLeashState(); L(" " + P(BossArena.EvaluateLeashHeal(tmp, 200f) == BossArena.kHealFull) + " leashHealPercent=1 → 813d 원래 동작(On_Regen 만피) = C8 롤백 경로"); tmp.leashHealPercent = 0.15f; tmp.resetHealsBoss = false; BossArena.ResetLeashState(); L(" " + P(BossArena.EvaluateLeashHeal(tmp, 300f) == BossArena.kHealNone) + " resetHealsBoss=0 → 회복 없음(워프만)"); // Q4b 실측 대비: 69회 리쉬 · 90 s 보스전이면 회복은 최대 몇 회인가 var real = WLBossArenaSettings.Instance; if (real != null && real.leashHealCooldownSeconds > 0f) { int maxHeals = Mathf.FloorToInt(90f / real.leashHealCooldownSeconds) + 1; L(" 참고: Q4b 런① = 리쉬 69회 / 보스전 90 s → 이 값으로는 최대 " + maxHeals + "회 × " + (real.leashHealPercent * 100f).ToString("F0") + "% (예전 = 69회 × 100 %)"); } } finally { UnityEngine.Object.DestroyImmediate(tmp); BossArena.ResetLeashState(); } } // ───────────────────────────────────────── ⑧ 보스 바 이름 static void Sec8_BossName() { L(""); L("── ⑧ 보스 HP 바 이름 (D-12 · Anubis(Clone))"); L(" " + P(WL.UI.BossHpBar.StripClone("Anubis(Clone)") == "Anubis") + " StripClone(\"Anubis(Clone)\") = \"" + WL.UI.BossHpBar.StripClone("Anubis(Clone)") + "\""); L(" " + P(WL.UI.BossHpBar.StripClone("Anubis (Clone)") == "Anubis") + " StripClone(\"Anubis (Clone)\") = \"" + WL.UI.BossHpBar.StripClone("Anubis (Clone)") + "\""); // 가짜 보스 오브젝트 이름 = "__WL813w_Anubis(Clone)"(프로브 접두사 + QA 가 본 이름) string objName = s_boss.name; string label = WL.UI.BossHpBar.ResolveBossLabel(s_boss); L(" " + P(label == objName.Replace("(Clone)", "") && !label.Contains("(Clone)")) + " 표가 없는 에디트 모드 → 폴백 = \"" + label + "\" (원본 오브젝트 이름 \"" + objName + "\" · 경로=" + WL.UI.BossHpBar.LastNameSource + ")"); var tmp = ScriptableObject.CreateInstance(); try { tmp.bossNameFromTable = false; string raw = WL.UI.BossHpBar.ResolveBossLabel(s_boss, tmp); L(" " + P(raw == objName) + " C8 롤백(bossNameFromTable=0) → 원본 이름 그대로 \"" + raw + "\""); } finally { UnityEngine.Object.DestroyImmediate(tmp); } // 인게임에서 실제로 나올 이름 = MonsterList → Localization 실측 var b = WLBossArenaSettings.Instance; int mid = b != null ? b.bossMonsterId : 10006; string nameId = JsonField(MonsterListJson, "n_MonsterID", mid.ToString(), "n_MonsterName"); string kor = nameId != null ? JsonField(LocalizationJson, "ID", nameId, "s_Korean") : null; L(" " + P(!string.IsNullOrEmpty(kor)) + " MonsterList " + mid + " → n_MonsterName=" + nameId + " → Localization s_Korean=\"" + (kor ?? "없음") + "\" (인게임 라벨 기대값 · 813tj 배너와 동일)"); } /// Export JSON 에서 keyField=keyValue 인 행의 outField 를 읽는다(표 로드 없이 실측). static string JsonField(string path, string keyField, string keyValue, string outField) { try { string txt = File.ReadAllText(path); string key = "\"" + keyField + "\":\"" + keyValue + "\""; int i = txt.IndexOf(key, StringComparison.Ordinal); if (i < 0) { key = "\"" + keyField + "\": \"" + keyValue + "\""; i = txt.IndexOf(key, StringComparison.Ordinal); } if (i < 0) return null; int rowStart = txt.LastIndexOf('{', i); int rowEnd = txt.IndexOf('}', i); if (rowStart < 0 || rowEnd < 0) return null; string row = txt.Substring(rowStart, rowEnd - rowStart); int j = row.IndexOf("\"" + outField + "\"", StringComparison.Ordinal); if (j < 0) return null; int q1 = row.IndexOf('"', row.IndexOf(':', j) + 1); int q2 = row.IndexOf('"', q1 + 1); if (q1 < 0 || q2 < 0) return null; return row.Substring(q1 + 1, q2 - q1 - 1); } catch { return null; } } // ───────────────────────────────────────── ⑨ GC static void Sec9_GC() { L(""); L("── ⑨ GC (ForceTick 100회)"); var c = WLRunGuideSettings.Instance; RunGuide.ResetDiagnostics(); Vector3 anchor1; RunGuide.TryGetAnchor(c, 1, out anchor1); RunGuide.BeginZone(1, "probe gc"); s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 8f); // Hold 도 Travel 도 아닌 중간 = 매 틱 판정 RunGuide.ForceTick(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); long a0 = Alloc(); for (int i = 0; i < 100; i++) RunGuide.ForceTick(); long a1 = Alloc(); L(" " + P(a1 - a0 <= 0) + " ForceTick ×100 = " + (a1 - a0) + " B"); long b0 = Alloc(); var junk = new string[256]; for (int i = 0; i < junk.Length; i++) junk[i] = new string('x', 4096); long b1 = Alloc(); L(" 대조군(의도적 할당 256×4096자) = " + (b1 - b0) + " B · " + P(b1 - b0 > 0) + " 측정기 동작 확인"); GC.KeepAlive(junk); RunGuide.StopGuide("probe ⑨ 종료"); } // ───────────────────────────────────────── ⑩ C8 static void Sec10_C8() { L(""); L("── ⑩ C8 롤백 (RuntimeDisabled → 원본 자동전투 100%)"); var c = WLRunGuideSettings.Instance; RunGuide.ResetDiagnostics(); Vector3 anchor1; RunGuide.TryGetAnchor(c, 1, out anchor1); RunGuide.BeginZone(1, "probe c8"); s_pcGo.transform.position = anchor1 + new Vector3(0f, 0f, 30f); s_mobFarGo.transform.position = anchor1 + new Vector3(0f, 0f, 60f); s_pc.Set_Target(s_mobFar); WLRunGuideSettings.RuntimeDisabled = true; int before = RunGuide.FarTargetIgnored; var st = RunGuide.State; for (int i = 0; i < 10; i++) RunGuide.ForceTick(); L(" " + P(!RunGuide.Active) + " RuntimeDisabled → Active=false"); L(" " + P(RunGuide.FarTargetIgnored == before && s_pc.isTarget) + " 틱 ×10 — 타깃 무수정(원본 그대로)"); L(" " + P(!RunGuide.Steering && s_pc.JoystickStatus == 0) + " 조종 0(JoystickStatus=" + s_pc.JoystickStatus + ")"); L(" " + P(RunGuide.State == st) + " 상태 불변"); WLRunGuideSettings.RuntimeDisabled = false; RunGuide.ForceTick(); L(" " + P(RunGuide.FarTargetIgnored == before + 1) + " 스위치 복귀 → 다시 동작"); RunGuide.StopGuide("probe ⑩ 종료"); s_pc.Del_Target(true); } }