// WL-814e 프로브 — EnvLook(814c) ↔ LookModeHub(814d) 브리지 + 프리셋 값 정정 실측 // unity command run_script --file AgentScripts/WL814e_Probe.cs --entry WL814e_Probe.RunAll --project-path E:\NerdNavis\WL_wt\ui // 산출물: /AgentScripts/WL814e_PROBE.txt // 🔴 에디트 모드 전용 · Play 0 · 로그인 0 · 임시 씬(NewScene) 저장 0 · AssetDatabase.SaveAssets 0 · 에셋 파일 쓰기 0. // 🔴 SO 필드를 잠시 바꾸는 구간은 전부 try/finally 로 원값 복구(SetDirty 0 이라 디스크에 안 쓰인다). // 🔴 Critter 의 전역 네임스페이스 `Environment` 는 System.Environment 와 충돌 → global:: 로 명시. using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Profiling; using UnityEngine.SceneManagement; using WL.Look.Env; using WL.Look.Toggle; public static class WL814e_Probe { const string kTxt = "AgentScripts/WL814e_PROBE.txt"; const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab"; static StringBuilder s_sb; static int s_pass, s_fail; static void L(string s) { s_sb.AppendLine(s); } static void Chk(string n, bool ok, string d) { if (ok) s_pass++; else s_fail++; L((ok ? " PASS " : " FAIL ") + n + " — " + d); } // 표(발주서 WL-814e §1-2) — 모드별 h / d / up / ortho / px / envLookOn / 표기 static readonly float[] kH = { 76f, 76f, 76f, 76f }; static readonly float[] kD = { 3f, 3f, 3f, 3f }; static readonly float[] kUp = { 2.4f, 2.4f, 2.4f, 2.4f }; static readonly float[] kOrtho = { 0f, 3.4f, 4.6f, 4.6f }; // 0번은 리그 해제 = 미사용 static readonly int[] kPx = { 0, 480, 480, 360 }; static readonly bool[] kEnv = { false, true, true, true }; static readonly string[] kName = { "기본", "도트A", "도트B(넓게)", "도트C(굵게)" }; public static void RunAll() { s_sb = new StringBuilder(); s_pass = 0; s_fail = 0; L("WL-814e 프로브 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion); L("playMode=" + Application.isPlaying); var cfg = WLEnvLookSettings.Instance; bool savedFollow = cfg != null && cfg.followLookModeHub; bool savedRtE = WLEnvLookSettings.RuntimeDisabled; bool savedRtL = WLLookModeSettings.RuntimeDisabled; Scene temp = default(Scene); GameObject map = null; try { A_Values(); temp = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single); map = Inst(kMapPrefab); L(""); L("임시 씬 '" + temp.name + "' · 맵 " + (map != null ? map.name : "(없음)") + " · 렌더러 " + (map != null ? map.GetComponentsInChildren(true).Length : 0) + "개"); B_GateOff(map); C_HubEvent(map); D_FollowOff(map); E_Unhook(map); F_Gc(); G_C8(map); } catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); } finally { try { EnvLook.UnhookHub(); EnvLook.RestoreNow("probe-final"); EnvLook.ResetForProbe(); } catch { } try { LookModeHub.SetEnvLook(false); LookModeHub.ResetAll(); } catch { } WLEnvLookSettings.RuntimeDisabled = savedRtE; WLLookModeSettings.RuntimeDisabled = savedRtL; if (cfg != null) cfg.followLookModeHub = savedFollow; // 🔴 원값 복구(SetDirty 0) if (map != null) UnityEngine.Object.DestroyImmediate(map); // 🔴 뒤에 도는 프로브(814d)가 Camera.main 을 뺏기지 않도록 빈 씬으로 정리한다(저장 0). if (temp.IsValid()) temp = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); L(""); L("SO 원값 복구 — followLookModeHub=" + (cfg != null ? cfg.followLookModeHub.ToString() : "?") + " · enabled=" + (cfg != null ? cfg.enabled.ToString() : "?") + " · RuntimeDisabled(Env/Look)=" + WLEnvLookSettings.RuntimeDisabled + "/" + WLLookModeSettings.RuntimeDisabled); L("씬 dirty=" + (temp.IsValid() ? temp.isDirty.ToString() : "(무효)") + " · 씬 저장 0 · SaveAssets 0"); L(""); L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail)); var p = Path.Combine(Directory.GetParent(Application.dataPath).FullName, kTxt); File.WriteAllText(p, s_sb.ToString(), new UTF8Encoding(false)); Debug.Log("[WL814e] → " + p + " · " + (s_fail == 0 ? "PASS" : "FAIL") + " " + s_pass + "/" + (s_pass + s_fail)); } } // ── A. 값 표 (ⓔ 앞부분) ────────────────────────────────────────────────── static void A_Values() { L(""); L("## A. SO 값 (WLLookModeSettings.asset · WLEnvLookSettings.asset · 전부 실측)"); var s = WLLookModeSettings.Instance; var c = WLEnvLookSettings.Instance; if (!Chk2("A0 두 SO 로드", s != null && c != null, "look=" + (s != null) + " env=" + (c != null))) return; L(" | 모드 | 표기 | 리그 | SO구도 | h | d | up | ortho | px | envLookOn |"); for (int i = 0; i < 4; i++) { var p = s.Preset(i); L(" | " + i + " | " + p.name + " | " + (p.pixelRig ? "직교" : "원근") + " | " + (p.useSoFraming ? "SO" : "프리셋") + " | " + p.cameraHeight + " | " + p.distance + " | " + p.distanceUp + " | " + p.orthoSize + " | " + p.pixelHeight + " | " + (p.envLookOn ? "1" : "0") + " |"); } Chk("A1 프리셋 4개", s.ModeCount == 4, "ModeCount=" + s.ModeCount); for (int i = 0; i < 4; i++) { var p = s.Preset(i); bool ok = p.name == kName[i] && Mathf.Abs(p.cameraHeight - kH[i]) < 0.001f && Mathf.Abs(p.distance - kD[i]) < 0.001f && Mathf.Abs(p.distanceUp - kUp[i]) < 0.001f && p.envLookOn == kEnv[i] && (i == 0 || (Mathf.Abs(p.orthoSize - kOrtho[i]) < 0.001f && p.pixelHeight == kPx[i])); Chk("A2." + i + " 모드 " + i + " = 표", ok, p.name + " · h " + p.cameraHeight + " d " + p.distance + " up " + p.distanceUp + " ortho " + p.orthoSize + " px " + p.pixelHeight + " env " + (p.envLookOn ? 1 : 0)); } Chk("A3 0번 = 원근 + 811o SO 구도", !s.Preset(0).pixelRig && s.Preset(0).useSoFraming, "pixelRig=" + s.Preset(0).pixelRig + " useSoFraming=" + s.Preset(0).useSoFraming); Chk("A4 1~3번 = 도트(직교 리그)", s.Preset(1).pixelRig && s.Preset(2).pixelRig && s.Preset(3).pixelRig, "3/3"); Chk("A5 각도 4행 동일(76/3/2.4)", s.Preset(0).cameraHeight == s.Preset(1).cameraHeight && s.Preset(1).cameraHeight == s.Preset(2).cameraHeight && s.Preset(2).cameraHeight == s.Preset(3).cameraHeight, "h " + s.Preset(0).cameraHeight + " 전부 동일"); Chk("A6 허브 유지 값", s.envFollowsCameraMode && !s.envLookOnAtStart && s.startMode == 0 && s.showButtons, "envFollows=" + s.envFollowsCameraMode + " envAtStart=" + s.envLookOnAtStart + " startMode=" + s.startMode + " showButtons=" + s.showButtons); Chk("A7 WLEnvLookSettings enabled 1 · followLookModeHub 1 · swapCharacters 0", c.enabled && c.followLookModeHub && !c.swapCharacters, "enabled=" + c.enabled + " follow=" + c.followLookModeHub + " swapChars=" + c.swapCharacters + " · grassDensity=" + c.grassDensity + "(유지)"); Chk("A8 Enabled 게이트 통과", WLEnvLookSettings.Enabled, "Enabled=" + WLEnvLookSettings.Enabled); } // ── B. ⓐ 허브 OFF → 맵 경계 → 적용 0 ───────────────────────────────────── static void B_GateOff(GameObject map) { L(""); L("## B. ⓐ 허브 EnvLookOn=false → 맵 진입 경계 → 적용 0"); if (map == null) return; EnvLook.UnhookHub(); EnvLook.ResetForProbe(); LookModeHub.SetEnvLook(false); Chk("B0 허브 OFF · 추종 ON", !LookModeHub.EnvLookOn && EnvLook.FollowHub, "EnvLookOn=" + LookModeHub.EnvLookOn + " FollowHub=" + EnvLook.FollowHub); bool applied = EnvLook.MapEnter(); int grass = map.GetComponentsInChildren(true).Length; Chk("B1 경계 적용 0", !applied && !EnvLook.IsApplied, "MapEnter=" + applied + " IsApplied=" + EnvLook.IsApplied); Chk("B2 스왑 0 · 잔디 0", EnvLook.SwappedRenderers == 0 && EnvLook.WaterSwapped == 0 && grass == 0, "렌더러 " + EnvLook.SwappedRenderers + " · 물 " + EnvLook.WaterSwapped + " · 잔디 " + grass); Chk("B3 게이트 카운터 +1", EnvLook.HubGated == 1, "HubGated=" + EnvLook.HubGated); } // ── C. ⓑ 허브 ON/OFF 이벤트 → 적용 / 원복 전부 일치 ────────────────────── static void C_HubEvent(GameObject map) { L(""); L("## C. ⓑ LookModeHub.EnvLookChanged 구독 → ON 적용 · OFF 원복"); if (map == null) return; EnvLook.UnhookHub(); EnvLook.ResetForProbe(); LookModeHub.SetEnvLook(false); var before = Snapshot(map); bool hooked = EnvLook.HookHub(); Chk("C1 구독 성공 · 현재 값(OFF) 1회 반영", hooked && EnvLook.HubHooked && EnvLook.HubEvents == 1 && !EnvLook.IsApplied, "HookHub=" + hooked + " HubHooked=" + EnvLook.HubHooked + " HubEvents=" + EnvLook.HubEvents + " IsApplied=" + EnvLook.IsApplied); Chk("C2 중복 구독 0", EnvLook.HookHub() == false && EnvLook.HubEvents == 1, "두 번째 HookHub=false · HubEvents=" + EnvLook.HubEvents); LookModeHub.SetEnvLook(true); int grassOn = map.GetComponentsInChildren(true).Length; Chk("C3 ON → 적용", EnvLook.IsApplied && EnvLook.SwappedRenderers > 0 && EnvLook.HubEvents == 2, "렌더러 " + EnvLook.SwappedRenderers + " · 슬롯 " + EnvLook.SwappedSlots + " · 물 " + EnvLook.WaterSwapped + " · 잔디 " + grassOn + "(" + EnvLook.GrassInstances + "개) · HubEvents=" + EnvLook.HubEvents + " · LastLog='" + EnvLook.LastLog + "'"); LookModeHub.SetEnvLook(false); var back = Snapshot(map); int mismatch = 0; foreach (var kv in before) { string now; if (back.TryGetValue(kv.Key, out now) && now != kv.Value) mismatch++; } int grassOff = map.GetComponentsInChildren(true).Length; Chk("C4 OFF → 원복 전부 일치", !EnvLook.IsApplied && mismatch == 0 && grassOff == 0 && EnvLook.HubEvents == 3, "불일치 " + mismatch + "/" + before.Count + " · 복원 렌더러 " + EnvLook.RestoredRenderers + " · 잔디 " + grassOff + " · HubEvents=" + EnvLook.HubEvents + " · LastLog='" + EnvLook.LastLog + "'"); Chk("C5 같은 값 재설정 → 이벤트 0", DoubleSet(false) && EnvLook.HubEvents == 3, "HubEvents=" + EnvLook.HubEvents); } static bool DoubleSet(bool on) { LookModeHub.SetEnvLook(on); return true; } // ── D. ⓒ followLookModeHub 0 → 814c 원래 동작 ──────────────────────────── static void D_FollowOff(GameObject map) { L(""); L("## D. ⓒ followLookModeHub = 0 → 814c 원래 동작(경계 자동 적용 · 허브 참조 0)"); var c = WLEnvLookSettings.Instance; if (map == null || c == null) return; EnvLook.UnhookHub(); EnvLook.ResetForProbe(); LookModeHub.SetEnvLook(false); // 허브는 꺼 둔 채로 bool saved = c.followLookModeHub; try { c.followLookModeHub = false; Chk("D0 추종 off", !EnvLook.FollowHub, "FollowHub=" + EnvLook.FollowHub + " · 허브 EnvLookOn=" + LookModeHub.EnvLookOn); bool applied = EnvLook.MapEnter(); int grass = map.GetComponentsInChildren(true).Length; Chk("D1 허브가 꺼져 있어도 경계에서 적용", applied && EnvLook.IsApplied && EnvLook.SwappedRenderers > 0, "MapEnter=" + applied + " · 렌더러 " + EnvLook.SwappedRenderers + " · 물 " + EnvLook.WaterSwapped + " · 잔디 " + grass + " · LastLog='" + EnvLook.LastLog + "'"); Chk("D2 게이트 0회", EnvLook.HubGated == 0, "HubGated=" + EnvLook.HubGated); EnvLook.RestoreNow("probe-d"); Chk("D3 추종 off 면 구독도 0", EnvLook.HookHub() == false && !EnvLook.HubHooked && EnvLook.HubEvents == 0, "HookHub=false · HubHooked=" + EnvLook.HubHooked + " · HubEvents=" + EnvLook.HubEvents); } finally { c.followLookModeHub = saved; } Chk("D4 추종 원복", EnvLook.FollowHub, "FollowHub=" + EnvLook.FollowHub); } // ── E. ⓓ 구독 해제 뒤 이벤트 → 무동작(누수 0) ──────────────────────────── static void E_Unhook(GameObject map) { L(""); L("## E. ⓓ 구독 해제 뒤 이벤트 → 무동작(누수 0)"); if (map == null) return; EnvLook.UnhookHub(); EnvLook.ResetForProbe(); LookModeHub.SetEnvLook(false); EnvLook.HookHub(); int e0 = EnvLook.HubEvents; Chk("E0 구독 상태", EnvLook.HubHooked && e0 == 1, "HubHooked=" + EnvLook.HubHooked + " HubEvents=" + e0); bool un = EnvLook.UnhookHub(); Chk("E1 해제", un && !EnvLook.HubHooked, "UnhookHub=" + un + " HubHooked=" + EnvLook.HubHooked); Chk("E2 이중 해제 무해", EnvLook.UnhookHub() == false, "두 번째 UnhookHub=false"); LookModeHub.SetEnvLook(true); int grass = map.GetComponentsInChildren(true).Length; Chk("E3 해제 뒤 ON 이벤트 무동작", EnvLook.HubEvents == e0 && !EnvLook.IsApplied && EnvLook.SwappedRenderers == 0 && grass == 0, "HubEvents=" + EnvLook.HubEvents + "(해제 시점 " + e0 + ") · IsApplied=" + EnvLook.IsApplied + " · 렌더러 " + EnvLook.SwappedRenderers + " · 잔디 " + grass); LookModeHub.SetEnvLook(false); Chk("E4 해제 뒤 OFF 이벤트 무동작", EnvLook.HubEvents == e0, "HubEvents=" + EnvLook.HubEvents); // 재구독 → 다시 산다(핸들러가 남아 두 번 불리지 않는다) EnvLook.HookHub(); int e1 = EnvLook.HubEvents; LookModeHub.SetEnvLook(true); Chk("E5 재구독 → 이벤트 정확히 1회", EnvLook.HubEvents == e1 + 1, "HubEvents " + e1 + " → " + EnvLook.HubEvents); LookModeHub.SetEnvLook(false); EnvLook.UnhookHub(); EnvLook.ResetForProbe(); } // ── F. ⓕ GC 0 ─────────────────────────────────────────────────────────── static void F_Gc() { L(""); L("## F. ⓕ GC — 브리지 게이트 경로 (허브 OFF · 맵 없음)"); EnvLook.UnhookHub(); EnvLook.ResetForProbe(); LookModeHub.SetEnvLook(false); for (int i = 0; i < 1000; i++) { EnvLook.MapEnter(); EnvLook.TickForProbe(); } // 워밍업 GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); long m0 = Profiler.GetMonoUsedSizeLong(), t0 = GC.GetTotalMemory(false); int g0 = GC.CollectionCount(0); for (int i = 0; i < 200000; i++) EnvLook.MapEnter(); for (int i = 0; i < 200000; i++) { if (EnvLook.FollowHub && EnvLook.HubHooked) { } } for (int i = 0; i < 200000; i++) EnvLook.TickForProbe(); long m1 = Profiler.GetMonoUsedSizeLong(), t1 = GC.GetTotalMemory(false); int g1 = GC.CollectionCount(0); Chk("F1 MapEnter/FollowHub/Tick ×200k GC 0", (m1 - m0) == 0 && (t1 - t0) == 0 && (g1 - g0) == 0, "monoΔ " + (m1 - m0) + " B · totalΔ " + (t1 - t0) + " B · gen0 " + (g1 - g0) + " · HubGated=" + EnvLook.HubGated); EnvLook.ResetForProbe(); } // ── G. ⓕ C8 (WLEnvLookSettings.enabled 0 → 브리지 무동작) ──────────────── static void G_C8(GameObject map) { L(""); L("## G. ⓕ C8 — WLEnvLookSettings.enabled = 0 → 브리지 무동작"); if (map == null) return; EnvLook.UnhookHub(); EnvLook.ResetForProbe(); LookModeHub.SetEnvLook(false); var before = Snapshot(map); try { WLEnvLookSettings.RuntimeDisabled = true; // = 에셋 enabled 0 과 같은 게이트 Chk("G0 게이트 off", !WLEnvLookSettings.Enabled, "Enabled=" + WLEnvLookSettings.Enabled); EnvLook.HookHub(); LookModeHub.SetEnvLook(true); // [배경] ON 상당 int grass = map.GetComponentsInChildren(true).Length; Chk("G1 이벤트를 받아도 적용 0", !EnvLook.IsApplied && EnvLook.SwappedRenderers == 0 && grass == 0, "IsApplied=" + EnvLook.IsApplied + " · 렌더러 " + EnvLook.SwappedRenderers + " · 잔디 " + grass + " · HubEvents=" + EnvLook.HubEvents); Chk("G2 경계도 적용 0", EnvLook.MapEnter() == false && !EnvLook.IsApplied, "MapEnter=false"); var back = Snapshot(map); int mismatch = 0; foreach (var kv in before) { string now; if (back.TryGetValue(kv.Key, out now) && now != kv.Value) mismatch++; } Chk("G3 머티리얼 무변경", mismatch == 0, "불일치 " + mismatch + "/" + before.Count); EnvLook.EnsureRunnerForProbe(); int runners = 0; foreach (var go in UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (go.name.StartsWith("[WL814c]")) runners++; Chk("G4 러너 오브젝트 0", runners == 0, runners + "개"); } finally { WLEnvLookSettings.RuntimeDisabled = false; LookModeHub.SetEnvLook(false); EnvLook.UnhookHub(); EnvLook.ResetForProbe(); } Chk("G5 게이트 복구", WLEnvLookSettings.Enabled, "Enabled=" + WLEnvLookSettings.Enabled); } // ── 헬퍼 ──────────────────────────────────────────────────────────────── static bool Chk2(string n, bool ok, string d) { Chk(n, ok, d); return ok; } static Dictionary Snapshot(GameObject root) { var d = new Dictionary(); foreach (var r in root.GetComponentsInChildren(true)) { var sb = new StringBuilder(); foreach (var m in r.sharedMaterials) sb.Append(m != null ? m.name : "(null)").Append('|'); d[r.GetInstanceID()] = sb.ToString(); } return d; } static GameObject Inst(string path) { var src = AssetDatabase.LoadAssetAtPath(path); if (src == null) { L(" 프리팹 없음 " + path); return null; } var go = (GameObject)PrefabUtility.InstantiatePrefab(src); go.transform.position = Vector3.zero; return go; } }