Project_WL/AgentScripts/WL813m2_Probe.cs

490 lines
29 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.

// WL813m2_Probe.cs — #813m2(자동 물약) 에디트 모드 검증 프로브
// 에디트 모드 전용 · Play 불필요 · 로그인 0 · 디스크 에셋/씬 무수정(런타임 클론 SO 를 리플렉션으로 주입 · 임시 오브젝트 즉시 파괴)
//
// 상주 에디터: unity command run_script --project-path <wt> --file AgentScripts/WL813m2_Probe.cs --entry WL813m2_Probe.RunAll
// 결과: Console + AgentScripts/WL813m2_PROBE.txt(커밋) + Screenshots_WL/WL813m2/probe_<HHmmss>.txt (RESULT PASS / RESULT FAIL n)
//
// 검사(발주서 WL-813m2 §1-3)
// ① 에셋 실측: 디스크 autoPotion* 5값 · 코드 기본값
// ② 임계 이하 자동 사용 1회(HP 30 % · MaxHP 310 실측값 · +124 → 217) · 임계 위(50 %)는 0
// ③ 쿨 중 재사용 0(거절 이벤트도 0 — IsReady 로 먼저 거른다)
// ④ 잔량 0 이면 0
// ⑤ 자동전투 OFF 면 0(옵션 requireAutoCombat=0 이면 다시 쓴다)
// ⑥ 사망·부활 중 0
// ⑦ GC 0(폴링 200회 · 대조군 대비)
// ⑧ C8: autoPotionEnabled=0 → Active=false · 사용 0 · 이벤트 0
// ⑨ 813y 버튼 표시 왕복: WLSurvivalUiBridge.Install() 뒤 PotionButton.CountProvider/CooldownRemainProvider 가 자동 사용을 그대로 읽는가
// ⑩ 정리(MyPC 원복 · 클론 파괴 · 캐시 복구 · 러너 0 · 씬 dirty 0)
//
// 에디트 모드 한계: ServerInfo(eCommon.Auto) 가 없다 → AutoPotion.AutoStateOverride 로 자동전투 상태를 준다.
// 실제 플래그 경로(813x AutoCombatOnEnter.IsAutoOn)는 Play 필요 = 병합 후 QA.
using System;
using System.IO;
using System.Reflection;
using System.Text;
using UnityEngine;
using WL.Combat.Survival;
public static class WL813m2_Probe
{
const string OutDir = "Screenshots_WL/WL813m2";
const string OutCommit = "AgentScripts/WL813m2_PROBE.txt";
const BindingFlags NP = BindingFlags.NonPublic | BindingFlags.Instance;
// PC MaxHP 실측 310(813u §표 B ④ · Q3·Q4-a 실측) — 자동 회복량 0.4 × 310 = 124
const double MaxHp = 310d;
static WLSurvivalSettings s_clone;
public static object RunAll()
{
var sb = new StringBuilder();
sb.AppendLine("# WL813m2 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0)");
int fail = 0;
float ts0 = Time.timeScale;
var pc0 = MyValue.MyPC;
bool dirty0 = UnityEngine.SceneManagement.SceneManager.GetActiveScene().isDirty;
try { fail += Asset(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ① 예외 " + ex); }
try { fail += Basic(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ② 예외 " + ex); }
try { fail += Cooldown(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ③ 예외 " + ex); }
try { fail += Empty(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ④ 예외 " + ex); }
try { fail += Manual(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑤ 예외 " + ex); }
try { fail += Death(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑥ 예외 " + ex); }
try { fail += Gc(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑦ 예외 " + ex); }
try { fail += C8(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑧ 예외 " + ex); }
try { fail += Button(sb); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑨ 예외 " + ex); }
try { fail += Cleanup(sb, ts0, pc0, dirty0); } catch (Exception ex) { fail++; sb.AppendLine("[FAIL] ⑩ 예외 " + ex); }
sb.AppendLine(fail == 0 ? "RESULT PASS" : "RESULT FAIL " + fail);
var s = sb.ToString();
Debug.Log(s);
try
{
Directory.CreateDirectory(OutDir);
File.WriteAllText(Path.Combine(OutDir, "probe_" + DateTime.Now.ToString("HHmmss") + ".txt"), s);
File.WriteAllText(OutCommit, s);
}
catch { /* 출력 저장 실패는 판정에 영향 없음 */ }
return s;
}
// ───────────────────────────────────────── 도구
static string P(bool ok) { 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(); }
/// <summary>디스크 에셋을 건드리지 않는 런타임 클론을 WLSurvivalSettings 캐시에 주입한다(813i 프로브와 같은 방식).</summary>
static WLSurvivalSettings UseClone()
{
var orig = WLSurvivalSettings.Instance;
if (orig == null) return null;
if (s_clone == null)
{
s_clone = UnityEngine.Object.Instantiate(orig);
s_clone.hideFlags = HideFlags.HideAndDontSave;
s_clone.name = "WLSurvivalSettings(probe clone)";
}
var fc = typeof(WLSurvivalSettings).GetField("s_cached", BindingFlags.NonPublic | BindingFlags.Static);
var fd = typeof(WLSurvivalSettings).GetField("s_lookupDone", BindingFlags.NonPublic | BindingFlags.Static);
if (fc == null || fd == null) return null;
fc.SetValue(null, s_clone); fd.SetValue(null, true);
return s_clone;
}
/// <summary>기본 실험 조건(생존 축 on · 물약 3/40 %/쿨 5 s · 자동 물약 on · HUD 회복 숫자 off).</summary>
static WLSurvivalSettings Arm()
{
var st = UseClone();
if (st == null) return null;
st.enabled = true; st.verboseLog = false;
st.potionCountPerRun = 3; st.potionHealRate = 0.4f; st.potionCooldownSeconds = 5f;
st.potionShowHealNumber = false; st.potionRefuseAtFullHp = true;
st.autoPotionEnabled = true; st.autoPotionHpRatio = 0.35f;
st.autoPotionRequireAutoCombat = true; st.autoPotionCheckSeconds = 0.25f; st.autoPotionLog = false;
PotionUse.ResetDiagnostics(); AutoPotion.ResetDiagnostics(); DeathFlow.ResetDiagnostics();
AutoPotion.AutoStateOverride = 1; // 에디트 모드엔 ServerInfo 가 없다 = 자동전투 ON 대역
return st;
}
static PCActor MakePC(string name, double maxHp, double hp)
{
var go = new GameObject("__WL813m2_" + name);
go.hideFlags = HideFlags.HideAndDontSave; // 씬에 저장되지 않는다
var pc = go.AddComponent<PCActor>();
pc.m_Role = eRole.PC; pc.m_SubRole = eSubRol.None;
var stat = new ActorStatInfo(eRole.PC);
stat.Set_Stat(eStat.MaxHP, maxHp); stat.Set_Stat(eStat.HP, hp);
typeof(Actor).GetField("m_Stat", NP).SetValue(pc, stat);
typeof(Actor).GetField("m_Enemy", NP).SetValue(pc, false);
MyValue.MyPC = pc;
return pc;
}
static void SetHp(Actor a, double hp) { a.Get_StatInfo().Set_Stat(eStat.HP, hp); }
static void SetDead(Actor a, bool dead) { typeof(Actor).GetField("DeadStatus", NP).SetValue(a, dead); }
static void Kill(params UnityEngine.Object[] objs)
{
foreach (var o in objs)
{
if (o == null) continue;
var c = o as Component;
UnityEngine.Object.DestroyImmediate(c != null ? c.gameObject : o);
}
}
/// <summary>러너 1프레임 대역 — SurvivalRunner.Update() 와 같은 순서(PotionUse 먼저 · AutoPotion 나중).</summary>
static void Step(float t) { PotionUse.Tick(t); AutoPotion.ForceTick(t); }
// ───────────────────────────────────────── ① 에셋 실측
static int Asset(StringBuilder sb)
{
WLSurvivalSettings.ClearCache();
var st = WLSurvivalSettings.Instance;
if (st == null) { sb.AppendLine("[FAIL] ① 에셋 없음 " + WLSurvivalSettings.ResourcesPath); return 1; }
var def = ScriptableObject.CreateInstance<WLSurvivalSettings>();
sb.AppendLine("[INFO] ① 디스크 = enabled " + st.enabled
+ " · autoPotionEnabled " + st.autoPotionEnabled
+ " · hpRatio " + st.autoPotionHpRatio.ToString("F2")
+ " · requireAutoCombat " + st.autoPotionRequireAutoCombat
+ " · checkSeconds " + st.autoPotionCheckSeconds.ToString("F2")
+ " · log " + st.autoPotionLog
+ " | 물약 " + st.potionCountPerRun + "개 · " + (st.potionHealRate * 100f).ToString("F0") + "% · 쿨 " + st.potionCooldownSeconds.ToString("F0") + "s");
sb.AppendLine("[INFO] ① 코드 기본 = autoPotionEnabled " + def.autoPotionEnabled + " · hpRatio " + def.autoPotionHpRatio.ToString("F2")
+ " · requireAutoCombat " + def.autoPotionRequireAutoCombat + " · checkSeconds " + def.autoPotionCheckSeconds.ToString("F2"));
// 낭비 0 조건: 임계 + 회복률 ≤ 1 이면 회복이 MaxHP 를 넘지 않는다
bool noWaste = st.autoPotionHpRatio + st.potionHealRate <= 1.0f + 1e-4f;
sb.AppendLine(P(noWaste) + " ① 회복 낭비 0 조건: hpRatio " + st.autoPotionHpRatio.ToString("F2") + " + healRate "
+ st.potionHealRate.ToString("F2") + " = " + (st.autoPotionHpRatio + st.potionHealRate).ToString("F2") + " ≤ 1.00");
Kill(def);
return noWaste ? 0 : 1;
}
// ───────────────────────────────────────── ② 임계 이하 자동 사용 1회 · 임계 위 0
static int Basic(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ② 클론 주입 실패"); return 1; }
int used = 0, changed = 0;
WL.Combat.Core.CombatHandler<PotionEvent> onUsed = (in PotionEvent e) => { used++; };
WL.Combat.Core.CombatHandler<PotionEvent> onChanged = (in PotionEvent e) => { changed++; };
PotionUse.Used.Add(onUsed); PotionUse.Changed.Add(onChanged);
var pc = MakePC("basic", MaxHp, MaxHp * 0.5d); // 155 = 50 %
int fails = 0;
try
{
Step(0f);
bool a = AutoPotion.Applied == 0 && AutoPotion.SkippedHpOk == 1 && used == 0 && Near(pc.Get_HP(), 155d)
&& PotionUse.Remaining == 3;
sb.AppendLine(P(a) + " ②a 임계 위(50 % > 35 %) 사용 0 — applied=" + AutoPotion.Applied + " skipHpOk=" + AutoPotion.SkippedHpOk
+ " hp=" + pc.Get_HP().ToString("F0") + " remaining=" + PotionUse.Remaining);
if (!a) fails++;
SetHp(pc, MaxHp * 0.3d); // 93 = 30 %
Step(1f);
bool b = AutoPotion.Applied == 1 && AutoPotion.Attempts == 1 && AutoPotion.Refusals == 0
&& Near(pc.Get_HP(), 217d) && Near(AutoPotion.LastHealed, 124d)
&& PotionUse.Remaining == 2 && used == 1 && AutoPotion.LastResult == PotionResult.Used;
sb.AppendLine(P(b) + " ②b 임계 이하(30 % ≤ 35 %) 자동 사용 1회 — hp 93→" + pc.Get_HP().ToString("F0") + " (기대 217 · +"
+ AutoPotion.LastHealed.ToString("F0") + ") remaining=" + PotionUse.Remaining + " Used 이벤트=" + used
+ " lastHpRatio=" + AutoPotion.LastHpRatio.ToString("F2") + " result=" + AutoPotion.LastResult
+ " · 813i Changed 이벤트=" + changed + "(813y 버튼이 구독)");
if (!b) fails++;
// 자동 사용 뒤 HP 가 임계 위로 올라갔다 = 다음 폴링에서 또 쓰지 않는다(쿨과 별개 이유)
int hpOk0 = AutoPotion.SkippedHpOk;
Step(20f); // 쿨(5 s) 완전히 지난 시점
bool c = AutoPotion.Applied == 1 && AutoPotion.SkippedHpOk == hpOk0 + 1 && PotionUse.Remaining == 2;
sb.AppendLine(P(c) + " ②c 회복 뒤(70 %) 쿨이 끝나도 재사용 0 — applied=" + AutoPotion.Applied
+ " skipHpOk=" + AutoPotion.SkippedHpOk + " remaining=" + PotionUse.Remaining);
if (!c) fails++;
sb.AppendLine("[INFO] ② " + AutoPotion.Dump());
}
finally { PotionUse.Used.Remove(onUsed); PotionUse.Changed.Remove(onChanged); MyValue.MyPC = null; Kill(pc); }
return fails;
}
// ───────────────────────────────────────── ③ 쿨 중 재사용 0(거절 이벤트도 0)
static int Cooldown(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ③ 클론 주입 실패"); return 1; }
int refused = 0;
WL.Combat.Core.CombatHandler<PotionEvent> onChanged = (in PotionEvent e) => { if (e.change == PotionChange.Refused) refused++; };
PotionUse.Changed.Add(onChanged);
var pc = MakePC("cd", MaxHp, MaxHp * 0.3d);
int fails = 0;
try
{
Step(0f); // 1회 사용 → 쿨 5 s
SetHp(pc, MaxHp * 0.1d); // 다시 위험(31 HP) — 임계 조건은 만족하지만 쿨 중
for (int i = 1; i <= 19; i++) Step(i * 0.25f); // t = 0.25 ~ 4.75 s (쿨 안)
bool a = AutoPotion.Applied == 1 && AutoPotion.Attempts == 1 && refused == 0
&& AutoPotion.SkippedNotReady == 19 && PotionUse.Remaining == 2 && Near(pc.Get_HP(), 31d);
sb.AppendLine(P(a) + " ③a 쿨 중 19회 폴링 재사용 0 — applied=" + AutoPotion.Applied + " attempts=" + AutoPotion.Attempts
+ " skipNotReady=" + AutoPotion.SkippedNotReady + " · 813i Refused 이벤트=" + refused + "(기대 0 · TryUse 를 부르지 않는다)"
+ " hp=" + pc.Get_HP().ToString("F0"));
if (!a) fails++;
Step(5f); // 쿨 종료 프레임
bool b = AutoPotion.Applied == 2 && PotionUse.Remaining == 1 && Near(pc.Get_HP(), 155d);
sb.AppendLine(P(b) + " ③b 쿨 종료(t=5.0) 즉시 사용 — applied=" + AutoPotion.Applied + " hp 31→" + pc.Get_HP().ToString("F0")
+ " (기대 155) remaining=" + PotionUse.Remaining);
if (!b) fails++;
}
finally { PotionUse.Changed.Remove(onChanged); MyValue.MyPC = null; Kill(pc); }
return fails;
}
// ───────────────────────────────────────── ④ 잔량 0 이면 0
static int Empty(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ④ 클론 주입 실패"); return 1; }
int refused = 0;
WL.Combat.Core.CombatHandler<PotionEvent> onChanged = (in PotionEvent e) => { if (e.change == PotionChange.Refused) refused++; };
PotionUse.Changed.Add(onChanged);
var pc = MakePC("empty", MaxHp, MaxHp * 0.3d);
int fails = 0;
try
{
float t = 0f;
for (int i = 0; i < 3; i++) { SetHp(pc, MaxHp * 0.3d); Step(t); t += 6f; } // 3개 전부 자동 사용
bool a = AutoPotion.Applied == 3 && PotionUse.Remaining == 0;
SetHp(pc, MaxHp * 0.1d);
int notReady0 = AutoPotion.SkippedNotReady;
for (int i = 0; i < 8; i++) { Step(t); t += 1f; } // 잔량 0 인 채 8회 폴링
bool b = AutoPotion.Applied == 3 && AutoPotion.Attempts == 3 && refused == 0
&& AutoPotion.SkippedNotReady == notReady0 + 8 && Near(pc.Get_HP(), 31d);
sb.AppendLine(P(a && b) + " ④ 잔량 0 사용 0 — 3개 소진 applied=" + AutoPotion.Applied + " remaining=" + PotionUse.Remaining
+ " · 이후 8회 폴링 attempts=" + AutoPotion.Attempts + " skipNotReady(+8)=" + (AutoPotion.SkippedNotReady - notReady0)
+ " Refused 이벤트=" + refused + " hp=" + pc.Get_HP().ToString("F0"));
if (!(a && b)) fails++;
PotionUse.ResetRun("probe run 2"); // 다음 런 = 다시 3개
SetHp(pc, MaxHp * 0.3d);
Step(t + 10f);
bool c = PotionUse.Remaining == 2 && AutoPotion.Applied == 4;
sb.AppendLine(P(c) + " ④b 런 리셋 뒤 다시 자동 사용 — remaining=" + PotionUse.Remaining + " applied=" + AutoPotion.Applied);
if (!c) fails++;
}
finally { PotionUse.Changed.Remove(onChanged); MyValue.MyPC = null; Kill(pc); }
return fails;
}
// ───────────────────────────────────────── ⑤ 자동전투 OFF 면 0
static int Manual(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ⑤ 클론 주입 실패"); return 1; }
var pc = MakePC("manual", MaxHp, MaxHp * 0.3d);
int fails = 0;
try
{
AutoPotion.AutoStateOverride = -1; // 수동 전투
for (int i = 0; i < 10; i++) Step(i * 0.3f);
bool a = AutoPotion.Applied == 0 && AutoPotion.Attempts == 0 && AutoPotion.SkippedManual == 10
&& PotionUse.Remaining == 3 && Near(pc.Get_HP(), 93d);
sb.AppendLine(P(a) + " ⑤a 자동전투 OFF 10회 폴링 사용 0 — applied=" + AutoPotion.Applied + " skipManual=" + AutoPotion.SkippedManual
+ " remaining=" + PotionUse.Remaining + " hp=" + pc.Get_HP().ToString("F0") + " · 버튼 수동 사용은 그대로 가능");
if (!a) fails++;
var manual = PotionUse.TryUse(pc); // 813j 버튼 경로는 자동전투와 무관
bool a2 = manual == PotionResult.Used && PotionUse.Remaining == 2;
sb.AppendLine(P(a2) + " ⑤b 자동전투 OFF 에서도 버튼(수동) 사용은 정상 — " + manual + " remaining=" + PotionUse.Remaining);
if (!a2) fails++;
st.autoPotionRequireAutoCombat = false; // 옵션 SO 를 끄면 수동 전투에서도 자동 사용
SetHp(pc, MaxHp * 0.2d);
Step(30f);
bool b = AutoPotion.Applied == 1 && PotionUse.Remaining == 1;
sb.AppendLine(P(b) + " ⑤c requireAutoCombat=0 이면 수동 전투에서도 자동 사용 — applied=" + AutoPotion.Applied
+ " remaining=" + PotionUse.Remaining + " hp=" + pc.Get_HP().ToString("F0"));
if (!b) fails++;
}
finally { st.autoPotionRequireAutoCombat = true; AutoPotion.AutoStateOverride = 1; MyValue.MyPC = null; Kill(pc); }
return fails;
}
// ───────────────────────────────────────── ⑥ 사망 · 부활 중 0
static int Death(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ⑥ 클론 주입 실패"); return 1; }
var pc = MakePC("death", MaxHp, MaxHp * 0.3d);
int fails = 0;
try
{
SetDead(pc, true);
Step(0f);
bool a = AutoPotion.Applied == 0 && AutoPotion.SkippedNoPc == 1 && PotionUse.Remaining == 3;
SetDead(pc, false);
DeathFlow.OnPCDied(pc); // state = Dying
int noPc0 = AutoPotion.SkippedNoPc;
for (int i = 0; i < 5; i++) Step(1f + i * 0.3f);
bool b = DeathFlow.State != DeathState.None && AutoPotion.Applied == 0 && AutoPotion.SkippedDeath == 5
&& AutoPotion.SkippedNoPc == noPc0 && PotionUse.Remaining == 3;
sb.AppendLine(P(a && b) + " ⑥ 사망·부활 중 사용 0 — 사망 플래그 skipNoPc=" + AutoPotion.SkippedNoPc
+ " · DeathFlow state=" + DeathFlow.State + " 5회 폴링 skipDeath=" + AutoPotion.SkippedDeath
+ " applied=" + AutoPotion.Applied + " remaining=" + PotionUse.Remaining);
if (!(a && b)) fails++;
DeathFlow.Abort("probe");
SetHp(pc, MaxHp * 0.3d);
Step(10f);
bool c = AutoPotion.Applied == 1;
sb.AppendLine(P(c) + " ⑥b 부활 흐름 종료 뒤에는 다시 자동 사용 — applied=" + AutoPotion.Applied
+ " state=" + DeathFlow.State + " hp=" + pc.Get_HP().ToString("F0"));
if (!c) fails++;
MyValue.MyPC = null; // PC 없음
int noPc1 = AutoPotion.SkippedNoPc;
Step(20f);
bool d = AutoPotion.SkippedNoPc == noPc1 + 1 && AutoPotion.Applied == 1;
sb.AppendLine(P(d) + " ⑥c PC 없음 무동작 — skipNoPc=" + AutoPotion.SkippedNoPc + " applied=" + AutoPotion.Applied);
if (!d) fails++;
}
finally { DeathFlow.Abort("probe cleanup"); MyValue.MyPC = null; Kill(pc); }
return fails;
}
// ───────────────────────────────────────── ⑦ GC 0
static int Gc(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ⑦ 클론 주입 실패"); return 1; }
st.autoPotionLog = false;
var pc = MakePC("gc", MaxHp, MaxHp * 0.6d); // 임계 위 = 매 폴링이 SkippedHpOk 경로
int fails = 0;
try
{
for (int i = 0; i < 50; i++) Step(i * 0.1f); // 워밍업(지연 초기화 · 델리게이트 캐시)
GC.Collect(); GC.WaitForPendingFinalizers();
long a0 = Alloc(); long m0 = Mono();
for (int i = 0; i < 200; i++) Step(100f + i * 0.1f);
long a1 = Alloc(); long m1 = Mono();
// 대조군 — Mono 힙은 4 KB 페이지 단위로만 늘어난다(813i 실측) → 800 KB 를 할당해 감도를 증명한다
GC.Collect(); GC.WaitForPendingFinalizers();
long b0 = Alloc();
var sink = new byte[200][];
for (int i = 0; i < 200; i++) sink[i] = new byte[4096];
long b1 = Alloc();
bool ok = (a1 - a0) <= 0 && (b1 - b0) > 0;
sb.AppendLine(P(ok) + " ⑦ GC 0 — 폴링 200회 Δ(GetTotalMemory)=" + (a1 - a0) + " B · Δ(MonoUsedSize)=" + (m1 - m0)
+ " B (기대 ≤ 0) · 대조군(byte[4096]×200 = 800 KB) Δ=" + (b1 - b0) + " B (감도 증명 · sink=" + sink.Length + ")"
+ " · applied=" + AutoPotion.Applied + "(기대 0) skipHpOk=" + AutoPotion.SkippedHpOk);
if (!ok) fails++;
}
finally { MyValue.MyPC = null; Kill(pc); }
return fails;
}
// ───────────────────────────────────────── ⑧ C8 롤백
static int C8(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ⑧ 클론 주입 실패"); return 1; }
int changed = 0;
WL.Combat.Core.CombatHandler<PotionEvent> onChanged = (in PotionEvent e) => { changed++; };
PotionUse.Changed.Add(onChanged);
var pc = MakePC("c8", MaxHp, MaxHp * 0.1d); // 10 % = 임계 한참 아래
int fails = 0;
try
{
st.autoPotionEnabled = false;
bool act0 = AutoPotion.Active;
for (int i = 0; i < 20; i++) Step(i * 0.5f);
bool a = !act0 && AutoPotion.Applied == 0 && AutoPotion.Attempts == 0 && changed == 0
&& PotionUse.Remaining == 3 && Near(pc.Get_HP(), 31d)
&& AutoPotion.SkippedHpOk == 0 && AutoPotion.SkippedNotReady == 0; // 폴링 자체가 없다
sb.AppendLine(P(a) + " ⑧a autoPotionEnabled=0 → Active=" + act0 + " · 20회 폴링 applied=" + AutoPotion.Applied
+ " attempts=" + AutoPotion.Attempts + " skip 전부 0=" + (AutoPotion.SkippedHpOk == 0 && AutoPotion.SkippedNotReady == 0)
+ " · 813i 이벤트=" + changed + " remaining=" + PotionUse.Remaining + " hp=" + pc.Get_HP().ToString("F0"));
if (!a) fails++;
st.autoPotionEnabled = true; st.enabled = false; // 생존 축 전체 off
bool act1 = AutoPotion.Active;
for (int i = 0; i < 5; i++) Step(20f + i * 0.5f);
bool b = !act1 && AutoPotion.Applied == 0 && changed == 0 && Near(pc.Get_HP(), 31d);
sb.AppendLine(P(b) + " ⑧b 생존 축 enabled=0 → Active=" + act1 + " · applied=" + AutoPotion.Applied
+ " 이벤트=" + changed + " hp=" + pc.Get_HP().ToString("F0"));
if (!b) fails++;
st.enabled = true;
AutoPotion.RuntimeDisabled = true;
bool act2 = AutoPotion.Active;
for (int i = 0; i < 5; i++) Step(40f + i * 0.5f);
bool c = !act2 && AutoPotion.Applied == 0 && changed == 0;
AutoPotion.RuntimeDisabled = false;
Step(60f);
bool d = AutoPotion.Active && AutoPotion.Applied == 1 && PotionUse.Remaining == 2;
sb.AppendLine(P(c && d) + " ⑧c RuntimeDisabled=1 사용 0(Active=" + act2 + ") → 0 으로 되돌리면 applied="
+ AutoPotion.Applied + " remaining=" + PotionUse.Remaining);
if (!(c && d)) fails++;
}
finally { PotionUse.Changed.Remove(onChanged); AutoPotion.RuntimeDisabled = false; MyValue.MyPC = null; Kill(pc); }
return fails;
}
// ───────────────────────────────────────── ⑨ 813y 물약 버튼 표시 왕복(UI 파일 수정 0 · public API 만)
static int Button(StringBuilder sb)
{
var st = Arm(); if (st == null) { sb.AppendLine("[FAIL] ⑨ 클론 주입 실패"); return 1; }
var pc = MakePC("btn", MaxHp, MaxHp * 0.3d);
bool installedHere = false;
int fails = 0;
try
{
string inst = WL.UI.WLSurvivalUiBridge.Install();
installedHere = inst.StartsWith("연결");
var cp = WL.UI.PotionButton.CountProvider;
var cr = WL.UI.PotionButton.CooldownRemainProvider;
var ct = WL.UI.PotionButton.CooldownTotalProvider;
if (cp == null || cr == null || ct == null)
{
sb.AppendLine("[FAIL] ⑨ 813y 연결 없음 — count=" + (cp != null) + " cdRemain=" + (cr != null) + " cdTotal=" + (ct != null) + " (" + inst + ")");
return 1;
}
int before = cp();
Step(0f);
int after = cp();
float cdRemain = cr(), cdTotal = ct();
bool a = before == 3 && after == 2 && AutoPotion.Applied == 1
&& Math.Abs(cdRemain - 5f) < 1e-3f && Math.Abs(cdTotal - 5f) < 1e-3f;
sb.AppendLine(P(a) + " ⑨ 버튼 표시 = 자동 사용을 그대로 읽는다 — CountProvider " + before + " → " + after
+ " · CooldownRemainProvider " + cdRemain.ToString("F2") + "/" + cdTotal.ToString("F2") + "s"
+ " · PotionButton.Update()→Refresh() 가 매 프레임 이 provider 를 다시 읽는다(PotionButton.cs:108) → 표시 갱신에 UI 수정 0");
if (!a) fails++;
}
finally
{
if (installedHere) WL.UI.WLSurvivalUiBridge.Uninstall();
MyValue.MyPC = null; Kill(pc);
}
return fails;
}
// ───────────────────────────────────────── ⑩ 정리
static int Cleanup(StringBuilder sb, float ts0, PCActor pc0, bool dirty0)
{
AutoPotion.ResetDiagnostics(); PotionUse.ResetDiagnostics(); DeathFlow.ResetDiagnostics();
Kill(s_clone); s_clone = null;
WLSurvivalSettings.ClearCache(); WLSurvivalSettings.RuntimeOverride = 0;
MyValue.MyPC = pc0;
var leftovers = UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None);
int n = 0;
for (int i = 0; i < leftovers.Length; i++)
if (leftovers[i] != null && leftovers[i].name.StartsWith("__WL813m2_")) { n++; Kill(leftovers[i]); }
var st = WLSurvivalSettings.Instance;
bool assetBack = st != null && st.autoPotionEnabled && Math.Abs(st.autoPotionHpRatio - 0.35f) < 1e-4f && st.enabled;
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
bool dirty = scene.isDirty;
bool ok = assetBack && n == 0 && Time.timeScale == ts0 && !SurvivalRunner.Exists && dirty == dirty0;
sb.AppendLine(P(ok) + " ⑩ 정리: 디스크 에셋 복구 " + assetBack + "(enabled=" + (st != null && st.enabled) + " autoPotion=" + (st != null && st.autoPotionEnabled) + ")"
+ " · 임시 오브젝트 잔여 " + n + " · timeScale " + Time.timeScale + "(시작 " + ts0 + ")"
+ " · 러너 생성 0 " + !SurvivalRunner.Exists + " · 씬 '" + scene.name + "' dirty=" + dirty + "(시작 " + dirty0 + ")"
+ " · MyPC 원복 " + (MyValue.MyPC == pc0));
return ok ? 0 : 1;
}
}