546 lines
31 KiB
C#
546 lines
31 KiB
C#
|
|
// WL813p_Probe.cs — #813p(3분 런 구조 · RunEvents · 집계 · 다음 런) 에디트 모드 검증 프로브
|
|||
|
|
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 씬/에셋 무수정(임시 GameObject 는 즉시 파괴)
|
|||
|
|
//
|
|||
|
|
// 상주 에디터: unity command run_script --project-path <wt> --file AgentScripts/WL813p_Probe.cs --entry WL813p_Probe.RunAll --timeout 300
|
|||
|
|
// 결과: Console + AgentScripts/WL813p_PROBE.txt (RESULT PASS / RESULT FAIL n)
|
|||
|
|
//
|
|||
|
|
// 검사 (발주서 WL-813p §2 ⓒ ⓓ)
|
|||
|
|
// ① 에셋·SOT: WLRunSettings 로드 · 값 전수 덤프 · zoneSpawnerIds/zoneClearKills 를 MonsterAppear.json 실측과 대조
|
|||
|
|
// ② 계약: RunEvents 6종 구독자 목록 · RunDirector 가 무는 11개 소스 이벤트(EnsureSubscribed 전/후 Count +1)
|
|||
|
|
// ③ 런 1회(승리): 존 4 클리어 → 게이트(ZonesCleared) → 보스 등장 → 보스 처치 → RunEnded(Win)
|
|||
|
|
// · 가짜 Killed/Spawned/SkillCast/LootPicked + Note* 로 넣은 값과 RunResult 덤프가 일치하는지 전수 비교
|
|||
|
|
// · 런 시작 전 존 누적 처치(스냅샷 차감)가 진행률에 섞이지 않는지
|
|||
|
|
// ④ 시간 종료: openGateAtSec 경과 → BossGateOpened(TimeLimit) · runSeconds 경과 → RunEnded(Timeout)
|
|||
|
|
// ⑤ 다음 런: RunDirector.Restart() → runIndex+1 · 집계 0 · 존 클리어 0 · BossArena 게이트 재무장(누적 카운터는 보존)
|
|||
|
|
// ⑥ RunTick 1 Hz: 같은 프레임 100회 강제 틱 = +0 · 0.5 s 씩 두 번 = +1 · SO tickIntervalSeconds 반영
|
|||
|
|
// ⑦ GC: 틱·집계 100회 반복 0 B(대조군 = 의도적 할당)
|
|||
|
|
// ⑧ C8: RuntimeDisabled / enabled=0 이면 StartRun·Tick 이 아무 일도 하지 않는다
|
|||
|
|
//
|
|||
|
|
// 에디트 모드 한계: 실제 스폰·PC·서버 데이터가 없다 → Restart() 의 On_Regen(존 시작점 워프)·levelFrom(ServerInfo)
|
|||
|
|
// 은 여기서 측정할 수 없다(플레이 모드 · 병합 후 QA). RunDirector 가 Application.isPlaying 으로 스스로 막는다.
|
|||
|
|
|
|||
|
|
using System;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Reflection;
|
|||
|
|
using System.Text;
|
|||
|
|
using UnityEngine;
|
|||
|
|
using WL.Combat.Boss;
|
|||
|
|
using WL.Combat.Core;
|
|||
|
|
using WL.Combat.Loot;
|
|||
|
|
using WL.Combat.Run;
|
|||
|
|
using WL.Combat.Survival;
|
|||
|
|
|
|||
|
|
public static class WL813p_Probe
|
|||
|
|
{
|
|||
|
|
const string OutCommit = "AgentScripts/WL813p_PROBE.txt";
|
|||
|
|
const string MonsterAppear = "Assets/ResWork/Table/Export/MonsterAppear.json";
|
|||
|
|
const BindingFlags NP = BindingFlags.NonPublic | BindingFlags.Instance;
|
|||
|
|
|
|||
|
|
static StringBuilder sb;
|
|||
|
|
static int s_fail;
|
|||
|
|
|
|||
|
|
static GameObject s_fbdGo, s_pcGo, s_bossGo;
|
|||
|
|
static GameObject[] s_zoneGo;
|
|||
|
|
static MobActor[] s_zoneMob;
|
|||
|
|
static MobActor s_bossMob;
|
|||
|
|
static Actor s_pc;
|
|||
|
|
static int s_resume;
|
|||
|
|
|
|||
|
|
// ── 마지막으로 받은 RunEvents 페이로드(구독 검증용)
|
|||
|
|
static int s_evStart, s_evZone, s_evGate, s_evBoss, s_evEnd, s_evTick;
|
|||
|
|
static RunResult s_lastEnd;
|
|||
|
|
static RunGateReason s_lastGateReason;
|
|||
|
|
static int s_lastZoneIndex = -1;
|
|||
|
|
|
|||
|
|
public static object RunAll()
|
|||
|
|
{
|
|||
|
|
sb = new StringBuilder();
|
|||
|
|
s_fail = 0;
|
|||
|
|
sb.AppendLine("# WL813p 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_Contract();
|
|||
|
|
Sec3_WinRun();
|
|||
|
|
Sec4_Timeout();
|
|||
|
|
Sec5_Restart();
|
|||
|
|
Sec6_TickRate();
|
|||
|
|
Sec7_Gc();
|
|||
|
|
Sec8_Rollback();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { s_fail++; L("[FAIL] 예외 " + ex); }
|
|||
|
|
finally { Teardown(); }
|
|||
|
|
|
|||
|
|
sb.AppendLine(s_fail == 0 ? "RESULT PASS" : "RESULT FAIL " + s_fail);
|
|||
|
|
string s = sb.ToString();
|
|||
|
|
Debug.Log(s);
|
|||
|
|
try { File.WriteAllText(OutCommit, s, new UTF8Encoding(false)); } catch { }
|
|||
|
|
return s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── 도구
|
|||
|
|
static void L(string m) { sb.AppendLine(m); }
|
|||
|
|
static string P(bool ok) { if (!ok) s_fail++; return ok ? "[PASS]" : "[FAIL]"; }
|
|||
|
|
static bool Near(double a, double b) { return Math.Abs(a - b) < 1e-3; }
|
|||
|
|
static long Alloc() { return GC.GetTotalMemory(false); }
|
|||
|
|
static long Mono() { return UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(); }
|
|||
|
|
|
|||
|
|
static MobActor MakeMob(string name, int spawnerId, eSubRol sub)
|
|||
|
|
{
|
|||
|
|
var go = new GameObject("__WL813p_" + name);
|
|||
|
|
var m = go.AddComponent<MobActor>();
|
|||
|
|
m.m_Role = eRole.Mob;
|
|||
|
|
m.m_SubRole = sub;
|
|||
|
|
var f = typeof(MobActor).GetField("m_SpawnerId", BindingFlags.NonPublic | BindingFlags.Instance);
|
|||
|
|
if (f != null) f.SetValue(m, spawnerId);
|
|||
|
|
return m;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void Kill(params UnityEngine.Object[] objs)
|
|||
|
|
{
|
|||
|
|
for (int i = 0; i < objs.Length; i++)
|
|||
|
|
if (objs[i] != null) UnityEngine.Object.DestroyImmediate(objs[i]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>MonsterAppear.json 의 한 행에서 정수 필드를 읽는다(코드 상수 0 · 실측 대조용).</summary>
|
|||
|
|
static int ReadAppear(int spawnerId, string field, int fallback)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
string txt = File.ReadAllText(MonsterAppear);
|
|||
|
|
int at = txt.IndexOf("\"n_SpawnerID\": \"" + spawnerId + "\"", StringComparison.Ordinal);
|
|||
|
|
if (at < 0) return fallback;
|
|||
|
|
int end = txt.IndexOf('}', at);
|
|||
|
|
int k = txt.IndexOf("\"" + field + "\": \"", at, StringComparison.Ordinal);
|
|||
|
|
if (k < 0 || k > end) return fallback;
|
|||
|
|
k += field.Length + 5;
|
|||
|
|
int q = txt.IndexOf('"', k);
|
|||
|
|
int v;
|
|||
|
|
return int.TryParse(txt.Substring(k, q - k), out v) ? v : fallback;
|
|||
|
|
}
|
|||
|
|
catch { return fallback; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ⓪ 준비
|
|||
|
|
static void Setup()
|
|||
|
|
{
|
|||
|
|
WLCombatCoreSettings.RuntimeDisabled = false;
|
|||
|
|
WLBossArenaSettings.RuntimeDisabled = false;
|
|||
|
|
WLRunSettings.RuntimeDisabled = false;
|
|||
|
|
WLRunSettings.ClearCache();
|
|||
|
|
WLBossArenaSettings.ClearCache();
|
|||
|
|
|
|||
|
|
BossArena.ResetAll();
|
|||
|
|
RunDirector.ResetAll();
|
|||
|
|
RunDirector.Unsubscribe();
|
|||
|
|
RunEvents.ResetDiagnostics();
|
|||
|
|
CombatEvents.ResetDiagnostics();
|
|||
|
|
|
|||
|
|
// 프로브가 RunEvents 를 실제로 구독한다(813q 가 할 일과 같은 계약).
|
|||
|
|
RunEvents.RunStarted.Add(OnRunStarted);
|
|||
|
|
RunEvents.ZoneCleared.Add(OnZoneCleared);
|
|||
|
|
RunEvents.BossGateOpened.Add(OnGateOpened);
|
|||
|
|
RunEvents.BossStarted.Add(OnBossStarted);
|
|||
|
|
RunEvents.RunEnded.Add(OnRunEnded);
|
|||
|
|
RunEvents.RunTick.Add(OnRunTick);
|
|||
|
|
|
|||
|
|
var cfg = WLRunSettings.Instance;
|
|||
|
|
var bcfg = WLBossArenaSettings.Instance;
|
|||
|
|
|
|||
|
|
// 가짜 FieldBossData 로 813d 게이트를 무장시킨다(존 카운터 KillsOf 가 살아야 스냅샷 차감을 측정할 수 있다).
|
|||
|
|
s_fbdGo = new GameObject("__WL813p_FieldBoss");
|
|||
|
|
s_fbdGo.transform.position = bcfg != null ? bcfg.arenaCenter : Vector3.zero;
|
|||
|
|
var fbd = s_fbdGo.AddComponent<FieldBossData>();
|
|||
|
|
fbd.isChapterBoss = false;
|
|||
|
|
fbd.n_MonsterID = bcfg != null ? bcfg.bossMonsterId : 10006;
|
|||
|
|
fbd.n_SpawnerId = bcfg != null ? bcfg.bossSpawnerId : 813010;
|
|||
|
|
fbd.RegenTime = 60f;
|
|||
|
|
BossArena.EnsureSubscribed();
|
|||
|
|
bool held = BossArena.HoldSpawn(fbd, delegate { s_resume++; });
|
|||
|
|
|
|||
|
|
// 가짜 존 몹 · 보스 · 메인 PC
|
|||
|
|
int zc = cfg != null && cfg.zoneSpawnerIds != null ? cfg.zoneSpawnerIds.Length : 0;
|
|||
|
|
s_zoneGo = new GameObject[zc];
|
|||
|
|
s_zoneMob = new MobActor[zc];
|
|||
|
|
for (int i = 0; i < zc; i++)
|
|||
|
|
{
|
|||
|
|
s_zoneMob[i] = MakeMob("zone" + i, cfg.zoneSpawnerIds[i], eSubRol.None);
|
|||
|
|
s_zoneGo[i] = s_zoneMob[i].gameObject;
|
|||
|
|
}
|
|||
|
|
s_bossMob = MakeMob("boss", cfg != null ? cfg.bossSpawnerId : 813010, eSubRol.Boss);
|
|||
|
|
s_bossGo = s_bossMob.gameObject;
|
|||
|
|
|
|||
|
|
s_pcGo = new GameObject("__WL813p_PC");
|
|||
|
|
s_pc = s_pcGo.AddComponent<Actor>();
|
|||
|
|
s_pc.m_Role = eRole.PC; s_pc.m_SubRole = eSubRol.None; // m_Enemy 기본 false → IsMainPC() true
|
|||
|
|
|
|||
|
|
L("");
|
|||
|
|
L("── ⓪ 준비");
|
|||
|
|
L(" BossArena.HoldSpawn=" + held + " GateArmed=" + BossArena.GateArmed + " 존 " + zc + "개 · 보스 " + (cfg != null ? cfg.bossSpawnerId : 0));
|
|||
|
|
L(" " + P(BossArena.GateArmed) + " 813d 게이트 무장(존 누적 카운터 사용 가능)");
|
|||
|
|
L(" " + P(s_pc.IsMainPC()) + " 가짜 메인 PC IsMainPC()=true");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static void OnRunStarted(in RunStartedEvent e) { s_evStart++; }
|
|||
|
|
static void OnZoneCleared(in ZoneClearedEvent e) { s_evZone++; s_lastZoneIndex = e.zoneIndex; }
|
|||
|
|
static void OnGateOpened(in BossGateOpenedEvent e) { s_evGate++; s_lastGateReason = e.reason; }
|
|||
|
|
static void OnBossStarted(in BossStartedEvent e) { s_evBoss++; }
|
|||
|
|
static void OnRunEnded(in RunResult e) { s_evEnd++; s_lastEnd = e; }
|
|||
|
|
static void OnRunTick(in RunTickEvent e) { s_evTick++; }
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ① 에셋 · SOT
|
|||
|
|
static void Sec1_Asset()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ① 에셋 · SOT (WLRunSettings)");
|
|||
|
|
var c = WLRunSettings.Instance;
|
|||
|
|
L(" " + P(c != null) + " Resources.Load(\"" + WLRunSettings.ResourcesPath + "\")");
|
|||
|
|
if (c == null) return;
|
|||
|
|
|
|||
|
|
L(" " + P(WLRunSettings.Enabled) + " Enabled(에셋·enabled·RuntimeDisabled·코어) = " + WLRunSettings.Enabled +
|
|||
|
|
" (core=" + WLCombatCoreSettings.Enabled + ")");
|
|||
|
|
L(" 값: enabled=" + c.enabled + " verboseLog=" + c.verboseLog + " runSeconds=" + c.runSeconds +
|
|||
|
|
" tickIntervalSeconds=" + c.tickIntervalSeconds + " pollSeconds=" + c.pollSeconds +
|
|||
|
|
" pauseTimerWhileWorldHeld=" + c.pauseTimerWhileWorldHeld);
|
|||
|
|
L(" 값: bossSpawnerId=" + c.bossSpawnerId + " openGateOnZonesCleared=" + c.openGateOnZonesCleared +
|
|||
|
|
" openGateAtSec=" + c.openGateAtSec + " autoStartOnMapEnter=" + c.autoStartOnMapEnter +
|
|||
|
|
" skipLobby=" + c.skipLobby + " endOnMapLeave=" + c.endOnMapLeave);
|
|||
|
|
L(" 값: restartWarpsPlayer=" + c.restartWarpsPlayer + " restartHealRate=" + c.restartHealRate +
|
|||
|
|
" restartRearmsBossGate=" + c.restartRearmsBossGate + " restartResetsPotions=" + c.restartResetsPotions);
|
|||
|
|
|
|||
|
|
bool lenOk = c.zoneSpawnerIds != null && c.zoneClearKills != null && c.zoneSpawnerIds.Length == c.zoneClearKills.Length;
|
|||
|
|
L(" " + P(lenOk) + " zoneSpawnerIds/zoneClearKills 길이 일치 = " +
|
|||
|
|
(c.zoneSpawnerIds != null ? c.zoneSpawnerIds.Length : -1) + " / " + (c.zoneClearKills != null ? c.zoneClearKills.Length : -1));
|
|||
|
|
|
|||
|
|
if (!lenOk) return;
|
|||
|
|
for (int i = 0; i < c.zoneSpawnerIds.Length; i++)
|
|||
|
|
{
|
|||
|
|
int max = ReadAppear(c.zoneSpawnerIds[i], "n_MaxMonsterCount", -1);
|
|||
|
|
bool ok = max > 0 && c.zoneClearKills[i] == max * 2;
|
|||
|
|
L(" " + P(ok) + " 존" + i + " id=" + c.zoneSpawnerIds[i] + " zoneClearKills=" + c.zoneClearKills[i] +
|
|||
|
|
" MonsterAppear n_MaxMonsterCount=" + max + " ×2=" + (max * 2));
|
|||
|
|
}
|
|||
|
|
int bossMax = ReadAppear(c.bossSpawnerId, "n_MaxMonsterCount", -1);
|
|||
|
|
L(" " + P(bossMax == 1) + " 보스 스포너 " + c.bossSpawnerId + " n_MaxMonsterCount=" + bossMax + " (MonsterAppear 실측)");
|
|||
|
|
L(" lootByGrade 길이 = WLLootSettings.GradeSlots(" + WLLootSettings.GradeSlots + ") + 1 = " + (WLLootSettings.GradeSlots + 1) +
|
|||
|
|
" ※ 발주서 §1 은 [6] 이라 적혀 있으나 실측 등급 슬롯은 " + WLLootSettings.GradeSlots + " 이라 그쪽에 맞췄다");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ② 계약(구독)
|
|||
|
|
static void Sec2_Contract()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ② 계약 — RunEvents 6종 · RunDirector 가 무는 소스 11종");
|
|||
|
|
L(" RunEvents 구독자(프로브 6개): RunStarted=" + RunEvents.RunStarted.Count + " ZoneCleared=" + RunEvents.ZoneCleared.Count +
|
|||
|
|
" BossGateOpened=" + RunEvents.BossGateOpened.Count + " BossStarted=" + RunEvents.BossStarted.Count +
|
|||
|
|
" RunEnded=" + RunEvents.RunEnded.Count + " RunTick=" + RunEvents.RunTick.Count);
|
|||
|
|
bool six = RunEvents.RunStarted.Count == 1 && RunEvents.ZoneCleared.Count == 1 && RunEvents.BossGateOpened.Count == 1 &&
|
|||
|
|
RunEvents.BossStarted.Count == 1 && RunEvents.RunEnded.Count == 1 && RunEvents.RunTick.Count == 1;
|
|||
|
|
L(" " + P(six) + " 813q 가 구독할 6종이 전부 살아 있다");
|
|||
|
|
|
|||
|
|
int k0 = CombatEvents.Killed.Count, s0 = CombatEvents.Spawned.Count, c0 = CombatEvents.SkillCast.Count,
|
|||
|
|
h0 = CombatEvents.HitConfirmed.Count, d0 = WL.Combat.Reaction.DamageAggregator.Flushed.Count,
|
|||
|
|
t0 = WL.Combat.Reaction.KillChain.TierReached.Count, e0 = WL.Combat.Reaction.KillChain.Ended.Count,
|
|||
|
|
g0 = WL.Combat.Growth.GrowthEvents.LevelUp.Count, l0 = LootBurst.Picked.Count,
|
|||
|
|
x0 = DeathFlow.Died.Count, p0 = PotionUse.Used.Count;
|
|||
|
|
|
|||
|
|
RunDirector.EnsureSubscribed();
|
|||
|
|
RunDirector.EnsureSubscribed(); // 중복 구독 방어 확인
|
|||
|
|
|
|||
|
|
bool add = CombatEvents.Killed.Count == k0 + 1 && CombatEvents.Spawned.Count == s0 + 1 &&
|
|||
|
|
CombatEvents.SkillCast.Count == c0 + 1 && CombatEvents.HitConfirmed.Count == h0 + 1 &&
|
|||
|
|
WL.Combat.Reaction.DamageAggregator.Flushed.Count == d0 + 1 &&
|
|||
|
|
WL.Combat.Reaction.KillChain.TierReached.Count == t0 + 1 && WL.Combat.Reaction.KillChain.Ended.Count == e0 + 1 &&
|
|||
|
|
WL.Combat.Growth.GrowthEvents.LevelUp.Count == g0 + 1 && LootBurst.Picked.Count == l0 + 1 &&
|
|||
|
|
DeathFlow.Died.Count == x0 + 1 && PotionUse.Used.Count == p0 + 1;
|
|||
|
|
L(" " + P(add) + " EnsureSubscribed ×2 → 소스 11종 각각 +1 (중복 구독 0) · Subscribed=" + RunDirector.Subscribed);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ③ 런 1회(승리)
|
|||
|
|
static void Sec3_WinRun()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ③ 런 1회(승리) — 존 4 클리어 → 게이트 → 보스 등장 → 처치");
|
|||
|
|
var c = WLRunSettings.Instance;
|
|||
|
|
if (c == null) { s_fail++; L(" [FAIL] 설정 없음"); return; }
|
|||
|
|
|
|||
|
|
// 런 시작 「전」 누적 처치 — 스냅샷 차감이 먹는지 보려고 존0 에 미리 5킬을 쌓는다.
|
|||
|
|
const int kPre = 5;
|
|||
|
|
for (int i = 0; i < kPre; i++) { BossArena.NoteSpawn(c.zoneSpawnerIds[0], false); BossArena.NoteKill(c.zoneSpawnerIds[0], false); }
|
|||
|
|
int preArena = BossArena.KillsOf(c.zoneSpawnerIds[0]);
|
|||
|
|
|
|||
|
|
s_evStart = s_evZone = s_evGate = s_evBoss = s_evEnd = s_evTick = 0;
|
|||
|
|
RunDirector.StartRun("probe-win");
|
|||
|
|
L(" " + P(s_evStart == 1 && RunDirector.Phase == RunPhase.Zones) + " RunStarted 1회 · phase=" + RunDirector.Phase +
|
|||
|
|
" runIndex=" + RunDirector.RunIndex + " · 시작 즉시 RunTick " + s_evTick + "회");
|
|||
|
|
L(" " + P(RunDirector.ZoneProgress(0) == 0) + " 스냅샷 차감 — BossArena.KillsOf(" + c.zoneSpawnerIds[0] + ")=" + preArena +
|
|||
|
|
" 인데 런 진행률=" + RunDirector.ZoneProgress(0) + " (0 이어야 한다)");
|
|||
|
|
|
|||
|
|
// 존별 처치 — 진짜 CombatEvents.Killed 로 넣는다(BossArena 카운터 + RunDirector 자체 카운터 둘 다 탄다).
|
|||
|
|
int totalZoneKills = 0;
|
|||
|
|
for (int z = 0; z < c.zoneSpawnerIds.Length; z++)
|
|||
|
|
{
|
|||
|
|
int need = c.zoneClearKills[z];
|
|||
|
|
for (int i = 0; i < need; i++)
|
|||
|
|
{
|
|||
|
|
CombatEvents.RaiseSpawned(s_zoneMob[z]);
|
|||
|
|
CombatEvents.RaiseKilled(s_zoneMob[z]);
|
|||
|
|
totalZoneKills++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
L(" " + P(s_evZone == c.zoneSpawnerIds.Length) + " ZoneCleared " + s_evZone + "회 / 존 " + c.zoneSpawnerIds.Length +
|
|||
|
|
"개 · 마지막 zoneIndex=" + s_lastZoneIndex + " · ZonesCleared=" + RunDirector.ZonesCleared);
|
|||
|
|
L(" " + P(s_evGate == 1 && s_lastGateReason == RunGateReason.ZonesCleared) + " BossGateOpened " + s_evGate +
|
|||
|
|
"회 reason=" + s_lastGateReason + " · BossArena.GateOpen=" + BossArena.GateOpen + " resume 호출=" + s_resume);
|
|||
|
|
|
|||
|
|
// 보스 등장
|
|||
|
|
CombatEvents.RaiseSpawned(s_bossMob);
|
|||
|
|
L(" " + P(s_evBoss == 1 && RunDirector.Phase == RunPhase.Boss) + " BossStarted " + s_evBoss + "회 · phase=" + RunDirector.Phase);
|
|||
|
|
|
|||
|
|
// 나머지 집계 — 스킬(진짜 이벤트) · 연쇄 · 피해 · 레벨 · 전리품 · 사망 · 물약
|
|||
|
|
const int kSkill = 7, kChain = 9, kDeath = 2, kPotion = 3;
|
|||
|
|
for (int i = 0; i < kSkill; i++) CombatEvents.RaiseSkillCast(s_pc, null);
|
|||
|
|
RunDirector.NoteChain(4); RunDirector.NoteChain(kChain); RunDirector.NoteChain(3); // 최대만 남아야 한다
|
|||
|
|
RunDirector.NoteDamage(1234.5d, 0d); RunDirector.NoteDamage(999d, 8888.5d); RunDirector.NoteDamage(50d, 100d);
|
|||
|
|
RunDirector.NoteLevel(11, 12); RunDirector.NoteLevel(12, 14);
|
|||
|
|
for (int i = 0; i < kDeath; i++) RunDirector.NoteDeath();
|
|||
|
|
for (int i = 0; i < kPotion; i++) RunDirector.NotePotion();
|
|||
|
|
|
|||
|
|
// 전리품 — 등급 1 은 진짜 LootBurst 경로(구독 배선 증명) · 나머지 등급은 Note 이음매
|
|||
|
|
int gradeSlots = WLLootSettings.GradeSlots;
|
|||
|
|
LootBurst.OnDropPicked(null, 1001, 1, null); // table_itemlist 없음 → ResolveGrade = 1
|
|||
|
|
int lootTotal = 1, expect1 = 1;
|
|||
|
|
for (int g = 2; g <= gradeSlots; g++) { RunDirector.NoteLoot(g, g, false); lootTotal += g; }
|
|||
|
|
RunDirector.NoteLoot(LootBurst.ResolveGrade(LootBurst.GoldItemId), 777, true); // 골드는 lootTotal 에 들어가지 않는다
|
|||
|
|
|
|||
|
|
// 보스 처치 = 승리
|
|||
|
|
CombatEvents.RaiseKilled(s_bossMob);
|
|||
|
|
var r = s_lastEnd;
|
|||
|
|
L(" " + P(s_evEnd == 1 && r.outcome == RunOutcome.Win) + " RunEnded " + s_evEnd + "회 outcome=" + r.outcome +
|
|||
|
|
" phase=" + RunDirector.Phase);
|
|||
|
|
|
|||
|
|
int expectKills = totalZoneKills + 1; // 존 처치 + 보스 1
|
|||
|
|
L(" 집계 대조 (입력 → RunResult)");
|
|||
|
|
L(" " + P(r.kills == expectKills) + " kills " + r.kills + " / 기대 " + expectKills);
|
|||
|
|
L(" " + P(r.bossKills == 1) + " bossKills " + r.bossKills + " / 기대 1");
|
|||
|
|
L(" " + P(r.maxChain == kChain) + " maxChain " + r.maxChain + " / 기대 " + kChain);
|
|||
|
|
L(" " + P(Near(r.maxDamage, 1234.5d)) + " maxDamage " + r.maxDamage + " / 기대 1234.5");
|
|||
|
|
L(" " + P(Near(r.maxDamageBurst, 8888.5d)) + " maxDamageBurst " + r.maxDamageBurst + " / 기대 8888.5");
|
|||
|
|
L(" " + P(r.levelFrom == 11 && r.levelTo == 14) + " level " + r.levelFrom + "→" + r.levelTo + " / 기대 11→14");
|
|||
|
|
L(" " + P(r.deaths == kDeath) + " deaths " + r.deaths + " / 기대 " + kDeath);
|
|||
|
|
L(" " + P(r.potionsUsed == kPotion) + " potionsUsed " + r.potionsUsed + " / 기대 " + kPotion);
|
|||
|
|
L(" " + P(r.skillCasts == kSkill) + " skillCasts " + r.skillCasts + " / 기대 " + kSkill);
|
|||
|
|
L(" " + P(r.lootTotal == lootTotal) + " lootTotal " + r.lootTotal + " / 기대 " + lootTotal);
|
|||
|
|
L(" " + P(r.gold == 777) + " gold " + r.gold + " / 기대 777");
|
|||
|
|
L(" " + P(r.lootByGrade != null && r.lootByGrade.Length == gradeSlots + 1) + " lootByGrade 길이 " +
|
|||
|
|
(r.lootByGrade != null ? r.lootByGrade.Length : -1) + " / 기대 " + (gradeSlots + 1));
|
|||
|
|
if (r.lootByGrade != null && r.lootByGrade.Length == gradeSlots + 1)
|
|||
|
|
{
|
|||
|
|
bool gradeOk = r.lootByGrade[0] == 0 && r.lootByGrade[1] == expect1;
|
|||
|
|
for (int g = 2; g <= gradeSlots; g++) if (r.lootByGrade[g] != g) gradeOk = false;
|
|||
|
|
var line = new StringBuilder();
|
|||
|
|
for (int g = 0; g <= gradeSlots; g++) { if (g > 0) line.Append(','); line.Append(r.lootByGrade[g]); }
|
|||
|
|
L(" " + P(gradeOk) + " lootByGrade[0.." + gradeSlots + "] = " + line + " / 기대 0,1,2,3,…," + gradeSlots);
|
|||
|
|
}
|
|||
|
|
L(" " + P(r.zonesCleared == r.zoneCount && r.zoneCount == c.zoneSpawnerIds.Length) + " zonesCleared " +
|
|||
|
|
r.zonesCleared + "/" + r.zoneCount);
|
|||
|
|
bool zsecOk = r.zoneClearSec != null && r.zoneClearSec.Length == r.zoneCount;
|
|||
|
|
if (zsecOk) for (int i = 0; i < r.zoneCount; i++) if (r.zoneClearSec[i] < 0f) zsecOk = false;
|
|||
|
|
L(" " + P(zsecOk) + " zoneClearSec 전 존 기록됨");
|
|||
|
|
bool zkOk = r.zoneKills != null && r.zoneKills.Length == r.zoneCount;
|
|||
|
|
if (zkOk) for (int i = 0; i < r.zoneCount; i++) if (r.zoneKills[i] != c.zoneClearKills[i]) zkOk = false;
|
|||
|
|
L(" " + P(zkOk) + " zoneKills = zoneClearKills(스냅샷 차감 후) · 존0 = " + (r.zoneKills != null && r.zoneKills.Length > 0 ? r.zoneKills[0] : -1));
|
|||
|
|
L(" " + P(r.bossGateSec >= 0f && r.bossStartSec >= 0f && r.bossClearSec >= 0f) + " bossGateSec=" +
|
|||
|
|
r.bossGateSec.ToString("F2") + " bossStartSec=" + r.bossStartSec.ToString("F2") + " bossClearSec=" + r.bossClearSec.ToString("F2"));
|
|||
|
|
L(" " + P(r.runIndex == 1 && Near(r.runSeconds, c.runSeconds)) + " runIndex=" + r.runIndex + " runSeconds=" + r.runSeconds);
|
|||
|
|
L(" BossArena 누적 보존 — KillsOf(" + c.zoneSpawnerIds[0] + ")=" + BossArena.KillsOf(c.zoneSpawnerIds[0]) +
|
|||
|
|
" (런 전 " + preArena + " + 이번 런 " + c.zoneClearKills[0] + ")");
|
|||
|
|
L(" " + P(BossArena.KillsOf(c.zoneSpawnerIds[0]) == preArena + c.zoneClearKills[0]) + " BossArena 값 리셋 0(발주서 §1)");
|
|||
|
|
|
|||
|
|
// 종료 뒤에는 아무것도 세지 않는다
|
|||
|
|
int killsAfter = r.kills;
|
|||
|
|
CombatEvents.RaiseKilled(s_zoneMob[0]);
|
|||
|
|
L(" " + P(RunDirector.LastResult.kills == killsAfter) + " 종료 후 처치는 집계에 안 들어간다(phase=" + RunDirector.Phase + ")");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ④ 시간 종료
|
|||
|
|
static void Sec4_Timeout()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ④ 시간 종료 — openGateAtSec → 게이트 · runSeconds → Timeout");
|
|||
|
|
var c = WLRunSettings.Instance;
|
|||
|
|
BossArena.ResetGate();
|
|||
|
|
s_evStart = s_evZone = s_evGate = s_evBoss = s_evEnd = s_evTick = 0;
|
|||
|
|
RunDirector.StartRun("probe-timeout");
|
|||
|
|
|
|||
|
|
// 존은 일부만 밀어 둔다(클리어 미달)
|
|||
|
|
int part = Math.Max(1, c.zoneClearKills[0] / 2);
|
|||
|
|
for (int i = 0; i < part; i++) CombatEvents.RaiseKilled(s_zoneMob[0]);
|
|||
|
|
L(" " + P(s_evZone == 0 && s_evGate == 0) + " 존 미달(" + RunDirector.ZoneProgress(0) + "/" + c.zoneClearKills[0] +
|
|||
|
|
") → ZoneCleared " + s_evZone + " · BossGateOpened " + s_evGate);
|
|||
|
|
|
|||
|
|
RunDirector.ShiftClockForProbe(c.openGateAtSec + 0.5f);
|
|||
|
|
RunDirector.ForceTick();
|
|||
|
|
L(" " + P(s_evGate == 1 && s_lastGateReason == RunGateReason.TimeLimit) + " openGateAtSec(" + c.openGateAtSec +
|
|||
|
|
"s) 경과 → BossGateOpened reason=" + s_lastGateReason + " phase=" + RunDirector.Phase);
|
|||
|
|
|
|||
|
|
RunDirector.ShiftClockForProbe(c.runSeconds);
|
|||
|
|
RunDirector.ForceTick();
|
|||
|
|
var r = s_lastEnd;
|
|||
|
|
L(" " + P(s_evEnd == 1 && r.outcome == RunOutcome.Timeout) + " RunEnded outcome=" + r.outcome +
|
|||
|
|
" totalSec=" + r.totalSec.ToString("F1") + " (runSeconds=" + c.runSeconds + ")");
|
|||
|
|
L(" " + P(r.bossKills == 0 && r.bossClearSec < 0f) + " 보스 미처치 — bossKills=" + r.bossKills +
|
|||
|
|
" bossClearSec=" + r.bossClearSec + " · zonesCleared=" + r.zonesCleared + "/" + r.zoneCount);
|
|||
|
|
L(" " + P(r.runIndex == 2) + " runIndex=" + r.runIndex + " (두 번째 런)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ⑤ 다음 런
|
|||
|
|
static void Sec5_Restart()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ⑤ 다음 런 — RunDirector.Restart()");
|
|||
|
|
var c = WLRunSettings.Instance;
|
|||
|
|
int arenaBefore = BossArena.KillsOf(c.zoneSpawnerIds[0]);
|
|||
|
|
int idxBefore = RunDirector.RunIndex;
|
|||
|
|
|
|||
|
|
s_evStart = s_evZone = s_evGate = s_evBoss = s_evEnd = s_evTick = 0;
|
|||
|
|
RunDirector.Restart();
|
|||
|
|
|
|||
|
|
L(" " + P(RunDirector.RunIndex == idxBefore + 1) + " runIndex " + idxBefore + " → " + RunDirector.RunIndex);
|
|||
|
|
L(" " + P(s_evStart == 1 && RunDirector.Phase == RunPhase.Zones) + " RunStarted 1회 · phase=" + RunDirector.Phase);
|
|||
|
|
L(" " + P(RunDirector.ZonesCleared == 0 && RunDirector.ZoneProgress(0) == 0) + " 집계 리셋 — zonesCleared=" +
|
|||
|
|
RunDirector.ZonesCleared + " zone0 진행=" + RunDirector.ZoneProgress(0));
|
|||
|
|
L(" " + P(!RunDirector.GateOpened && !RunDirector.BossStarted) + " 게이트·보스 상태 리셋 — GateOpened=" +
|
|||
|
|
RunDirector.GateOpened + " BossStarted=" + RunDirector.BossStarted);
|
|||
|
|
L(" " + P(!BossArena.GateOpen && BossArena.GateArmed) + " BossArena.ResetGate() — GateOpen=" + BossArena.GateOpen +
|
|||
|
|
" GateArmed=" + BossArena.GateArmed + " BossKilled=" + BossArena.BossKilled);
|
|||
|
|
L(" " + P(BossArena.KillsOf(c.zoneSpawnerIds[0]) == arenaBefore) + " BossArena 존 누적 보존 " + arenaBefore +
|
|||
|
|
" → " + BossArena.KillsOf(c.zoneSpawnerIds[0]) + " (리셋 금지 조항)");
|
|||
|
|
|
|||
|
|
// 재시작 뒤 다시 존을 밀면 정상 진행되는가(스냅샷 재설정 확인)
|
|||
|
|
for (int i = 0; i < c.zoneClearKills[0]; i++) CombatEvents.RaiseKilled(s_zoneMob[0]);
|
|||
|
|
L(" " + P(s_evZone == 1) + " 재시작 후 존0 재클리어 ZoneCleared=" + s_evZone + " (진행 " + RunDirector.ZonesCleared + "/" + RunDirector.ZoneCount + ")");
|
|||
|
|
L(" ※ Restart() 의 PC 워프(MyActor.On_Regen → Set_Warp(Get_ReincanationPos))는 에디트 모드에 PC·LoadMapMgr 가 없어 미측정 =「미확인」");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ⑥ RunTick 1 Hz
|
|||
|
|
static void Sec6_TickRate()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ⑥ RunTick 주기 (SO tickIntervalSeconds)");
|
|||
|
|
var c = WLRunSettings.Instance;
|
|||
|
|
int t0 = s_evTick;
|
|||
|
|
for (int i = 0; i < 100; i++) RunDirector.ForceTick();
|
|||
|
|
int t1 = s_evTick;
|
|||
|
|
L(" " + P(t1 == t0) + " 같은 프레임 강제 틱 100회 → RunTick +" + (t1 - t0) + " (간격 미만이면 0 이어야 한다)");
|
|||
|
|
|
|||
|
|
RunDirector.ShiftClockForProbe(c.tickIntervalSeconds * 0.5f); RunDirector.ForceTick();
|
|||
|
|
int t2 = s_evTick;
|
|||
|
|
L(" " + P(t2 == t1) + " 절반(" + (c.tickIntervalSeconds * 0.5f) + "s) 경과 → +" + (t2 - t1));
|
|||
|
|
RunDirector.ShiftClockForProbe(c.tickIntervalSeconds * 0.5f + 0.01f); RunDirector.ForceTick();
|
|||
|
|
int t3 = s_evTick;
|
|||
|
|
L(" " + P(t3 == t2 + 1) + " 나머지 절반 경과 → +" + (t3 - t2) + " = 1 Hz(tickIntervalSeconds=" + c.tickIntervalSeconds + ")");
|
|||
|
|
|
|||
|
|
int t4 = s_evTick;
|
|||
|
|
for (int i = 0; i < 10; i++) { RunDirector.ShiftClockForProbe(c.tickIntervalSeconds); RunDirector.ForceTick(); }
|
|||
|
|
L(" " + P(s_evTick == t4 + 10) + " 10초 진행 → RunTick +" + (s_evTick - t4) + " (기대 10)");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ⑦ GC
|
|||
|
|
static void Sec7_Gc()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ⑦ GC — 틱·집계 100회");
|
|||
|
|
var c = WLRunSettings.Instance;
|
|||
|
|
RunDirector.EndRun(RunOutcome.Abandon);
|
|||
|
|
BossArena.ResetGate();
|
|||
|
|
RunDirector.StartRun("probe-gc");
|
|||
|
|
|
|||
|
|
// 워밍업(JIT)
|
|||
|
|
for (int i = 0; i < 20; i++) { RunDirector.NoteChain(i); RunDirector.NoteDamage(i, i); RunDirector.NoteLoot(1, 1, false); RunDirector.ForceTick(); }
|
|||
|
|
|
|||
|
|
int g0 = GC.CollectionCount(0); long a0 = Alloc(), m0 = Mono();
|
|||
|
|
for (int i = 0; i < 100; i++)
|
|||
|
|
{
|
|||
|
|
RunDirector.NoteKill(c.zoneSpawnerIds[0], false);
|
|||
|
|
RunDirector.NoteChain(i);
|
|||
|
|
RunDirector.NoteDamage(i, i * 2);
|
|||
|
|
RunDirector.NoteLoot(1, 1, false);
|
|||
|
|
RunDirector.NoteDeath(); RunDirector.NotePotion(); RunDirector.NoteSkillCast();
|
|||
|
|
RunDirector.ShiftClockForProbe(c.tickIntervalSeconds);
|
|||
|
|
RunDirector.ForceTick();
|
|||
|
|
}
|
|||
|
|
long a1 = Alloc(), m1 = Mono(); int g1 = GC.CollectionCount(0);
|
|||
|
|
|
|||
|
|
// 대조군 — 의도적 할당(측정 감도 증명)
|
|||
|
|
long b0 = Alloc(), n0 = Mono();
|
|||
|
|
var junk = new string[100];
|
|||
|
|
for (int i = 0; i < 100; i++) junk[i] = "wl813p-" + i;
|
|||
|
|
long b1 = Alloc(), n1 = Mono();
|
|||
|
|
|
|||
|
|
bool zero = (a1 - a0) <= 0 && (m1 - m0) <= 0;
|
|||
|
|
L(" 본시험 ΔGetTotalMemory=" + (a1 - a0) + " B · ΔMonoUsed=" + (m1 - m0) + " B · gc0 " + g0 + "→" + g1);
|
|||
|
|
L(" 대조군 ΔGetTotalMemory=" + (b1 - b0) + " B · ΔMonoUsed=" + (n1 - n0) + " B (문자열 100개 · junk[" + junk.Length + "])");
|
|||
|
|
L(" " + P(zero) + " 틱·집계 100회 할당 0 B");
|
|||
|
|
RunDirector.EndRun(RunOutcome.Abandon);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ⑧ C8
|
|||
|
|
static void Sec8_Rollback()
|
|||
|
|
{
|
|||
|
|
L("");
|
|||
|
|
L("── ⑧ C8 롤백");
|
|||
|
|
RunDirector.ResetAll();
|
|||
|
|
WLRunSettings.RuntimeDisabled = true;
|
|||
|
|
s_evStart = s_evTick = 0;
|
|||
|
|
RunDirector.StartRun("probe-c8");
|
|||
|
|
RunDirector.ForceTick();
|
|||
|
|
L(" " + P(!WLRunSettings.Enabled && s_evStart == 0 && RunDirector.Phase == RunPhase.Idle) +
|
|||
|
|
" RuntimeDisabled=1 → Enabled=" + WLRunSettings.Enabled + " · StartRun 무동작 · phase=" + RunDirector.Phase);
|
|||
|
|
WLRunSettings.RuntimeDisabled = false;
|
|||
|
|
|
|||
|
|
WLCombatCoreSettings.RuntimeDisabled = true;
|
|||
|
|
RunDirector.StartRun("probe-c8-core");
|
|||
|
|
L(" " + P(!WLRunSettings.Enabled && RunDirector.Phase == RunPhase.Idle) +
|
|||
|
|
" 코어 off → Enabled=" + WLRunSettings.Enabled + " · 런 구조도 안 걸린다(집계 소스가 죽으므로)");
|
|||
|
|
WLCombatCoreSettings.RuntimeDisabled = false;
|
|||
|
|
|
|||
|
|
L(" " + P(WLRunSettings.Enabled) + " 복구 후 Enabled=" + WLRunSettings.Enabled);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ───────────────────────────────────────── ⑨ 정리
|
|||
|
|
static void Teardown()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
RunEvents.RunStarted.Remove(OnRunStarted);
|
|||
|
|
RunEvents.ZoneCleared.Remove(OnZoneCleared);
|
|||
|
|
RunEvents.BossGateOpened.Remove(OnGateOpened);
|
|||
|
|
RunEvents.BossStarted.Remove(OnBossStarted);
|
|||
|
|
RunEvents.RunEnded.Remove(OnRunEnded);
|
|||
|
|
RunEvents.RunTick.Remove(OnRunTick);
|
|||
|
|
|
|||
|
|
RunDirector.EndRun(RunOutcome.Abandon);
|
|||
|
|
RunDirector.Unsubscribe();
|
|||
|
|
RunDirector.ResetAll();
|
|||
|
|
BossArena.Unsubscribe();
|
|||
|
|
BossArena.ResetAll();
|
|||
|
|
WLRunSettings.RuntimeDisabled = false;
|
|||
|
|
WLCombatCoreSettings.RuntimeDisabled = false;
|
|||
|
|
WLBossArenaSettings.RuntimeDisabled = false;
|
|||
|
|
|
|||
|
|
if (s_zoneGo != null) for (int i = 0; i < s_zoneGo.Length; i++) Kill(s_zoneGo[i]);
|
|||
|
|
Kill(s_bossGo, s_pcGo, s_fbdGo);
|
|||
|
|
s_zoneGo = null; s_zoneMob = null; s_bossGo = s_pcGo = s_fbdGo = null;
|
|||
|
|
|
|||
|
|
L("");
|
|||
|
|
L("── ⑨ 정리 — 임시 GameObject 파괴 · 구독 해제 · 런타임 스위치 복구 (에셋/씬 저장 0)");
|
|||
|
|
L(" RunEvents.TotalRaised=" + RunEvents.TotalRaised + " last=" + RunEvents.LastEvent);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) { L(" [WARN] 정리 예외 " + ex.Message); }
|
|||
|
|
}
|
|||
|
|
}
|