556 lines
31 KiB
C#
556 lines
31 KiB
C#
// WL-813q 프로브 — 런 HUD + 런 결과 화면 실측 (에디트 모드 · Play 0 · 로그인 0).
|
||
// unity command run_script --file AgentScripts/WL813q_Probe.cs --entry WL813q_Probe.RunAll
|
||
// 산출물: <worktree>/AgentScripts/WL813q_PROBE.txt
|
||
// 🔴 NewGameUI.prefab 은 LoadPrefabContents 로 열고 **저장하지 않는다**(UnloadPrefabContents(false)).
|
||
// HUD·결과 노드는 HideFlags.DontSave 라 프리팹에 굽히지 않는다.
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using UnityEngine;
|
||
using UnityEngine.Profiling;
|
||
using UnityEditor;
|
||
using TMPro;
|
||
using WL.UI;
|
||
using WL.Combat.Run;
|
||
using WL.Combat.Loot;
|
||
|
||
public static class WL813q_Probe
|
||
{
|
||
const string kPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
|
||
|
||
static StringBuilder _o;
|
||
static int _pass, _fail;
|
||
static GameObject _root; // 프리팹 미리보기 씬 루트(FindObjectsByType 는 미리보기 씬을 못 본다)
|
||
|
||
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
|
||
static void N(string s) { _o.AppendLine(" " + s); }
|
||
static bool Chk(bool ok, string what)
|
||
{
|
||
if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); }
|
||
else { _fail++; _o.AppendLine(" [FAIL] " + what); }
|
||
return ok;
|
||
}
|
||
|
||
static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } }
|
||
|
||
public static string RunAll()
|
||
{
|
||
_o = new StringBuilder();
|
||
_pass = _fail = 0;
|
||
_o.AppendLine("# WL813q Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
|
||
_o.AppendLine("playMode=" + Application.isPlaying);
|
||
|
||
GameObject prefabRoot = null;
|
||
bool savedRuntimeDisabled = WLRunUiSettings.RuntimeDisabled;
|
||
try
|
||
{
|
||
Step0_Asset();
|
||
prefabRoot = Step1_Nodes();
|
||
Step2_Hud();
|
||
Step3_ResultWin();
|
||
Step4_ArrayCopy();
|
||
Step5_Timeout();
|
||
Step6_Abandon();
|
||
Step7_RevivePriority();
|
||
Step8_Gc();
|
||
Step9_C8();
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
_fail++;
|
||
_o.AppendLine(" [FAIL] 예외 " + e.GetType().Name + " : " + e.Message);
|
||
_o.AppendLine(e.StackTrace);
|
||
}
|
||
finally
|
||
{
|
||
WLRunUiSettings.RuntimeDisabled = savedRuntimeDisabled;
|
||
Step10_Cleanup(prefabRoot);
|
||
}
|
||
|
||
_o.AppendLine();
|
||
_o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " (pass=" + _pass + " fail=" + _fail + ")");
|
||
string path = Path.Combine(Path.Combine(Root, "AgentScripts"), "WL813q_PROBE.txt");
|
||
File.WriteAllText(path, _o.ToString(), new UTF8Encoding(false));
|
||
return _o.ToString() + "\n(파일: " + path + ")";
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ⓪ 에셋 · 계약
|
||
static void Step0_Asset()
|
||
{
|
||
H("⓪ 에셋 (WLRunUiSettings · NEW)");
|
||
WLRunUiSettings.ClearCache();
|
||
var s = WLRunUiSettings.Instance;
|
||
Chk(s != null, "Resources.Load(\"" + WLRunUiSettings.ResourcesPath + "\")");
|
||
if (s == null) return;
|
||
Chk(WLRunUiSettings.Enabled, "Enabled(에셋·enabled_·RuntimeDisabled) = " + WLRunUiSettings.Enabled);
|
||
N("값: enabled_=" + s.enabled_ + " hud=" + s.runHudEnabled + " result=" + s.resultEnabled +
|
||
" topMargin=" + s.runHudTopMarginPx + "px width=" + s.runHudWidthPx + "px timerPx=" + s.timerFontPx +
|
||
" warn=" + s.timerWarnSeconds + "s pulse=" + s.zonePulseScale + "×" + s.zonePulseSeconds + "s");
|
||
N("문구: win=\"" + s.resultWinTitle + "\" timeout=\"" + s.resultTimeoutTitle + "\" next=\"" + s.resultNextRunText +
|
||
"\" close=\"" + s.resultCloseText + "\" zone=\"" + s.phaseZoneFormat + "\" boss=\"" + s.phaseBossText + "\"");
|
||
N("행 순서: " + string.Join(" · ", Array.ConvertAll(s.resultRows, r => r.ToString())));
|
||
Chk(s.resultRows != null && s.resultRows.Length == 9, "본문 행 9종(발주서 §1-2 순서)");
|
||
Chk(!s.resultShowOnAbandon, "Abandon 팝업 없음(기본값)");
|
||
Chk(s.resultDeferWhileRevive, "부활 팝업 우선(기본값)");
|
||
|
||
// 813p 계약 — UI 는 수치를 갖지 않는다
|
||
var rs = WLRunSettings.Instance;
|
||
Chk(rs != null, "813p WLRunSettings 존재(런 길이·존 수의 주인)");
|
||
if (rs != null)
|
||
N("813p 값(읽기만): runSeconds=" + rs.runSeconds + " zoneSpawnerIds=" + rs.zoneSpawnerIds.Length +
|
||
" zoneClearKills=[" + string.Join(",", Array.ConvertAll(rs.zoneClearKills, x => x.ToString())) + "]");
|
||
Chk(WLLootSettings.GradeSlots == 9, "WLLootSettings.GradeSlots = 9 → lootByGrade 길이 10(813p 계약)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ① 노드 조립
|
||
static GameObject Step1_Nodes()
|
||
{
|
||
H("① 노드 — NewGameUI 프리팹(저장 0) · Safe Area · 보스 바와 겹침");
|
||
var root = PrefabUtility.LoadPrefabContents(kPrefab);
|
||
_root = root;
|
||
Chk(root != null, "LoadPrefabContents(" + kPrefab + ")");
|
||
if (root == null) return null;
|
||
|
||
var hud = WLVignetteUtil.FindUiPath(root.transform, "IngameUIs/WL_HUD");
|
||
Chk(hud != null, "IngameUIs/WL_HUD 경로 실측");
|
||
if (hud != null)
|
||
{
|
||
bool safe = hud.GetComponentInParent<SafeAreaFitter>(true) != null || hud.GetComponent<SafeAreaFitter>() != null;
|
||
Chk(safe, "WL_HUD 가 SafeAreaFitter 아래 = Safe Area 안 (813y 실측 재확인)");
|
||
}
|
||
|
||
RunHud.Subscribe(true);
|
||
RunResultUI.Subscribe(true);
|
||
N("Bind HUD : " + RunHud.Bind(root.transform));
|
||
N("Bind 결과 : " + RunResultUI.Bind(root.transform));
|
||
Chk(RunHud.Bound, "RunHud 노드 생성(WL_RunHud · HideFlags.DontSave)");
|
||
Chk(RunResultUI.Bound, "RunResultUI 노드 생성(WL_RunResult · HideFlags.DontSave)");
|
||
Chk(RunEvents.RunStarted.Count == 2, "RunStarted 구독자 2(HUD+결과) — 실측 " + RunEvents.RunStarted.Count);
|
||
Chk(RunEvents.RunTick.Count == 1 && RunEvents.ZoneCleared.Count == 1 &&
|
||
RunEvents.BossGateOpened.Count == 1 && RunEvents.BossStarted.Count == 1 && RunEvents.RunEnded.Count == 2,
|
||
"6종 구독 배선 = Tick1/Zone1/Gate1/Boss1/Ended2");
|
||
|
||
// 813c 보스 HP 바와 겹치지 않는가(둘 다 상단 · px 기준 산술)
|
||
var hs = WLHudLayoutSettings.Instance;
|
||
var s = WLRunUiSettings.Instance;
|
||
if (hs != null && s != null)
|
||
{
|
||
float barTop = hs.bossBarTopMarginPx;
|
||
float barBottom = barTop + hs.bossBarSizePx.y + hs.bossBarNameFontPx + hs.bossBarNameGapPx;
|
||
N("보스 바 = 상단 " + barTop + " ~ " + barBottom + " px · 런 HUD 상단 = " + s.runHudTopMarginPx + " px");
|
||
Chk(s.runHudTopMarginPx >= barBottom, "런 HUD 가 보스 HP 바 아래(겹침 0 · 여유 " +
|
||
(s.runHudTopMarginPx - barBottom).ToString("F1") + " px)");
|
||
}
|
||
N("HUD 배치 : " + RunHud.ApplyLayout());
|
||
N("결과 배치: " + RunResultUI.ApplyLayout());
|
||
return root;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ② HUD 시퀀스
|
||
static void Step2_Hud()
|
||
{
|
||
H("② HUD — RunStarted → RunTick×3 → ZoneCleared×4 → BossGateOpened → BossStarted");
|
||
var s = WLRunUiSettings.Instance;
|
||
RunHud.ResetState();
|
||
|
||
WLRunUiProbeHooks.RaiseRunStarted(1, 240f, 4, "probe");
|
||
Chk(RunHud.StartedSeen == 1, "RunStarted 수신 1");
|
||
Chk(RunHud.ZoneCells == 4, "존 칸 4개 생성 — 실측 " + RunHud.ZoneCells);
|
||
Chk(RunHud.Visible, "HUD 표시 on");
|
||
Chk(RunHud.TimerText == "04:00", "타이머 리셋 = 04:00(runSeconds 240 · 페이로드에서) — 실측 \"" + RunHud.TimerText + "\"");
|
||
Chk(RunHud.PhaseText == "존 1", "단계 = \"존 1\" — 실측 \"" + RunHud.PhaseText + "\"");
|
||
|
||
// 틱 3회: 남은 200s(존0 6/12) → 남은 150s(존1 7/14) → 남은 25s(경고색)
|
||
WLRunUiProbeHooks.RaiseRunTick(40f, 200f, 0, 6, 12, 0, 4, RunPhase.Zones, 1);
|
||
N("tick1 timer=\"" + RunHud.TimerText + "\" 존0 fill=" + RunHud.ZoneFill(0).ToString("F2") + " 단계=\"" + RunHud.PhaseText + "\"");
|
||
Chk(RunHud.TimerText == "03:20", "틱1 mm:ss = 03:20");
|
||
Chk(Mathf.Abs(RunHud.ZoneFill(0) - 0.5f) < 0.001f, "틱1 존0 게이지 = 6/12 = 0.50 — 실측 " + RunHud.ZoneFill(0).ToString("F3"));
|
||
|
||
WLRunUiProbeHooks.RaiseRunTick(90f, 150f, 1, 7, 14, 1, 4, RunPhase.Zones, 1);
|
||
Chk(RunHud.TimerText == "02:30", "틱2 mm:ss = 02:30");
|
||
Chk(Mathf.Abs(RunHud.ZoneFill(0) - 1f) < 0.001f && Mathf.Abs(RunHud.ZoneFill(1) - 0.5f) < 0.001f,
|
||
"틱2 존0=1.00(클리어) · 존1=0.50 — 실측 " + RunHud.ZoneFill(0).ToString("F2") + "/" + RunHud.ZoneFill(1).ToString("F2"));
|
||
Chk(RunHud.PhaseText == "존 2", "틱2 단계 = \"존 2\" — 실측 \"" + RunHud.PhaseText + "\"");
|
||
|
||
WLRunUiProbeHooks.RaiseRunTick(215f, 25f, 2, 3, 12, 2, 4, RunPhase.Zones, 1);
|
||
Chk(RunHud.TimerText == "00:25", "틱3 mm:ss = 00:25");
|
||
var timerColor = FindTimerColor();
|
||
Chk(s != null && ApproxColor(timerColor, s.timerWarnColor),
|
||
"마지막 " + (s != null ? s.timerWarnSeconds : 0f) + "s 경고색 #" +
|
||
(s != null ? ColorUtility.ToHtmlStringRGB(s.timerWarnColor) : "?") + " — 실측 #" + ColorUtility.ToHtmlStringRGB(timerColor));
|
||
|
||
// 존 클리어 4회 + 펄스
|
||
float now = Time.unscaledTime;
|
||
for (int i = 0; i < 4; i++)
|
||
WLRunUiProbeHooks.RaiseZoneCleared(i, 813001 + i, 30f + i * 20f, 12, 12, i == 3);
|
||
Chk(RunHud.ZoneClearedSeen == 4, "ZoneCleared 4회 수신");
|
||
bool allFull = true;
|
||
for (int i = 0; i < 4; i++) if (RunHud.ZoneFill(i) < 0.999f) allFull = false;
|
||
Chk(allFull, "존 칸 4개 전부 채움");
|
||
RunHud.Tick(now);
|
||
float peak = RunHud.ZoneScale(3);
|
||
RunHud.Tick(now + (s != null ? s.zonePulseSeconds : 0.3f));
|
||
float back = RunHud.ZoneScale(3);
|
||
N("펄스 실측: 시작 scale=" + peak.ToString("F3") + " → " + (s != null ? s.zonePulseSeconds : 0.3f) + "s 뒤 " + back.ToString("F3"));
|
||
Chk(peak > 1.001f && Mathf.Abs(back - 1f) < 0.001f,
|
||
"존 클리어 펄스 " + (s != null ? s.zonePulseScale : 0f) + "× → " + (s != null ? s.zonePulseSeconds : 0f) + "s 안에 1.0 복귀");
|
||
|
||
WLRunUiProbeHooks.RaiseBossGateOpened(150f, RunGateReason.TimeLimit, 813010, true);
|
||
Chk(RunHud.GateSeen == 1 && RunHud.PhaseText == (s != null ? s.phaseBossText : "BOSS"),
|
||
"게이트 개방 → 단계 라벨 \"" + RunHud.PhaseText + "\"");
|
||
var phaseColor = FindPhaseColor();
|
||
Chk(s != null && ApproxColor(phaseColor, s.phaseBossColor), "보스 단계 색 #" + ColorUtility.ToHtmlStringRGB(phaseColor));
|
||
|
||
WLRunUiProbeHooks.RaiseBossStarted(155f, 813010);
|
||
Chk(RunHud.BossSeen == 1, "BossStarted 수신 1 (boss=null 계약 경로 · 예외 0)");
|
||
N(RunHud.Dump().TrimEnd());
|
||
}
|
||
|
||
static Color FindTimerColor()
|
||
{
|
||
var t = FindLabel("Timer");
|
||
return t != null ? t.color : Color.magenta;
|
||
}
|
||
static Color FindPhaseColor()
|
||
{
|
||
var t = FindLabel("Phase");
|
||
return t != null ? t.color : Color.magenta;
|
||
}
|
||
static TMP_Text FindLabel(string name)
|
||
{
|
||
if (_root == null) return null;
|
||
var all = _root.GetComponentsInChildren<TextMeshProUGUI>(true);
|
||
for (int i = 0; i < all.Length; i++)
|
||
if (all[i] != null && all[i].name == name && all[i].transform.parent != null &&
|
||
all[i].transform.parent.name == "WL_RunHud") return all[i];
|
||
return null;
|
||
}
|
||
static bool ApproxColor(Color a, Color b)
|
||
{
|
||
return Mathf.Abs(a.r - b.r) < 0.01f && Mathf.Abs(a.g - b.g) < 0.01f && Mathf.Abs(a.b - b.b) < 0.01f;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ③ 결과 Win
|
||
static float[] _srcZoneSec;
|
||
static int[] _srcZoneKills;
|
||
static int[] _srcLoot;
|
||
|
||
static RunResult MakeResult(RunOutcome outcome)
|
||
{
|
||
// 813p 처럼 **같은 배열을 재사용**한다(참조 보관 금지 계약을 그대로 재현)
|
||
if (_srcZoneSec == null)
|
||
{
|
||
_srcZoneSec = new float[4];
|
||
_srcZoneKills = new int[4];
|
||
_srcLoot = new int[10];
|
||
}
|
||
_srcZoneSec[0] = 31.5f; _srcZoneSec[1] = 62.25f; _srcZoneSec[2] = 95f; _srcZoneSec[3] = -1f;
|
||
_srcZoneKills[0] = 12; _srcZoneKills[1] = 14; _srcZoneKills[2] = 12; _srcZoneKills[3] = 5;
|
||
for (int g = 0; g < 10; g++) _srcLoot[g] = 0;
|
||
_srcLoot[1] = 7; _srcLoot[2] = 3; _srcLoot[4] = 2; _srcLoot[9] = 1; // 등급 3·5~8 은 0 → 생략돼야 한다
|
||
|
||
return new RunResult
|
||
{
|
||
outcome = outcome,
|
||
runIndex = 2,
|
||
totalSec = 154f, // 02:34
|
||
runSeconds = 240f,
|
||
zoneClearSec = _srcZoneSec,
|
||
zoneKills = _srcZoneKills,
|
||
zoneCount = 4,
|
||
zonesCleared = 3,
|
||
bossGateSec = 150f,
|
||
bossStartSec = 152f,
|
||
bossClearSec = outcome == RunOutcome.Win ? 153.5f : -1f,
|
||
kills = 99,
|
||
bossKills = outcome == RunOutcome.Win ? 1 : 0,
|
||
maxChain = 9,
|
||
maxDamage = 1234.5,
|
||
maxDamageBurst = 8888.5,
|
||
levelFrom = 11,
|
||
levelTo = 14,
|
||
lootByGrade = _srcLoot,
|
||
lootTotal = 13,
|
||
gold = 777,
|
||
deaths = 2,
|
||
potionsUsed = 3,
|
||
skillCasts = 7,
|
||
time = Time.unscaledTime,
|
||
frame = Time.frameCount,
|
||
};
|
||
}
|
||
|
||
static void Step3_ResultWin()
|
||
{
|
||
H("③ 결과 화면 — RunEnded(Win)");
|
||
var s = WLRunUiSettings.Instance;
|
||
RunResultUI.ResetState();
|
||
var r = MakeResult(RunOutcome.Win);
|
||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||
|
||
Chk(RunResultUI.EndedSeen == 1, "RunEnded 수신 1");
|
||
Chk(RunResultUI.IsOpen, "결과 화면 표시(부활 팝업 없음)");
|
||
Chk(RunResultUI.TitleText == (s != null ? s.resultWinTitle : ""),
|
||
"제목 = \"" + (s != null ? s.resultWinTitle : "") + "\" — 실측 \"" + RunResultUI.TitleText + "\"");
|
||
Chk(!RunHud.Visible, "런 종료 → HUD 숨김(runHudHideOnEnd)");
|
||
|
||
var c = RunResultUI.Copied;
|
||
Chk(c.outcome == RunOutcome.Win && c.runIndex == 2 && Mathf.Approximately(c.totalSec, 154f) &&
|
||
c.kills == 99 && c.bossKills == 1 && c.maxChain == 9 && c.levelFrom == 11 && c.levelTo == 14 &&
|
||
c.gold == 777 && c.deaths == 2 && c.potionsUsed == 3 && c.zonesCleared == 3,
|
||
"복사본 스칼라 = 입력과 일치(outcome/runIndex/total/kills/boss/chain/level/gold/deaths/potions/zones)");
|
||
|
||
N("행 값 실측:");
|
||
N(" 총 시간 = \"" + RunResultUI.RowValueText(WLRunResultRow.TotalTime) + "\"");
|
||
N(" 보스 처치 = \"" + RunResultUI.RowValueText(WLRunResultRow.BossClear) + "\"");
|
||
N(" 처치 수 = \"" + RunResultUI.RowValueText(WLRunResultRow.Kills) + "\"");
|
||
N(" 최대 연쇄 = \"" + RunResultUI.RowValueText(WLRunResultRow.MaxChain) + "\"");
|
||
N(" 최대 피해 = \"" + RunResultUI.RowValueText(WLRunResultRow.MaxDamage) + "\"");
|
||
N(" 레벨 = \"" + RunResultUI.RowValueText(WLRunResultRow.Level) + "\"");
|
||
N(" 등급별 = \"" + RunResultUI.RowValueText(WLRunResultRow.LootByGrade) + "\"");
|
||
N(" 골드 = \"" + RunResultUI.RowValueText(WLRunResultRow.Gold) + "\"");
|
||
N(" 사망/물약 = \"" + RunResultUI.RowValueText(WLRunResultRow.DeathsPotions) + "\"");
|
||
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.TotalTime) == "02:34", "총 시간 154s → 02:34");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.BossClear) == "02:34", "보스 처치 153.5s → 02:34(올림)");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.Kills) == "99 (보스 1)", "처치 수 = 99 (보스 1)");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.MaxChain) == "9", "최대 연쇄 = 9");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.MaxDamage) == "8889",
|
||
"최대 피해 = 합산(maxDamageBurst 8888.5 → 8889 · 0.5 는 올림) · 단일(1234.5)이 아니다 — 실측 \"" +
|
||
RunResultUI.RowValueText(WLRunResultRow.MaxDamage) + "\"");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.Level) == "11 → 14", "레벨 11 → 14");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.Gold) == "777", "골드 = 777");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.DeathsPotions) == "2 / 3", "사망/물약 = 2 / 3");
|
||
|
||
string loot = RunResultUI.RowValueText(WLRunResultRow.LootByGrade);
|
||
Chk(loot.Contains(">7") && loot.Contains(">3") && loot.Contains(">2") && loot.Contains(">1"),
|
||
"등급별 획득 = 1급 7 · 2급 3 · 4급 2 · 9급 1 (0 인 등급 생략 · 4칸)");
|
||
int colorTags = 0;
|
||
for (int i = 0; i + 6 < loot.Length; i++) if (loot.Substring(i, 7) == "<color=") colorTags++;
|
||
Chk(colorTags == 4, "등급 색 태그 4개(813f 팔레트 + 813y 밝기 하한) — 실측 " + colorTags);
|
||
var text = WLCombatTextSettings.Instance;
|
||
if (text != null)
|
||
N(" 등급 색: 1=" + text.GradeColorTag(1) + "→" + text.GradeColorTagBright(1) +
|
||
" 2=" + text.GradeColorTagBright(2) + " 4=" + text.GradeColorTagBright(4) + " 9=" + text.GradeColorTagBright(9));
|
||
Chk(RunResultUI.VisibleRows == 9, "본문 9행 표시 — 실측 " + RunResultUI.VisibleRows);
|
||
N(RunResultUI.Dump().TrimEnd());
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ④ 배열 복사 증명
|
||
static void Step4_ArrayCopy()
|
||
{
|
||
H("④ 🔴 배열 3종 즉시 복사(813p 재사용 계약) — 원본을 덮어써도 화면 값 불변");
|
||
string beforeLoot = RunResultUI.RowValueText(WLRunResultRow.LootByGrade);
|
||
float beforeZ0 = RunResultUI.CopiedZoneClearSec(0);
|
||
int beforeK0 = RunResultUI.CopiedZoneKills(0);
|
||
int beforeG1 = RunResultUI.CopiedLootByGrade(1);
|
||
int growBefore = RunResultUI.BufferGrowCount;
|
||
|
||
// 813p RunDirector 가 다음 런에서 하는 그대로 — **같은 배열 인스턴스**를 덮어쓴다
|
||
for (int i = 0; i < _srcZoneSec.Length; i++) { _srcZoneSec[i] = -999f; _srcZoneKills[i] = -999; }
|
||
for (int g = 0; g < _srcLoot.Length; g++) _srcLoot[g] = 999;
|
||
|
||
var c = RunResultUI.Copied;
|
||
Chk(!ReferenceEquals(c.zoneClearSec, _srcZoneSec), "zoneClearSec 참조가 원본과 다르다(자기 버퍼)");
|
||
Chk(!ReferenceEquals(c.zoneKills, _srcZoneKills), "zoneKills 참조가 원본과 다르다(자기 버퍼)");
|
||
Chk(!ReferenceEquals(c.lootByGrade, _srcLoot), "lootByGrade 참조가 원본과 다르다(자기 버퍼)");
|
||
Chk(Mathf.Approximately(RunResultUI.CopiedZoneClearSec(0), beforeZ0) && RunResultUI.CopiedZoneKills(0) == beforeK0,
|
||
"존 배열 값 불변 — clearSec[0]=" + RunResultUI.CopiedZoneClearSec(0).ToString("F2") +
|
||
" kills[0]=" + RunResultUI.CopiedZoneKills(0) + " (원본은 −999 로 덮어씀)");
|
||
Chk(RunResultUI.CopiedLootByGrade(1) == beforeG1, "loot 배열 값 불변 — [1]=" + RunResultUI.CopiedLootByGrade(1) + " (원본은 999)");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.LootByGrade) == beforeLoot, "결과 화면 등급 줄 문자열 불변");
|
||
N(" 원본 배열 현재값: zoneClearSec[0]=" + _srcZoneSec[0] + " zoneKills[0]=" + _srcZoneKills[0] + " loot[1]=" + _srcLoot[1]);
|
||
|
||
// 두 번째 런: 버퍼를 다시 잡지 않는다(= 런마다 할당 0)
|
||
var r2 = MakeResult(RunOutcome.Win);
|
||
RunResultUI.CopyResult(in r2);
|
||
Chk(RunResultUI.BufferGrowCount == growBefore, "두 번째 복사에서 버퍼 증설 0(런마다 배열 할당 0) — 증설 누계 " + RunResultUI.BufferGrowCount);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ⑤ Timeout
|
||
static void Step5_Timeout()
|
||
{
|
||
H("⑤ 시간 종료(Timeout)");
|
||
var s = WLRunUiSettings.Instance;
|
||
RunResultUI.Hide();
|
||
var r = MakeResult(RunOutcome.Timeout);
|
||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||
Chk(RunResultUI.IsOpen, "결과 화면 표시");
|
||
Chk(RunResultUI.TitleText == (s != null ? s.resultTimeoutTitle : ""),
|
||
"제목 = \"" + (s != null ? s.resultTimeoutTitle : "") + "\" — 실측 \"" + RunResultUI.TitleText + "\"");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.BossClear) == (s != null ? s.resultDashText : "—"),
|
||
"보스 처치 −1 → \"" + RunResultUI.RowValueText(WLRunResultRow.BossClear) + "\"(미발생 대시)");
|
||
Chk(RunResultUI.RowValueText(WLRunResultRow.Kills) == "99 (보스 0)", "보스 처치 0");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ⑥ Abandon
|
||
static void Step6_Abandon()
|
||
{
|
||
H("⑥ 이탈(Abandon) — 팝업 없음 · HUD 리셋만");
|
||
RunResultUI.Hide();
|
||
int shownBefore = RunResultUI.ShownCount;
|
||
int skipBefore = RunResultUI.SkipAbandonCount;
|
||
var r = MakeResult(RunOutcome.Abandon);
|
||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||
Chk(!RunResultUI.IsOpen, "팝업 0 (open=" + RunResultUI.IsOpen + ")");
|
||
Chk(RunResultUI.ShownCount == shownBefore, "표시 호출 0 (누계 " + RunResultUI.ShownCount + ")");
|
||
Chk(RunResultUI.SkipAbandonCount == skipBefore + 1, "Abandon 생략 카운터 +1");
|
||
Chk(!RunResultUI.Pending, "대기(pending) 0 — 나중에 저절로 뜨지 않는다");
|
||
Chk(!RunHud.Visible, "HUD 숨김(리셋)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ⑦ 부활 팝업 우선
|
||
static void Step7_RevivePriority()
|
||
{
|
||
H("⑦ 813i/813tj 부활 팝업과 동시 — 부활 우선 · 결과는 닫힌 뒤");
|
||
var dlg = _root != null ? _root.GetComponentsInChildren<ReviveDialog>(true) : new ReviveDialog[0];
|
||
Chk(dlg != null && dlg.Length > 0, "프리팹에서 ReviveDialog 발견 — " + (dlg != null ? dlg.Length : 0) + "개");
|
||
if (dlg == null || dlg.Length == 0) return;
|
||
var d = dlg[0];
|
||
N("ReviveDialog.Initialize : " + d.Initialize());
|
||
N("ReviveDialog.ShowNowOnce: " + d.ShowNowOnce());
|
||
Chk(ReviveDialog.Busy, "부활 팝업 Busy = " + ReviveDialog.Busy);
|
||
|
||
RunResultUI.Hide();
|
||
int deferBefore = RunResultUI.DeferredCount;
|
||
var r = MakeResult(RunOutcome.Win);
|
||
WLRunUiProbeHooks.RaiseRunEnded(in r);
|
||
Chk(!RunResultUI.IsOpen, "결과 화면 아직 안 뜬다(부활 우선)");
|
||
Chk(RunResultUI.Pending, "결과 대기 중 = " + RunResultUI.Pending);
|
||
Chk(RunResultUI.DeferredCount > deferBefore, "미룸 카운터 +" + (RunResultUI.DeferredCount - deferBefore));
|
||
|
||
d.HideNow();
|
||
Chk(!ReviveDialog.Busy, "부활 팝업 닫힘 → Busy=false");
|
||
N("Tick: " + RunResultUI.Tick(Time.unscaledTime));
|
||
Chk(RunResultUI.IsOpen, "부활 팝업이 닫힌 뒤 결과 화면 표시");
|
||
Chk(!RunResultUI.Pending, "대기 해제");
|
||
|
||
// [다음 런] · [닫기]
|
||
int nextBefore = RunResultUI.NextRunCount;
|
||
N("[다음 런] : " + RunResultUI.OnNextRun());
|
||
Chk(RunResultUI.NextRunCount == nextBefore + 1, "[다음 런] 클릭 경로 = RunDirector.Restart() 1회");
|
||
Chk(!RunResultUI.IsOpen, "[다음 런] 뒤 결과 화면 닫힘");
|
||
N("RunDirector 상태: phase=" + RunDirector.Phase + " runIndex=" + RunDirector.RunIndex +
|
||
" restartCount=" + RunDirector.RestartCount);
|
||
RunResultUI.Show();
|
||
N("[닫기] : " + RunResultUI.OnClose());
|
||
Chk(!RunResultUI.IsOpen, "[닫기] 뒤 결과 화면 닫힘");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ⑧ GC
|
||
static void Step8_Gc()
|
||
{
|
||
H("⑧ GC — 틱 100회 할당 0 (대조군 2종)");
|
||
// 계기: GC.GetAllocatedBytesForCurrentThread() = 이 스레드가 지금까지 할당한 누적 바이트(정밀).
|
||
// GC.GetTotalMemory 는 힙 페이지 단위라 이 규모에서 감도가 없다(대조군 B 로 실측 확인).
|
||
N("계기 = GC.GetTotalMemory (Boehm 힙 크기) + GC.CollectionCount(0) · " +
|
||
"GetAllocatedBytesForCurrentThread 절대값=" + GC.GetAllocatedBytesForCurrentThread() +
|
||
" (0 이면 이 런타임에 미구현 → 쓰지 않는다)");
|
||
|
||
// 본시험: 같은 초를 100번 틱한다 → 표시값이 안 바뀌므로 SetText 0회
|
||
RunHud.SetVisible(true);
|
||
WLRunUiProbeHooks.RaiseRunTick(40f, 200f, 0, 6, 12, 0, 4, RunPhase.Zones, 1); // 준비(첫 쓰기)
|
||
int wBefore = RunHud.TimerWrites + RunHud.PhaseWrites + RunHud.FillWrites;
|
||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||
long n0 = GC.GetAllocatedBytesForCurrentThread();
|
||
long m0 = GC.GetTotalMemory(true); int g0 = GC.CollectionCount(0);
|
||
for (int i = 0; i < 100; i++)
|
||
WLRunUiProbeHooks.RaiseRunTick(40f, 200f, 0, 6, 12, 0, 4, RunPhase.Zones, 1);
|
||
long n1 = GC.GetAllocatedBytesForCurrentThread();
|
||
long m1 = GC.GetTotalMemory(false); int g1 = GC.CollectionCount(0);
|
||
int wAfter = RunHud.TimerWrites + RunHud.PhaseWrites + RunHud.FillWrites;
|
||
N("본시험(같은 초 100틱): ΔGetTotalMemory=" + (m1 - m0) + " B · gc0 " + g0 + "→" + g1 +
|
||
" · 표시 쓰기 " + wBefore + "→" + wAfter + " (Δ할당 계기 참고값 " + (n1 - n0) + " B · 미구현)");
|
||
Chk(wAfter == wBefore, "🔴 구조 증명: 값이 그대로면 SetText/fillAmount 쓰기 0회 = 만들 문자열 자체가 없다");
|
||
Chk(m1 - m0 == 0 && g1 == g0, "ΔGC = 0 B · gc0 불변 (RunTick 100회)");
|
||
|
||
// 대조군 A: 초가 매번 바뀌는 100틱 — 값이 바뀌므로 SetText 를 100번 부른다
|
||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||
long a0 = GC.GetTotalMemory(true); int ag0 = GC.CollectionCount(0);
|
||
int wa0 = RunHud.TimerWrites;
|
||
for (int i = 0; i < 100; i++)
|
||
WLRunUiProbeHooks.RaiseRunTick(i, 200f - i, 0, 6, 12, 0, 4, RunPhase.Zones, 1);
|
||
long a1 = GC.GetTotalMemory(false);
|
||
N("대조군 A(초가 바뀌는 100틱 · SetText " + (RunHud.TimerWrites - wa0) + "회): ΔGetTotalMemory=" + (a1 - a0) +
|
||
" B · gc0 " + ag0 + "→" + GC.CollectionCount(0) +
|
||
" — 우리 코드의 mm:ss 는 char 버퍼(문자열 0)이고, 에디터에서는 TMP 가 " +
|
||
"`#if UNITY_EDITOR m_text = InternalTextBackingArrayToString()` 로 인스펙터용 문자열을 만든다(빌드에는 없는 줄).");
|
||
|
||
// 대조군 B: 측정 감도(813p·813i 선례)
|
||
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
||
long bt0 = GC.GetTotalMemory(true); int bg0 = GC.CollectionCount(0);
|
||
var sink = new List<string>(200);
|
||
for (int i = 0; i < 200; i++) sink.Add(new string((char)('a' + (i % 26)), 40000)); // ≈ 16 MB
|
||
long bt1 = GC.GetTotalMemory(false); int bg1 = GC.CollectionCount(0);
|
||
N("대조군 B(40,000자 문자열 200개 ≈ 16 MB): ΔGetTotalMemory=" + (bt1 - bt0) + " B · gc0 " + bg0 + "→" + bg1 +
|
||
" (sink=" + sink.Count + ") — 계기 감도 확인. 100 KB 규모는 Boehm 힙 여유분에 묻혀 Δ0 이 나온다(실측).");
|
||
Chk(bt1 - bt0 > 0, "측정 감도 있음(대조군 B ΔGetTotalMemory > 0)");
|
||
sink.Clear();
|
||
|
||
// mm:ss 포맷 자체 검증
|
||
var buf = new char[8];
|
||
Chk(new string(buf, 0, RunHud.FormatMMSS(buf, 0f)) == "00:00" &&
|
||
new string(buf, 0, RunHud.FormatMMSS(buf, -5f)) == "00:00" &&
|
||
new string(buf, 0, RunHud.FormatMMSS(buf, 59.4f)) == "01:00" &&
|
||
new string(buf, 0, RunHud.FormatMMSS(buf, 240f)) == "04:00" &&
|
||
new string(buf, 0, RunHud.FormatMMSS(buf, 6000f)) == "99:59",
|
||
"mm:ss 경계값(0 · 음수 · 올림 · 240 · 상한 99:59)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ⑨ C8
|
||
static void Step9_C8()
|
||
{
|
||
H("⑨ C8 롤백 — enabled = 0 이면 HUD·결과 0 · 813p 는 그대로");
|
||
var s = WLRunUiSettings.Instance;
|
||
bool runBefore = WLRunSettings.Enabled;
|
||
|
||
WLRunUiSettings.RuntimeDisabled = true;
|
||
Chk(!WLRunUiSettings.Enabled, "RuntimeDisabled=1 → Enabled=False");
|
||
N("HUD Tick: " + RunHud.Tick(Time.unscaledTime));
|
||
N("결과 Tick: " + RunResultUI.Tick(Time.unscaledTime));
|
||
Chk(!RunHud.Visible && !RunResultUI.IsOpen, "HUD 숨김 · 결과 닫힘");
|
||
N("Bind(off): " + RunHud.Bind(null));
|
||
WLRunUiSettings.RuntimeDisabled = false;
|
||
|
||
// 에셋 필드 자체(enabled_)도 같은 게이트를 탄다 — 메모리에서만 껐다가 즉시 되돌린다(저장 0)
|
||
if (s != null)
|
||
{
|
||
bool saved = s.enabled_;
|
||
s.enabled_ = false;
|
||
Chk(!WLRunUiSettings.Enabled, "asset.enabled_=0 → Enabled=False (같은 게이트)");
|
||
s.enabled_ = saved;
|
||
Chk(WLRunUiSettings.Enabled, "복구 후 Enabled=True");
|
||
}
|
||
Chk(WLRunSettings.Enabled == runBefore, "813p WLRunSettings.Enabled 불변 = " + WLRunSettings.Enabled + " (런 구조는 그대로 동작)");
|
||
}
|
||
|
||
// ─────────────────────────────────────────────── ⑩ 정리
|
||
static void Step10_Cleanup(GameObject prefabRoot)
|
||
{
|
||
H("⑩ 정리 — 구독 해제 · 노드 파괴 · 프리팹 저장 0");
|
||
try
|
||
{
|
||
N("RunHud : " + RunHud.Teardown());
|
||
N("RunResultUI: " + RunResultUI.Teardown());
|
||
bool zero = RunEvents.RunStarted.Count == 0 && RunEvents.RunTick.Count == 0 &&
|
||
RunEvents.ZoneCleared.Count == 0 && RunEvents.BossGateOpened.Count == 0 &&
|
||
RunEvents.BossStarted.Count == 0 && RunEvents.RunEnded.Count == 0;
|
||
Chk(zero, "RunEvents 구독자 전부 0 (Started " + RunEvents.RunStarted.Count + " Tick " + RunEvents.RunTick.Count +
|
||
" Zone " + RunEvents.ZoneCleared.Count + " Gate " + RunEvents.BossGateOpened.Count +
|
||
" Boss " + RunEvents.BossStarted.Count + " Ended " + RunEvents.RunEnded.Count + ")");
|
||
N("RunEvents.TotalRaised=" + RunEvents.TotalRaised + " last=" + RunEvents.LastEvent +
|
||
" · Dispatched(Tick)=" + RunEvents.RunTick.Dispatched);
|
||
Chk(Mathf.Approximately(Time.timeScale, 1f), "TimeScale 무접촉 = " + Time.timeScale);
|
||
}
|
||
catch (Exception e) { _o.AppendLine(" [FAIL] 정리 예외 " + e.GetType().Name + " : " + e.Message); _fail++; }
|
||
if (prefabRoot != null)
|
||
{
|
||
PrefabUtility.UnloadPrefabContents(prefabRoot); // 저장 인자 없음 = 저장하지 않는다
|
||
_root = null;
|
||
N("UnloadPrefabContents — 프리팹 저장 0");
|
||
}
|
||
}
|
||
}
|