Project_WL/AgentScripts/WL813w6_Probe.cs

574 lines
30 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.

// WL813w6_Probe.cs — #813w6(재스폰 보스 고정 D-43 · 결과 HP 스냅샷 순서 D-44) 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject·임시 SO 는 즉시 파괴/회수)
//
// 상주 에디터:
// unity command run_script --project-path <wt> --file AgentScripts/WL813w6_Probe.cs --entry WL813w6_Probe.RunAll --timeout 300
// 결과: Console + AgentScripts/WL813w6_PROBE.txt (RESULT PASS / RESULT FAIL n)
//
// 무엇을 증명하나 (발주서 WL-813w6 §1 · 근거 = qa/W2-9_재채점_v7.md D-43 · D-44)
// ① 에셋·SOT — 813w6 신규 6필드 값 실측 · 정합성 · 코드 상수 0
// ② D-43 — 런 밖 가짜 `Spawned(isBoss)` → **즉시 고정 + 홀드 무장** · 게이트 개방 뒤 등장은 무개입
// ③ D-43 — 홀드가 「게이트 개방까지」 유지되는가(시간제 만료 뒤에도) · 게이트가 열리면 해제되는가
// ④ D-43 — 게이트 개방 전 런 중 이탈 → **회복 없이** 되돌림(PreGateLeashResets)
// ⑤ D-44 — 진짜 `RunDirector.EndRun` 발행 시점의 페이로드가 **Park 이전 HP**(24.4 %) 인가 ·
// 발행 도중 `ParkCount` 불변 · 발행 뒤 `PumpDeferredPark` 로 고정
// ⑥ GC 0 — 스냅샷 ×100 · 홀드 판정 ×100 · 미뤄둔 고정 펌프 ×100 · 게이트 전 틱 ×100
// ⑦ C8 롤백 — 신규 6스위치 0 → 813w5 동작 그대로 · RuntimeDisabled → 무동작
//
// 에디트 모드 한계(보고서 「미확인」)
// · 살아 있는 MobActor(m_Stat·Animator·NavMeshAgent)가 없어 ParkBoss 의 실제 워프·만피는 못 돌린다
// (813d/813w3/813w5 와 같은 한계) → 고정 「판단·홀드 무장」까지를 증명한다.
// · 같은 이유로 실제 보스 HP 읽기(Get_HP/Get_MaxHP)는 주입값(SetBossHpForProbe)으로 대체한다.
// · Time.unscaledTime 이 스크립트 실행 중 멈춰 있다 → 시간 의존 판정은 BossArena.SetProbeTime 으로 먹인다.
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 WL813w6_Probe
{
const string OutCommit = "AgentScripts/WL813w6_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;
// ⑤ 가 쓰는 「발행 시점」 기록
static float s_seenRatio;
static double s_seenHp, s_seenMax;
static int s_seenParkCount, s_seenDeferred;
static bool s_seenPending;
static int s_seenCount;
static readonly CombatHandler<RunResult> h_seen = OnRunEndedSeen;
public static object RunAll()
{
sb = new StringBuilder();
s_fail = 0;
sb.AppendLine("# WL813w6 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_RespawnPark();
Sec3_HoldUntilGate();
Sec4_PreGateLeash();
Sec5_HpSnapshotOrder();
Sec6_GC();
Sec7_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); }
/// <summary>실에셋을 복사한 임시 SO(실에셋은 절대 안 건드린다). 쓰고 나면 DestroyImmediate + 캐시 원복.</summary>
static WLBossArenaSettings Tmp()
{
var t = UnityEngine.Object.Instantiate(WLBossArenaSettings.Instance);
t.hideFlags = HideFlags.HideAndDontSave;
return t;
}
static void UseTmp(WLBossArenaSettings t) { WLBossArenaSettings.SetInstanceForProbe(t); }
static void UseReal() { WLBossArenaSettings.SetInstanceForProbe(null); }
static void OnRunEndedSeen(in RunResult e)
{
s_seenCount++;
s_seenRatio = e.bossHpRatio; s_seenHp = e.bossHp; s_seenMax = e.bossHpMax;
s_seenParkCount = BossArena.ParkCount;
s_seenDeferred = BossArena.DeferredParkCount;
s_seenPending = BossArena.ParkPending;
}
/// <summary>가짜 보스(살아 있는 것으로 보이는 MobActor · m_Stat 은 없다).</summary>
static MobActor MakeBoss(string name)
{
var go = new GameObject(name);
var m = go.AddComponent<MobActor>();
m.m_Role = eRole.Mob; m.m_SubRole = eSubRol.Boss;
return m;
}
static void KillBoss(MobActor m)
{
if (m == null) return;
BossArena.SetBossForProbe(null);
UnityEngine.Object.DestroyImmediate(m.gameObject);
}
static void Setup()
{
s_prevMyPc = MyValue.MyPC;
s_pcGo = new GameObject("__WL813w6_PC");
s_pc = s_pcGo.AddComponent<PCActor>();
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None;
MyValue.MyPC = s_pc;
WLBossArenaSettings.RuntimeDisabled = false;
BossArena.ResetAll();
BossArena.EnsureSubscribed();
BossArena.EnsureRunSubscribed();
RunDirector.ResetAll();
// 게이트 무장 — 원본 훅과 같은 진입점(HoldSpawn)으로 Arm 한다.
var cfg = WLBossArenaSettings.Instance;
s_fbdGo = new GameObject("__WL813w6_FBD");
s_fbd = s_fbdGo.AddComponent<FieldBossData>();
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(BossArena.Subscribed) + " BossArena.Subscribed(CombatEvents)");
L(" " + P(BossArena.RunSubscribed) + " BossArena.RunSubscribed(RunEvents.RunEnded · 813w5)");
L(" " + P(held && BossArena.GateArmed && !BossArena.GateOpen) +
" 🔴 HoldSpawn(FieldBossData 813010) → 게이트 무장(armed=" + BossArena.GateArmed + " open=" + BossArena.GateOpen + ")");
}
static void NoopResume() { }
static void Teardown()
{
try
{
UseReal();
BossArena.SetProbeTime(-1f);
BossArena.SetBossHpForProbe(-2f, 0d, 0d);
BossArena.ClearBossHold();
BossArena.SetRunActiveForProbe(0);
BossArena.SetBossForProbe(null);
RunEvents.RunEnded.Remove(h_seen);
BossArena.ResetAll();
RunDirector.ResetAll();
WLBossArenaSettings.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
// ─────────────────────────────────────────────────────────────────────
static void Sec1_Asset()
{
L("");
L("── ① 에셋 (Assets/WL/Combat/Settings/Resources/WL/WLBossArenaSettings.asset · 값 전부 SO = C45)");
var b = WLBossArenaSettings.Instance;
if (b == null) { L(" [FAIL] 에셋 없음"); s_fail++; return; }
L(" 신규 6필드: parkBossOnRespawn=" + b.parkBossOnRespawn +
" bossHoldUntilGateOpen=" + b.bossHoldUntilGateOpen +
" preGateLeashDistanceOnly=" + b.preGateLeashDistanceOnly +
" parkBossOnRunEndDeferred=" + b.parkBossOnRunEndDeferred +
" parkBossOnRunEndDelaySeconds=" + b.parkBossOnRunEndDelaySeconds +
" snapshotBossHpOnRunEnd=" + b.snapshotBossHpOnRunEnd);
L(" " + P(b.parkBossOnRespawn) + " parkBossOnRespawn=1 (D-43 재스폰 즉시 고정)");
L(" " + P(b.bossHoldUntilGateOpen) + " bossHoldUntilGateOpen=1 (발주서 §1-1 기본 = 게이트 개방까지)");
L(" " + P(b.preGateLeashDistanceOnly) + " preGateLeashDistanceOnly=1 (게이트 전 거리 판정 · 회복 0)");
L(" " + P(b.parkBossOnRunEndDeferred) + " parkBossOnRunEndDeferred=1 (D-44 스냅샷 → 발행 → Park)");
L(" " + P(Mathf.Approximately(b.parkBossOnRunEndDelaySeconds, 0f)) + " parkBossOnRunEndDelaySeconds=0 (다음 Tick)");
L(" " + P(b.snapshotBossHpOnRunEnd) + " snapshotBossHpOnRunEnd=1 (RunResult.bossHpRatio 채움)");
L(" 기존 값(813w5) 대조: nextRunBossHoldSeconds=" + b.nextRunBossHoldSeconds +
" gateMinSeconds=" + b.gateMinSeconds + " arenaRadius=" + b.arenaRadius + " leashMargin=" + b.leashMargin);
L(" " + P(b.parkBossOnRunEndDelaySeconds < b.nextRunBossHoldSeconds) +
" 지연(" + b.parkBossOnRunEndDelaySeconds + "s) < 홀드(" + b.nextRunBossHoldSeconds + "s) — 고정 전 공백이 홀드보다 짧다");
L(" " + P(b.parkBossOnRunEnd) + " 813w5 parkBossOnRunEnd=1 유지(런 종료 고정 경로 자체는 그대로)");
}
// ─────────────────────────────────────────────────────────────────────
// ② D-43 — 런 밖 재스폰 보스 즉시 고정
// ─────────────────────────────────────────────────────────────────────
static void Sec2_RespawnPark()
{
L("");
L("── ② D-43 재스폰 보스 고정 (Q9 R2n: ParkCount=0 · 홀드 미무장 · t=41.9 mobStatus=Chase)");
var cfg = WLBossArenaSettings.Instance;
BossArena.ClearBossHold();
BossArena.SetRunActiveForProbe(-1); // 런 밖(결과창·다음 런 대기)
L(" " + P(!BossArena.RunActive) + " RunActive=false 강제 · GateOpen=" + BossArena.GateOpen);
var boss = MakeBoss("__WL813w6_Boss_A");
try
{
BossArena.SetBossForProbe(boss);
L(" " + P(!boss.IsDead()) + " 가짜 보스 살아 있음(activeInHierarchy=true)");
L(" " + P(!BossArena.BossHeld) + " 스폰 전 홀드 없음(BossHeld=false)");
int a0 = BossArena.RespawnParkAttempts;
BossArena.NoteSpawn(cfg.bossSpawnerId, true); // = CombatEvents.Spawned(isBoss) 가 들어오는 그 경로
L(" " + P(BossArena.RespawnParkAttempts == a0 + 1) +
" 🔴 Spawned(isBoss · 게이트 닫힘) → 고정 시도 +1 (attempts=" + BossArena.RespawnParkAttempts +
" · 실제 워프 성공 " + BossArena.RespawnParkCount + "회 = 에디트 모드 한계)");
L(" " + P(BossArena.BossHeld) + " 🔴 **홀드 무장됨**(BossHeld=true) — D-43 의 본질(홀드 미무장)이 사라졌다");
L(" " + P(BossArena.HoldUntilGate) + " 🔴 홀드 모드 = 「게이트 개방까지」(HoldUntilGate=true)");
L(" " + P(BossArena.BossSpawned) + " BossSpawned=true(813p 폴백이 BossStarted 를 낼 수 있다 · D-27ⓒ 유지)");
// 게이트가 열린 뒤의 등장(= 정상 보스전)은 건드리지 않는다
BossArena.ClearBossHold();
BossArena.ForceOpenGate();
int a1 = BossArena.RespawnParkAttempts;
BossArena.NoteSpawn(cfg.bossSpawnerId, true);
L(" " + P(BossArena.GateOpen && BossArena.RespawnParkAttempts == a1) +
" 게이트 개방 뒤 Spawned(isBoss) → 고정 0(정상 보스전 · attempts 불변 " + BossArena.RespawnParkAttempts + ")");
L(" " + P(!BossArena.BossHeld) + " 그 보스는 홀드 없이 싸운다(BossHeld=false)");
// 잡몹 스폰은 이 경로에 들어오지 않는다
int a2 = BossArena.RespawnParkAttempts;
BossArena.NoteSpawn(cfg.zoneSpawnerIds[0], false);
L(" " + P(BossArena.RespawnParkAttempts == a2) + " 잡몹 Spawned(isBoss=false) → 고정 0");
}
finally { KillBoss(boss); }
}
// ─────────────────────────────────────────────────────────────────────
// ③ D-43 — 홀드가 「게이트 개방까지」 유지되는가
// ─────────────────────────────────────────────────────────────────────
static void Sec3_HoldUntilGate()
{
L("");
L("── ③ 홀드 = 「게이트 개방까지」 (813w5 는 30 s 뒤 풀려 R1n t=77 에 중심 21.89 m 까지 왔다)");
var cfg = WLBossArenaSettings.Instance;
BossArena.ResetAll();
BossArena.EnsureSubscribed();
BossArena.HoldSpawn(s_fbd, NoopResume);
BossArena.SetRunActiveForProbe(-1);
BossArena.SetProbeTime(1000f);
BossArena.ArmBossHoldForProbe();
L(" arm@t=1000 → BossHoldUntil=" + BossArena.BossHoldUntil.ToString("F1") +
" (= 1000 + nextRunBossHoldSeconds " + cfg.nextRunBossHoldSeconds + ")");
L(" " + P(BossArena.HoldUntilGate) + " HoldUntilGate=true(bossHoldUntilGateOpen=1)");
BossArena.SetProbeTime(1000f + cfg.nextRunBossHoldSeconds - 1f);
L(" " + P(BossArena.BossHeld) + " t=만료 1s → 홀드 유지(시간제 구간 · 813w5 와 동일)");
BossArena.SetProbeTime(1000f + cfg.nextRunBossHoldSeconds + 1f);
L(" " + P(BossArena.BossHeld) + " 🔴 t=만료 +1s → **여전히 홀드**(813w5 라면 여기서 풀려 존0 로 걸어 나갔다)");
BossArena.SetProbeTime(1000f + 10000f);
L(" " + P(BossArena.BossHeld) + " 🔴 t=만료 +9970s → 여전히 홀드(게이트가 닫혀 있는 한 유지)");
int hc0 = BossArena.HoldClearedByGate;
BossArena.ForceOpenGate();
L(" " + P(BossArena.HoldClearedByGate == hc0 + 1) + " 🔴 게이트 개방(Release) → 홀드 해제 +1 (" +
BossArena.HoldClearedByGate + "회)");
L(" " + P(!BossArena.BossHeld && !BossArena.HoldUntilGate && BossArena.BossHoldUntil < 0f) +
" 홀드 완전 해제(BossHeld=false · HoldUntilGate=false · BossHoldUntil=" + BossArena.BossHoldUntil.ToString("F1") +
") = 정상 보스전");
BossArena.SetProbeTime(-1f);
}
// ─────────────────────────────────────────────────────────────────────
// ④ D-43 — 게이트 개방 전 거리 판정(회복 0)
// ─────────────────────────────────────────────────────────────────────
static void Sec4_PreGateLeash()
{
L("");
L("── ④ 게이트 개방 전 리쉬 = 거리 판정만 (런 중이라도 회복 0 · D-27 ③ 재발 방지)");
var b = WLBossArenaSettings.Instance;
float lim = b.arenaRadius + b.leashMargin;
L(" 아레나 중심=" + BossArena.Center.ToString("F1") + " 반경=" + b.arenaRadius +
" leashMargin=" + b.leashMargin + " → 되돌림 경계 " + lim + "m");
BossArena.ResetAll();
BossArena.EnsureSubscribed();
BossArena.HoldSpawn(s_fbd, NoopResume);
BossArena.SetRunActiveForProbe(1); // 런 중 강제
BossArena.ClearBossHold(); // 홀드 없이(= C8 로 홀드를 꺼도 거리는 지킨다)
L(" " + P(BossArena.RunActive && !BossArena.GateOpen && !BossArena.BossHeld) +
" RunActive=true · GateOpen=false · BossHeld=false (D-43 의 그 구간)");
int r0 = BossArena.PreGateLeashResets, h0 = BossArena.LeashHealCount;
BossArena.LeashTick();
L(" " + P(BossArena.PreGateLeashResets == r0) + " 보스 없음 → 보스 가드에서 먼저 끊긴다(카운터 불변)");
var boss = MakeBoss("__WL813w6_Boss_B");
try
{
// 중심에서 100 m 밖에 세운다(= 존0 쪽으로 걸어 나간 상태 · Q9 R2n 은 중심 12.7~19.0 m 였다)
boss.transform.position = BossArena.Center + new Vector3(100f, 0f, 0f);
BossArena.SetBossForProbe(boss);
float d = Vector3.Distance(new Vector3(boss.transform.position.x, 0f, boss.transform.position.z),
new Vector3(BossArena.Center.x, 0f, BossArena.Center.z));
L(" " + P(d > lim) + " 가짜 보스 위치 " + boss.transform.position.ToString("F1") + " → 중심 거리 " +
d.ToString("F1") + "m > 경계 " + lim + "m");
string err = null;
try { BossArena.LeashTick(); }
catch (Exception ex) { err = ex.GetType().Name; } // Set_Warp 는 NavMeshAgent 가 필요하다(에디트 모드 한계)
L(" " + P(BossArena.PreGateLeashResets == r0 + 1) +
" 🔴 게이트 전 이탈 → 되돌림 경로 진입 +1 (PreGateLeashResets=" + BossArena.PreGateLeashResets +
(err != null ? " · 워프 본체는 " + err + " = 에디트 모드 한계)" : ")"));
L(" " + P(BossArena.LeashHealCount == h0) + " 🔴 **회복 0**(LeashHealCount 불변 " + BossArena.LeashHealCount +
") — D-27 ③(런 밖 회복으로 HP 되돌아옴) 재발 없음");
// C8 — preGateLeashDistanceOnly=0 → 813w5 그대로(경로 미진입)
var t = Tmp();
try
{
t.preGateLeashDistanceOnly = false;
UseTmp(t);
int r1 = BossArena.PreGateLeashResets;
try { BossArena.LeashTick(); } catch { }
L(" " + P(BossArena.PreGateLeashResets == r1) + " C8 preGateLeashDistanceOnly=0 → 게이트 전 경로 미진입(813w5 그대로)");
}
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); }
}
finally { KillBoss(boss); BossArena.SetRunActiveForProbe(0); }
}
// ─────────────────────────────────────────────────────────────────────
// ⑤ D-44 — HP 스냅샷 → RunEnded 발행 → Park 순서
// ─────────────────────────────────────────────────────────────────────
static void Sec5_HpSnapshotOrder()
{
L("");
L("── ⑤ D-44 결과 페이로드 보스 HP (Q9 R3: 실제 24.4 % = 439/1800 인데 성적표는 100 %)");
BossArena.ResetAll();
BossArena.EnsureSubscribed();
BossArena.EnsureRunSubscribed();
BossArena.HoldSpawn(s_fbd, NoopResume);
RunDirector.ResetAll();
// Q9 R3 실측값을 그대로 주입한다(에디트 모드에는 m_Stat 이 없다).
BossArena.SetBossHpForProbe(439f / 1800f, 439d, 1800d);
float ratio; double hp, max;
BossArena.SnapshotBossHp(out ratio, out hp, out max);
L(" " + P(Mathf.Abs(ratio - 0.2439f) < 0.001f && hp == 439d && max == 1800d) +
" SnapshotBossHp → ratio=" + ratio.ToString("F4") + " hp=" + hp + " max=" + max + " (= 24.4 %)");
RunEvents.RunEnded.Add(h_seen);
s_seenCount = 0;
try
{
var boss = MakeBoss("__WL813w6_Boss_C");
try
{
BossArena.SetBossForProbe(boss);
int p0 = BossArena.ParkCount, d0 = BossArena.DeferredParkCount;
RunDirector.StartRun("probe w6");
RunDirector.EndRun(RunOutcome.Timeout); // 🔴 진짜 발행(CombatEventList.Dispatch 는 internal)
L(" " + P(s_seenCount == 1) + " RunEnded 1회 수신(구독자 시점 기록)");
L(" " + P(Mathf.Abs(s_seenRatio - 0.2439f) < 0.001f) +
" 🔴 **발행 시점 페이로드 bossHpRatio=" + s_seenRatio.ToString("F4") + "** (= Park 이전 24.4 %) · hp=" +
s_seenHp + "/" + s_seenMax);
L(" " + P(s_seenParkCount == p0 && s_seenDeferred == d0) +
" 🔴 발행 도중 고정 0(ParkCount " + s_seenParkCount + " · DeferredParkCount " + s_seenDeferred +
") — 만피 복구가 결과창보다 **뒤**에 온다");
L(" " + P(s_seenPending) + " 발행 시점에 「미뤄 둔 고정」 예약됨(ParkPending=true)");
L(" " + P(Mathf.Abs(RunDirector.LastResult.bossHpRatio - 0.2439f) < 0.001f) +
" RunDirector.LastResult.bossHpRatio=" + RunDirector.LastResult.bossHpRatio.ToString("F4") +
" (결과창이 늦게 떠도 읽을 수 있다 · 813p 계약)");
BossArena.PumpDeferredPark();
L(" " + P(BossArena.DeferredParkCount == d0 + 1 && !BossArena.ParkPending) +
" 🔴 발행 뒤 Tick → 미뤄 둔 고정 실행(DeferredParkCount=" + BossArena.DeferredParkCount +
" · ParkPending=" + BossArena.ParkPending + ")");
L(" " + P(BossArena.BossHeld) + " 고정 실패(에디트 모드 워프 불가)해도 **홀드는 무장**(BossHeld=true)");
BossArena.PumpDeferredPark();
L(" " + P(BossArena.DeferredParkCount == d0 + 1) + " 두 번째 펌프 → 중복 실행 0");
}
finally { KillBoss(boss); }
// 보스가 없으면 1(813p 계약 = 「값 없음」)
BossArena.SetBossHpForProbe(-2f, 0d, 0d);
BossArena.SetBossForProbe(null);
BossArena.SnapshotBossHp(out ratio, out hp, out max);
L(" " + P(ratio < 0f && hp == 0d && max == 0d) + " 보스 없음 → ratio=1 · hp/max=0 (결과창은 「미처치」만 쓴다)");
// C8 — snapshotBossHpOnRunEnd=0
var t = Tmp();
try
{
t.snapshotBossHpOnRunEnd = false;
UseTmp(t);
BossArena.SetBossHpForProbe(0.5f, 900d, 1800d);
BossArena.SnapshotBossHp(out ratio, out hp, out max);
L(" " + P(ratio < 0f) + " C8 snapshotBossHpOnRunEnd=0 → ratio=1(813p 계약 그대로 · 필드는 비어 있다)");
}
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t); BossArena.SetBossHpForProbe(-2f, 0d, 0d); }
// C8 — parkBossOnRunEndDeferred=0 → 813w5 그대로(발행 도중 즉시 고정)
var t2 = Tmp();
try
{
t2.parkBossOnRunEndDeferred = false;
UseTmp(t2);
RunDirector.ResetAll();
int d1 = BossArena.DeferredParkCount, n1 = BossArena.RunEndNotices;
RunDirector.StartRun("probe w6 c8");
RunDirector.EndRun(RunOutcome.Abandon);
L(" " + P(BossArena.RunEndNotices == n1 + 1 && !BossArena.ParkPending && BossArena.DeferredParkCount == d1) +
" C8 parkBossOnRunEndDeferred=0 → 발행 도중 즉시 고정(예약 0 · 813w5 그대로)");
}
finally { UseReal(); UnityEngine.Object.DestroyImmediate(t2); }
}
finally { RunEvents.RunEnded.Remove(h_seen); RunDirector.ResetAll(); }
}
// ─────────────────────────────────────────────────────────────────────
// ⑥ GC 0
// ─────────────────────────────────────────────────────────────────────
static void Sec6_GC()
{
L("");
L("── ⑥ GC (모바일 · 매 프레임 도는 경로는 0 B 여야 한다)");
BossArena.ResetAll();
BossArena.EnsureSubscribed();
BossArena.HoldSpawn(s_fbd, NoopResume);
BossArena.SetRunActiveForProbe(1);
BossArena.SetBossForProbe(null);
BossArena.SetBossHpForProbe(0.244f, 439d, 1800d);
float r; double h, m;
BossArena.SnapshotBossHp(out r, out h, out m); // 워밍업
long a0 = Alloc();
for (int i = 0; i < 100; i++) BossArena.SnapshotBossHp(out r, out h, out m);
long a1 = Alloc();
L(" " + P(a1 - a0 == 0) + " SnapshotBossHp ×100 → Δ " + (a1 - a0) + " B");
BossArena.SetProbeTime(500f);
BossArena.ArmBossHoldForProbe();
bool held = BossArena.BossHeld;
a0 = Alloc();
for (int i = 0; i < 100; i++) held = BossArena.BossHeld;
a1 = Alloc();
L(" " + P(a1 - a0 == 0) + " BossHeld(게이트 개방까지 판정 포함) ×100 → Δ " + (a1 - a0) + " B · held=" + held);
a0 = Alloc();
for (int i = 0; i < 100; i++) BossArena.PumpDeferredPark(); // 예약 없음 = 즉시 return
a1 = Alloc();
L(" " + P(a1 - a0 == 0) + " PumpDeferredPark(예약 없음) ×100 → Δ " + (a1 - a0) + " B");
BossArena.ClearBossHold();
BossArena.LeashTick();
a0 = Alloc();
for (int i = 0; i < 100; i++) BossArena.LeashTick(); // 게이트 전 경로(보스 없음 = 가드)
a1 = Alloc();
L(" " + P(a1 - a0 == 0) + " LeashTick(게이트 전 경로) ×100 → Δ " + (a1 - a0) + " B");
BossArena.SetProbeTime(-1f);
BossArena.SetBossHpForProbe(-2f, 0d, 0d);
BossArena.SetRunActiveForProbe(0);
}
// ─────────────────────────────────────────────────────────────────────
// ⑦ C8 롤백
// ─────────────────────────────────────────────────────────────────────
static void Sec7_C8()
{
L("");
L("── ⑦ C8 롤백 (신규 6스위치 0 → 813w5 동작 그대로 · RuntimeDisabled → 무동작)");
var cfg = WLBossArenaSettings.Instance;
BossArena.ResetAll();
BossArena.EnsureSubscribed();
BossArena.HoldSpawn(s_fbd, NoopResume);
BossArena.SetRunActiveForProbe(-1);
// parkBossOnRespawn=0
var t = Tmp();
var boss = MakeBoss("__WL813w6_Boss_D");
try
{
t.parkBossOnRespawn = false;
UseTmp(t);
BossArena.SetBossForProbe(boss);
BossArena.ClearBossHold();
int a0 = BossArena.RespawnParkAttempts;
BossArena.NoteSpawn(cfg.bossSpawnerId, true);
L(" " + P(BossArena.RespawnParkAttempts == a0 && !BossArena.BossHeld) +
" C8 parkBossOnRespawn=0 → 재스폰 고정 0 · 홀드 0(813w5 그대로)");
// bossHoldUntilGateOpen=0 → 시간제 홀드만(만료되면 풀린다)
t.parkBossOnRespawn = true;
t.bossHoldUntilGateOpen = false;
BossArena.SetProbeTime(2000f);
BossArena.ClearBossHold();
BossArena.NoteSpawn(cfg.bossSpawnerId, true);
L(" " + P(BossArena.BossHeld && !BossArena.HoldUntilGate) +
" C8 bossHoldUntilGateOpen=0 → 시간제 홀드만(HoldUntilGate=false · BossHoldUntil=" +
BossArena.BossHoldUntil.ToString("F1") + ")");
BossArena.SetProbeTime(2000f + cfg.nextRunBossHoldSeconds + 1f);
L(" " + P(!BossArena.BossHeld) + " C8 만료 뒤 홀드 해제 = 813w5 동작(30 s 뒤 풀린다)");
BossArena.SetProbeTime(-1f);
}
finally { KillBoss(boss); UseReal(); UnityEngine.Object.DestroyImmediate(t); }
// RuntimeDisabled — 전부 무동작
var boss2 = MakeBoss("__WL813w6_Boss_E");
try
{
BossArena.SetBossForProbe(boss2);
BossArena.ClearBossHold();
WLBossArenaSettings.RuntimeDisabled = true;
L(" " + P(!WLBossArenaSettings.Active) + " RuntimeDisabled=1 → Active=false");
int a0 = BossArena.RespawnParkAttempts;
BossArena.NoteSpawn(cfg.bossSpawnerId, true);
L(" " + P(BossArena.RespawnParkAttempts == a0 && !BossArena.BossHeld) + " → 재스폰 고정 0 · 홀드 0");
float r; double h, m;
BossArena.SetBossHpForProbe(0.5f, 900d, 1800d);
BossArena.SnapshotBossHp(out r, out h, out m);
L(" " + P(r < 0f) + " → HP 스냅샷 1(RunResult 필드는 비어 있다)");
int n0 = BossArena.RunEndNotices;
RunDirector.ResetAll();
RunDirector.StartRun("probe c8 off");
RunDirector.EndRun(RunOutcome.Win);
L(" " + P(BossArena.RunEndNotices == n0 && !BossArena.ParkPending) + " → RunEnded 통지 0 · 예약 0 · 예외 0");
}
finally
{
WLBossArenaSettings.RuntimeDisabled = false;
BossArena.SetBossHpForProbe(-2f, 0d, 0d);
KillBoss(boss2);
BossArena.SetRunActiveForProbe(0);
RunDirector.ResetAll();
}
L(" " + P(true) + " 복구 확인 — Active=" + WLBossArenaSettings.Active +
" · 신규 스위치 6개를 전부 0 으로 두면 813w5 판(292ccf674) 동작 그대로다");
}
}