// 발주서 WL-813t4 — 프로브 (에디트 모드 · Play 0 · 로그인 0) // // unity command --project-path E:\NerdNavis\WL_wt\systems run_script --file AgentScripts/WL813t4_Probe.cs --entry WL813t4_Probe.Run --timeout 900 // // ① 값 실측 — arenaCenter · 스테이지 5·10 앵커 · 813w WLRunGuideSettings 의 아레나 참조 // ② Telegraph.StageScale 연결(입장 → def.telegraphScale · 이탈/실패/클리어/Abort → 1) // ③ 리스폰 0 / 배치 / 회수 seam 재실측(815b StopRespawnOverride·DeployOverride·RecallOverride) // ④ C8 진리표(값 원복 = 813d 동작) // ⑤ GC Δ public static class WL813t4_Probe { static System.Text.StringBuilder s; static int s_pass, s_fail; static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813t4P] " + t); } static void Chk(string name, bool ok, string got) { if (ok) { s_pass++; L(" [PASS] " + name + " — " + got); } else { s_fail++; L(" [FAIL] " + name + " — " + got); } } static string Abs(string rel) { return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), rel.Replace('/', System.IO.Path.DirectorySeparatorChar)); } static readonly UnityEngine.Vector3 kNew = new UnityEngine.Vector3(179f, 0.01f, 4f); static readonly UnityEngine.Vector3 kOld = new UnityEngine.Vector3(139f, 0.8f, -20f); public static string Run() { s = new System.Text.StringBuilder(); s_pass = 0; s_fail = 0; L("===== WL-813t4 프로브 · " + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ====="); var arena = UnityEngine.Resources.Load("WL/WLBossArenaSettings"); var tbl = UnityEngine.Resources.Load("WL/WLStageTable"); var guide = UnityEngine.Resources.Load("WL/WLRunGuideSettings"); var stg = UnityEngine.Resources.Load("WL/WLStageSettings"); if (arena == null || tbl == null || stg == null) { L("[에러] SO 로드 실패"); return Flush(); } Section1_Values(arena, tbl, guide, stg); Section2_StageScale(tbl); Section3_Spawner(tbl); Section4_C8(arena, stg, tbl); Section5_GC(); L(""); L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " — PASS " + s_pass + " / FAIL " + s_fail); return Flush(); } static string Flush() { try { System.IO.File.WriteAllText(Abs("AgentScripts/WL813t4_PROBE.txt"), s.ToString(), new System.Text.UTF8Encoding(true)); } catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813t4P] 저장 실패: " + e.Message); } return s.ToString(); } static bool Near(UnityEngine.Vector3 a, UnityEngine.Vector3 b) { return UnityEngine.Vector3.Distance(a, b) < 0.005f; } // ───────────────────────────────────────────────────────────────────────── static void Section1_Values(WL.Combat.Boss.WLBossArenaSettings arena, WL.Combat.Stage.WLStageTable tbl, WL.Combat.Run.WLRunGuideSettings guide, WL.Combat.Stage.WLStageSettings stg) { L(""); L("── ① 값 실측 (F-28) ──────────────────────────────────────────────"); Chk("WLBossArenaSettings.arenaCenter", Near(arena.arenaCenter, kNew), arena.arenaCenter.ToString("F2")); Chk("useFieldBossPositionAsCenter 유지 0", !arena.useFieldBossPositionAsCenter, arena.useFieldBossPositionAsCenter ? "1" : "0"); Chk("arenaRadius 20 유지", UnityEngine.Mathf.Abs(arena.arenaRadius - 20f) < 0.01f, arena.arenaRadius.ToString("F1")); Chk("BossArena.Center 가 새 좌표를 읽는다", Near(WL.Combat.Boss.BossArena.Center, kNew), WL.Combat.Boss.BossArena.Center.ToString("F2")); var d5 = tbl.Get(4); var d10 = tbl.Get(9); Chk("WLStageTable 스테이지 5 anchor", d5 != null && Near(d5.anchor, kNew), d5 != null ? d5.anchor.ToString("F2") + " r" + d5.barrierRadius.ToString("F0") : "null"); Chk("스테이지 5 barrierRadius 20 유지", d5 != null && UnityEngine.Mathf.Abs(d5.barrierRadius - 20f) < 0.01f, d5 != null ? d5.barrierRadius.ToString("F1") : "null"); Chk("WLStageTable 스테이지 10 anchor", d10 != null && Near(d10.anchor, kNew), d10 != null ? d10.anchor.ToString("F2") + " r" + d10.barrierRadius.ToString("F0") : "null"); Chk("스테이지 10 barrierRadius 20 유지", d10 != null && UnityEngine.Mathf.Abs(d10.barrierRadius - 20f) < 0.01f, d10 != null ? d10.barrierRadius.ToString("F1") : "null"); // 나머지 8스테이지 앵커는 손대지 않았다 bool others = true; string ol = ""; for (int i = 0; i < tbl.Count; i++) { if (i == 4 || i == 9) continue; var d = tbl.Get(i); if (d == null) continue; if (Near(d.anchor, kNew)) { others = false; ol += " #" + d.id; } } Chk("스테이지 5·10 외 앵커 무변경", others, others ? "8스테이지 그대로" : "바뀐 것" + ol); // 813w WLRunGuideSettings — 아레나 좌표 필드가 따로 있는가(있으면 동기 필요) L(" [813w 아레나 참조 실측] WLRunGuideSettings 의 Vector3 계열 필드 전수:"); int v3 = 0; bool sep = false; var ft = typeof(WL.Combat.Run.WLRunGuideSettings).GetFields( System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); for (int i = 0; i < ft.Length; i++) { var f = ft[i]; if (f.FieldType != typeof(UnityEngine.Vector3) && f.FieldType != typeof(UnityEngine.Vector3[])) continue; v3++; string val = ""; if (guide != null) { if (f.FieldType == typeof(UnityEngine.Vector3)) val = ((UnityEngine.Vector3)f.GetValue(guide)).ToString("F2"); else { var arr = f.GetValue(guide) as UnityEngine.Vector3[]; val = "["; if (arr != null) for (int k = 0; k < arr.Length; k++) val += (k > 0 ? ", " : "") + arr[k].ToString("F2"); val += "]"; } } L(" · " + f.Name + " : " + f.FieldType.Name + " = " + val); if (f.Name.IndexOf("arena", System.StringComparison.OrdinalIgnoreCase) >= 0 || f.Name.IndexOf("boss", System.StringComparison.OrdinalIgnoreCase) >= 0) sep = true; } Chk("813w 에 아레나 좌표 별도 필드 없음(= BossArena.Center 를 읽는다 · 동기 불필요)", !sep, "Vector3 계열 " + v3 + "개 · 아레나/보스 좌표 필드 " + (sep ? "있음" : "0") + " · 보스 유도는 반경만(bossArriveRadius " + (guide != null ? guide.bossArriveRadius.ToString("F0") : "?") + " / bossHoldRadius " + (guide != null ? guide.bossHoldRadius.ToString("F0") : "?") + ")"); Chk("WLStageSettings.Enabled", WL.Combat.Stage.WLStageSettings.Enabled, WL.Combat.Stage.WLStageSettings.Enabled ? "1" : "0"); L(" 참고 — 스테이지 배치 산포 반경 = barrierRadius × spawnScatterFactor " + stg.spawnScatterFactor.ToString("F2") + " = " + (20f * stg.spawnScatterFactor).ToString("F1") + " m · recallExcess " + (stg.recallExcess ? "1" : "0")); } // ───────────────────────────────────────────────────────────────────────── static void Section2_StageScale(WL.Combat.Stage.WLStageTable tbl) { L(""); L("── ② Telegraph.StageScale 연결 (StageDirector 2줄) ─────────────────"); WL.Combat.Stage.StageDirector.ResetAll(); WL.Combat.Telegraph.Telegraph.StageScale = 1f; Chk("초기값", UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f, WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2")); // 입장 → def.telegraphScale int[] idx = { 0, 4, 9 }; for (int i = 0; i < idx.Length; i++) { var def = tbl.Get(idx[i]); WL.Combat.Stage.StageDirector.Enter(idx[i], "probe"); float got = WL.Combat.Telegraph.Telegraph.StageScale; Chk("입장 스테이지 " + (def != null ? def.id : 0) + " → StageScale " + (def != null ? def.telegraphScale.ToString("F2") : "?"), def != null && UnityEngine.Mathf.Abs(got - def.telegraphScale) < 0.0001f, got.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State); if (i == 0) { // 이탈(Abort) → 1 WL.Combat.Stage.StageDirector.Abort("probe 이탈"); Chk("이탈(Abort) → StageScale 1", UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f, WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State); } } // 클리어 · 실패 — private 이라 리플렉션으로 같은 경로를 태운다(SetState 를 지난다) var t = typeof(WL.Combat.Stage.StageDirector); var bf = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static; WL.Combat.Stage.StageDirector.Enter(0, "probe clear"); float before = WL.Combat.Telegraph.Telegraph.StageScale; var mClear = t.GetMethod("Clear", bf); if (mClear != null) mClear.Invoke(null, null); Chk("클리어 → StageScale 1 (전 " + before.ToString("F2") + ")", UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f, WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State); WL.Combat.Stage.StageDirector.Enter(0, "probe fail"); before = WL.Combat.Telegraph.Telegraph.StageScale; var mFail = t.GetMethod("Fail", bf); if (mFail != null) mFail.Invoke(null, new object[] { WL.Combat.Stage.StageFailReason.TimeLimit }); Chk("실패 → StageScale 1 (전 " + before.ToString("F2") + ")", UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f, WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State); // 로비 이탈 WL.Combat.Stage.StageDirector.Enter(9, "probe lobby"); before = WL.Combat.Telegraph.Telegraph.StageScale; WL.Combat.Stage.StageDirector.ToLobby(); Chk("로비 이탈 → StageScale 1 (전 " + before.ToString("F2") + ")", UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f, WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2") + " · 상태 " + WL.Combat.Stage.StageDirector.State); // 재도전 → 다시 걸린다 WL.Combat.Stage.StageDirector.Enter(0, "probe retry"); WL.Combat.Telegraph.Telegraph.StageScale = 1f; // 일부러 흐트러뜨린다 WL.Combat.Stage.StageDirector.Retry(); var d1 = tbl.Get(0); Chk("재도전 → StageScale 다시 " + (d1 != null ? d1.telegraphScale.ToString("F2") : "?"), d1 != null && UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - d1.telegraphScale) < 0.0001f, WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2")); WL.Combat.Stage.StageDirector.Abort("probe 정리"); WL.Combat.Stage.StageDirector.ResetAll(); WL.Combat.Telegraph.Telegraph.StageScale = 1f; } // ───────────────────────────────────────────────────────────────────────── static void Section3_Spawner(WL.Combat.Stage.WLStageTable tbl) { L(""); L("── ③ 리스폰 0 · 배치 · 회수 seam (815b 프로브 이음매 재사용) ────────"); var stops = new System.Collections.Generic.List(); var deploys = new System.Collections.Generic.List(); var recalls = new System.Collections.Generic.List(); WL.Combat.Stage.StageSpawner.StopRespawnOverride = delegate (WL.Combat.Stage.StageDef d, int sid) { stops.Add(sid); return true; }; WL.Combat.Stage.StageSpawner.DeployOverride = delegate (WL.Combat.Stage.StageDef d, WL.Combat.Stage.StageWave w, int want) { deploys.Add(w.spawnerId + "×" + want); return want; }; WL.Combat.Stage.StageSpawner.RecallOverride = delegate (WL.Combat.Stage.StageDef d, int sid) { recalls.Add(sid); return 1; }; try { var def10 = tbl.Get(9); stops.Clear(); deploys.Clear(); WL.Combat.Stage.StageDirector.ResetAll(); WL.Combat.Stage.StageDirector.Enter(9, "probe spawner"); string sl = ""; for (int i = 0; i < stops.Count; i++) sl += (i > 0 ? ", " : "") + stops[i]; int distinct = 0; for (int i = 0; i < stops.Count; i++) { bool dup = false; for (int j = 0; j < i; j++) if (stops[j] == stops[i]) dup = true; if (!dup) distinct++; } Chk("스테이지 10 스포너 5기 리젠 차단 1회씩", stops.Count == 5 && distinct == 5, "차단 호출 " + stops.Count + "회 · 스포너 " + distinct + "기 [" + sl + "]"); string dl = ""; int sum = 0; for (int i = 0; i < deploys.Count; i++) dl += (i > 0 ? ", " : "") + deploys[i]; for (int i = 0; def10 != null && def10.waves != null && i < def10.waves.Length; i++) sum += def10.waves[i].count; Chk("고정 수 배치 = 표 합계 " + (def10 != null ? def10.TotalCount : 0), def10 != null && WL.Combat.Stage.StageSpawner.Deployed >= def10.TotalCount && sum == def10.TotalCount, "묶음 " + deploys.Count + "개 [" + dl + "] · Deployed " + WL.Combat.Stage.StageSpawner.Deployed); // 초과분 회수 경로 — Recall 이 스포너마다 1회 recalls.Clear(); int n = WL.Combat.Stage.StageSpawner.Recall(def10, "probe 초과분 회수"); int rd = 0; for (int i = 0; i < recalls.Count; i++) { bool dup = false; for (int j = 0; j < i; j++) if (recalls[j] == recalls[i]) dup = true; if (!dup) rd++; } string rl = ""; for (int i = 0; i < recalls.Count; i++) rl += (i > 0 ? ", " : "") + recalls[i]; Chk("초과분 회수 — 스포너 5기 1회씩", recalls.Count == 5 && rd == 5, "회수 호출 " + recalls.Count + "회 · 스포너 " + rd + "기 · 반환 " + n + " [" + rl + "]"); // 같은 스포너를 두 번 쓰는 표에서도 1회만 — AlreadyRequested var dup2 = new WL.Combat.Stage.StageDef(); dup2.id = 999; dup2.anchor = kNew; dup2.barrierRadius = 20f; var w1 = new WL.Combat.Stage.StageWave(); w1.spawnerId = 813001; w1.count = 3; var w2 = new WL.Combat.Stage.StageWave(); w2.spawnerId = 813001; w2.count = 4; dup2.waves = new WL.Combat.Stage.StageWave[] { w1, w2 }; stops.Clear(); bool all; WL.Combat.Stage.StageSpawner.StopRespawns(dup2, out all); Chk("같은 스포너 2묶음 → 차단 1회(중복 방지)", stops.Count == 1, "차단 " + stops.Count + "회 · allStopped " + all); WL.Combat.Stage.StageDirector.Abort("probe 정리"); WL.Combat.Stage.StageDirector.ResetAll(); } finally { WL.Combat.Stage.StageSpawner.StopRespawnOverride = null; WL.Combat.Stage.StageSpawner.DeployOverride = null; WL.Combat.Stage.StageSpawner.RecallOverride = null; } L(" 🔴 실기(Play) 리젠 0 · On_Regen/Off 실제 동작은 이 세션에서 미확인(815b §7-1·2 한계 그대로 · 살아 있는 MobActor 필요)."); } // ───────────────────────────────────────────────────────────────────────── static void Section4_C8(WL.Combat.Boss.WLBossArenaSettings arena, WL.Combat.Stage.WLStageSettings stg, WL.Combat.Stage.WLStageTable tbl) { L(""); L("── ④ C8 진리표 (값 원복 = 813d 동작) ──────────────────────────────"); WL.Combat.Telegraph.Telegraph.StageScale = 1f; WL.Combat.Stage.StageDirector.ResetAll(); // ⓐ arenaCenter 를 옛 값으로 되돌리면 BossArena.Center 도 그대로 813d 동작 var keep = arena.arenaCenter; arena.arenaCenter = kOld; Chk("arenaCenter 원복 → BossArena.Center 813d 값", Near(WL.Combat.Boss.BossArena.Center, kOld), WL.Combat.Boss.BossArena.Center.ToString("F2")); arena.arenaCenter = keep; Chk("복구", Near(WL.Combat.Boss.BossArena.Center, kNew), WL.Combat.Boss.BossArena.Center.ToString("F2")); // ⓑ arenaEnabled 0 → 813d 아레나 자체가 꺼진다(중심 값은 남는다) bool ka = arena.arenaEnabled; arena.arenaEnabled = false; Chk("arenaEnabled 0 → 아레나 off (값은 남는다)", !arena.arenaEnabled, "arenaEnabled 0 · Center " + WL.Combat.Boss.BossArena.Center.ToString("F2")); arena.arenaEnabled = ka; // ⓒ 스테이지 축 off → 입장 자체가 안 된다 = StageScale 무변경(815f 병합 전과 동일) WL.Combat.Stage.WLStageSettings.RuntimeDisabled = true; bool entered = WL.Combat.Stage.StageDirector.Enter(0, "probe c8"); Chk("WLStageSettings off → 입장 false · StageScale 1 유지", !entered && UnityEngine.Mathf.Abs(WL.Combat.Telegraph.Telegraph.StageScale - 1f) < 0.0001f, "Enter=" + entered + " · StageScale " + WL.Combat.Telegraph.Telegraph.StageScale.ToString("F2")); WL.Combat.Stage.WLStageSettings.RuntimeDisabled = false; // ⓓ 815f 예고 축 off → StageScale 이 걸려도 예고 자체가 안 붙는다 var tel = UnityEngine.Resources.Load("WL/WLTelegraphSettings"); Chk("815f WLTelegraphSettings.enabled(0 이면 예고 전부 off = 코드 무수정 롤백)", tel != null, tel != null ? "enabled " + (tel.enabled ? "1" : "0") : "SO 없음"); // ⓔ telegraphScale 을 1 로 두면 배수 없음 var d = tbl.Get(2); Chk("표의 telegraphScale 1 인 스테이지(3) → 배수 없음", d != null && UnityEngine.Mathf.Abs(d.telegraphScale - 1f) < 0.0001f, d != null ? "스테이지 " + d.id + " telegraphScale " + d.telegraphScale.ToString("F2") : "null"); WL.Combat.Stage.StageDirector.ResetAll(); WL.Combat.Telegraph.Telegraph.StageScale = 1f; } // ───────────────────────────────────────────────────────────────────────── static void Section5_GC() { L(""); L("── ⑤ GC ──────────────────────────────────────────────────────────"); var c = WL.Combat.Boss.BossArena.Center; if (c.x == 12345f) L("(warmup)"); System.GC.Collect(); long a = System.GC.GetTotalMemory(true); float acc = 0f; for (int i = 0; i < 1000; i++) { var p = WL.Combat.Boss.BossArena.Center; WL.Combat.Telegraph.Telegraph.StageScale = 1f + (i % 3) * 0.1f; acc += p.x; } WL.Combat.Telegraph.Telegraph.StageScale = 1f; long b = System.GC.GetTotalMemory(true); Chk("BossArena.Center + StageScale ×1000 GC Δ", b - a <= 0, "Δ " + (b - a) + " B (acc " + acc.ToString("F0") + ")"); } }