324 lines
17 KiB
C#
324 lines
17 KiB
C#
// 발주서 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");
|
||
}
|
||
}
|