diff --git a/AgentScripts/WL814d_PROBE.txt b/AgentScripts/WL814d_PROBE.txt index 4a69c33a3..b9be114d6 100644 --- a/AgentScripts/WL814d_PROBE.txt +++ b/AgentScripts/WL814d_PROBE.txt @@ -1,4 +1,4 @@ -# WL814d Probe 2026-09-11 00:19:58 (edit mode · Play 0 · 로그인 0) +# WL814d Probe 2026-09-11 00:47:52 (edit mode · Play 0 · 로그인 0) playMode=False 현재 씬 = '' dirty(before)=False rootCount=0 @@ -9,9 +9,9 @@ playMode=False | 모드 | 이름 | 리그 | SO구도 | cameraHeight | distance | distanceUp | orthoSize | pixelHeight | envLookOn | |---|---|---|---|---|---|---|---|---|---| | 0 | 기본 | 원근 | O | 76 | 3 | 2.4 | 3.8 | 480 | 0 | - | 1 | 도트A | 직교 | - | 74 | 3.4 | 2.2 | 3.8 | 480 | 1 | - | 2 | 도트B | 직교 | - | 55 | 4.5 | 1.8 | 4.2 | 480 | 1 | - | 3 | 도트C | 직교 | - | 45 | 5.5 | 1.5 | 4.6 | 480 | 1 | + | 1 | 도트A | 직교 | - | 76 | 3 | 2.4 | 3.4 | 480 | 1 | + | 2 | 도트B(넓게) | 직교 | - | 76 | 3 | 2.4 | 4.6 | 480 | 1 | + | 3 | 도트C(굵게) | 직교 | - | 76 | 3 | 2.4 | 4.6 | 360 | 1 | [PASS] 0번 = 원근 + 811o SO 구도 그대로(리그 해제) [PASS] 1~3번 = 도트(직교 리그) [PASS] 순환 0→1→2→3→0 (Wrap(4)=0 Wrap(-1)=3) @@ -94,37 +94,37 @@ playMode=False [PASS] 모드 0 카메라 라벨 = "카메라: 기본" (실측 "카메라: 기본") [PASS] 보스 프리셋 arr_cCamData[1] 무변경 — h 78(시작 78) [PASS] step 1 → 모드 1 (실측 1) - | 1 | 도트A | O | True | 3.8 | 640×480 | 74/3.4/3.4/2.2 | 74/3.4/2.2 | 32.7° | 2 | 카메라: 도트A | 배경: ON | + | 1 | 도트A | O | True | 3.4 | 640×480 | 76/3/3/2.4 | 76/3/2.4 | 36.8° | 2 | 카메라: 도트A | 배경: ON | [PASS] 모드 1 리그 조립 = True (실측 True) [PASS] 모드 1 원본 카메라 orthographic = True (실측 True) - [PASS] 모드 1 orthographicSize = 3.8 (실측 3.8) + [PASS] 모드 1 orthographicSize = 3.4 (실측 3.4) [PASS] 모드 1 RT 세로 = pixelHeight 480 (실측 480) - [PASS] 모드 1 구도 = h 74 · d 3.4 · up 2.2 (실측 74/3.4/2.2) + [PASS] 모드 1 구도 = h 76 · d 3 · up 2.4 (실측 76/3/2.4) [PASS] 모드 1 URP 스택 = [ViewCamera, UICamera] 2개(UI 소실 0) — 실측 2 [PASS] 모드 1 UICamera 가 스택 맨 뒤(UI 가 맨 위에 그려진다) [PASS] 모드 1 카메라 라벨 = "카메라: 도트A" (실측 "카메라: 도트A") [PASS] 보스 프리셋 arr_cCamData[1] 무변경 — h 78(시작 78) [PASS] step 2 → 모드 2 (실측 2) - | 2 | 도트B | O | True | 4.2 | 640×480 | 55/4.5/4.5/1.8 | 55/4.5/1.8 | 13.4° | 2 | 카메라: 도트B | 배경: ON | + | 2 | 도트B(넓게) | O | True | 4.6 | 640×480 | 76/3/3/2.4 | 76/3/2.4 | 36.8° | 2 | 카메라: 도트B(넓게) | 배경: ON | [PASS] 모드 2 리그 조립 = True (실측 True) [PASS] 모드 2 원본 카메라 orthographic = True (실측 True) - [PASS] 모드 2 orthographicSize = 4.2 (실측 4.2) + [PASS] 모드 2 orthographicSize = 4.6 (실측 4.6) [PASS] 모드 2 RT 세로 = pixelHeight 480 (실측 480) - [PASS] 모드 2 구도 = h 55 · d 4.5 · up 1.8 (실측 55/4.5/1.8) + [PASS] 모드 2 구도 = h 76 · d 3 · up 2.4 (실측 76/3/2.4) [PASS] 모드 2 URP 스택 = [ViewCamera, UICamera] 2개(UI 소실 0) — 실측 2 [PASS] 모드 2 UICamera 가 스택 맨 뒤(UI 가 맨 위에 그려진다) - [PASS] 모드 2 카메라 라벨 = "카메라: 도트B" (실측 "카메라: 도트B") + [PASS] 모드 2 카메라 라벨 = "카메라: 도트B(넓게)" (실측 "카메라: 도트B(넓게)") [PASS] 보스 프리셋 arr_cCamData[1] 무변경 — h 78(시작 78) [PASS] step 3 → 모드 3 (실측 3) - | 3 | 도트C | O | True | 4.6 | 640×480 | 45/5.5/5.5/1.5 | 45/5.5/1.5 | 2.6° | 2 | 카메라: 도트C | 배경: ON | + | 3 | 도트C(굵게) | O | True | 4.6 | 480×360 | 76/3/3/2.4 | 76/3/2.4 | 36.8° | 2 | 카메라: 도트C(굵게) | 배경: ON | [PASS] 모드 3 리그 조립 = True (실측 True) [PASS] 모드 3 원본 카메라 orthographic = True (실측 True) [PASS] 모드 3 orthographicSize = 4.6 (실측 4.6) - [PASS] 모드 3 RT 세로 = pixelHeight 480 (실측 480) - [PASS] 모드 3 구도 = h 45 · d 5.5 · up 1.5 (실측 45/5.5/1.5) + [PASS] 모드 3 RT 세로 = pixelHeight 360 (실측 360) + [PASS] 모드 3 구도 = h 76 · d 3 · up 2.4 (실측 76/3/2.4) [PASS] 모드 3 URP 스택 = [ViewCamera, UICamera] 2개(UI 소실 0) — 실측 2 [PASS] 모드 3 UICamera 가 스택 맨 뒤(UI 가 맨 위에 그려진다) - [PASS] 모드 3 카메라 라벨 = "카메라: 도트C" (실측 "카메라: 도트C") + [PASS] 모드 3 카메라 라벨 = "카메라: 도트C(굵게)" (실측 "카메라: 도트C(굵게)") [PASS] 보스 프리셋 arr_cCamData[1] 무변경 — h 78(시작 78) [PASS] step 4 → 모드 0 (실측 0) | 0 | 기본 | - | False | 5 | — | 76/3/3/2.4 | 76/3/2.4 | 36.8° | 1 | 카메라: 기본 | 배경: OFF | @@ -147,15 +147,15 @@ playMode=False ── E. 세션 유지 — 로비 강등 · 인게임 복원 · 맵 재진입 재조립 [PASS] 인게임(Stage) 판정 - [PASS] 인게임 모드 2 적용 — applied=2 리그=True h=55 ortho=4.2 + [PASS] 인게임 모드 2 적용 — applied=2 리그=True h=76 ortho=4.6 [PASS] 로비 → 카메라 기본 복귀(적용 0 · 리그 해제) · 고른 모드 2 유지 — 실측 mode=2 applied=0 리그=False ortho=False h=76 - [PASS] 인게임 재진입 → 모드 2 자동 복원 — ortho=4.2 h=55 (로비강등 1회) + [PASS] 인게임 재진입 → 모드 2 자동 복원 — ortho=4.6 h=76 (로비강등 1회) [PASS] 맵 이탈 → 리그 해제되지만 오버라이드 플래그는 유지(다음 진입에서 같은 값으로 재조립) — 실측 True - [PASS] 맵 재진입 → 같은 값으로 재조립 ortho=4.2 - [LookMode] 고른모드=2(도트B) 적용모드=2 배경룩=True · 리그조립=True 리그오버라이드=True · 구도적용=True 런타임프리셋=True(h 55 · d 4.5 · up 1.8) · 적용 9회 · 모드변경 7 · 배경변경 7 · 로비강등 1 + [PASS] 맵 재진입 → 같은 값으로 재조립 ortho=4.6 + [LookMode] 고른모드=2(도트B(넓게)) 적용모드=2 배경룩=True · 리그조립=True 리그오버라이드=True · 구도적용=True 런타임프리셋=True(h 76 · d 3 · up 2.4) · 적용 9회 · 모드변경 7 · 배경변경 7 · 로비강등 1 ── F. GC — 전환 외 프레임당 할당 0 - [PASS] 허브 Tick + UI Tick ×200 → Δ0 B (gen0 233→233) + [PASS] 허브 Tick + UI Tick ×200 → Δ0 B (gen0 242→242) [PASS] 라벨 쓰기 0회(값이 그대로면 안 쓴다) — 실측 0 [PASS] 적용 0회(모드가 그대로면 두 기능을 건드리지 않는다) — 실측 0 [PASS] 대조군 16 MB 할당 감지 Δ16781312 B = 계기 감도 확인 diff --git a/AgentScripts/WL814e_PROBE.txt b/AgentScripts/WL814e_PROBE.txt new file mode 100644 index 000000000..1dbf9d0d3 --- /dev/null +++ b/AgentScripts/WL814e_PROBE.txt @@ -0,0 +1,67 @@ +WL-814e 프로브 · 2026-09-11 00:47:34 · Unity 6000.3.23f1 +playMode=False + +## A. SO 값 (WLLookModeSettings.asset · WLEnvLookSettings.asset · 전부 실측) + PASS A0 두 SO 로드 — look=True env=True + | 모드 | 표기 | 리그 | SO구도 | h | d | up | ortho | px | envLookOn | + | 0 | 기본 | 원근 | SO | 76 | 3 | 2.4 | 3.8 | 480 | 0 | + | 1 | 도트A | 직교 | 프리셋 | 76 | 3 | 2.4 | 3.4 | 480 | 1 | + | 2 | 도트B(넓게) | 직교 | 프리셋 | 76 | 3 | 2.4 | 4.6 | 480 | 1 | + | 3 | 도트C(굵게) | 직교 | 프리셋 | 76 | 3 | 2.4 | 4.6 | 360 | 1 | + PASS A1 프리셋 4개 — ModeCount=4 + PASS A2.0 모드 0 = 표 — 기본 · h 76 d 3 up 2.4 ortho 3.8 px 480 env 0 + PASS A2.1 모드 1 = 표 — 도트A · h 76 d 3 up 2.4 ortho 3.4 px 480 env 1 + PASS A2.2 모드 2 = 표 — 도트B(넓게) · h 76 d 3 up 2.4 ortho 4.6 px 480 env 1 + PASS A2.3 모드 3 = 표 — 도트C(굵게) · h 76 d 3 up 2.4 ortho 4.6 px 360 env 1 + PASS A3 0번 = 원근 + 811o SO 구도 — pixelRig=False useSoFraming=True + PASS A4 1~3번 = 도트(직교 리그) — 3/3 + PASS A5 각도 4행 동일(76/3/2.4) — h 76 전부 동일 + PASS A6 허브 유지 값 — envFollows=True envAtStart=False startMode=0 showButtons=True + PASS A7 WLEnvLookSettings enabled 1 · followLookModeHub 1 · swapCharacters 0 — enabled=True follow=True swapChars=False · grassDensity=0.5(유지) + PASS A8 Enabled 게이트 통과 — Enabled=True + +임시 씬 '' · 맵 WL_Nature · 렌더러 1299개 + +## B. ⓐ 허브 EnvLookOn=false → 맵 진입 경계 → 적용 0 + PASS B0 허브 OFF · 추종 ON — EnvLookOn=False FollowHub=True + PASS B1 경계 적용 0 — MapEnter=False IsApplied=False + PASS B2 스왑 0 · 잔디 0 — 렌더러 0 · 물 0 · 잔디 0 + PASS B3 게이트 카운터 +1 — HubGated=1 + +## C. ⓑ LookModeHub.EnvLookChanged 구독 → ON 적용 · OFF 원복 + PASS C1 구독 성공 · 현재 값(OFF) 1회 반영 — HookHub=True HubHooked=True HubEvents=1 IsApplied=False + PASS C2 중복 구독 0 — 두 번째 HookHub=false · HubEvents=1 + PASS C3 ON → 적용 — 렌더러 1268 · 슬롯 1268 · 물 5 · 잔디 4(8175개) · HubEvents=2 · LastLog='적용 814d 토글' + PASS C4 OFF → 원복 전부 일치 — 불일치 0/1299 · 복원 렌더러 1273 · 잔디 0 · HubEvents=3 · LastLog='복원 814d 토글' + PASS C5 같은 값 재설정 → 이벤트 0 — HubEvents=3 + +## D. ⓒ followLookModeHub = 0 → 814c 원래 동작(경계 자동 적용 · 허브 참조 0) + PASS D0 추종 off — FollowHub=False · 허브 EnvLookOn=False + PASS D1 허브가 꺼져 있어도 경계에서 적용 — MapEnter=True · 렌더러 1268 · 물 5 · 잔디 4 · LastLog='적용 map-enter' + PASS D2 게이트 0회 — HubGated=0 + PASS D3 추종 off 면 구독도 0 — HookHub=false · HubHooked=False · HubEvents=0 + PASS D4 추종 원복 — FollowHub=True + +## E. ⓓ 구독 해제 뒤 이벤트 → 무동작(누수 0) + PASS E0 구독 상태 — HubHooked=True HubEvents=1 + PASS E1 해제 — UnhookHub=True HubHooked=False + PASS E2 이중 해제 무해 — 두 번째 UnhookHub=false + PASS E3 해제 뒤 ON 이벤트 무동작 — HubEvents=1(해제 시점 1) · IsApplied=False · 렌더러 0 · 잔디 0 + PASS E4 해제 뒤 OFF 이벤트 무동작 — HubEvents=1 + PASS E5 재구독 → 이벤트 정확히 1회 — HubEvents 2 → 3 + +## F. ⓕ GC — 브리지 게이트 경로 (허브 OFF · 맵 없음) + PASS F1 MapEnter/FollowHub/Tick ×200k GC 0 — monoΔ 0 B · totalΔ 0 B · gen0 0 · HubGated=201000 + +## G. ⓕ C8 — WLEnvLookSettings.enabled = 0 → 브리지 무동작 + PASS G0 게이트 off — Enabled=False + PASS G1 이벤트를 받아도 적용 0 — IsApplied=False · 렌더러 0 · 잔디 0 · HubEvents=2 + PASS G2 경계도 적용 0 — MapEnter=false + PASS G3 머티리얼 무변경 — 불일치 0/1299 + PASS G4 러너 오브젝트 0 — 0개 + PASS G5 게이트 복구 — Enabled=True + +SO 원값 복구 — followLookModeHub=True · enabled=True · RuntimeDisabled(Env/Look)=False/False +씬 dirty=False · 씬 저장 0 · SaveAssets 0 + +RESULT PASS 39/39 diff --git a/AgentScripts/WL814e_Probe.cs b/AgentScripts/WL814e_Probe.cs new file mode 100644 index 000000000..647d81fb1 --- /dev/null +++ b/AgentScripts/WL814e_Probe.cs @@ -0,0 +1,347 @@ +// 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; + } +} diff --git a/Assets/WL/Look/Env/EnvLook.cs b/Assets/WL/Look/Env/EnvLook.cs index 54f04f3ce..be9d8b5e9 100644 --- a/Assets/WL/Look/Env/EnvLook.cs +++ b/Assets/WL/Look/Env/EnvLook.cs @@ -12,6 +12,9 @@ // ④ 잔디 : 지면 메시에 Critter MeshInstancesBehaviour 를 런타임 부착(GPU 인스턴싱 · 렌더 전용). // ⑤ 물 : 물 렌더러를 Critter Water 머티리얼로 스왑(반사 카메라는 붙이지 않는다 = 성능). // 맵을 벗어나면 · enabled=0 · OnApplicationQuit · 에디터 플레이모드 종료에서 **전부 원복**. +// ■ 814d 허브 브리지 (WL-814e · SO `followLookModeHub` 기본 1) — 맵 진입 자동 적용을 허브 +// `LookModeHub.EnvLookOn` 으로 게이트하고 `EnvLookChanged` 를 구독한다(구독 시 현재 값 1회 반영). +// `followLookModeHub 0` = 814c 원래 동작(허브 참조 0). // // ■ 🔴 왜 「되돌리기」가 중요한가 // 머티리얼·렌더러 피처는 디스크 위의 프로젝트 에셋이다. 값을 바꾼 채로 두면 PD 가 「Save Project」 를 @@ -36,6 +39,7 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; +using WL.Look.Toggle; namespace WL.Look.Env { @@ -178,6 +182,7 @@ namespace WL.Look.Env /// 프로브·A/B 용: 처음 상태로 되돌린다. public static void ResetForProbe() { + UnhookHub(); HubEvents = HubGated = 0; // 🔴 814e — 구독을 남기지 않는다(누수 0) RestoreNow("probe-reset"); // 🔴 RestoreNow 는 「적용 중이 아니면」 곧장 돌아가므로 캐시를 여기서 무조건 비운다. // (안 비우면 이전에 훑어 「스왑할 것 없음」으로 표시된 렌더러가 영영 건너뛰어진다) @@ -609,9 +614,41 @@ namespace WL.Look.Env return; } - ApplyNow("map-enter"); + MapEnter(); } + // ── 814d 허브 브리지 (WL-814e) — SO followLookModeHub 가 1 일 때만 산다 ───────── + static bool s_hooked; + public static int HubEvents, HubGated; + public static bool HubHooked { get { return s_hooked; } } + /// 허브 추종 스위치. 에셋이 없으면 false = 814c 원래 동작(허브 참조 0). + public static bool FollowHub { get { var c = Cfg; return c != null && c.followLookModeHub; } } + /// 맵 진입 적용 — 추종 중이면 허브가 ON 일 때만(프로브가 부르는 경계와 같은 코드). + public static bool MapEnter() { if (FollowHub && !LookModeHub.EnvLookOn) { HubGated++; return false; } return ApplyNow("map-enter"); } + + /// 구독 + 현재 값 1회 반영(이벤트는 「변할 때만」 오므로). 중복 구독 0. + public static bool HookHub() + { + if (s_hooked || !FollowHub) return false; + s_hooked = true; + LookModeHub.EnvLookChanged -= OnHubEnvLook; + LookModeHub.EnvLookChanged += OnHubEnvLook; + OnHubEnvLook(LookModeHub.EnvLookOn); + return true; + } + + /// 구독 해제(누수 0). 이미 해제되어 있으면 무동작. + public static bool UnhookHub() + { + if (!s_hooked) return false; + s_hooked = false; + LookModeHub.EnvLookChanged -= OnHubEnvLook; + return true; + } + + static void OnHubEnvLook(bool on) + { HubEvents++; if (on) ApplyNow("814d 토글"); else RestoreNow("814d 토글"); } + // ═════════════════════════════════════════════════════════════════════ // 러너 (숨김 GameObject 1개 · SO off 면 아예 만들지 않는다 = C8) // ═════════════════════════════════════════════════════════════════════ @@ -640,7 +677,8 @@ namespace WL.Look.Env internal sealed class EnvLookRunner : MonoBehaviour { void Update() { EnvLook.Tick(); } - void OnDisable() { EnvLook.RestoreNow("runner-disable"); } + void OnEnable() { EnvLook.HookHub(); } + void OnDisable() { EnvLook.UnhookHub(); EnvLook.RestoreNow("runner-disable"); } void OnApplicationQuit() { EnvLook.RestoreNow("app-quit"); } } diff --git a/Assets/WL/Look/Env/Resources/WL/WLEnvLookSettings.asset b/Assets/WL/Look/Env/Resources/WL/WLEnvLookSettings.asset index 60061d8aa..ef3598811 100644 --- a/Assets/WL/Look/Env/Resources/WL/WLEnvLookSettings.asset +++ b/Assets/WL/Look/Env/Resources/WL/WLEnvLookSettings.asset @@ -12,8 +12,9 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 385742e771ac6e2478cf9a0913b8d2ed, type: 3} m_Name: WLEnvLookSettings m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Env.WLEnvLookSettings - enabled: 0 + enabled: 1 verboseLog: 0 + followLookModeHub: 1 applyOnlyInBattleMap: 1 restoreOnExit: 1 pollSeconds: 0.5 diff --git a/Assets/WL/Look/Env/WLEnvLookSettings.cs b/Assets/WL/Look/Env/WLEnvLookSettings.cs index 80d59a3f0..2b2194e37 100644 --- a/Assets/WL/Look/Env/WLEnvLookSettings.cs +++ b/Assets/WL/Look/Env/WLEnvLookSettings.cs @@ -21,6 +21,8 @@ namespace WL.Look.Env public bool enabled = true; [Tooltip("적용/복원 1줄 로그")] public bool verboseLog = false; + [Tooltip("🔴 WL-814e — 1 이면 814d 룩 토글 허브(LookModeHub.EnvLookOn)를 따른다: 맵 진입 자동 적용도 허브가 ON 일 때만 · [배경] 버튼으로 즉시 적용/원복. 0 이면 814c 원래 동작(맵 진입 자동 적용)")] + public bool followLookModeHub = true; // ── 맵 경계 ────────────────────────────────────────────────────────── [Tooltip("인게임 맵에서만 적용(로비/타이틀 제외)")] diff --git a/Assets/WL/Look/Toggle/Resources/WL/WLLookModeSettings.asset b/Assets/WL/Look/Toggle/Resources/WL/WLLookModeSettings.asset index eb0b46f3a..87fdc9f5f 100644 --- a/Assets/WL/Look/Toggle/Resources/WL/WLLookModeSettings.asset +++ b/Assets/WL/Look/Toggle/Resources/WL/WLLookModeSettings.asset @@ -32,30 +32,30 @@ MonoBehaviour: - name: "\uB3C4\uD2B8A" pixelRig: 1 useSoFraming: 0 - cameraHeight: 74 - distance: 3.4 - distanceUp: 2.2 - orthoSize: 3.8 + cameraHeight: 76 + distance: 3 + distanceUp: 2.4 + orthoSize: 3.4 pixelHeight: 480 envLookOn: 1 - - name: "\uB3C4\uD2B8B" + - name: "\uB3C4\uD2B8B(\uB113\uAC8C)" pixelRig: 1 useSoFraming: 0 - cameraHeight: 55 - distance: 4.5 - distanceUp: 1.8 - orthoSize: 4.2 - pixelHeight: 480 - envLookOn: 1 - - name: "\uB3C4\uD2B8C" - pixelRig: 1 - useSoFraming: 0 - cameraHeight: 45 - distance: 5.5 - distanceUp: 1.5 + cameraHeight: 76 + distance: 3 + distanceUp: 2.4 orthoSize: 4.6 pixelHeight: 480 envLookOn: 1 + - name: "\uB3C4\uD2B8C(\uAD75\uAC8C)" + pixelRig: 1 + useSoFraming: 0 + cameraHeight: 76 + distance: 3 + distanceUp: 2.4 + orthoSize: 4.6 + pixelHeight: 360 + envLookOn: 1 showButtons: 1 buttonParentPath: IngameUIs/WL_HUD ownCanvas: 1