// WL-813g (#813) — 에디트 모드 실측 프로브(Play 0 · 합성 이벤트). // 실행: unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpSlots // unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpText // unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpDamage // unity command run_script --file AgentScripts/WL813g_Probe.cs --entry WL813g_Probe.DumpPrefab // 🔴 이 파일은 독립 어셈블리로 컴파일된다 — Assembly-CSharp 의 internal(Dispatch 등)은 못 쓴다. // 합성 이벤트는 Assembly-CSharp 안의 헬퍼(KillChainText.RaiseFakeTier 등)를 통해 낸다(813c 교훈). // 🔴 씬은 저장하지 않는다. 만든 임시 오브젝트는 전부 DestroyImmediate 로 회수한다. using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEngine; public static class WL813g_Probe { const string LogDir = "Logs"; public const string NewGameUIPath = "Assets/Res_Addr/MainUI/NewGameUI.prefab"; public const string DmgPrefabPath = "Assets/Res_Addr/Ingame/HUDDMGUI.prefab"; public const string SkillJson = "Assets/ResWork/Table/Export/_WL813n_SkillList.json"; public const string GlobalJson = "Assets/ResWork/Table/Export/GlobalValue.json"; const int UILayer = 5; static void Write(string name, string body) { System.IO.Directory.CreateDirectory(LogDir); System.IO.File.WriteAllText(System.IO.Path.Combine(LogDir, name), body, new System.Text.UTF8Encoding(true)); } static string Desc(RectTransform rt) { if (rt == null) return "(rt 없음)"; return "aMin=" + rt.anchorMin + " aMax=" + rt.anchorMax + " pos=" + rt.anchoredPosition + " size=" + rt.sizeDelta + " pivot=" + rt.pivot + " scale=" + rt.localScale; } // ─────────────────────────────────────────── 표 읽기(에디트 모드에서 table_* 싱글턴이 없어서 JSON 직독) // 표는 단일 라인 JSON 배열이고 값은 전부 문자열이다(813n 실측) → 행 단위로 잘라 정규식으로 읽는다. // (run_script 는 파일마다 독립 어셈블리라 Newtonsoft 참조를 가정하지 않는다.) static List SplitRows(string path) { var rows = new List(); try { var txt = System.IO.File.ReadAllText(path, System.Text.Encoding.UTF8).TrimStart(''); foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(txt, "\\{[^{}]*\\}")) rows.Add(m.Value); } catch (System.Exception e) { Debug.LogWarning("[813g] 표 읽기 실패 " + path + " : " + e.Message); } return rows; } static string Field(string row, string key) { var m = System.Text.RegularExpressions.Regex.Match(row, "\"" + key + "\"\\s*:\\s*\"([^\"]*)\""); return m.Success ? m.Groups[1].Value : null; } static Dictionary ReadSkillMp() { var map = new Dictionary(); foreach (var row in SplitRows(SkillJson)) { var id = Field(row, "n_SkillID"); var mp = Field(row, "n_MP"); int i, v; if (id != null && mp != null && int.TryParse(id, out i) && int.TryParse(mp, out v)) map[i] = v; } return map; } static Dictionary ReadSkillCool() { var map = new Dictionary(); foreach (var row in SplitRows(SkillJson)) { var id = Field(row, "n_SkillID"); var cd = Field(row, "f_CoolTime"); int i; float v; if (id != null && cd != null && int.TryParse(id, out i) && float.TryParse(cd, out v)) map[i] = v; } return map; } static float ReadGlobal(string id, float fallback) { foreach (var row in SplitRows(GlobalJson)) { if (Field(row, "s_ID") != id) continue; float v; if (float.TryParse(Field(row, "n_Value"), out v)) return v; } return fallback; } // ─────────────────────────────────────────── ① 슬롯 오버라이드 (발주서 §3 검증 = 4×4 덤프) public static object DumpSlots() { var sb = new StringBuilder(); var preset = WL.UI.WLSkillSlotPreset.Instance; sb.AppendLine("=== WL-813g ① 슬롯 장착 로컬 오버라이드 (에디트 모드 · Play 0) ==="); if (preset == null) { var s0 = "🔴 WLSkillSlotPreset.asset 로드 실패"; Write("WL813g_probe_slots.txt", s0); return s0; } sb.AppendLine("[해석 표 4×4]"); sb.Append(WL.UI.WLSkillSlotOverride.ResolveDump(preset)); var mp = ReadSkillMp(); var cool = ReadSkillCool(); float mpPool = ReadGlobal("MP", -1f); float mpRegen = ReadGlobal("MP_REGEN", -1f); sb.AppendLine(); sb.AppendLine("[MP 게이트 · Actor.cs:733-741 과 같은 식 · SKILL_COST=0 가정 · MP 풀=" + mpPool + " (GlobalValue 실측) · 회복=" + mpRegen + "/s]"); for (int r = 0; r < preset.rows.Length; r++) { var row = preset.rows[r]; int n = preset.SlotCountFor(row.classId); sb.Append("class ").Append(row.classId).Append(" :"); for (int s = 0; s < n; s++) { int id = preset.SkillIdFor(row.classId, s); int need = mp.ContainsKey(id) ? mp[id] : -1; float cd = cool.ContainsKey(id) ? cool[id] : -1f; bool dim = mpPool > 0f && need > mpPool; sb.Append(" [").Append(s).Append("] id=").Append(id).Append(" mp=").Append(need) .Append(" cool=").Append(cd).Append("s dim@만탱=").Append(dim); } sb.AppendLine(); } // ── 합성 SD_Equip 에 실제 ApplyTo 를 태워 before→after 를 잰다(서버 데이터 무접촉) sb.AppendLine(); sb.AppendLine("[합성 SD_Equip 적용 · 실제 WLSkillSlotOverride.ApplyTo 경로]"); foreach (var row in preset.rows) { var equip = new SD_Equip(); equip.Preset = 0; equip.Equip = new Dictionary(); equip.Equip[eItem.PC] = row.classId; equip.Skill = new Dictionary>(); var lst = new List(); for (int i = 0; i < 6; i++) lst.Add(0); lst[0] = 700001; // 서버 프리셋 흉내(패시브·엉뚱한 값) lst[1] = 0; lst[2] = 0; lst[3] = 0; equip.Skill[0] = lst; var restore = new List(); string log = WL.UI.WLSkillSlotOverride.ApplyTo(equip, preset, row.classId, restore); sb.AppendLine(" " + log); var after = new StringBuilder(" Equip.Skill[0] = "); for (int i = 0; i < lst.Count; i++) after.Append(lst[i]).Append(i < lst.Count - 1 ? ", " : ""); after.Append(" (슬롯 4·5 = 물약/회피 예약 · 건드리지 않음)"); sb.AppendLine(after.ToString()); } sb.AppendLine(); sb.AppendLine("[C8 롤백] overrideEnabled=false 또는 에셋 삭제 → ApplyTo 미호출 = 서버 프리셋 원본"); var s1 = sb.ToString(); Write("WL813g_probe_slots.txt", s1); return s1; } // ─────────────────────────────────────────── ② 텍스트 연출 3종 (합성 이벤트) static Canvas MakeCanvas(out GameObject go) { go = new GameObject("WL813g_TempCanvas", typeof(RectTransform), typeof(Canvas), typeof(UnityEngine.UI.CanvasScaler)); go.layer = UILayer; var canvas = go.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; var scaler = go.GetComponent(); // 값을 가정하지 않는다 — 실제 NewGameUI.prefab 루트의 CanvasScaler 설정을 그대로 복제한다(802b/813c 규약). scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920f, 1080f); scaler.screenMatchMode = UnityEngine.UI.CanvasScaler.ScreenMatchMode.Expand; scaler.matchWidthOrHeight = 0f; var srcPrefab = AssetDatabase.LoadAssetAtPath(NewGameUIPath); var src = srcPrefab != null ? srcPrefab.GetComponent() : null; if (src != null) { scaler.uiScaleMode = src.uiScaleMode; scaler.referenceResolution = src.referenceResolution; scaler.screenMatchMode = src.screenMatchMode; scaler.matchWidthOrHeight = src.matchWidthOrHeight; } LastScalerInfo = "CanvasScaler(원본 " + (src != null ? "복제" : "「미확인」 기본값") + ") mode=" + scaler.uiScaleMode + " ref=" + scaler.referenceResolution + " match=" + scaler.screenMatchMode + "/" + scaler.matchWidthOrHeight; var rt = (RectTransform)go.transform; rt.sizeDelta = new Vector2(1080f, 1920f); // 에디트 모드에서 캔버스가 스스로 크기를 못 잡으므로 기준 화면 주입 return canvas; } static string LastScalerInfo = ""; static RectTransform Child(RectTransform parent, string name) { var go = new GameObject(name, typeof(RectTransform)); go.layer = UILayer; var rt = (RectTransform)go.transform; rt.SetParent(parent, false); return rt; } public static object DumpText() { var sb = new StringBuilder(); sb.AppendLine("=== WL-813g ② 텍스트 연출 3종 · 합성 이벤트 (에디트 모드 · Play 0) ==="); var st = WL.UI.WLCombatTextSettings.Instance; if (st == null) { var s0 = "🔴 WLCombatTextSettings.asset 로드 실패"; Write("WL813g_probe_text.txt", s0); return s0; } sb.AppendLine("설정: enabled=" + WL.UI.WLCombatTextSettings.Enabled + " killChain=" + st.killChainSeconds + "s/punch " + st.killChainPunchScale + " loot=" + st.lootToastSeconds + "s/" + st.lootToastLines + "줄" + " stat=" + st.statPopupSeconds + "s/" + st.statPopupRows + "줄"); GameObject canvasGo; var canvas = MakeCanvas(out canvasGo); try { var croot = (RectTransform)canvas.transform; sb.AppendLine(LastScalerInfo + " → unitsPerPx=" + st.UnitsPerPx(canvas).ToString("F4") + " (802b 실측 1.7778 과 대조)"); // ── (A) 연쇄 처치 문구 var kcRt = Child(croot, "WL_KillChainText"); var kc = kcRt.gameObject.AddComponent(); sb.AppendLine(); sb.AppendLine("[A] KillChainText"); sb.AppendLine(" init: " + kc.Initialize()); sb.AppendLine(" 구독자 수(KillChain.TierReached) = " + WL.Combat.Reaction.KillChain.TierReached.Count); var tiers = WL.Combat.Reaction.WLReactionSettings.Instance != null ? WL.Combat.Reaction.WLReactionSettings.Instance.tiers : null; if (tiers == null) sb.AppendLine(" 🔴 WLReactionSettings.tiers 없음(811e 에셋 미로드)"); else for (int i = 0; i < tiers.Length; i++) { sb.AppendLine(" ── 합성 " + WL.UI.KillChainText.RaiseFakeTier(i, tiers[i].name, tiers[i].kills)); sb.Append(" ").Append(kc.Dump()); } kc.Subscribe(false); sb.AppendLine(" 구독 해제 후 구독자 = " + WL.Combat.Reaction.KillChain.TierReached.Count); // ── (B) 전리품 토스트 var ltRt = Child(croot, "WL_LootToast"); var lt = ltRt.gameObject.AddComponent(); sb.AppendLine(); sb.AppendLine("[B] LootToast"); sb.AppendLine(" init: " + lt.Initialize()); sb.AppendLine(" 구독자 수(CombatEvents.Killed) = " + WL.Combat.Core.CombatEvents.Killed.Count); sb.AppendLine(" ── " + WL.UI.LootToast.RaiseFakeKilled() + " → killedSeen=" + lt.KilledSeen); // 등급 1~6 을 순서대로 밀어 넣어 동시 줄 상한을 잰다(표 이름이 없으면 ID 를 이름으로 쓴다) int[] fakeItems = { 1001, 1002, 1003, 1004, 1005 }; for (int i = 0; i < fakeItems.Length; i++) sb.AppendLine(" push[" + i + "] " + lt.Push(fakeItems[i], 1) + " → visible=" + lt.VisibleLines); sb.AppendLine(" 중복 병합 " + lt.Push(fakeItems[fakeItems.Length - 1], 2) + " → visible=" + lt.VisibleLines); sb.Append(lt.Dump()); lt.Subscribe(false); sb.AppendLine(" 구독 해제 후 구독자 = " + WL.Combat.Core.CombatEvents.Killed.Count); // ── (C) 레벨업 스탯 팝업 var spRt = Child(croot, "WL_StatPopup"); var sp = spRt.gameObject.AddComponent(); sb.AppendLine(); sb.AppendLine("[C] StatPopup"); sb.AppendLine(" init: " + sp.Initialize()); sb.AppendLine(" 구독자 수(CombatEvents.LevelUp) = " + WL.Combat.Core.CombatEvents.LevelUp.Count); sb.AppendLine(" ── " + WL.UI.StatPopup.RaiseFakeLevelUp(7) + " (PC 없음 → 스냅샷 대체 경로도 값 0 · 표시는 제목만)"); sb.Append(sp.Dump()); sb.AppendLine(" ── 813k 페이로드 모양으로 직접 표시:"); sb.AppendLine(" " + sp.ShowFake(8, new[] { "STR", "DEX", "INT" }, new double[] { 120, 80, 45 }, new double[] { 128, 84, 48 })); sb.Append(sp.Dump()); sp.Subscribe(false); sb.AppendLine(" 구독 해제 후 구독자 = " + WL.Combat.Core.CombatEvents.LevelUp.Count); } finally { if (canvasGo != null) Object.DestroyImmediate(canvasGo); } var s1 = sb.ToString(); Write("WL813g_probe_text.txt", s1); return s1; } // ─────────────────────────────────────────── ③ 데미지 숫자 리듬 (실제 HUDDMGUI 프리팹) public static object DumpDamage() { var sb = new StringBuilder(); sb.AppendLine("=== WL-813g ③ 데미지 숫자 리듬 · 실제 HUDDMGUI 프리팹 (에디트 모드 · Play 0) ==="); sb.AppendLine("설정: " + HUDDMGUI.SettingsDump()); var prefab = AssetDatabase.LoadAssetAtPath(DmgPrefabPath); if (prefab == null) { var s0 = "🔴 " + DmgPrefabPath + " 없음"; Write("WL813g_probe_damage.txt", s0); return s0; } GameObject canvasGo; var canvas = MakeCanvas(out canvasGo); var made = new List(); try { HUDDMGUI.ClearActiveList(); var st = WL.UI.WLCombatTextSettings.Instance; int cap = st != null ? st.damageMaxConcurrent : 0; var croot = (RectTransform)canvas.transform; System.Func spawn = () => { var go = (GameObject)Object.Instantiate(prefab, croot); go.name = "HUDDMGUI_" + made.Count; made.Add(go); return go.GetComponent(); }; // (1) 일반 vs 크리 — 표시 시간 · 배율(기준서 §B 요소 2 = 0.6 / 1.0 / 1.6배) sb.AppendLine(); sb.AppendLine("[1] 일반 / 크리 규격"); var a = spawn(); a.SetCore(null, "", 1234, eStat.None); sb.AppendLine(" 일반 : " + a.DumpState()); var b = spawn(); b.SetCore(null, "", 1234, eStat.CRI); sb.AppendLine(" 크리 : " + b.DumpState()); float sa = a.dmgs[0].transform.localScale.x, sbb = b.dmgs[1].transform.localScale.x; sb.AppendLine(" 크기 비 = " + (sa > 0f ? (sbb / sa).ToString("F3") : "?") + " (목표 " + (st != null ? st.damageCritScale : 1f) + ")"); // (2) 합산 윈도우 — 같은 대상 · 같은 색이면 새 숫자 대신 합산(811e 0.25 s) sb.AppendLine(); sb.AppendLine("[2] 합산 윈도우 (같은 대상 · 같은 색)"); var targetGo = new GameObject("WL813g_FakeVictim"); made.Add(targetGo); var t = targetGo.transform; HUDDMGUI.ClearActiveList(); var m0 = spawn(); m0.SetCore(t, "", 100, eStat.None); sb.AppendLine(" 1타 : " + m0.DumpState() + " | active=" + HUDDMGUI.ActiveCount); var m1 = spawn(); m1.SetCore(t, "", 250, eStat.None); sb.AppendLine(" 2타 : " + m0.DumpState() + " | active=" + HUDDMGUI.ActiveCount + " merged=" + HUDDMGUI.MergedCount); var m2 = spawn(); m2.SetCore(t, "", 900, eStat.CRI); sb.AppendLine(" 3타(크리) : " + m0.DumpState() + " | active=" + HUDDMGUI.ActiveCount + " merged=" + HUDDMGUI.MergedCount); sb.AppendLine(" → 합산 창 안에서는 인스턴스가 늘지 않고 합계·타수만 오른다(크리 섞이면 크리 규격으로 승격)"); // (3) 동시 상한 — 상한 초과 시 오래된 일반부터 회수 sb.AppendLine(); sb.AppendLine("[3] 동시 표시 상한 (설정 " + cap + ")"); HUDDMGUI.ClearActiveList(); int n = Mathf.Max(1, cap) + 5; for (int i = 0; i < n; i++) { var g = new GameObject("victim" + i); made.Add(g); var h = spawn(); h.SetCore(g.transform, "", 10 + i, (i % 4 == 0) ? eStat.CRI : eStat.None); } sb.AppendLine(" " + n + "개 요청 → active=" + HUDDMGUI.ActiveCount + " (상한 " + cap + ") evicted=" + HUDDMGUI.EvictedCount + " · 상한 준수=" + (cap <= 0 || HUDDMGUI.ActiveCount <= cap)); // (4) C8 롤백 — 설정 off 면 원본 상수(0.9 s · 배율 1) sb.AppendLine(); sb.AppendLine("[4] C8 롤백 (RuntimeDisabled=true = 에셋 없음과 같은 경로)"); HUDDMGUI.ClearActiveList(); WL.UI.WLCombatTextSettings.RuntimeDisabled = true; var c = spawn(); c.SetCore(null, "", 777, eStat.CRI); sb.AppendLine(" 크리(롤백) : " + c.DumpState()); sb.AppendLine(" 설정 덤프 : " + HUDDMGUI.SettingsDump()); WL.UI.WLCombatTextSettings.RuntimeDisabled = false; } finally { for (int i = made.Count - 1; i >= 0; i--) if (made[i] != null) Object.DestroyImmediate(made[i]); if (canvasGo != null) Object.DestroyImmediate(canvasGo); HUDDMGUI.ClearActiveList(); } var s1 = sb.ToString(); Write("WL813g_probe_damage.txt", s1); return s1; } // ─────────────────────────────────────────── ④ 프리팹 노드 (통째 Read 금지 · 노드만) public static object DumpPrefab() { var sb = new StringBuilder(); sb.AppendLine("=== WL-813g ④ NewGameUI.prefab 추가 노드 실측 ==="); var root = PrefabUtility.LoadPrefabContents(NewGameUIPath); try { var ingame = root.transform.Find("IngameUIs"); if (ingame == null) { sb.AppendLine("🔴 IngameUIs 없음"); } else { var hud = ingame.Find("WL_HUD"); sb.AppendLine("IngameUIs/WL_HUD : " + (hud != null ? "있음 children=" + hud.childCount : "🔴 없음")); if (hud != null) { sb.AppendLine(" SafeAreaFitter=" + (hud.GetComponent() != null)); foreach (Transform c in hud) sb.AppendLine(" child " + c.name + " " + Desc(c as RectTransform) + " comps=" + string.Join(",", System.Array.ConvertAll(c.GetComponents(), x => x.GetType().Name))); } var pad = ingame.Find("BattleUI"); sb.AppendLine("IngameUIs/BattleUI : " + (pad != null ? "있음" : "🔴 없음")); if (pad != null) sb.AppendLine(" comps=" + string.Join(",", System.Array.ConvertAll(pad.GetComponents(), x => x.GetType().Name))); } } finally { PrefabUtility.UnloadPrefabContents(root); } var s1 = sb.ToString(); Write("WL813g_probe_prefab.txt", s1); return s1; } }