Project_WL/AgentScripts/WL813f2_Probe.cs

324 lines
17 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.

// 발주서 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");
}
}