600 lines
30 KiB
C#
600 lines
30 KiB
C#
// WL-813o2 프로브 — 선택스킬 카드 풀 검증(D-41) + 결과창 모달 차단 연장(D-42) 실측
|
||
// (에디트 모드 · Play 0 · 로그인 0 · 씬 저장 0 · 프리팹 저장 0)
|
||
// unity command run_script --file AgentScripts/WL813o2_Probe.cs --entry WL813o2_Probe.RunAll
|
||
// 산출물: <worktree>/AgentScripts/WL813o2_PROBE.txt (풀 검증 표 포함 · 커밋)
|
||
//
|
||
// 🔴 원본 `Assets/Script/**` 은 **읽기만** 한다(파싱·grep). 수정 0줄.
|
||
// 🔴 Addressables 키 = 이 프로젝트는 그룹이 **폴더 엔트리**(`m_Address: Assets/Res_Addr/<Folder>`)라
|
||
// 자식 에셋의 주소 = 그 에셋 경로다 → 키 존재 ⟺ 그 경로에 에셋이 있다(실측으로 확인한다).
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using UnityEngine;
|
||
using UnityEditor;
|
||
using WL.UI;
|
||
using WL.Combat.Run;
|
||
using WL.Combat.Diagnostics;
|
||
|
||
public static class WL813o2_Probe
|
||
{
|
||
const string kTableTxt = "Assets/ResWork/Table/table_selectskill.txt";
|
||
const string kSelectSkillDir = "Assets/Script/Character/Projectile/SelectSkill";
|
||
const string kAddrGroupDir = "Assets/AddressableAssetsData/AssetGroups";
|
||
|
||
static StringBuilder _o;
|
||
static int _pass, _fail;
|
||
static GameObject _fakeInfos;
|
||
static GameObject _fakeHook;
|
||
|
||
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; } }
|
||
static string OutPath(string name) { return Path.Combine(Path.Combine(Root, "AgentScripts"), name); }
|
||
|
||
public static string RunAll()
|
||
{
|
||
_o = new StringBuilder();
|
||
_pass = _fail = 0;
|
||
_o.AppendLine("# WL813o2 Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
|
||
_o.AppendLine("playMode=" + Application.isPlaying);
|
||
|
||
// 🔴 프로브는 SO 「메모리 인스턴스」의 값을 잠깐 바꾼다(A/B). 원값을 반드시 되돌린다(디스크 diff 0).
|
||
var s0 = WLZonePickupSettings.Instance;
|
||
var g0 = WLErrorGuardSettings.Instance;
|
||
bool bkValidate = s0 != null && s0.validatePool;
|
||
bool bkRuntimeKey = s0 != null && s0.runtimeKeyCheck;
|
||
int[] bkExclude = s0 != null ? s0.excludeSkillIds : null;
|
||
bool bkExtend = g0 != null && g0.suppressUntilNextRunStart;
|
||
|
||
try
|
||
{
|
||
Step0_Settings();
|
||
Step1_AddrGroups();
|
||
Step2_Fakes();
|
||
Step3_GrepOriginal();
|
||
Step4_PoolTable();
|
||
Step5_RuntimeKeyCheck();
|
||
Step6_Draw100();
|
||
Step7_Control_ValidateOff();
|
||
Step8_GC();
|
||
Step9_C8();
|
||
Step10_D42();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Chk(false, "예외 — " + ex.GetType().Name + ": " + ex.Message);
|
||
_o.AppendLine(ex.StackTrace);
|
||
}
|
||
finally
|
||
{
|
||
if (s0 != null) { s0.validatePool = bkValidate; s0.runtimeKeyCheck = bkRuntimeKey; s0.excludeSkillIds = bkExclude; }
|
||
if (g0 != null) g0.suppressUntilNextRunStart = bkExtend;
|
||
Cleanup();
|
||
}
|
||
|
||
_o.AppendLine();
|
||
_o.AppendLine("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " — pass=" + _pass + " fail=" + _fail);
|
||
string p = OutPath("WL813o2_PROBE.txt");
|
||
File.WriteAllText(p, _o.ToString(), new UTF8Encoding(true));
|
||
return (_fail == 0 ? "PASS" : "FAIL") + " pass=" + _pass + " fail=" + _fail + " → " + p;
|
||
}
|
||
|
||
// ─────────────── ⓪ SO 값
|
||
static WLZonePickupSettings _s;
|
||
static WLErrorGuardSettings _g;
|
||
|
||
static void Step0_Settings()
|
||
{
|
||
H("⓪ 값(SO) — 코드 상수 0 (C45)");
|
||
WLZonePickupSettings.ClearCache();
|
||
WLErrorGuardSettings.ClearCache();
|
||
_s = WLZonePickupSettings.Instance;
|
||
_g = WLErrorGuardSettings.Instance;
|
||
Chk(_s != null, "WLZonePickupSettings.asset 로드");
|
||
Chk(_g != null, "WLErrorGuardSettings.asset 로드");
|
||
if (_s == null || _g == null) return;
|
||
|
||
N("validatePool=" + _s.validatePool + " runtimeKeyCheck=" + _s.runtimeKeyCheck +
|
||
" maxRedraws=" + _s.maxRedraws + " logExclusion=" + _s.logExclusion + " cardCount=" + _s.cardCount);
|
||
N("excludeSkillIds = [" + Join(_s.excludeSkillIds) + "]");
|
||
N("noPrefabEffects(" + Len(_s.noPrefabEffects) + ") = " + string.Join(", ", _s.noPrefabEffects ?? new string[0]));
|
||
for (int i = 0; i < Len(_s.extraKeys); i++)
|
||
N("extraKeys[" + i + "] " + _s.extraKeys[i].skillEffect + " → " + _s.extraKeys[i].assetKey +
|
||
" (" + _s.extraKeys[i].source + ")");
|
||
N("경로형식: select=" + _s.selectSkillPathFormat + " · skill=" + _s.skillPathFormat + " · effect=" + _s.effectPathFormat);
|
||
N("[813x2] suppressErrorModalInRun=" + _g.suppressErrorModalInRun +
|
||
" suppressOnlyWhileRunning=" + _g.suppressOnlyWhileRunning +
|
||
" suppressUntilNextRunStart=" + _g.suppressUntilNextRunStart);
|
||
Chk(_s.validatePool, "validatePool=1(풀 검증 켜짐)");
|
||
Chk(Len(_s.excludeSkillIds) > 0, "excludeSkillIds 비어 있지 않음");
|
||
Chk(Len(_s.extraKeys) > 0, "extraKeys(코드 문자열 키) 채워짐");
|
||
Chk(_g.suppressUntilNextRunStart, "suppressUntilNextRunStart=1(D-42 연장 켜짐)");
|
||
Chk(_s.cardCount == 3, "카드 3장(기획안 F-3)");
|
||
}
|
||
|
||
static int Len(Array a) { return a != null ? a.Length : 0; }
|
||
static string Join(int[] a)
|
||
{
|
||
if (a == null || a.Length == 0) return "";
|
||
var sb = new StringBuilder();
|
||
for (int i = 0; i < a.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(a[i]); }
|
||
return sb.ToString();
|
||
}
|
||
|
||
// ─────────────── ① Addressables 그룹 = 폴더 엔트리인지 실측
|
||
static void Step1_AddrGroups()
|
||
{
|
||
H("① Addressables 그룹 실측 — 키 존재 ⟺ 그 경로의 에셋 존재");
|
||
string[] want = { "Projectile", "Skill", "Effect", "SelectSkill" };
|
||
for (int i = 0; i < want.Length; i++)
|
||
{
|
||
string p = kAddrGroupDir + "/" + want[i] + ".asset";
|
||
string txt = File.Exists(Path.Combine(Root, p)) ? File.ReadAllText(Path.Combine(Root, p)) : "";
|
||
var m = Regex.Match(txt, @"m_Address:\s*(\S+)");
|
||
string addr = m.Success ? m.Groups[1].Value : "(없음)";
|
||
N(want[i] + " 그룹 m_Address = " + addr + " (엔트리 " + Regex.Matches(txt, "m_Address:").Count + "개)");
|
||
Chk(addr == "Assets/Res_Addr/" + want[i], want[i] + " = 폴더 엔트리 1개(자식 주소 = 에셋 경로)");
|
||
}
|
||
}
|
||
|
||
// ─────────────── ② 원본 테이블·Info 대역 (813o 프로브와 같은 방식)
|
||
static void Step2_Fakes()
|
||
{
|
||
H("② 원본 데이터 경로 — table_selectskill.Awake 를 그대로 태운다");
|
||
_fakeInfos = new GameObject("WL813o2_Probe_Infos");
|
||
_fakeInfos.hideFlags = HideFlags.HideAndDontSave;
|
||
|
||
var tbl = _fakeInfos.AddComponent<table_selectskill>();
|
||
tbl.m_json = AssetDatabase.LoadAssetAtPath<TextAsset>(kTableTxt);
|
||
Chk(tbl.m_json != null, "테이블 TextAsset " + kTableTxt);
|
||
var awake = typeof(table_selectskill).GetMethod("Awake", BindingFlags.NonPublic | BindingFlags.Instance);
|
||
if (awake != null) awake.Invoke(tbl, null);
|
||
Chk(table_selectskill.Ins != null, "table_selectskill.Ins 설정(원본 Awake)");
|
||
|
||
var info = _fakeInfos.AddComponent<InGameInfo>();
|
||
InGameInfo.Ins = info;
|
||
var actors = _fakeInfos.AddComponent<ActorInfo>();
|
||
ActorInfo.Ins = actors;
|
||
Chk(InGameInfo.Ins != null && ActorInfo.Ins != null, "InGameInfo.Ins · ActorInfo.Ins 설정");
|
||
|
||
int all = table_selectskill.Ins != null ? table_selectskill.Ins.Get_DataList().Count : 0;
|
||
N("테이블 행 수 실측 = " + all + " (발주서 「기획안 42행」과 다르다 → 실측값을 쓴다)");
|
||
Chk(all > 0, "행 파싱 성공");
|
||
}
|
||
|
||
// ─────────────── ③ 원본 코드 문자열 grep (읽기만)
|
||
static readonly List<string> _codeKeys = new List<string>();
|
||
|
||
static void Step3_GrepOriginal()
|
||
{
|
||
H("③ 원본 효과 클래스 grep — `Shoot_Projectile(\"…\")` 코드 문자열 (읽기만 · 수정 0)");
|
||
string dir = Path.Combine(Root, kSelectSkillDir);
|
||
Chk(Directory.Exists(dir), "원본 폴더 " + kSelectSkillDir);
|
||
if (!Directory.Exists(dir)) return;
|
||
|
||
var files = Directory.GetFiles(dir, "*.cs", SearchOption.AllDirectories);
|
||
N("파일 " + files.Length + "개");
|
||
var rx = new Regex("Shoot_Projectile\\s*\\(\\s*\"([^\"]+)\"");
|
||
for (int f = 0; f < files.Length; f++)
|
||
{
|
||
var lines = File.ReadAllLines(files[f]);
|
||
for (int i = 0; i < lines.Length; i++)
|
||
{
|
||
string trimmed = lines[i].TrimStart();
|
||
bool commented = trimmed.StartsWith("//");
|
||
var m = rx.Match(lines[i]);
|
||
if (!m.Success) continue;
|
||
string cls = Path.GetFileNameWithoutExtension(files[f]);
|
||
N((commented ? " (주석) " : " ") + cls + ".cs:" + (i + 1) + " 키=\"" + m.Groups[1].Value + "\"");
|
||
if (!commented) _codeKeys.Add(cls + "|" + m.Groups[1].Value);
|
||
}
|
||
}
|
||
Chk(_codeKeys.Count > 0, "살아 있는 Shoot_Projectile 문자열 " + _codeKeys.Count + "건");
|
||
|
||
// SO extraKeys 가 이 문자열들을 전부 덮는가
|
||
for (int i = 0; i < _codeKeys.Count; i++)
|
||
{
|
||
var parts = _codeKeys[i].Split('|');
|
||
string effect = parts[0]; // 클래스명 = eEffect 이름과 같다(원본 프리팹 이름 규칙)
|
||
string key = "Assets/Res_Addr/Projectile/" + parts[1] + ".prefab";
|
||
bool covered = false;
|
||
for (int k = 0; k < Len(_s.extraKeys); k++)
|
||
if (_s.extraKeys[k].skillEffect == effect && _s.extraKeys[k].assetKey == key) covered = true;
|
||
Chk(covered, "SO extraKeys 가 " + effect + " → " + key + " 를 덮는다");
|
||
}
|
||
}
|
||
|
||
// ─────────────── ④ 풀 검증 표 (전 행 · 에디트 모드)
|
||
class Row
|
||
{
|
||
public int id; public string effect; public int stage; public bool eligible;
|
||
public string primary = ""; public bool primaryOk = true;
|
||
public string extra = ""; public bool extraOk = true;
|
||
public string name = "";
|
||
}
|
||
static readonly List<Row> _rows = new List<Row>();
|
||
|
||
static bool AssetExists(string path)
|
||
{
|
||
return !string.IsNullOrEmpty(path) && AssetDatabase.LoadAssetAtPath<GameObject>(path) != null;
|
||
}
|
||
|
||
static void Step4_PoolTable()
|
||
{
|
||
H("④ 🔴 풀 검증 표 — 전 행 × 참조 키 × 실존 (에디트 모드 · AssetDatabase)");
|
||
var all = table_selectskill.Ins.Get_DataList();
|
||
for (int i = 0; i < all.Count; i++)
|
||
{
|
||
var r = all[i] as SelectSkillTableData;
|
||
if (r == null) continue;
|
||
var row = new Row { id = r.ID, effect = r.SkillEffect.ToString(), stage = r.EnableStage, name = r.Name };
|
||
row.eligible = r.EnableStage <= 1; // 원본 Get_DataList 의 curstage = 1 (하드코딩 · 읽기만)
|
||
|
||
bool noPrefab = false;
|
||
for (int k = 0; k < Len(_s.noPrefabEffects); k++)
|
||
if (_s.noPrefabEffects[k] == row.effect) noPrefab = true;
|
||
|
||
if (r.SkillEffect != eEffect.None && !noPrefab)
|
||
{
|
||
string fmt = row.effect.Contains("Select") ? _s.selectSkillPathFormat
|
||
: row.effect.Contains("Skill") ? _s.skillPathFormat : _s.effectPathFormat;
|
||
row.primary = string.Format(fmt, row.effect);
|
||
row.primaryOk = AssetExists(row.primary);
|
||
}
|
||
for (int k = 0; k < Len(_s.extraKeys); k++)
|
||
{
|
||
var x = _s.extraKeys[k];
|
||
if (x == null || x.skillEffect != row.effect) continue;
|
||
row.extra = x.assetKey;
|
||
row.extraOk = AssetExists(x.assetKey);
|
||
}
|
||
_rows.Add(row);
|
||
}
|
||
|
||
_o.AppendLine();
|
||
_o.AppendLine(" | ID | SkillEffect | Stg | 뽑기가능 | 1차 키 | 실존 | 추가 키 | 실존 | 판정 |");
|
||
_o.AppendLine(" |---|---|---|---|---|---|---|---|---|");
|
||
var missing = new List<Row>();
|
||
for (int i = 0; i < _rows.Count; i++)
|
||
{
|
||
var r = _rows[i];
|
||
bool bad = !r.primaryOk || !r.extraOk;
|
||
if (bad) missing.Add(r);
|
||
_o.AppendLine(" | " + r.id + " | " + r.effect + " | " + r.stage + " | " + (r.eligible ? "O" : "-") +
|
||
" | " + (r.primary == "" ? "(없음)" : Short(r.primary)) + " | " + (r.primary == "" ? "-" : (r.primaryOk ? "O" : "🔴X")) +
|
||
" | " + (r.extra == "" ? "-" : Short(r.extra)) + " | " + (r.extra == "" ? "-" : (r.extraOk ? "O" : "🔴X")) +
|
||
" | " + (bad ? "🔴 제외" : "사용") + " |");
|
||
}
|
||
_o.AppendLine();
|
||
N("전체 " + _rows.Count + "행 · 부재 키 행 " + missing.Count + "건");
|
||
for (int i = 0; i < missing.Count; i++)
|
||
N(" 🔴 행 " + missing[i].id + " " + missing[i].effect + " (Stage " + missing[i].stage + " · " +
|
||
(missing[i].eligible ? "지금 뽑힘" : "지금은 안 뽑힘") + ") — 부재 키 " +
|
||
(!missing[i].primaryOk ? missing[i].primary : missing[i].extra));
|
||
|
||
// SO 목록과 실측이 일치하는가
|
||
for (int i = 0; i < missing.Count; i++)
|
||
{
|
||
bool listed = false;
|
||
for (int k = 0; k < Len(_s.excludeSkillIds); k++) if (_s.excludeSkillIds[k] == missing[i].id) listed = true;
|
||
Chk(listed, "부재 키 행 " + missing[i].id + "(" + missing[i].effect + ")이 SO excludeSkillIds 에 있다");
|
||
}
|
||
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||
{
|
||
bool real = false;
|
||
for (int i = 0; i < missing.Count; i++) if (missing[i].id == _s.excludeSkillIds[k]) real = true;
|
||
Chk(real, "SO excludeSkillIds[" + _s.excludeSkillIds[k] + "] 는 실측 부재 행이다(과잉 제외 0)");
|
||
}
|
||
Chk(missing.Count > 0, "D-41 재현 — 부재 키 행이 실제로 있다");
|
||
}
|
||
|
||
static string Short(string p) { return p.Replace("Assets/Res_Addr/", ""); }
|
||
|
||
// ─────────────── ⑤ 런타임 경로(게임 코드)로 같은 판정을 하는가
|
||
static void Step5_RuntimeKeyCheck()
|
||
{
|
||
H("⑤ 런타임 검사 — 게임 코드 `ZonePickupPool` 이 같은 결론을 내는가");
|
||
ZonePickupPool.ResetCache();
|
||
var all = table_selectskill.Ins.Get_DataList();
|
||
int agree = 0, disagree = 0;
|
||
for (int i = 0; i < all.Count; i++)
|
||
{
|
||
var r = all[i] as SelectSkillTableData;
|
||
if (r == null) continue;
|
||
string miss;
|
||
bool keysOk = ZonePickupPool.KeysExist(_s, r, out miss);
|
||
Row t = null;
|
||
for (int k = 0; k < _rows.Count; k++) if (_rows[k].id == r.ID) t = _rows[k];
|
||
bool expect = t == null || (t.primaryOk && t.extraOk);
|
||
if (keysOk == expect) agree++;
|
||
else { disagree++; N(" 불일치 행 " + r.ID + " runtime=" + keysOk + " expect=" + expect + " miss=" + miss); }
|
||
}
|
||
Chk(disagree == 0, "에디트 모드 표와 런타임 판정 완전 일치(" + agree + "행 · 불일치 " + disagree + ")");
|
||
|
||
// 제외 판정 + 로그 1줄
|
||
ZonePickupPool.ResetCache();
|
||
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||
{
|
||
var r = table_selectskill.Ins.Get_Data(_s.excludeSkillIds[k]);
|
||
string why;
|
||
bool ex = ZonePickupPool.Excluded(_s, r, out why);
|
||
Chk(ex, "행 " + _s.excludeSkillIds[k] + " 제외 판정 — 사유 \"" + why + "\"");
|
||
}
|
||
N("카운터: checked=" + ZonePickupPool.CheckedCount + " staticExclude=" + ZonePickupPool.StaticExcludeCount +
|
||
" missingKey=" + ZonePickupPool.MissingKeyCount + " skipped=" + ZonePickupPool.SkippedCheckCount);
|
||
|
||
// SO 정적 목록을 비우고 **키 검사만으로도** 같은 행이 걸리는가 (근본 검사 증명)
|
||
var saved = _s.excludeSkillIds;
|
||
_s.excludeSkillIds = new int[0];
|
||
ZonePickupPool.ResetCache();
|
||
var onlyKey = new List<int>();
|
||
for (int i = 0; i < all.Count; i++)
|
||
{
|
||
var r = all[i] as SelectSkillTableData;
|
||
if (r == null) continue;
|
||
string why;
|
||
if (ZonePickupPool.Excluded(_s, r, out why)) onlyKey.Add(r.ID);
|
||
}
|
||
N("정적 목록 없이 키 검사만으로 제외된 행 = [" + Join(onlyKey.ToArray()) + "]");
|
||
Chk(onlyKey.Count == Len(saved), "키 검사만으로도 같은 수(" + onlyKey.Count + ")가 걸린다 = 목록은 캐시일 뿐");
|
||
_s.excludeSkillIds = saved;
|
||
ZonePickupPool.ResetCache();
|
||
}
|
||
|
||
// ─────────────── ⑥ 3장 뽑기 100회
|
||
static void Step6_Draw100()
|
||
{
|
||
H("⑥ 3장 뽑기 100회 — 제외 행이 한 번도 안 나오는가");
|
||
ZonePickup.ResetState();
|
||
ZonePickupPool.ResetCache();
|
||
var seen = new Dictionary<int, int>();
|
||
int ok = 0, dupe = 0, short_ = 0;
|
||
for (int n = 0; n < 100; n++)
|
||
{
|
||
if (!ZonePickup.DrawForProbe()) { short_++; continue; }
|
||
ok++;
|
||
if (ZonePickup.CardCount != 3) short_++;
|
||
var ids = new HashSet<int>();
|
||
for (int i = 0; i < ZonePickup.CardCount; i++)
|
||
{
|
||
int id = ZonePickup.CardId(i);
|
||
if (!ids.Add(id)) dupe++;
|
||
seen[id] = seen.ContainsKey(id) ? seen[id] + 1 : 1;
|
||
}
|
||
}
|
||
Chk(ok == 100, "100회 전부 성공 — 실측 " + ok);
|
||
Chk(short_ == 0, "카드 수 3장 미달 0 — 실측 " + short_);
|
||
Chk(dupe == 0, "한 뽑기 안 ID 중복 0 — 실측 " + dupe);
|
||
|
||
var keys = new List<int>(seen.Keys); keys.Sort();
|
||
var sb = new StringBuilder();
|
||
for (int i = 0; i < keys.Count; i++) sb.Append(keys[i]).Append("×").Append(seen[keys[i]]).Append(" ");
|
||
N("나온 ID 분포(300장): " + sb);
|
||
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||
{
|
||
int id = _s.excludeSkillIds[k];
|
||
int c = seen.ContainsKey(id) ? seen[id] : 0;
|
||
Chk(c == 0, "🔴 제외 행 " + id + " 등장 0회 — 실측 " + c + "회");
|
||
}
|
||
N("ExcludedCount=" + ZonePickup.ExcludedCount + " RedrawCount=" + ZonePickup.RedrawCount +
|
||
" last=\"" + ZonePickup.LastExcludeReason + "\"");
|
||
Chk(ZonePickup.ExcludedCount > 0, "실제로 제외가 걸렸다(후필터가 살아 있다) — " + ZonePickup.ExcludedCount + "건");
|
||
}
|
||
|
||
// ─────────────── ⑦ 대조군 — validatePool = 0 이면 「별」이 다시 나온다
|
||
static void Step7_Control_ValidateOff()
|
||
{
|
||
H("⑦ 대조군 — validatePool=0 (813o 동작) 이면 제외 행이 다시 뽑힌다");
|
||
bool saved = _s.validatePool;
|
||
_s.validatePool = false;
|
||
ZonePickup.ResetState();
|
||
ZonePickupPool.ResetCache();
|
||
var seen = new Dictionary<int, int>();
|
||
for (int n = 0; n < 100; n++)
|
||
{
|
||
if (!ZonePickup.DrawForProbe()) continue;
|
||
for (int i = 0; i < ZonePickup.CardCount; i++)
|
||
{
|
||
int id = ZonePickup.CardId(i);
|
||
seen[id] = seen.ContainsKey(id) ? seen[id] + 1 : 1;
|
||
}
|
||
}
|
||
int hit = 0;
|
||
for (int k = 0; k < Len(_s.excludeSkillIds); k++)
|
||
if (seen.ContainsKey(_s.excludeSkillIds[k])) hit += seen[_s.excludeSkillIds[k]];
|
||
N("validatePool=0 에서 제외 대상 등장 = " + hit + "장 / 300장");
|
||
Chk(hit > 0, "대조군에서는 실제로 나온다 = ⑥의 0회가 필터 덕분임을 증명");
|
||
Chk(ZonePickup.ExcludedCount == 0, "validatePool=0 이면 제외 0 · 재뽑기 0(813o 동작 그대로)");
|
||
Chk(ZonePickup.RedrawCount == 0, "재뽑기 0회");
|
||
_s.validatePool = saved;
|
||
}
|
||
|
||
// ─────────────── ⑧ GC
|
||
static void Step8_GC()
|
||
{
|
||
H("⑧ GC — 검증 코드 자체의 할당(캐시가 선 뒤)");
|
||
_s.validatePool = true;
|
||
ZonePickupPool.ResetCache();
|
||
var all = table_selectskill.Ins.Get_DataList();
|
||
var okRow = table_selectskill.Ins.Get_Data(101); // 사용 행
|
||
var badRow = table_selectskill.Ins.Get_Data(_s.excludeSkillIds[Len(_s.excludeSkillIds) - 1]); // 제외 행
|
||
string w0;
|
||
ZonePickupPool.Excluded(_s, okRow, out w0); // 캐시 워밍
|
||
ZonePickupPool.Excluded(_s, badRow, out w0);
|
||
ZonePickupPool.KeyExists(_s.extraKeys[0].assetKey);
|
||
|
||
// ⓐ 판정 자체 — 캐시 히트 경로는 문자열도 만들지 않는다
|
||
long a = GC.GetTotalMemory(true);
|
||
for (int i = 0; i < 2000; i++)
|
||
{
|
||
string w;
|
||
ZonePickupPool.Excluded(_s, okRow, out w);
|
||
ZonePickupPool.Excluded(_s, badRow, out w);
|
||
ZonePickupPool.KeyExists(_s.extraKeys[0].assetKey);
|
||
}
|
||
long judge = GC.GetTotalMemory(false) - a;
|
||
N("판정 6,000회(캐시 히트) Δ = " + judge + " B");
|
||
Chk(judge == 0, "🔴 캐시가 선 뒤 판정 할당 = 0 B — 실측 " + judge);
|
||
|
||
// ⓑ 전 행 1회 판정(캐시 채우기) 뒤에는 판정 횟수가 늘지 않는다
|
||
ZonePickupPool.ResetCache();
|
||
for (int i = 0; i < all.Count; i++) { string w; ZonePickupPool.Excluded(_s, all[i] as SelectSkillTableData, out w); }
|
||
int after1 = ZonePickupPool.CheckedCount;
|
||
for (int r = 0; r < 50; r++)
|
||
for (int i = 0; i < all.Count; i++) { string w; ZonePickupPool.Excluded(_s, all[i] as SelectSkillTableData, out w); }
|
||
Chk(ZonePickupPool.CheckedCount == after1,
|
||
"판정은 ID 당 1회 — 58행×51회 뒤에도 누적 " + ZonePickupPool.CheckedCount + "회(=행 수)");
|
||
|
||
// ⓒ 뽑기 전체 비교(참고) — 차이는 **재뽑기로 늘어난 원본 호출** 몫이다
|
||
ZonePickup.ResetState();
|
||
ZonePickup.DrawForProbe();
|
||
long c0 = GC.GetTotalMemory(true);
|
||
for (int i = 0; i < 200; i++) ZonePickup.DrawForProbe();
|
||
long onB = GC.GetTotalMemory(false) - c0;
|
||
int onCalls = 200 + ZonePickup.RedrawCount;
|
||
|
||
bool saved = _s.validatePool;
|
||
_s.validatePool = false;
|
||
ZonePickup.ResetState();
|
||
ZonePickup.DrawForProbe();
|
||
long c1 = GC.GetTotalMemory(true);
|
||
for (int i = 0; i < 200; i++) ZonePickup.DrawForProbe();
|
||
long offB = GC.GetTotalMemory(false) - c1;
|
||
_s.validatePool = saved;
|
||
|
||
N("검증 ON 뽑기 200회 Δ = " + onB + " B · 원본 Get_DataList 호출 " + onCalls + "회(재뽑기 " + (onCalls - 200) + ")");
|
||
N("검증 OFF 뽑기 200회 Δ = " + offB + " B · 원본 Get_DataList 호출 200회");
|
||
double perOn = onCalls > 0 ? (double)onB / onCalls : 0, perOff = offB > 0 ? (double)offB / 200 : 0;
|
||
N("원본 호출 1회당 = ON " + perOn.ToString("F0") + " B · OFF " + perOff.ToString("F0") +
|
||
" B → 차이는 검증 코드가 아니라 **원본 LINQ 셔플을 몇 번 더 부르느냐**다");
|
||
Chk(perOff <= 0 || perOn <= perOff * 2.0,
|
||
"호출 1회당 할당이 대조군의 2배를 넘지 않는다(검증 코드 몫 ≈ 0)");
|
||
N("🔴 실사용 빈도 = 존 클리어 1회당 뽑기 1회(런당 최대 " + _s.cardCount + "장×존 수) — 매 프레임 경로 아님");
|
||
}
|
||
|
||
// ─────────────── ⑨ C8
|
||
static void Step9_C8()
|
||
{
|
||
H("⑨ C8 — 스위치 하나로 813o 이전 100%");
|
||
_s.validatePool = false;
|
||
Chk(!ZonePickupPool.Active(_s), "validatePool=0 → 검증 축 off");
|
||
ZonePickup.ResetState(); ZonePickupPool.ResetCache();
|
||
ZonePickup.DrawForProbe();
|
||
Chk(ZonePickup.ExcludedCount == 0 && ZonePickup.RedrawCount == 0, "제외 0 · 재뽑기 0");
|
||
_s.validatePool = true;
|
||
|
||
WLZonePickupSettings.RuntimeDisabled = true;
|
||
Chk(!WLZonePickupSettings.Enabled, "RuntimeDisabled → 픽업 축 전체 off(813o C8 그대로)");
|
||
WLZonePickupSettings.RuntimeDisabled = false;
|
||
Chk(WLZonePickupSettings.Enabled, "복구");
|
||
|
||
bool savedG = _g.suppressUntilNextRunStart;
|
||
_g.suppressUntilNextRunStart = false;
|
||
Chk(_g.suppressUntilNextRunStart == false, "suppressUntilNextRunStart=0 → 813x2 원래 동작으로 되돌린다");
|
||
_g.suppressUntilNextRunStart = savedG;
|
||
}
|
||
|
||
// ─────────────── ⑩ D-42 — 런 종료 뒤에도 모달 차단, 다음 런 시작에 복원
|
||
static void Step10_D42()
|
||
{
|
||
H("⑩ 🔴 D-42 — RunEnded 뒤 결과창~다음 런 시작까지 모달 0");
|
||
|
||
_fakeHook = new GameObject("WL813o2_Probe_Hook");
|
||
_fakeHook.hideFlags = HideFlags.HideAndDontSave;
|
||
var hook = _fakeHook.AddComponent<ErrorLogHookManager>();
|
||
RunErrorModalGuard.ResetHookCache();
|
||
RunErrorModalGuard.SetHookForTest(hook);
|
||
RunErrorModalGuard.ResetCounters();
|
||
RunErrorModalGuard.ForceState = 0;
|
||
RunErrorModalGuard.Subscribe();
|
||
|
||
RunDirector.ResetAll();
|
||
Chk(WLRunSettings.Enabled, "813p WLRunSettings.Enabled(StartRun 전제) — 실측 " + WLRunSettings.Enabled);
|
||
N("초기 Phase=" + RunDirector.Phase + " IsRunning=" + RunDirector.IsRunning);
|
||
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||
Chk(!RunErrorModalGuard.ShouldSuppress(), "Idle(런 밖 · 시작 전) → 차단 안 함 = 원본 모달 살아 있음");
|
||
Chk(hook.enabled, "훅 enabled=True");
|
||
|
||
RunDirector.StartRun("probe");
|
||
N("StartRun → Phase=" + RunDirector.Phase + " IsRunning=" + RunDirector.IsRunning);
|
||
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||
Chk(RunErrorModalGuard.ShouldSuppress(), "런 중 → 차단(813x2 그대로)");
|
||
Chk(!hook.enabled, "런 중 훅 enabled=False(원본 OnDisable 이 구독을 끊는다)");
|
||
|
||
RunDirector.EndRun(RunOutcome.Win);
|
||
N("EndRun → Phase=" + RunDirector.Phase + " IsRunning=" + RunDirector.IsRunning);
|
||
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||
Chk(RunErrorModalGuard.ShouldSuppress(), "🔴 런 종료 뒤(결과창)에도 차단 유지 = D-42 해소");
|
||
Chk(!hook.enabled, "🔴 결과창에서 훅 enabled=False 유지(모달 0)");
|
||
|
||
// 결과창에서 가짜 예외 1건 → 모달 0(= 훅이 내려가 있어 원본 HandleLog 미호출) · 파일 로그는 계속
|
||
int before = RunErrorModalGuard.SuppressedCount;
|
||
Debug.LogError("[WL813o2] 결과창 가짜 오류(모달 0 확인용)");
|
||
N("가짜 오류 1건 → captured=" + RunErrorModalGuard.CapturedCount +
|
||
" suppressed=" + RunErrorModalGuard.SuppressedCount + " written=" + RunErrorModalGuard.WrittenCount);
|
||
Chk(RunErrorModalGuard.SuppressedCount == before + 1, "🔴 결과창 예외가 「모달차단」으로 기록된다(모달 0)");
|
||
Chk(RunErrorModalGuard.HookDisabledByUs, "훅은 우리가 내려둔 상태");
|
||
N("로그 파일 = " + RunErrorModalGuard.LastFilePath);
|
||
|
||
// 813x2 원래 동작(연장 off)이면 결과창에서 복원된다 = D-42 재현
|
||
bool saved = _g.suppressUntilNextRunStart;
|
||
_g.suppressUntilNextRunStart = false;
|
||
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||
Chk(!RunErrorModalGuard.ShouldSuppress(), "연장 off → 결과창에서 차단 해제(= Q9 가 본 D-42 상태 재현)");
|
||
Chk(hook.enabled, "연장 off 면 결과창에서 훅 enabled=True(원본 모달 부활)");
|
||
_g.suppressUntilNextRunStart = saved;
|
||
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||
Chk(!hook.enabled, "연장 on 으로 되돌리면 다시 내려간다");
|
||
|
||
// 다음 런 시작 → 복원(그리고 런 중이니 다시 내려간다)
|
||
RunDirector.StartRun("probe-next");
|
||
N("다음 StartRun → Phase=" + RunDirector.Phase);
|
||
Chk(RunDirector.Phase == RunPhase.Zones, "Phase=Zones(Ended 를 벗어났다 = 연장 래치 해제)");
|
||
_g.suppressUntilNextRunStart = false; // 연장분만 떼고 보면
|
||
Chk(RunErrorModalGuard.ShouldSuppress(), "연장분 없이도 런 중이라 차단 = 복원 뒤 곧바로 다시 off");
|
||
_g.suppressUntilNextRunStart = saved;
|
||
|
||
// 로비 복귀(Idle) 에서는 복원된다
|
||
RunDirector.ResetAll();
|
||
RunErrorModalGuard.Apply(RunErrorModalGuard.ShouldSuppress(), _g);
|
||
Chk(!RunErrorModalGuard.ShouldSuppress(), "Phase=Idle(로비/타이틀) → 차단 해제 = 원본 모달 복귀");
|
||
Chk(hook.enabled, "훅 enabled=True 복원 · HookDisabledByUs=" + RunErrorModalGuard.HookDisabledByUs);
|
||
}
|
||
|
||
// ─────────────── 정리
|
||
static void Cleanup()
|
||
{
|
||
H("⑪ 정리");
|
||
try
|
||
{
|
||
RunErrorModalGuard.Restore();
|
||
RunErrorModalGuard.Unsubscribe();
|
||
RunErrorModalGuard.ResetHookCache();
|
||
RunDirector.ResetAll();
|
||
RunDirector.Unsubscribe();
|
||
ZonePickup.Teardown();
|
||
ZonePickupPool.ResetCache();
|
||
WLZonePickupSettings.RuntimeDisabled = false;
|
||
if (_fakeHook != null) UnityEngine.Object.DestroyImmediate(_fakeHook);
|
||
if (_fakeInfos != null) UnityEngine.Object.DestroyImmediate(_fakeInfos);
|
||
table_selectskill.Ins = null; InGameInfo.Ins = null; ActorInfo.Ins = null;
|
||
_fakeHook = null; _fakeInfos = null;
|
||
N("임시 오브젝트 제거 · 구독 해제 · 디스크 SO 무변경(값은 메모리 인스턴스만 손댔다)");
|
||
Chk(!ZonePickup.Bound, "픽업 노드 0");
|
||
Chk(true, "정리 완료");
|
||
}
|
||
catch (Exception ex) { N("정리 중 예외(무시): " + ex.Message); }
|
||
}
|
||
}
|