Project_WL/AgentScripts/WL813f2_Probe.cs

324 lines
17 KiB
C#
Raw Normal View History

// 발주서 WL-813f2 — 전리품 등급 하한 백(엘리트 ≥3 · 보스 ≥5) 검증 프로브
//
// 실행 (CLAUDE.md §1: using 금지 · 네임스페이스 풀어 쓰기)
// 에디트 모드 : unity command run_script --file AgentScripts/WL813f2_Probe.cs --entry WL813f2_Probe.Edit --timeout 600
//
// Edit() 가 재는 것 — 씬/프리팹/에셋 변경 0(가짜 오브젝트·가짜 테이블은 전부 회수):
// (a) WLLootSettings 값 — eliteBagId · eliteFloorDrops · bossBagId · beamMinGrade
// (b) 테이블 부트스트랩 — table_randombag / table_itemlist 를 TextAsset 에서 에디트 모드에 실제로 올린다
// (813f 는 싱글턴이 없어 백 추첨을 못 쟀다. 여기서는 비활성 GO + 리플렉션 Awake/Start 로 실제 경로를 태운다)
// (c) RollBag(8130002) ×100 → ResolveGrade 등급 분포 · 등급 ≥3 비율 · 빈손 횟수
// (d) RollBag(8130003) ×100 → 등급 ≥5 비율 · 빔(BeamPrefab) 뜨는 비율
// (e) RollBag(8130001) ×N → 존 잡몹 백 등급 분포(등급 1~2 비율) — 기준서 §E 4-1 「등급 색 구분」 근거
// (f) 엘리트 버스트 시뮬 ×200 — BurstCount(Elite) 건수마다 BagForBurst → 8130002 가 정확히 몇 건인가
// (g) 보스 버스트 시뮬 ×200 — 전 건 8130003 인가
// (h) 가짜 Killed(Elite/Boss) → 구독·BurstPlanned 동작
// (i) GC — RollBag 1000회 · BagForBurst 1000회 할당 바이트
// (j) C8 — eliteBagId/bossBagId 0 이면 존 백 그대로(기존 동작)
// (k) 데이터 — RandomBag 행 수 · 새 백 2개 행 전수 · 기존 202행 무변경 확인
//
// 수치는 전부 SO/테이블에서 읽는다(C45). 프로브 상수는 "어디를 보나" 뿐이다.
public static class WL813f2_Probe
{
const string kBagJson = "Assets/ResWork/Table/Export/RandomBag.json";
const string kItemJson = "Assets/ResWork/Table/Export/ItemList.json";
const string kOutDir = "AgentScripts/staging/WL813f2";
const int kZoneBag = 8130001;
const int kEliteBag = 8130002;
const int kBossBag = 8130003;
const int kRolls = 100;
static System.Text.StringBuilder s;
static void L(string t) { s.AppendLine(t); UnityEngine.Debug.Log("[WL813f2] " + t); }
static string Abs(string rel)
{
return System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(),
rel.Replace('/', System.IO.Path.DirectorySeparatorChar));
}
static string Flush(string name)
{
try
{
System.IO.Directory.CreateDirectory(Abs(kOutDir));
System.IO.File.WriteAllText(Abs(kOutDir + "/" + name), s.ToString(), new System.Text.UTF8Encoding(true));
}
catch (System.Exception e) { UnityEngine.Debug.LogWarning("[WL813f2] 로그 저장 실패: " + e.Message); }
return s.ToString();
}
// ── 테이블 부트스트랩 ────────────────────────────────────────────────────
// table_missionbase.Awake() 가 json_last = m_json.text 를 읽고, 파생 Start() 가 역직렬화한다.
// 에디트 모드에서는 Awake/Start 가 자동으로 안 불리므로 비활성 GO 에 붙이고 리플렉션으로 직접 부른다.
static UnityEngine.GameObject s_tableHost;
// Awake 는 m_json(TextAsset · 인스펙터 참조)을 읽으므로 태우지 않는다.
// 대신 protected json_last 에 파일 본문을 직접 넣고 Ins 를 세운 뒤 Start() 만 실제로 호출한다.
static T Boot<T>(string jsonPath) where T : UnityEngine.MonoBehaviour
{
if (s_tableHost == null)
{
s_tableHost = new UnityEngine.GameObject("WL813f2_TableHost");
s_tableHost.SetActive(false); // 비활성 = Awake 자동 호출 없음
s_tableHost.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
}
var comp = s_tableHost.AddComponent<T>();
var fj = typeof(table_missionbase).GetField("json_last",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (fj == null) { L(" ! json_last 필드 없음"); return null; }
fj.SetValue(comp, System.IO.File.ReadAllText(Abs(jsonPath))); // BOM 은 StreamReader 가 벗긴다
var fi = typeof(T).GetField("Ins",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (fi == null) { L(" ! Ins 정적 필드 없음: " + typeof(T).Name); return null; }
fi.SetValue(null, comp);
Call(comp, "Start");
return comp;
}
static void Call(object o, string name)
{
var m = o.GetType().GetMethod(name, System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
if (m == null) { L(" ! 메서드 없음: " + name); return; }
m.Invoke(o, null);
}
static int BagRows(int bagId)
{
try { var l = table_randombag.Ins.Get_DataList(bagId); return l == null ? -1 : l.Count; }
catch (System.Exception) { return -1; } // Get_DataList 는 없는 백이면 KeyNotFound 를 던진다
}
static Actor MakeFakeMob(string name, eSubRol subRole)
{
var go = new UnityEngine.GameObject(name);
go.hideFlags = UnityEngine.HideFlags.HideAndDontSave;
go.SetActive(false); // 비활성 = Awake/Start 안 탐
var a = go.AddComponent<MobActor>();
a.m_SubRole = subRole;
return a;
}
static void Teardown()
{
table_randombag.Ins = null;
table_itemlist.Ins = null;
if (s_tableHost != null) UnityEngine.Object.DestroyImmediate(s_tableHost);
s_tableHost = null;
}
// ── 분포 집계 ───────────────────────────────────────────────────────────
static string GradeTable(int[] hist, int total, int empty)
{
var sb = new System.Text.StringBuilder();
for (int g = 1; g <= 9; g++)
if (hist[g] > 0) sb.Append("g" + g + ":" + hist[g] + "(" + (100f * hist[g] / total).ToString("0.0") + "%) ");
sb.Append("| 빈손 " + empty);
return sb.ToString();
}
static int RollGrades(int bagId, int rolls, int[] hist, out int empty, out int beam)
{
empty = 0; beam = 0;
var st = WL.Combat.Loot.WLLootSettings.Instance;
int min = 99;
for (int i = 0; i < rolls; i++)
{
int itemId, amount;
if (!WL.Combat.Loot.LootBurst.RollBag(bagId, out itemId, out amount)) { empty++; min = 0; continue; }
int g = WL.Combat.Loot.LootBurst.ResolveGrade(itemId);
if (g >= 1 && g <= 9) hist[g]++;
if (g < min) min = g;
if (g >= st.beamMinGrade && WL.Combat.Loot.LootBurst.BeamPrefab(st, g) != null) beam++;
}
return min;
}
public static string Edit()
{
s = new System.Text.StringBuilder();
L("═══ WL-813f2 등급 하한 백 프로브 (edit mode) " + System.DateTime.Now.ToString("HH:mm:ss") + " ═══");
WL.Combat.Loot.WLLootSettings.ClearCache();
var st = WL.Combat.Loot.WLLootSettings.Instance;
if (st == null) { L("!! WLLootSettings 로드 실패"); return Flush("EDIT.txt"); }
WL.Combat.Loot.LootBurst.EnsureSubscribed();
// (a) 값
L("");
L("[a] WLLootSettings — enabled=" + st.enabled + " burstEnabled=" + st.burstEnabled);
L(" eliteBagId=" + st.eliteBagId + " eliteFloorDrops=" + st.eliteFloorDrops + " bossBagId=" + st.bossBagId);
L(" eliteBurst=" + st.eliteBurstMin + "~" + st.eliteBurstMax + " bossBurst=" + st.bossBurstMin + "~" + st.bossBurstMax);
L(" beamMinGrade=" + st.beamMinGrade + " burstRollsBag=" + st.burstRollsBag + " burstGoldShare=" + st.burstGoldShare);
// (b) 테이블 부트스트랩
L("");
L("[b] 테이블 부트스트랩");
L(" 부트 전: table_randombag.Ins=" + (table_randombag.Ins != null) + " table_itemlist.Ins=" + (table_itemlist.Ins != null));
try
{
Boot<table_randombag>(kBagJson);
Boot<table_itemlist>(kItemJson);
}
catch (System.Exception e) { L(" !! 부트 실패: " + e.GetType().Name + " " + e.Message); }
bool booted = table_randombag.Ins != null && table_itemlist.Ins != null;
L(" 부트 후: table_randombag.Ins=" + (table_randombag.Ins != null) + " table_itemlist.Ins=" + (table_itemlist.Ins != null));
if (booted)
{
L(" 행 수: 8130001=" + BagRows(kZoneBag) + " 8130002=" + BagRows(kEliteBag) + " 8130003=" + BagRows(kBossBag) +
" 전체=" + table_randombag.Ins.Get_DataList().Count);
L(" ResolveGrade 표본: 10101012=" + WL.Combat.Loot.LootBurst.ResolveGrade(10101012) +
" 10101017=" + WL.Combat.Loot.LootBurst.ResolveGrade(10101017) +
" 10101022=" + WL.Combat.Loot.LootBurst.ResolveGrade(10101022) +
" 10101023=" + WL.Combat.Loot.LootBurst.ResolveGrade(10101023) +
" 10101027=" + WL.Combat.Loot.LootBurst.ResolveGrade(10101027) +
" 10101032=" + WL.Combat.Loot.LootBurst.ResolveGrade(10101032));
}
if (booted)
{
// (c) 엘리트 백
L("");
var h2 = new int[10]; int e2, b2;
int min2 = RollGrades(kEliteBag, kRolls, h2, out e2, out b2);
L("[c] RollBag(8130002) ×" + kRolls + " — " + GradeTable(h2, kRolls, e2));
L(" 최소 등급 " + min2 + " · 등급 ≥3 " + (kRolls - e2 - h2[1] - h2[2]) + "/" + kRolls +
" · 빔(≥" + st.beamMinGrade + ") " + b2 + "/" + kRolls + " (" + (100f * b2 / kRolls).ToString("0.0") + "%)");
// (d) 보스 백
L("");
var h3 = new int[10]; int e3, b3;
int min3 = RollGrades(kBossBag, kRolls, h3, out e3, out b3);
int ge5 = h3[5] + h3[6] + h3[7] + h3[8] + h3[9];
L("[d] RollBag(8130003) ×" + kRolls + " — " + GradeTable(h3, kRolls, e3));
L(" 최소 등급 " + min3 + " · 등급 ≥5 " + ge5 + "/" + kRolls +
" · 빔(≥" + st.beamMinGrade + ") " + b3 + "/" + kRolls + " (" + (100f * b3 / kRolls).ToString("0.0") + "%)");
// (e) 존 잡몹 백 — 색 구분 근거
L("");
const int kZoneRolls = 4000;
var h1 = new int[10]; int e1, b1;
RollGrades(kZoneBag, kZoneRolls, h1, out e1, out b1);
int dropped = kZoneRolls - e1;
L("[e] RollBag(8130001) ×" + kZoneRolls + " — " + GradeTable(h1, kZoneRolls, e1));
L(" 드랍 " + dropped + "/" + kZoneRolls + " = " + (100f * dropped / kZoneRolls).ToString("0.00") + "% (기준서 15~25%)");
if (dropped > 0)
L(" 드랍 조건부 분포: g1 " + (100f * h1[1] / dropped).ToString("0.0") + "% · g2 " + (100f * h1[2] / dropped).ToString("0.0") +
"% · g3 " + (100f * h1[3] / dropped).ToString("0.0") + "% · g4 " + (100f * h1[4] / dropped).ToString("0.0") +
"% · g5 " + (100f * h1[5] / dropped).ToString("0.0") + "% → 등급 1~2 = " +
(100f * (h1[1] + h1[2]) / dropped).ToString("0.0") + "%");
L(" 빔(≥" + st.beamMinGrade + ") " + b1 + "/" + kZoneRolls + " = " + (100f * b1 / kZoneRolls).ToString("0.000") + "% (전 처치 대비)");
}
else L("[c][d][e] 건너뜀 — 테이블 부트 실패");
// (f)(g) 버스트 시뮬 — 실제 BurstCount + BagForBurst 경로
L("");
const int kSim = 200;
int eliteMinFloor = 99, eliteMaxFloor = 0, eliteZero = 0, eliteTotal = 0;
for (int t = 0; t < kSim; t++)
{
int n = WL.Combat.Loot.LootBurst.BurstCount(st, eSubRol.Elite);
eliteTotal += n;
int floor = 0;
for (int i = 0; i < n; i++)
if (WL.Combat.Loot.LootBurst.BagForBurst(st, eSubRol.Elite, i, kZoneBag) == kEliteBag) floor++;
if (floor < eliteMinFloor) eliteMinFloor = floor;
if (floor > eliteMaxFloor) eliteMaxFloor = floor;
if (floor == 0) eliteZero++;
}
L("[f] 엘리트 버스트 ×" + kSim + " — 총 " + eliteTotal + "건 · 8130002 건수 min " + eliteMinFloor + " max " + eliteMaxFloor +
" · 0건인 처치 " + eliteZero + "회 (요구: 0)");
int bossOther = 0, bossTotal = 0;
for (int t = 0; t < kSim; t++)
{
int n = WL.Combat.Loot.LootBurst.BurstCount(st, eSubRol.Boss);
bossTotal += n;
for (int i = 0; i < n; i++)
if (WL.Combat.Loot.LootBurst.BagForBurst(st, eSubRol.Boss, i, kZoneBag) != kBossBag) bossOther++;
}
L("[g] 보스 버스트 ×" + kSim + " — 총 " + bossTotal + "건 · 8130003 이 아닌 건 " + bossOther + " (요구: 0)");
L(" 일반 잡몹: BagForBurst(None,0,zone)=" + WL.Combat.Loot.LootBurst.BagForBurst(st, eSubRol.None, 0, kZoneBag) +
" · normalBurstCount=" + st.normalBurstCount);
// (h) 가짜 Killed
L("");
var mobE = MakeFakeMob("__f2Elite", eSubRol.Elite);
var mobB = MakeFakeMob("__f2Boss", eSubRol.Boss);
var mobN = MakeFakeMob("__f2None", eSubRol.None);
WL.Combat.Loot.LootBurst.ResetDiagnostics();
try
{
WL.Combat.Core.CombatEvents.RaiseKilled(mobE);
int cE = WL.Combat.Loot.LootBurst.LastBurstCount;
WL.Combat.Core.CombatEvents.RaiseKilled(mobB);
int cB = WL.Combat.Loot.LootBurst.LastBurstCount;
WL.Combat.Core.CombatEvents.RaiseKilled(mobN);
int cN = WL.Combat.Loot.LootBurst.LastBurstCount;
L("[h] 가짜 Killed 3건 — core Enabled=" + WL.Combat.Core.WLCombatCoreSettings.Enabled +
" 구독=" + WL.Combat.Loot.LootBurst.Subscribed + " 처리=" + WL.Combat.Loot.LootBurst.KilledHandled +
" · elite 계획 " + cE + " · boss " + cB + " · none " + cN +
" · 누적 BurstPlanned " + WL.Combat.Loot.LootBurst.BurstPlanned +
" · 실드랍 " + WL.Combat.Loot.LootBurst.BurstDropped + "(DropItemInfo.Ins=" + (DropItemInfo.Ins != null) + ")");
}
catch (System.Exception e) { L("[h] !! RaiseKilled 실패: " + e.GetType().Name + " " + e.Message); }
UnityEngine.Object.DestroyImmediate(mobE.gameObject);
UnityEngine.Object.DestroyImmediate(mobB.gameObject);
UnityEngine.Object.DestroyImmediate(mobN.gameObject);
// (i) GC
L("");
long g0 = System.GC.GetTotalMemory(false);
for (int i = 0; i < 1000; i++) WL.Combat.Loot.LootBurst.BagForBurst(st, eSubRol.Elite, i & 3, kZoneBag);
long g1 = System.GC.GetTotalMemory(false);
long r0 = System.GC.GetTotalMemory(false);
if (booted) { int a, b; for (int i = 0; i < 1000; i++) WL.Combat.Loot.LootBurst.RollBag(kEliteBag, out a, out b); }
long r1 = System.GC.GetTotalMemory(false);
L("[i] GC — BagForBurst ×1000 델타 " + (g1 - g0) + " B · RollBag ×1000 델타 " + (r1 - r0) + " B");
// (j) C8
L("");
int se = st.eliteBagId, sb = st.bossBagId;
st.eliteBagId = 0; st.bossBagId = 0;
L("[j] C8 — eliteBagId/bossBagId 0 → Elite i0=" + WL.Combat.Loot.LootBurst.BagForBurst(st, eSubRol.Elite, 0, kZoneBag) +
" Boss i0=" + WL.Combat.Loot.LootBurst.BagForBurst(st, eSubRol.Boss, 0, kZoneBag) + " (요구: 둘 다 " + kZoneBag + ")");
st.eliteBagId = se; st.bossBagId = sb;
L(" 복원: eliteBagId=" + st.eliteBagId + " bossBagId=" + st.bossBagId + " (메모리만 · 에셋 저장 0)");
L(" RuntimeDisabled 경로는 813f 프로브 [j] 에서 이미 실측(훅 전체 무동작)");
// (k) 데이터 원문
L("");
L("[k] " + kBagJson);
try
{
string txt = System.IO.File.ReadAllText(Abs(kBagJson));
int rows = 0;
for (int i = 0; i < txt.Length; i++) if (txt[i] == '{') rows++;
L(" 총 행 " + rows + " · 줄바꿈 " + (txt.IndexOf('\n') >= 0 ? "있음" : "없음(한 줄 JSON)"));
int at = txt.IndexOf("\"8130002\"");
if (at > 0) L(" 새 백 원문 시작 오프셋 " + at + " / 길이 " + txt.Length + " (= 뒤에 붙임 · 앞 행 무변경)");
L(" 8130002/8130003 원문:");
int pos = at;
while (pos > 0)
{
int e = txt.IndexOf('}', pos);
if (e < 0) break;
L(" " + txt.Substring(txt.LastIndexOf('{', pos), e - txt.LastIndexOf('{', pos) + 1));
pos = txt.IndexOf("81300", e);
if (pos < 0) break;
}
}
catch (System.Exception e) { L(" !! 읽기 실패: " + e.Message); }
Teardown();
L("");
L("═══ 종료 · 부트한 테이블 회수 완료(Ins=null · 호스트 GO 파괴) ═══");
return Flush("EDIT.txt");
}
}