490 lines
29 KiB
C#
490 lines
29 KiB
C#
// 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;
|
||
}
|
||
}
|