Project_WL/AgentScripts/WL813q3_Probe.cs

385 lines
24 KiB
C#
Raw Normal View History

// WL-813q3 프로브 — 결과창 「보스 처치 : 미처치 (HP 남음 N %)」 가 813w6 `RunEnded` 페이로드
// `bossHpRatio` 를 쓰는지 실측(에디트 모드 · Play 0 · 로그인 0 · #813).
// unity command run_script --file AgentScripts/WL813q3_Probe.cs --entry WL813q3_Probe.RunAll
// 산출물: <worktree>/AgentScripts/WL813q3_PROBE.txt
//
// 🔴 에셋 저장 0 — WLRunUiSettings 값은 **메모리에서만** 바꾸고 finally 에서 원복한다.
// AssetDatabase.SaveAssets / EditorUtility.SetDirty 호출 **0**. 파일 크기·수정시각을 전후로 찍어 증거로 남긴다.
// 🔴 Gameplay 는 읽기·공개 API 호출만(파일 수정 0). Play 진입 0.
// 🔴 핵심 계측 = **「늦게 그려도 값이 안 바뀐다」**.
// RunEnded 를 받은 뒤 BossArena 가 미뤄 둔 Park(만피 복구)을 실제로 돌리고, 보스 HP 바 미러까지
// 100 % 로 만든 다음 결과창을 **다시 그려서** 문구가 그대로 24 % 인지 본다.
// 스위치를 끄면(813q2 동작) 같은 자리에서 100 % 가 나온다 = D-44 재현.
using System;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEditor;
using WL.UI;
using WL.Combat.Run;
using WL.Combat.Boss;
public static class WL813q3_Probe
{
const string kAsset = "Assets/WL/UI/Settings/Resources/WL/WLRunUiSettings.asset";
static StringBuilder _o;
static int _pass, _fail, _note;
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
static void N(string s) { _o.AppendLine(" " + s); }
static void Note(string s) { _note++; _o.AppendLine(" [NOTE] " + 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; } }
static string OutPath(string n) { return Path.Combine(Path.Combine(Root, "AgentScripts"), n); }
/// <summary>본문에서 한 행의 값 문자열을 뽑는다(노드 없이도 되는 유일한 경로 = BodyText).</summary>
static string BodyRow(string label)
{
string body = RunResultUI.BodyText;
if (string.IsNullOrEmpty(body)) return "";
var lines = body.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
int k = lines[i].IndexOf(" : ", StringComparison.Ordinal);
if (k < 0) continue;
if (lines[i].Substring(0, k) == label) return lines[i].Substring(k + 3);
}
return "";
}
/// <summary>합성 RunResult — 배열 3종은 813p 계약대로 길이만 맞춘다.</summary>
static RunResult Fake(RunOutcome outcome, float bossClearSec, float ratio, double hp, double maxHp)
{
return new RunResult
{
outcome = outcome,
runIndex = 1,
totalSec = 241f,
runSeconds = 270f,
zoneClearSec = new float[] { 61.5f, -1f },
zoneKills = new int[] { 17, 12 },
zoneCount = 2,
zonesCleared = 1,
bossGateSec = 170f,
bossStartSec = 173f,
bossClearSec = bossClearSec,
bossHpRatio = ratio,
bossHp = hp,
bossHpMax = maxHp,
kills = 29,
bossKills = bossClearSec >= 0f ? 1 : 0,
maxChain = 7,
maxDamage = 4321d,
maxDamageBurst = 8888.5d,
levelFrom = 11,
levelTo = 12,
lootByGrade = new int[] { 0, 5, 2, 0, 0, 0, 0, 0, 0, 0 },
lootTotal = 7,
gold = 777,
deaths = 1,
potionsUsed = 2,
skillCasts = 40,
time = 0f,
frame = 0,
};
}
public static string RunAll()
{
_o = new StringBuilder(16000);
_o.AppendLine("== WL-813q3 프로브 — 결과창 보스 HP = RunEnded 페이로드 스냅샷 ==");
_o.AppendLine(" " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion +
" · isPlaying=" + Application.isPlaying + " · project=" + Root);
WLRunUiSettings.ClearCache();
var s = WLRunUiSettings.Instance;
if (s == null) { _o.AppendLine(" [FAIL] WLRunUiSettings 에셋 없음 — 중단"); return Flush(); }
string abs = Path.Combine(Root, kAsset.Replace('/', Path.DirectorySeparatorChar));
var fi0 = new FileInfo(abs); fi0.Refresh();
long len0 = fi0.Length; DateTime mt0 = fi0.LastWriteTimeUtc;
// ── 원복용 스냅샷 ────────────────────────────────────────────────────
bool o_payload = s.resultBossHpFromPayload, o_fromUi = s.resultBossHpFromUi;
bool o_cards = s.resultCountCardPicksInUi, o_elites = s.resultCountElitesInUi;
bool o_enabled = s.enabled_, o_result = s.resultEnabled;
var o_rows = s.resultRows;
float o_barRatio = BossHpBar.RunBossHpRatio;
bool o_subscribed = RunResultUI.Subscribed;
try
{
// ── ⓪ 설정·행 배열 실측 ─────────────────────────────────────────
H("⓪ 설정 실측 — 신규 값 2개 · 신규 행 1개");
N("에셋 = " + kAsset + " (" + len0 + " B · " + mt0.ToString("yyyy-MM-dd HH:mm:ss") + " UTC)");
N("resultBossHpFromPayload=" + s.resultBossHpFromPayload + " · resultBossHpFromUi=" + s.resultBossHpFromUi +
" · resultCountCardPicksInUi=" + s.resultCountCardPicksInUi + " · resultCountElitesInUi=" + s.resultCountElitesInUi);
N("문구 = \"" + s.resultBossNotClearedHpFormat + "\" / \"" + s.resultBossNotClearedText + "\" · 카드 행 이름 = \"" + s.rowCardPicksLabel + "\"");
Chk(s.resultBossHpFromPayload, "resultBossHpFromPayload 기본 on(813w6 페이로드 우선)");
Chk(s.resultCountCardPicksInUi, "resultCountCardPicksInUi 기본 on");
Chk(!string.IsNullOrEmpty(s.rowCardPicksLabel), "rowCardPicksLabel 값 있음");
var sbRows = new StringBuilder();
bool hasCard = false, hasElite = false, hasBoss = false;
for (int i = 0; i < s.resultRows.Length; i++)
{
if (i > 0) sbRows.Append(" · ");
sbRows.Append(s.resultRows[i]);
if (s.resultRows[i] == WLRunResultRow.CardPicks) hasCard = true;
if (s.resultRows[i] == WLRunResultRow.EliteKills) hasElite = true;
if (s.resultRows[i] == WLRunResultRow.BossClear) hasBoss = true;
}
N("resultRows(" + s.resultRows.Length + ") = " + sbRows);
Chk(hasBoss, "BossClear 행 있음");
Chk(hasElite, "EliteKills 행 있음(813q2)");
Chk(hasCard, "CardPicks 행 있음(813q3 신설)");
// 상자 안에 11행이 들어가는지 산술 실측(제목 + 행 + 버튼)
float top = s.resultPadTopPx + s.resultTitleFontPx + s.resultTitleGapPx;
float rowsH = s.resultRowHeightPx * s.resultRows.Length;
float btnTop = s.resultBoxSizePx.y - (s.resultButtonBottomPx + s.resultButtonSizePx.y);
N("배치 = 제목영역 " + top + "px + 행 " + s.resultRows.Length + "×" + s.resultRowHeightPx + "=" + rowsH +
"px → 본문 끝 " + (top + rowsH) + "px · 버튼 상단 " + btnTop + "px · 상자 " + s.resultBoxSizePx.y + "px");
Chk(top + rowsH <= btnTop, "행이 버튼을 안 침범(" + (top + rowsH) + " ≤ " + btnTop + ") = 잘림 0(산술)");
// ── ① 813w6 계약 확인 ────────────────────────────────────────────
H("① 813w6 페이로드 계약 — RunResult 에 보스 HP 3필드");
var probe = Fake(RunOutcome.Timeout, -1f, 0.2439f, 439d, 1800d);
N("합성 RunResult · bossHpRatio=" + probe.bossHpRatio.ToString("F4") + " bossHp=" + probe.bossHp + " bossHpMax=" + probe.bossHpMax);
Chk(Math.Abs(probe.bossHpRatio - 0.2439f) < 1e-5f, "RunResult.bossHpRatio 필드 존재·대입(컴파일이 곧 증거)");
Chk(probe.bossHp == 439d && probe.bossHpMax == 1800d, "RunResult.bossHp / bossHpMax 필드 존재·대입");
N("구독자 수 RunEnded=" + RunEvents.RunEnded.Count + " RunStarted=" + RunEvents.RunStarted.Count);
// ── ② 가짜 RunEnded(0.2439) → 「HP 남음 24 %」 ────────────────────
H("② 가짜 RunEnded(bossHpRatio 0.2439) → 「미처치 (HP 남음 24 %)」");
if (!RunResultUI.Subscribed) RunResultUI.Subscribe(true);
RunResultUI.ResetState();
BossHpBar.RunBossHpRatio = -1f; // 바는 아무것도 안 물었다 = 폴백이 못 쓰는 상태
RunResultUI.CopyResult(in probe); // = OnRunEnded 가 즉시 하는 그 복사
N("BuildBody → " + RunResultUI.BuildBody(s));
string v2 = BodyRow(s.rowBossClearLabel);
N("보스 처치 행 = \"" + v2 + "\" · 원천 = " + RunResultUI.LastBossHpSource +
" · 스냅샷 = " + (RunResultUI.BossHpRatioAtEnd * 100f).ToString("F2") + "% (" +
RunResultUI.BossHpAtEnd + "/" + RunResultUI.BossHpMaxAtEnd + ")");
Chk(v2 == "미처치 (HP 남음 24 %)", "문구 = \"미처치 (HP 남음 24 %)\" (0.2439 → 24)");
Chk(RunResultUI.LastBossHpSource == "페이로드", "원천 = 페이로드(BossHpBar 를 안 읽었다)");
Chk(Math.Abs(RunResultUI.BossHpRatioAtEnd - 0.2439f) < 1e-5f, "BossHpRatioAtEnd = 0.2439(구조체째 복사)");
Chk(RunResultUI.BossHpAtEnd == 439d && RunResultUI.BossHpMaxAtEnd == 1800d, "BossHp/BossHpMax 스냅샷도 함께 복사");
// ── ③ 🔴 부활 미룸 = 늦게 그려도 스냅샷 유지 ───────────────────────
H("③ 🔴 부활 미룸(늦은 그리기) — Park 로 만피가 되어도 값 불변");
BossArena.SetBossHpForProbe(1f, 1800d, 1800d); // Park → On_Regen 만피
BossHpBar.RunBossHpRatio = 1f; // 813q2 가 읽던 UI 미러도 만피
int deferBefore = BossArena.DeferredParkCount;
BossArena.PumpDeferredPark(); // 813w6 이 발행 뒤로 미뤄 둔 고정을 실제로 실행
N("Park 실행 DeferredParkCount " + deferBefore + " → " + BossArena.DeferredParkCount +
" · BossHpBar.RunBossHpRatio = " + BossHpBar.RunBossHpRatio.ToString("F2") +
" · BossArena.LastBossHpRatio = " + BossArena.LastBossHpRatio.ToString("F2"));
string show3 = RunResultUI.Show(); // ← 부활 팝업이 닫힌 뒤의 「늦은 그리기」
N("늦은 Show() → " + show3);
string v3 = BodyRow(s.rowBossClearLabel);
N("보스 처치 행(늦게) = \"" + v3 + "\" · 원천 = " + RunResultUI.LastBossHpSource);
Chk(v3 == "미처치 (HP 남음 24 %)", "🔴 늦게 그려도 24 % 그대로(813w6 §6-1 잔여 해소)");
Chk(Math.Abs(RunResultUI.BossHpRatioAtEnd - 0.2439f) < 1e-5f, "스냅샷 버퍼가 만피에 안 덮인다");
H("③-B C8 — resultBossHpFromPayload = 0 이면 같은 자리에서 100 %(D-44 재현)");
s.resultBossHpFromPayload = false;
RunResultUI.BuildBody(s);
string v3b = BodyRow(s.rowBossClearLabel);
N("행 = \"" + v3b + "\" · 원천 = " + RunResultUI.LastBossHpSource);
Chk(v3b == "미처치 (HP 남음 100 %)", "스위치 off → 813q2 동작(만피 100 %) = D-44 재현");
Chk(RunResultUI.LastBossHpSource == "UI바", "off 일 때 원천 = UI바(BossHpBar)");
s.resultBossHpFromPayload = true;
RunResultUI.BuildBody(s);
Chk(BodyRow(s.rowBossClearLabel) == "미처치 (HP 남음 24 %)", "스위치 복구 → 다시 24 %");
// ── ④ 진짜 RunDirector.EndRun 경로 ─────────────────────────────────
H("④ 진짜 발행 경로 — RunDirector.StartRun → EndRun(Timeout)");
BossArena.SetBossHpForProbe(0.2439f, 439d, 1800d);
BossHpBar.RunBossHpRatio = -1f;
RunResultUI.ResetState();
RunDirector.StartRun("WL813q3 프로브");
int endedBefore = RunResultUI.EndedSeen; // StartRun 이 앞 런을 Abandon 으로 닫을 수 있다
N("StartRun 후 phase=" + RunDirector.Phase + " running=" + RunDirector.IsRunning +
" (EndedSeen=" + endedBefore + ")");
if (RunDirector.IsRunning)
{
RunDirector.EndRun(RunOutcome.Timeout);
N("EndRun 후 EndedSeen " + endedBefore + " → " + RunResultUI.EndedSeen +
" · LastResult.bossHpRatio=" + RunDirector.LastResult.bossHpRatio.ToString("F4") +
" · 결과창 스냅샷=" + RunResultUI.BossHpRatioAtEnd.ToString("F4"));
string v4 = BodyRow(s.rowBossClearLabel);
N("보스 처치 행(진짜 발행) = \"" + v4 + "\" · 원천 = " + RunResultUI.LastBossHpSource);
Chk(RunResultUI.EndedSeen == endedBefore + 1, "결과창이 RunEnded 를 실제로 받았다");
Chk(Math.Abs(RunResultUI.BossHpRatioAtEnd - RunDirector.LastResult.bossHpRatio) < 1e-6f,
"결과창 스냅샷 = RunDirector.LastResult.bossHpRatio (같은 값)");
if (RunDirector.LastResult.bossHpRatio >= 0f)
{
Chk(v4 == "미처치 (HP 남음 24 %)", "진짜 발행에서도 24 %");
// 발행 뒤 Park → 다시 그려도 그대로
BossArena.SetBossHpForProbe(1f, 1800d, 1800d);
BossHpBar.RunBossHpRatio = 1f;
BossArena.PumpDeferredPark();
RunResultUI.BuildBody(s);
Chk(BodyRow(s.rowBossClearLabel) == "미처치 (HP 남음 24 %)", "진짜 발행 + Park 뒤 늦은 그리기도 24 %");
}
else
{
Note("BossArena 가 에디트 모드에서 비활성(snapshotBossHpOnRunEnd/Active) — 페이로드 1 · " +
"값 경로는 ②③ 합성으로만 실측(진짜 발행의 값은 「미확인」)");
}
}
else
{
Note("RunDirector 가 에디트 모드에서 안 돈다(WLRunSettings/Cfg) — 진짜 발행 경로는 「미확인」");
}
// ── ⑤ 처치 케이스 · 값 없음 폴백 ───────────────────────────────────
H("⑤ 처치 케이스(MM:SS) · 페이로드 1 폴백 · 둘 다 없음");
RunResultUI.CopyResult(Fake(RunOutcome.Win, 154f, 0f, 0d, 1800d));
RunResultUI.BuildBody(s);
string v5a = BodyRow(s.rowBossClearLabel);
N("처치 런(bossClearSec 154) = \"" + v5a + "\"");
Chk(v5a == "02:34", "처치면 「02:34」(HP 는 안 본다)");
BossHpBar.RunBossHpRatio = 0.436f; // 813q2 가 읽던 값이 살아 있는 상태
RunResultUI.CopyResult(Fake(RunOutcome.Timeout, -1f, -1f, 0d, 0d));
RunResultUI.BuildBody(s);
string v5b = BodyRow(s.rowBossClearLabel);
N("페이로드 1 + 바 43.6 % = \"" + v5b + "\" · 원천 = " + RunResultUI.LastBossHpSource);
Chk(v5b == "미처치 (HP 남음 44 %)", "페이로드가 1 이면 813q2 폴백(44 %)");
Chk(RunResultUI.LastBossHpSource == "UI바", "폴백 원천 표시 = UI바");
BossHpBar.RunBossHpRatio = -1f;
RunResultUI.BuildBody(s);
string v5c = BodyRow(s.rowBossClearLabel);
N("둘 다 없음 = \"" + v5c + "\" · 원천 = " + RunResultUI.LastBossHpSource);
Chk(v5c == s.resultBossNotClearedText, "둘 다 없으면 퍼센트 없이 「미처치」");
Chk(RunResultUI.LastBossHpSource == "없음", "원천 = 없음");
// ── ⑥ 결과창 행 실측 ───────────────────────────────────────────────
H("⑥ 결과창 행 실측 — 엘리트 · 카드 선택 · 사망/물약 · 본문 전체");
N("813o ZonePickup.PicksThisRun(현재) = " + ZonePickup.PicksThisRun + " · PickCount=" + ZonePickup.PickCount);
RunResultUI.CopyResult(Fake(RunOutcome.Timeout, -1f, 0.2439f, 439d, 1800d));
RunResultUI.BuildBody(s);
N("카드 선택 종료스냅 = " + RunResultUI.CardPicksAtEnd + " · 엘리트 종료스냅 = " + RunResultUI.EliteKillsAtEnd);
N("카드 선택 행 = \"" + BodyRow(s.rowCardPicksLabel) + "\" · 엘리트 행 = \"" + BodyRow(s.rowEliteKillsLabel) +
"\" · 사망/물약 행 = \"" + BodyRow(s.rowDeathsLabel) + "\"");
Chk(BodyRow(s.rowCardPicksLabel) == RunResultUI.CardPicksAtEnd.ToString(), "카드 선택 행 = 종료 스냅샷 값");
Chk(BodyRow(s.rowEliteKillsLabel) == RunResultUI.EliteKillsAtEnd.ToString(), "엘리트 행 = 종료 스냅샷 값(813q2 그대로)");
Chk(BodyRow(s.rowDeathsLabel) == "1 / 2", "사망/물약 행 = 「1 / 2」(RunResult 값 그대로)");
Chk(RunResultUI.VisibleRows == s.resultRows.Length, "본문 행 수 = resultRows 길이 (" + RunResultUI.VisibleRows + ")");
N("본문 = " + RunResultUI.BodyText.Replace("\n", " | "));
// 카드 선택 스냅샷이 「런 종료 순간」인지 — 뒤에 카운터가 늘어도 화면은 안 바뀐다
N("(카드 카운터는 813o 소유 · 프로브는 읽기만 — 스냅샷 시점만 확인한다)");
// ── ⑦ GC ───────────────────────────────────────────────────────────
H("⑦ GC — 매 프레임 경로 0 B · 이번에 늘어난 줄 0 B");
RunResultUI.Tick(0f); RunResultUI.BuildBody(s); // 워밍업(첫 회 버퍼)
long g0 = GC.GetTotalMemory(false);
for (int i = 0; i < 100; i++) RunResultUI.Tick(i);
long g1 = GC.GetTotalMemory(false);
Chk(g1 - g0 == 0, "Tick ×100(매 프레임 경로) Δ = " + (g1 - g0) + " B");
int sink = 0; float fsink = 0f;
g0 = GC.GetTotalMemory(false);
for (int i = 0; i < 100; i++) { sink += ZonePickup.PicksThisRun; fsink += RunResultUI.BossHpRatioAtEnd; }
g1 = GC.GetTotalMemory(false);
Chk(g1 - g0 == 0, "이번에 늘어난 읽기(PicksThisRun + BossHpRatioAtEnd) ×100 Δ = " + (g1 - g0) + " B (sink " + sink + "/" + fsink.ToString("F1") + ")");
g0 = GC.GetTotalMemory(false);
for (int i = 0; i < 100; i++) RunResultUI.LastBossHpSource = "페이로드";
g1 = GC.GetTotalMemory(false);
Chk(g1 - g0 == 0, "LastBossHpSource 대입(리터럴) ×100 Δ = " + (g1 - g0) + " B");
g0 = GC.GetTotalMemory(false);
for (int i = 0; i < 100; i++) RunResultUI.BuildBody(s);
g1 = GC.GetTotalMemory(false);
N("참고 — BuildBody ×100 Δ = " + (g1 - g0) + " B (런 종료 1회 경로 · 813q 부터 본문 문자열을 만든다 · 매 프레임 아님)");
// ── ⑧ C8 롤백 ──────────────────────────────────────────────────────
H("⑧ C8 롤백 — 값 하나씩");
s.resultCountCardPicksInUi = false;
RunResultUI.BuildBody(s);
Chk(BodyRow(s.rowCardPicksLabel) == "0", "resultCountCardPicksInUi=0 → 카드 선택 행 「0」");
s.resultCountCardPicksInUi = true;
s.resultRows = new WLRunResultRow[] { WLRunResultRow.TotalTime, WLRunResultRow.BossClear };
RunResultUI.BuildBody(s);
Chk(RunResultUI.VisibleRows == 2 && BodyRow(s.rowCardPicksLabel) == "", "resultRows 에서 CardPicks 를 빼면 행이 사라진다");
s.resultRows = o_rows;
RunResultUI.BuildBody(s);
Chk(RunResultUI.VisibleRows == o_rows.Length, "행 배열 복구 = " + RunResultUI.VisibleRows + "행");
s.resultBossHpFromPayload = false; s.resultBossHpFromUi = false;
RunResultUI.BuildBody(s);
Chk(BodyRow(s.rowBossClearLabel) == s.resultBossNotClearedText, "둘 다 off → 퍼센트 없이 「미처치」");
s.resultBossHpFromPayload = o_payload; s.resultBossHpFromUi = o_fromUi;
WLRunUiSettings.RuntimeDisabled = true;
string tickOff = RunResultUI.Tick(0f);
N("RuntimeDisabled Tick → " + tickOff);
Chk(tickOff.StartsWith("off"), "RuntimeDisabled=1 → Tick 무동작");
WLRunUiSettings.RuntimeDisabled = false;
Chk(RunResultUI.Tick(0f) == "tick ok", "복구 → tick ok");
s.resultEnabled = false;
Chk(RunResultUI.Tick(0f).StartsWith("off"), "resultEnabled=0 → 무동작");
s.resultEnabled = o_result;
// ── ⑨ 뒷정리 확인 ──────────────────────────────────────────────────
H("⑨ 뒷정리 — 에셋 저장 0 · 구독·주입 원복");
BossArena.SetBossHpForProbe(-2f, 0d, 0d); // 주입 해제
BossHpBar.RunBossHpRatio = o_barRatio;
RunResultUI.ResetState();
if (!o_subscribed) RunResultUI.Subscribe(false);
N("BossArena 주입 해제 · BossHpBar.RunBossHpRatio=" + BossHpBar.RunBossHpRatio +
" · 결과창 구독=" + RunResultUI.Subscribed + "(시작 시 " + o_subscribed + ")");
N("BossArena.BossHpSnapshots=" + BossArena.BossHpSnapshots + " DeferredParkCount=" + BossArena.DeferredParkCount);
}
catch (Exception ex)
{
_fail++;
_o.AppendLine(" [FAIL] 예외 — " + ex.GetType().Name + " : " + ex.Message);
_o.AppendLine(ex.StackTrace);
}
finally
{
// 🔴 SO 원복(메모리만 · 저장 0)
s.resultBossHpFromPayload = o_payload; s.resultBossHpFromUi = o_fromUi;
s.resultCountCardPicksInUi = o_cards; s.resultCountElitesInUi = o_elites;
s.enabled_ = o_enabled; s.resultEnabled = o_result; s.resultRows = o_rows;
WLRunUiSettings.RuntimeDisabled = false;
try { BossArena.SetBossHpForProbe(-2f, 0d, 0d); } catch { }
BossHpBar.RunBossHpRatio = o_barRatio;
}
var fi1 = new FileInfo(abs); fi1.Refresh();
N("에셋 후 = " + fi1.Length + " B · " + fi1.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss") + " UTC");
Chk(fi1.Length == len0 && fi1.LastWriteTimeUtc == mt0, "에셋 파일 무변동 = 저장 0(SaveAssets/SetDirty 호출 0)");
N("SO 값 원복 = payload:" + s.resultBossHpFromPayload + " fromUi:" + s.resultBossHpFromUi +
" cards:" + s.resultCountCardPicksInUi + " elites:" + s.resultCountElitesInUi +
" enabled_:" + s.enabled_ + " result:" + s.resultEnabled + " 행수:" + s.resultRows.Length);
return Flush();
}
static string Flush()
{
_o.AppendLine();
_o.AppendLine("== 합계 PASS " + _pass + " / FAIL " + _fail + " / NOTE " + _note + " ==");
string p = OutPath("WL813q3_PROBE.txt");
try { File.WriteAllText(p, _o.ToString(), new UTF8Encoding(true)); } catch { }
return _o.ToString() + "\n(파일: " + p + ")";
}
}