// WL813w7_Probe.cs — #813w7(다음 런 존 재무장 · 결과창 중 PC 정지 D-45) 프로브 // 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수) // // 상주 에디터: // unity command run_script --project-path --file AgentScripts/WL813w7_Probe.cs --entry WL813w7_Probe.RunAll --timeout 300 // 결과: Console + AgentScripts/WL813w7_PROBE.txt (RESULT PASS / RESULT FAIL n) // // 무엇을 증명하나 (발주서 WL-813w7 §1 · 근거 = qa/W2-10_재채점_v8.md D-45) // ① 에셋·SOT — 813w7 신규 12필드 값 실측 · 이중 스폰 방지 부등식 · 코드 상수 0 // ② 원인 산식 — 존0 앵커 ↔ 아레나 중심 24.1 m ≤ arenaRadius+zoneSpawnHoldMargin 30 m // = 813w3 리젠 차단의 사정권 안이다(D-45 의 직접 원인 · 값으로 재현) // ③ 수리 ① — 가짜 `RunEnded` → PC 조종 잡힘(JoystickStatus 1) · 타깃 해제 · // 가짜 `RunStarted("restart")` → 조종 원복(0) · 상한 만료 자동 해제 // ④ 수리 ②-a — 리젠 차단 판정(ShouldHoldZoneSpawn) 진리표: 게이트 전 0 · 런 존 0 · 그 밖 1 // ⑤ 수리 ②-b — 빈 존 감시 타이밍(8 s) · Restart 즉시 재무장 · 1웨이브 마리 수(WaveSize) // ⑥ 수리 ③ — 런 밖 처치는 다음 런 존 진행에 안 센다(StartRun 스냅샷 차감) // ⑦ GC 0 — 정지 틱 ×100 · 빈 존 감시 ×100 · 차단 판정 ×100 · 런 존 판정 ×100 // ⑧ C8 롤백 — 신규 스위치 0 → 813w6 동작 그대로 · RuntimeDisabled → 무동작 // // 에디트 모드 한계(보고서 「미확인」) // · `ActorInfo.Ins` 도 `table_monsterappear.Ins` 도 없다(플레이 모드 전용) → 실제 `On_Regen()` 재무장은 // 못 돌린다. 「언제 재무장을 트리거하는가」와 「1웨이브 마리 수 산정」까지를 증명한다. // · `Time.unscaledTime` 이 스크립트 실행 중 멈춰 있다 → 시간 판정은 전부 인자로 먹인다. // · PC 에 살아 있는 타깃(m_Target)을 붙일 수 없다(Set_Outline 이 렌더러를 만진다) → 타깃 해제는 // 「isTarget 플래그가 내려가는가」로 증명한다. 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 WL813w7_Probe { const string OutCommit = "AgentScripts/WL813w7_PROBE.txt"; static StringBuilder sb; static int s_fail; static GameObject s_pcGo, s_fbdGo; static PCActor s_pc, s_prevMyPc; static FieldBossData s_fbd; public static object RunAll() { sb = new StringBuilder(); s_fail = 0; sb.AppendLine("# WL813w7 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_Cause(); Sec3_RunEndHold(); Sec4_HoldDecision(); Sec5_ZoneRearm(); Sec6_OutsideRunKills(); Sec7_GC(); Sec8_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 WLBossArenaSettings TmpB() { var t = UnityEngine.Object.Instantiate(WLBossArenaSettings.Instance); t.hideFlags = HideFlags.HideAndDontSave; return t; } static WLRunGuideSettings TmpG() { var t = UnityEngine.Object.Instantiate(WLRunGuideSettings.Instance); t.hideFlags = HideFlags.HideAndDontSave; return t; } static void UseTmpB(WLBossArenaSettings t) { WLBossArenaSettings.SetInstanceForProbe(t); } static void UseRealB() { WLBossArenaSettings.SetInstanceForProbe(null); } static void UseTmpG(WLRunGuideSettings t) { WLRunGuideSettings.SetInstanceForProbe(t); } static void UseRealG() { WLRunGuideSettings.SetInstanceForProbe(null); } static float DistXZ(Vector3 a, Vector3 b) { float dx = a.x - b.x, dz = a.z - b.z; return Mathf.Sqrt(dx * dx + dz * dz); } static void Setup() { s_prevMyPc = MyValue.MyPC; s_pcGo = new GameObject("__WL813w7_PC"); s_pc = s_pcGo.AddComponent(); s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None; MyValue.MyPC = s_pc; WLBossArenaSettings.RuntimeDisabled = false; WLRunGuideSettings.RuntimeDisabled = false; BossArena.ResetAll(); BossArena.EnsureSubscribed(); RunDirector.ResetAll(); RunGuide.EnsureSubscribed(); RunGuide.ResetDiagnostics(); // 게이트 무장 — 원본 훅과 같은 진입점(HoldSpawn)으로 Arm 한다(s_zones 에 존 id 가 들어간다). var cfg = WLBossArenaSettings.Instance; s_fbdGo = new GameObject("__WL813w7_FBD"); s_fbd = s_fbdGo.AddComponent(); s_fbd.n_SpawnerId = cfg != null ? cfg.bossSpawnerId : 813010; s_fbd.n_MonsterID = cfg != null ? cfg.bossMonsterId : 10006; bool held = BossArena.HoldSpawn(s_fbd, NoopResume); L(""); L("── ⓪ 준비"); L(" " + P(WLBossArenaSettings.Instance != null) + " WLBossArenaSettings 로드 · Active=" + WLBossArenaSettings.Active); L(" " + P(WLRunGuideSettings.Instance != null) + " WLRunGuideSettings 로드 · Enabled=" + WLRunGuideSettings.Enabled); L(" " + P(WLRunSettings.Instance != null) + " WLRunSettings 로드 · Enabled=" + WLRunSettings.Enabled); L(" " + P(RunGuide.Subscribed) + " RunGuide.Subscribed(RunEvents)"); L(" " + P(held && BossArena.GateArmed && !BossArena.GateOpen) + " HoldSpawn(FieldBossData) → 게이트 무장(armed=" + BossArena.GateArmed + " open=" + BossArena.GateOpen + ")"); } static void NoopResume() { } static void Teardown() { try { UseRealB(); UseRealG(); BossArena.SetProbeTime(-1f); BossArena.SetRunActiveForProbe(0); BossArena.SetBossForProbe(null); BossArena.ResetAll(); RunGuide.EndRunEndHold("teardown"); RunGuide.ResetDiagnostics(); RunDirector.ResetAll(); WLBossArenaSettings.RuntimeDisabled = false; WLRunGuideSettings.RuntimeDisabled = false; MyValue.MyPC = s_prevMyPc; if (s_pcGo != null) UnityEngine.Object.DestroyImmediate(s_pcGo); if (s_fbdGo != null) UnityEngine.Object.DestroyImmediate(s_fbdGo); } catch { } } // ───────────────────────────────────────────────────────────────────── // ① 에셋 · SOT (C45 — 값은 전부 .asset) // ───────────────────────────────────────────────────────────────────── static void Sec1_Asset() { L(""); L("── ① 에셋 (813w7 신규 12필드 · 값 전부 SO = C45)"); var g = WLRunGuideSettings.Instance; var b = WLBossArenaSettings.Instance; if (g == null || b == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; } L(" RunGuide: holdPcOnRunEnd=" + g.holdPcOnRunEnd + " runEndHoldMaxSeconds=" + g.runEndHoldMaxSeconds + " runEndHoldClearsTarget=" + g.runEndHoldClearsTarget + " runEndHoldStopsAgent=" + g.runEndHoldStopsAgent); L(" " + P(g.holdPcOnRunEnd) + " holdPcOnRunEnd=1 (발주서 §1-2 ① 기본값)"); L(" " + P(g.runEndHoldMaxSeconds > 0f) + " runEndHoldMaxSeconds>0 (결과창을 닫고 방치해도 스스로 풀린다)"); L(" " + P(g.runEndHoldClearsTarget) + " runEndHoldClearsTarget=1 (자동 스킬 루프 차단 · PCActor.cs:93 isTarget 조건)"); L(" " + P(g.runEndHoldStopsAgent) + " runEndHoldStopsAgent=1 (잔여 경로 제거)"); L(" BossArena: holdZoneSpawnOnlyAfterGate=" + b.holdZoneSpawnOnlyAfterGate + " holdZoneSpawnSkipsRunZones=" + b.holdZoneSpawnSkipsRunZones + " rearmZoneSpawnsEnabled=" + b.rearmZoneSpawnsEnabled + " rearmZoneEmptySeconds=" + b.rearmZoneEmptySeconds + " rearmZoneCheckSeconds=" + b.rearmZoneCheckSeconds + " rearmZoneRegenMax=" + b.rearmZoneRegenMax + " rearmZoneRunZonesOnly=" + b.rearmZoneRunZonesOnly + " rearmZoneSpawnsOnRestart=" + b.rearmZoneSpawnsOnRestart); L(" " + P(b.holdZoneSpawnOnlyAfterGate) + " holdZoneSpawnOnlyAfterGate=1 (차단을 보스전으로 좁힌다)"); L(" " + P(b.holdZoneSpawnSkipsRunZones) + " holdZoneSpawnSkipsRunZones=1 (런 사냥 존은 대상 제외)"); L(" " + P(b.rearmZoneSpawnsEnabled) + " rearmZoneSpawnsEnabled=1"); L(" " + P(b.rearmZoneSpawnsOnRestart) + " rearmZoneSpawnsOnRestart=1 ([다음 런] 즉시 1회)"); L(" " + P(b.rearmZoneRegenMax > 0) + " rearmZoneRegenMax>0 (테이블 실패 시 폴백·상한)"); // 🔴 이중 스폰 방지 부등식 — 원본 리듬 = 시체 3.0 s(Actor.cs:631) + f_SpawnerDelay 2.0 s(813e2 표 A) = 5.0 s const float kOriginalRegenRhythm = 5.0f; // 게임 수치가 아니라 813e2 실측 리듬(검증 기준값) L(" " + P(b.rearmZoneEmptySeconds > kOriginalRegenRhythm) + " 🔴 rearmZoneEmptySeconds(" + b.rearmZoneEmptySeconds + "s) > 원본 리듬 " + kOriginalRegenRhythm + "s — 멀쩡한 스포너와 겹치지 않는다(이중 스폰 방지)"); L(" " + P(b.rearmZoneCheckSeconds > 0f && b.rearmZoneCheckSeconds < b.rearmZoneEmptySeconds) + " 감시 주기(" + b.rearmZoneCheckSeconds + "s) < 임계(" + b.rearmZoneEmptySeconds + "s)"); L(" " + P(b.holdZoneSpawnNearArena) + " 813w3 holdZoneSpawnNearArena=1 유지(차단 경로 자체는 그대로 · 조건만 좁혔다)"); } // ───────────────────────────────────────────────────────────────────── // ② 원인 산식 — 존0 이 리젠 차단 사정권 안이다 // ───────────────────────────────────────────────────────────────────── static void Sec2_Cause() { L(""); L("── ② 🔴 D-45 원인 산식 (Q10: R1n 0/18 · R2n 1/18 PC (112.93,0.06,−16.60) 고정 무피해 · R3n 14/18 정지)"); var b = WLBossArenaSettings.Instance; var g = WLRunGuideSettings.Instance; var rs = WLRunSettings.Instance; if (b == null || g == null || rs == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; } Vector3 center = b.arenaCenter; float lim = b.arenaRadius + b.zoneSpawnHoldMargin; L(" 아레나 중심 " + center.ToString("F2") + " · 차단 반경 = arenaRadius " + b.arenaRadius + " + zoneSpawnHoldMargin " + b.zoneSpawnHoldMargin + " = " + lim.ToString("F0") + " m"); var fb = g.zoneAnchorFallbacks; for (int i = 0; i < rs.zoneSpawnerIds.Length; i++) { if (fb == null || i >= fb.Length) continue; float d = DistXZ(fb[i], center); L(" 존" + i + "(id " + rs.zoneSpawnerIds[i] + ") 앵커 " + fb[i].ToString("F2") + " → 중심 " + d.ToString("F1") + " m · 차단 사정권 " + (d <= lim ? "안 🔴" : "밖")); } float d0 = fb != null && fb.Length > 0 ? DistXZ(fb[0], center) : -1f; L(" " + P(d0 >= 0f && d0 <= lim) + " 🔴 존0 이 차단 사정권 안(" + d0.ToString("F1") + " m ≤ " + lim.ToString("F0") + " m) = D-45 의 직접 원인"); // PC 고정 좌표(Q10 R2n 실측)가 존0 앵커에 도착한 자리인가 = 「PC 는 존에 있는데 몹이 없다」 var pcSeen = new Vector3(112.93f, 0.06f, -16.60f); float dPc = fb != null && fb.Length > 0 ? DistXZ(pcSeen, fb[0]) : -1f; L(" " + P(dPc >= 0f && dPc <= g.zoneArriveRadius) + " Q10 R2n PC 좌표 " + pcSeen.ToString("F2") + " ↔ 존0 앵커 " + dPc.ToString("F1") + " m ≤ zoneArriveRadius " + g.zoneArriveRadius + " = **도착(Hold)했는데 때릴 몹이 없었다**"); L(" 차단 방식 = MobControlMgr.StopAllCoroutines() → PhaseAllKill 의 `killmobcount = 0`(MobControlMgr.cs:218)이"); L(" 함께 끊겨 카운터가 n_MaxMonsterCount 에 굳는다 → `==` 판정(:214)이 두 번 다시 참이 안 된다 = 영구 소진."); L(" (MyCoroutine.Set_Coroutine = StartCoroutine(MyCoroutine.cs) 이므로 StopAllCoroutines 에 걸린다 · 실측)"); } // ───────────────────────────────────────────────────────────────────── // ③ 수리 ① — 결과창 중 PC 정지 // ───────────────────────────────────────────────────────────────────── static void Sec3_RunEndHold() { L(""); L("── ③ 수리 ① 결과창 중 PC 정지 (D-45 원인 = 결과창 100 s 사이 z0 처치 26 → 47)"); var g = WLRunGuideSettings.Instance; RunGuide.ResetDiagnostics(); s_pc.Set_JoystickStatus(3); s_pc.isTarget = false; int holds0 = RunGuide.RunEndHolds; RunGuide.ProbeRunEnded(RunOutcome.Win); RunGuide.SetRunEndHoldUntilForProbe(-1f); // 에디트 모드는 Time 이 안 흘러 상한 판정을 따로 검증한다(아래 ⓑ) L(" " + P(RunGuide.RunEndHeld && RunGuide.RunEndHolds == holds0 + 1) + " 가짜 RunEnded(Win) → 정지 시작(RunEndHeld=" + RunGuide.RunEndHeld + " RunEndHolds=" + RunGuide.RunEndHolds + ")"); L(" " + P(RunGuide.State == RunGuideState.Off && RunGuide.DestKind == RunGuideDest.None) + " 가이드는 꺼진다(State=" + RunGuide.State + " Dest=" + RunGuide.DestKind + ") — 813w6 계약 유지"); // 정지 1틱 — 조종을 잡고 타깃을 뗀다 s_pc.isTarget = true; int clears0 = RunGuide.RunEndTargetClears; RunGuide.RunEndHoldTick(g, s_pc, 100f); L(" " + P(s_pc.JoystickStatus == 1 && RunGuide.Steering) + " 🔴 정지 틱 → JoystickStatus=" + s_pc.JoystickStatus + "(1 = 원본 자동 교전 접근 루프 차단 · PCActor.cs:139) Steering=" + RunGuide.Steering); L(" " + P(!s_pc.isTarget && RunGuide.RunEndTargetClears == clears0 + 1) + " 🔴 정지 틱 → 타깃 해제(isTarget=" + s_pc.isTarget + " RunEndTargetClears=" + RunGuide.RunEndTargetClears + ") = 자동 스킬 루프(PCActor.cs:93)도 돌지 않는다"); int ticks0 = RunGuide.RunEndHoldTicks; RunGuide.RunEndHoldTick(g, s_pc, 101f); RunGuide.RunEndHoldTick(g, s_pc, 102f); L(" " + P(RunGuide.RunEndHoldTicks == ticks0 + 2 && RunGuide.RunEndHeld) + " 정지가 이어진다(틱 " + RunGuide.RunEndHoldTicks + " · 상한 전)"); // 복귀 ⓐ — 다음 런 시작(RunDirector.Restart → StartRun("restart")) int rel0 = RunGuide.RunEndHoldReleases; RunGuide.ProbeRunStarted(2, "restart"); L(" " + P(!RunGuide.RunEndHeld && RunGuide.RunEndHoldReleases == rel0 + 1) + " 🔴 가짜 RunStarted(\"restart\") → 정지 해제(RunEndHeld=" + RunGuide.RunEndHeld + " 해제 " + RunGuide.RunEndHoldReleases + " 사유 '" + RunGuide.LastRunEndHoldReason + "')"); L(" " + P(s_pc.JoystickStatus == 0 && !RunGuide.Steering) + " 🔴 조종 원복 JoystickStatus=" + s_pc.JoystickStatus + "(0 = 원본 그대로) Steering=" + RunGuide.Steering); L(" " + P(RunGuide.RestartGuides >= 1) + " 813w4 재유도는 그대로 산다(RestartGuides=" + RunGuide.RestartGuides + ") — 정지 해제가 유도보다 앞"); // 복귀 ⓑ — 상한 만료(결과창을 [닫기]로 닫고 런을 다시 시작하지 않는 경우) RunGuide.StopGuide("프로브"); RunGuide.BeginRunEndHold("상한 시험"); RunGuide.SetRunEndHoldUntilForProbe(200f); RunGuide.RunEndHoldTick(g, s_pc, 199f); bool stillHeld = RunGuide.RunEndHeld; RunGuide.RunEndHoldTick(g, s_pc, 201f); L(" " + P(stillHeld && !RunGuide.RunEndHeld) + " 🔴 상한 만료 자동 해제(199 s 유지 → 201 s 해제 · 사유 '" + RunGuide.LastRunEndHoldReason + "')"); L(" " + P(s_pc.JoystickStatus == 0) + " 만료 해제에서도 조종 원복(JoystickStatus=" + s_pc.JoystickStatus + ")"); // 이미 스킬 이동 잠금(2)이면 건드리지 않는다 RunGuide.ResetDiagnostics(); s_pc.Set_JoystickStatus(2); RunGuide.BeginRunEndHold("잠금 시험"); RunGuide.SetRunEndHoldUntilForProbe(-1f); RunGuide.RunEndHoldTick(g, s_pc, 300f); L(" " + P(s_pc.JoystickStatus == 2 && !RunGuide.Steering) + " JoystickStatus=2(스킬 이동 잠금)는 건드리지 않는다(=" + s_pc.JoystickStatus + " Steering=" + RunGuide.Steering + ")"); RunGuide.EndRunEndHold("프로브 정리"); s_pc.Set_JoystickStatus(3); RunGuide.ResetDiagnostics(); } // ───────────────────────────────────────────────────────────────────── // ④ 수리 ②-a — 리젠 차단 판정 진리표 // ───────────────────────────────────────────────────────────────────── static void Sec4_HoldDecision() { L(""); L("── ④ 수리 ②-a 리젠 차단 판정(ShouldHoldZoneSpawn · 순수 함수)"); var rs = WLRunSettings.Instance; int runZone = rs != null && rs.zoneSpawnerIds != null && rs.zoneSpawnerIds.Length > 0 ? rs.zoneSpawnerIds[0] : 813001; int other = 813005; L(" " + P(BossArena.IsRunZoneSpawner(runZone)) + " IsRunZoneSpawner(" + runZone + ")=true (런 사냥 존)"); L(" " + P(!BossArena.IsRunZoneSpawner(other)) + " IsRunZoneSpawner(" + other + ")=false (런 밖 존)"); var t = TmpB(); try { UseTmpB(t); t.holdZoneSpawnNearArena = true; t.holdZoneSpawnOnlyAfterGate = true; t.holdZoneSpawnSkipsRunZones = true; L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, other, false)) + " 🔴 게이트 **닫힘** + 런 밖 존 → 차단 안 함(= 813w6 이 만든 「다음 런 내내 차단」이 사라진다)"); L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, runZone, true)) + " 🔴 게이트 열림 + **런 사냥 존** → 차단 안 함(D-45 재발 차단)"); L(" " + P(BossArena.ShouldHoldZoneSpawn(t, other, true)) + " 게이트 열림 + 런 밖 존 → 차단(813w3 잡몹 유입 차단 의도 유지)"); // C8 — 두 스위치를 끄면 813w3 그대로(보스 생존 = 상시 차단) t.holdZoneSpawnOnlyAfterGate = false; t.holdZoneSpawnSkipsRunZones = false; L(" " + P(BossArena.ShouldHoldZoneSpawn(t, runZone, false)) + " C8: 두 스위치 0 → 게이트 전 + 런 존도 차단 = 813w3 동작 그대로"); t.holdZoneSpawnNearArena = false; L(" " + P(!BossArena.ShouldHoldZoneSpawn(t, other, true)) + " C8: holdZoneSpawnNearArena=0 → 언제나 차단 0"); } finally { UseRealB(); UnityEngine.Object.DestroyImmediate(t); } } // ───────────────────────────────────────────────────────────────────── // ⑤ 수리 ②-b — 빈 존 감시 · 재무장 // ───────────────────────────────────────────────────────────────────── static void Sec5_ZoneRearm() { L(""); L("── ⑤ 수리 ②-b 존 재무장(원본 리셋 API 없음 → MobActor.On_Regen = MobControlMgr.cs:221 과 같은 호출)"); var b = WLBossArenaSettings.Instance; var rs = WLRunSettings.Instance; int z0 = rs != null && rs.zoneSpawnerIds != null && rs.zoneSpawnerIds.Length > 0 ? rs.zoneSpawnerIds[0] : 813001; // 감시 목록 — 런 존만 BossArena.RebuildRearmZonesForProbe(); L(" 감시 대상 " + BossArena.RearmZoneCount + "개: " + (BossArena.RearmZoneCount > 0 ? BossArena.RearmZoneIdAt(0).ToString() : "-") + (BossArena.RearmZoneCount > 1 ? " / " + BossArena.RearmZoneIdAt(1) : "")); L(" " + P(rs != null && BossArena.RearmZoneCount == rs.zoneSpawnerIds.Length) + " 대상 = WLRunSettings.zoneSpawnerIds 전부(rearmZoneRunZonesOnly=1)"); L(" " + P(BossArena.RearmZoneCount > 0 && BossArena.RearmZoneIdAt(0) == z0) + " 첫 대상 = 존0(" + z0 + ")"); // 1웨이브 마리 수 — 에디트 모드엔 table_monsterappear 가 없어 SO 폴백을 쓴다 int wave = BossArena.WaveSize(b, z0); L(" " + P(wave > 0 && wave <= b.rearmZoneRegenMax) + " WaveSize(" + z0 + ")=" + wave + " (테이블 n_MaxMonsterCount → 없으면 rearmZoneRegenMax " + b.rearmZoneRegenMax + " · 상한도 그 값)"); // 타이밍 — 살아 있으면 타이머가 안 돈다 / 비면 임계까지 기다렸다 트리거 BossArena.SetRunActiveForProbe(1); BossArena.NoteSpawn(z0, false); // alive 1 BossArena.ZoneRearmTick(b, 100f); L(" " + P(BossArena.AliveOf(z0) > 0 && BossArena.RearmZoneEmptySinceAt(0) < 0f) + " alive=" + BossArena.AliveOf(z0) + " → 빈 시각 미기록(" + BossArena.RearmZoneEmptySinceAt(0) + ")"); BossArena.NoteKill(z0, false); // alive 0 BossArena.ZoneRearmTick(b, 100f); L(" " + P(BossArena.AliveOf(z0) == 0 && Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), 100f)) + " alive=0 → 빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + " 기록"); BossArena.ZoneRearmTick(b, 100f + b.rearmZoneEmptySeconds - 1f); L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), 100f)) + " 임계(" + b.rearmZoneEmptySeconds + "s) 전 → 트리거 없음(빈 시각 그대로 " + BossArena.RearmZoneEmptySinceAt(0) + ")"); float fire = 100f + b.rearmZoneEmptySeconds + 0.5f; BossArena.ZoneRearmTick(b, fire); L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), fire)) + " 🔴 임계 경과 → 재무장 트리거(빈 시각이 " + BossArena.RearmZoneEmptySinceAt(0) + " 로 갱신 = RearmZone 호출)"); L(" (에디트 모드는 ActorInfo.Ins 가 없어 실제 On_Regen 은 0마리 — 「언제 트리거되는가」까지 증명 · §미확인)"); // 런 밖에서는 감시하지 않는다(결과창 동안 몹을 우리가 늘리지 않는다) BossArena.SetRunActiveForProbe(-1); float before = BossArena.RearmZoneEmptySinceAt(0); BossArena.ZoneRearmTick(b, fire + 100f); L(" " + P(Mathf.Approximately(BossArena.RearmZoneEmptySinceAt(0), before)) + " 런 밖(결과창)에서는 감시 정지(빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + " 불변)"); BossArena.SetRunActiveForProbe(1); // [다음 런] 즉시 재무장 진입점 int n = BossArena.RearmZoneSpawns("프로브 다음 런"); L(" " + P(BossArena.RearmZoneCount > 0) + " RearmZoneSpawns() 진입점 동작(재등장 " + n + "마리 · 에디트 모드라 0 · 목록 " + BossArena.RearmZoneCount + "개 재구축)"); BossArena.SetRunActiveForProbe(0); } // ───────────────────────────────────────────────────────────────────── // ⑥ 수리 ③ — 런 밖 처치는 다음 런 존 진행에 안 센다 // ───────────────────────────────────────────────────────────────────── static void Sec6_OutsideRunKills() { L(""); L("── ⑥ 수리 ③ 런 밖 처치가 존 카운터에 안 실린다(Q10: 결과창 동안 z0 26 → 47)"); var rs = WLRunSettings.Instance; int z0 = rs.zoneSpawnerIds[0]; RunDirector.ResetAll(); RunDirector.EnsureSubscribed(); BossArena.SetRunActiveForProbe(-1); // 런 밖(결과창) int arena0 = BossArena.KillsOf(z0); int gateEvals0 = BossArena.GateSuspendedEvals; for (int i = 0; i < 21; i++) // Q10 실측과 같은 21마리 { RunDirector.NoteKill(z0, false); // 런 밖 → IsRunning=false → 무시 BossArena.NoteKill(z0, false); // 누적값은 813p 계약대로 계속 센다 } L(" " + P(BossArena.KillsOf(z0) == arena0 + 21) + " BossArena 누적은 계속 센다(" + arena0 + " → " + BossArena.KillsOf(z0) + ") = 813p 계약(리셋 금지) 유지"); L(" " + P(BossArena.GateSuspendedEvals > gateEvals0) + " 런 밖 게이트 평가는 건너뛴다(GateSuspendedEvals " + gateEvals0 + " → " + BossArena.GateSuspendedEvals + ")"); BossArena.SetRunActiveForProbe(1); RunDirector.StartRun("probe restart"); // ← 여기서 s_zoneBase 를 다시 잡는다 L(" " + P(RunDirector.ZoneProgress(0) == 0) + " 🔴 다음 런 시작 직후 존0 진행 = " + RunDirector.ZoneProgress(0) + "/" + RunDirector.ZoneRequired(0) + " — 런 밖 21처치가 한 건도 안 실렸다(StartRun 스냅샷 차감)"); RunDirector.NoteKill(z0, false); L(" " + P(RunDirector.ZoneProgress(0) == 1) + " 런 안 처치 1건은 정상 반영(" + RunDirector.ZoneProgress(0) + "/" + RunDirector.ZoneRequired(0) + ")"); RunDirector.ResetAll(); BossArena.SetRunActiveForProbe(0); RunGuide.ResetDiagnostics(); } // ───────────────────────────────────────────────────────────────────── // ⑦ GC 0 // ───────────────────────────────────────────────────────────────────── static void Sec7_GC() { L(""); L("── ⑦ GC 0 (틱 경로 · 각 100회)"); var g = WLRunGuideSettings.Instance; var b = WLBossArenaSettings.Instance; var rs = WLRunSettings.Instance; int z0 = rs.zoneSpawnerIds[0]; int other = 813005; // ⓐ 결과창 정지 틱 RunGuide.ResetDiagnostics(); s_pc.Set_JoystickStatus(3); RunGuide.BeginRunEndHold("GC"); RunGuide.SetRunEndHoldUntilForProbe(-1f); RunGuide.RunEndHoldTick(g, s_pc, 10f); // 워밍업(조종 획득 1회) long a0 = Alloc(); for (int i = 0; i < 100; i++) RunGuide.RunEndHoldTick(g, s_pc, 10f + i); long a1 = Alloc(); L(" " + P(a1 - a0 <= 0) + " 정지 틱 ×100 = " + (a1 - a0) + " B"); RunGuide.EndRunEndHold("GC 끝"); s_pc.Set_JoystickStatus(3); // ⓑ 빈 존 감시 BossArena.SetRunActiveForProbe(1); BossArena.ZoneRearmTick(b, 500f); long b0 = Alloc(); for (int i = 0; i < 100; i++) BossArena.ZoneRearmTick(b, 500f + i * 0.01f); long b1 = Alloc(); L(" " + P(b1 - b0 <= 0) + " 빈 존 감시 ×100 = " + (b1 - b0) + " B"); BossArena.SetRunActiveForProbe(0); // ⓒ 차단 판정 · 런 존 판정 long c0 = Alloc(); bool acc = false; for (int i = 0; i < 100; i++) acc ^= BossArena.ShouldHoldZoneSpawn(b, i % 2 == 0 ? z0 : other, i % 2 == 0); long c1 = Alloc(); L(" " + P(c1 - c0 <= 0) + " 차단 판정 ×100 = " + (c1 - c0) + " B (acc=" + acc + ")"); long d0 = Alloc(); for (int i = 0; i < 100; i++) acc ^= BossArena.IsRunZoneSpawner(i % 2 == 0 ? z0 : other); long d1 = Alloc(); L(" " + P(d1 - d0 <= 0) + " 런 존 판정 ×100 = " + (d1 - d0) + " B (acc=" + acc + ")"); RunGuide.ResetDiagnostics(); } // ───────────────────────────────────────────────────────────────────── // ⑧ C8 롤백 // ───────────────────────────────────────────────────────────────────── static void Sec8_C8() { L(""); L("── ⑧ C8 롤백 (신규 스위치 0 → 813w6 동작 그대로)"); // ⓐ holdPcOnRunEnd = 0 → 런 종료 정지 경로가 통째로 죽는다 var tg = TmpG(); try { UseTmpG(tg); tg.holdPcOnRunEnd = false; RunGuide.ResetDiagnostics(); s_pc.Set_JoystickStatus(3); RunGuide.ProbeRunEnded(RunOutcome.Timeout); L(" " + P(!RunGuide.RunEndHeld && RunGuide.RunEndHolds == 0) + " holdPcOnRunEnd=0 → 정지 0(RunEndHeld=" + RunGuide.RunEndHeld + " RunEndHolds=" + RunGuide.RunEndHolds + ")"); L(" " + P(s_pc.JoystickStatus == 0) + " 조종도 잡지 않는다(JoystickStatus=" + s_pc.JoystickStatus + ") = 813w6 그대로"); // 정지 중에 스위치를 끄면 그 자리에서 풀린다 tg.holdPcOnRunEnd = true; RunGuide.BeginRunEndHold("C8"); tg.holdPcOnRunEnd = false; RunGuide.RunEndHoldTick(tg, s_pc, 900f); L(" " + P(!RunGuide.RunEndHeld) + " 정지 중 스위치 off → 즉시 해제(사유 '" + RunGuide.LastRunEndHoldReason + "')"); } finally { UseRealG(); UnityEngine.Object.DestroyImmediate(tg); RunGuide.ResetDiagnostics(); } // ⓑ rearmZoneSpawnsEnabled = 0 → 감시·재무장 경로가 통째로 죽는다 var tb = TmpB(); try { UseTmpB(tb); BossArena.SetRunActiveForProbe(1); BossArena.RebuildRearmZonesForProbe(); tb.rearmZoneSpawnsEnabled = false; BossArena.ZoneRearmTick(tb, 1000f); L(" " + P(BossArena.RearmZoneEmptySinceAt(0) < 0f) + " rearmZoneSpawnsEnabled=0 → 감시 무동작(빈 시각 " + BossArena.RearmZoneEmptySinceAt(0) + ")"); L(" " + P(BossArena.RearmZoneSpawns("C8") == 0) + " RearmZoneSpawns() 도 0"); tb.rearmZoneSpawnsEnabled = true; tb.rearmZoneSpawnsOnRestart = false; L(" " + P(BossArena.RearmZoneSpawns("C8") == 0) + " rearmZoneSpawnsOnRestart=0 → [다음 런] 즉시 재무장 0"); } finally { UseRealB(); BossArena.SetRunActiveForProbe(0); UnityEngine.Object.DestroyImmediate(tb); } // ⓒ RuntimeDisabled — 축 전체 off WLRunGuideSettings.RuntimeDisabled = true; RunGuide.ResetDiagnostics(); RunGuide.BeginRunEndHold("RuntimeDisabled"); RunGuide.ForceTick(); L(" " + P(!WLRunGuideSettings.Enabled && !RunGuide.Active) + " RunGuide RuntimeDisabled → Active=false"); L(" " + P(!RunGuide.RunEndHeld) + " Tick 이 정지를 풀고 원본으로 돌려준다(RunEndHeld=" + RunGuide.RunEndHeld + ")"); WLRunGuideSettings.RuntimeDisabled = false; WLBossArenaSettings.RuntimeDisabled = true; var b2 = WLBossArenaSettings.Instance; BossArena.SetRunActiveForProbe(1); BossArena.RebuildRearmZonesForProbe(); BossArena.ZoneRearmTick(b2, 2000f); L(" " + P(BossArena.RearmZoneEmptySinceAt(0) < 0f) + " BossArena RuntimeDisabled → 감시 무동작"); WLBossArenaSettings.RuntimeDisabled = false; BossArena.SetRunActiveForProbe(0); RunGuide.ResetDiagnostics(); } }