Project_WL/AgentScripts/WL814d_Probe.cs

543 lines
34 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-814d 프로브 — 인게임 룩 토글(카메라 모드 순환 + 배경 룩 훅) 실측 (에디트 모드 · Play 0 · 로그인 0).
// unity command run_script --file AgentScripts/WL814d_Probe.cs --entry WL814d_Probe.Edit --project-path E:\NerdNavis\WL_wt\ui
// 산출물: <worktree>/AgentScripts/WL814d_PROBE.txt
// 🔴 NewGameUI.prefab 은 LoadPrefabContents 로 열고 **저장하지 않는다**(UnloadPrefabContents(false)).
// 🔴 씬에 만드는 것은 전부 HideFlags.DontSave + DestroyImmediate · 씬 저장 0 · 에셋 쓰기 0.
using System;
using System.IO;
using System.Text;
using TMPro;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.UI;
using WL.CameraFraming;
using WL.Look.Toggle;
using WL.PixelArt;
using WL.UI;
public static class WL814d_Probe
{
const string kUiPrefab = "Assets/Res_Addr/MainUI/NewGameUI.prefab";
const string kCamPrefab = "Assets/ResWork/Prefabs/Ingame/Main Camera.prefab";
const string kOut = "AgentScripts/WL814d_PROBE.txt";
static StringBuilder _o;
static int _pass, _fail;
static void L(string s) { _o.AppendLine(s); }
static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); }
static void N(string s) { _o.AppendLine(" " + s); }
static bool Chk(bool ok, string what)
{
if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); }
else { _fail++; _o.AppendLine(" [FAIL] " + what); }
return ok;
}
static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } }
// 프로브가 만든 것
static GameObject _uiRoot, _camGo, _uiCamGo, _infoGo;
static Camera _srcCam, _uiCam;
static RealCamera _realCam;
public static string Edit()
{
_o = new StringBuilder(); _pass = _fail = 0;
L("# WL814d Probe " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " (edit mode · Play 0 · 로그인 0)");
L("playMode=" + Application.isPlaying);
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
L("현재 씬 = '" + scene.name + "' dirty(before)=" + scene.isDirty + " rootCount=" + scene.rootCount);
var savedRt = WLLookModeSettings.RuntimeDisabled;
try
{
A_Asset();
B_Scene();
C_Nodes();
D_ModeCycle();
E_Session();
F_Gc();
G_C8();
}
catch (Exception e)
{
_fail++;
L(" [FAIL] 예외 " + e.GetType().Name + " : " + e.Message);
L(e.StackTrace);
}
finally
{
WLLookModeSettings.RuntimeDisabled = savedRt;
Z_Cleanup();
}
L("");
L("RESULT " + (_fail == 0 ? "PASS" : "FAIL") + " (pass=" + _pass + " fail=" + _fail + ")");
var after = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
L("씬 정리 후: dirty=" + after.isDirty + " rootCount=" + after.rootCount + " (저장 0)");
string path = Path.Combine(Root, kOut);
File.WriteAllText(path, _o.ToString(), new UTF8Encoding(false));
return _o.ToString() + "\n(파일: " + path + ")";
}
// ─────────────────────────────────────────── A. 에셋 · 계약
static void A_Asset()
{
H("A. 에셋 · 계약 (814d SO · 814b SO · 811o SO)");
WLLookModeSettings.ClearCache();
var s = WLLookModeSettings.Instance;
Chk(s != null, "WLLookModeSettings.asset 로드(Resources/WL/WLLookModeSettings)");
if (s == null) return;
Chk(s.enabled_ && WLLookModeSettings.Enabled, "enabled_ = 1");
Chk(s.ModeCount == 4, "모드 4개 — 실측 " + s.ModeCount);
L(" | 모드 | 이름 | 리그 | SO구도 | cameraHeight | distance | distanceUp | orthoSize | pixelHeight | envLookOn |");
L(" |---|---|---|---|---|---|---|---|---|---|");
for (int i = 0; i < s.ModeCount; i++)
{
var p = s.Preset(i);
L(" | " + i + " | " + p.name + " | " + (p.pixelRig ? "직교" : "원근") + " | " + (p.useSoFraming ? "O" : "-") +
" | " + p.cameraHeight + " | " + p.distance + " | " + p.distanceUp +
" | " + p.orthoSize + " | " + p.pixelHeight + " | " + (p.envLookOn ? "1" : "0") + " |");
}
Chk(!s.Preset(0).pixelRig && s.Preset(0).useSoFraming, "0번 = 원근 + 811o SO 구도 그대로(리그 해제)");
Chk(s.Preset(1).pixelRig && s.Preset(2).pixelRig && s.Preset(3).pixelRig, "1~3번 = 도트(직교 리그)");
Chk(s.Wrap(4) == 0 && s.Wrap(-1) == 3, "순환 0→1→2→3→0 (Wrap(4)=" + s.Wrap(4) + " Wrap(-1)=" + s.Wrap(-1) + ")");
var px = WLPixelCameraSettings.Instance;
N("814b WLPixelCameraSettings: enabled=" + (px != null ? px.enabled.ToString() : "?") +
" orthoSize=" + (px != null ? px.orthoSize.ToString() : "?") +
" pixelHeight=" + (px != null ? px.pixelHeight.ToString() : "?") +
" prefab=" + (px != null && px.pixelCameraSystemPrefab != null));
Chk(px != null && !px.enabled, "🔴 814b SO enabled = 0(F-23) — 강제 조립 API 가 필요한 이유(실측)");
Chk(px != null && px.pixelCameraSystemPrefab != null, "814b 리그 프리팹 배선됨(URP 패키지 해제 확인)");
var fr = WLCameraFramingSettings.Instance;
N("811o WLCameraFramingSettings: enabled=" + (fr != null ? fr.enabled.ToString() : "?") +
" framingEnabled=" + (fr != null ? fr.framingEnabled.ToString() : "?") +
" h=" + (fr != null ? fr.cameraHeight.ToString() : "?") +
" d=" + (fr != null ? fr.distance.ToString() : "?") +
" up=" + (fr != null ? fr.distanceUp.ToString() : "?") +
" poll=" + (fr != null ? fr.framingPollSeconds.ToString() : "?") + "s");
Chk(fr != null && fr.enabled && fr.framingEnabled, "811o 구도 오버라이드가 켜져 있다(런타임 프리셋의 전제)");
}
// ─────────────────────────────────────────── B. 씬(원본 카메라 · UI 카메라)
static void B_Scene()
{
H("B. 임시 씬 — 원본 Main Camera(RealCamera 포함) + UICamera(스택 오버레이)");
var pf = AssetDatabase.LoadAssetAtPath<GameObject>(kCamPrefab);
Chk(pf != null, "원본 카메라 프리팹 로드 " + kCamPrefab);
if (pf == null) return;
_camGo = UnityEngine.Object.Instantiate(pf);
_camGo.name = "__WL814d_MainCamera";
_camGo.hideFlags = HideFlags.DontSave; // 씬 저장 0 · Camera.main 은 찾을 수 있어야 한다
_srcCam = _camGo.GetComponent<Camera>();
_realCam = _camGo.GetComponent<RealCamera>();
Chk(_srcCam != null && _realCam != null, "Camera + RealCamera 보유");
Chk(_camGo.tag == "MainCamera", "tag = MainCamera");
Chk(Camera.main == _srcCam, "Camera.main 이 이 카메라로 해석된다(리그 강제 조립의 전제) — 실측 " +
(Camera.main != null ? Camera.main.name : "null"));
// 원본 프리팹 실측값을 arr_cCamData 에 확인/보강(에디트 모드는 Awake 미실행 → 부작용 0)
if (_realCam.arr_cCamData == null || _realCam.arr_cCamData.Length < 2)
{
_realCam.arr_cCamData = new cCamData[2];
_realCam.arr_cCamData[0] = new cCamData { minDistance = 3f, maxDistance = 3f, DistanceUp = 2f, cameraHeight = 65f };
_realCam.arr_cCamData[1] = new cCamData { minDistance = 2f, maxDistance = 3f, DistanceUp = 3.5f, cameraHeight = 78f };
}
N("프리팹 arr_cCamData[0] h=" + _realCam.arr_cCamData[0].cameraHeight +
" min=" + _realCam.arr_cCamData[0].minDistance + " max=" + _realCam.arr_cCamData[0].maxDistance +
" up=" + _realCam.arr_cCamData[0].DistanceUp +
" · [1](보스 프리셋) h=" + _realCam.arr_cCamData[1].cameraHeight);
N("원본 카메라 orthographic=" + _srcCam.orthographic + " fov=" + _srcCam.fieldOfView +
" cullingMask=" + _srcCam.cullingMask + " depth=" + _srcCam.depth);
// UICamera 를 스택에 넣는다 — 원본 NewGameUI.cs:38 이 런타임에 하는 일과 같다(UI 소실 검증용)
_uiCamGo = new GameObject("__WL814d_UICamera", typeof(Camera));
_uiCamGo.hideFlags = HideFlags.DontSave;
_uiCam = _uiCamGo.GetComponent<Camera>();
_uiCam.orthographic = true; _uiCam.depth = 6f; _uiCam.cullingMask = 32; _uiCam.clearFlags = CameraClearFlags.Nothing;
var uiData = _uiCam.GetUniversalAdditionalCameraData();
if (uiData != null) uiData.renderType = CameraRenderType.Overlay;
var srcData = _srcCam.GetUniversalAdditionalCameraData();
if (srcData != null && srcData.cameraStack != null) { srcData.cameraStack.Clear(); srcData.cameraStack.Add(_uiCam); }
Chk(srcData != null && srcData.cameraStack.Count == 1 && srcData.cameraStack[0] == _uiCam,
"원본 스택 = [UICamera] (NewGameUI.cs:38 재현)");
_bossPreset = _realCam.arr_cCamData[1].cameraHeight;
// 가짜 InGameInfo — 로비/인게임 판정만 바꾼다(에디트 모드 Awake 미실행 · Ins 는 순수 static 필드)
_infoGo = new GameObject("__WL814d_InGameInfo");
_infoGo.hideFlags = HideFlags.DontSave;
_info = _infoGo.AddComponent<InGameInfo>();
InGameInfo.Ins = _info;
_modeField = typeof(InGameInfo).GetField("m_GameMode",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
Chk(_modeField != null, "InGameInfo.m_GameMode 리플렉션(프로브 전용 · 게임 코드 0)");
if (_modeField != null) _modeField.SetValue(_info, eGameMode.Stage);
Chk(!LookModeHub.InLobby, "인게임(Stage) 판정 — LookModeHub.InLobby=" + LookModeHub.InLobby);
}
static float _bossPreset;
static InGameInfo _info;
static System.Reflection.FieldInfo _modeField;
// ─────────────────────────────────────────── C. 버튼 노드 · 좌표
static void C_Nodes()
{
H("C. HUD 버튼 — NewGameUI 프리팹(저장 0) · Safe Area · 좌표 표");
_uiRoot = PrefabUtility.LoadPrefabContents(kUiPrefab);
Chk(_uiRoot != null, "LoadPrefabContents(" + kUiPrefab + ")");
if (_uiRoot == null) return;
var ingame = WLVignetteUtil.FindUiPath(_uiRoot.transform, "IngameUIs");
var hud = WLVignetteUtil.FindUiPath(_uiRoot.transform, "IngameUIs/WL_HUD");
Chk(hud != null, "IngameUIs/WL_HUD 경로 실측");
if (hud != null)
Chk(hud.GetComponentInParent<SafeAreaFitter>(true) != null || hud.GetComponent<SafeAreaFitter>() != null,
"WL_HUD 가 SafeAreaFitter 아래 = Safe Area 안 (802b/813y 실측 재확인)");
N("Bind: " + WLLookToggleUi.Bind(_uiRoot.transform));
Chk(WLLookToggleUi.Bound, "WL_LookToggle 노드 생성(HideFlags.DontSave)");
var s = WLLookModeSettings.Instance;
Chk(WLLookToggleUi.SortingOrder == s.sortingOrder,
"전용 Canvas sortingOrder=" + WLLookToggleUi.SortingOrder + " — 813q2 결과창 40 · 813z 부활 60 보다 아래");
// 🔴 Unity 는 부모 캔버스 계층이 비활성이면 overrideSorting 을 되돌린다.
// 프리팹 저장 상태의 IngameUIs 는 active=False 라 조립 시점에는 false 다 → 인게임(활성)에서 켜지는지 실측한다.
N("조립 직후 overrideSorting=" + WLLookToggleUi.OverrideSorting + " (IngameUIs 활성=" +
(ingame != null ? ingame.gameObject.activeSelf.ToString() : "?") + ")");
if (ingame != null)
{
bool wasOn = ingame.gameObject.activeSelf;
ingame.gameObject.SetActive(true); // 프리팹 미리보기 안에서만 · 저장 0
WLLookToggleUi.Tick();
Chk(WLLookToggleUi.OverrideSorting && WLLookToggleUi.SortingOrder == s.sortingOrder,
"IngameUIs 활성 → overrideSorting=" + WLLookToggleUi.OverrideSorting +
" sortingOrder=" + WLLookToggleUi.SortingOrder + "(중첩 캔버스로 인정 · Tick 이 다시 건다)");
N("활성 첫 프레임 재배치: " + WLLookToggleUi.Dump());
ingame.gameObject.SetActive(wasOn);
}
N("Layout: " + WLLookToggleUi.ApplyLayout());
N(WLLookToggleUi.Dump());
// px → 캔버스 유닛 계수는 NewGameUI 루트 캔버스(CanvasScaler)에서 와야 한다(813q 실측 1.7778).
var rootCanvas = _uiRoot.GetComponent<Canvas>();
var scaler = rootCanvas != null ? rootCanvas.GetComponent<CanvasScaler>() : null;
float u0 = WLVignetteUtil.UnitsPerPx(rootCanvas);
N("루트 캔버스 = " + (rootCanvas != null ? rootCanvas.name : "없음") +
" scaler=" + (scaler != null ? scaler.uiScaleMode + " ref=" + scaler.referenceResolution + " match=" + scaler.screenMatchMode : "없음") +
" → unitsPerPx=" + u0.ToString("F4"));
Chk(Mathf.Abs(u0 - 1.7778f) < 0.001f, "unitsPerPx = 1.7778 (813q·802b 실측과 같은 계수) — 실측 " + u0.ToString("F4"));
var lookRt = FindNode(_uiRoot.transform, "WL_LookToggle") as RectTransform;
Chk(lookRt != null && Mathf.Abs(lookRt.sizeDelta.x - s.buttonSizePx.x * u0) < 0.01f,
"노드 폭 = 200 px × " + u0.ToString("F4") + " = " + (s.buttonSizePx.x * u0).ToString("F2") +
" 유닛 — 실측 " + (lookRt != null ? lookRt.sizeDelta.x.ToString("F2") : "-"));
// 터치 통과 0 — 버튼 배경은 raycastTarget · 라벨은 아님
var node = FindNode(_uiRoot.transform, "WL_LookToggle");
Chk(node != null, "WL_LookToggle 노드 탐색");
if (node != null)
{
var bgs = node.GetComponentsInChildren<Image>(true);
int rc = 0; for (int i = 0; i < bgs.Length; i++) if (bgs[i].raycastTarget) rc++;
Chk(bgs.Length == 2 && rc == 2, "버튼 배경 2개 전부 raycastTarget = 터치 통과 0 — 실측 " + rc + "/" + bgs.Length);
var lbs = node.GetComponentsInChildren<TextMeshProUGUI>(true);
int lrc = 0; for (int i = 0; i < lbs.Length; i++) if (lbs[i].raycastTarget) lrc++;
Chk(lrc == 0, "라벨은 raycastTarget 0(버튼이 먹는다) — 실측 " + lrc);
Chk(node.gameObject.hideFlags == HideFlags.DontSave, "노드 hideFlags = DontSave(프리팹에 굽지 않는다)");
}
// ── 좌표 표 (px · 세로 1080×1920 Safe Area 기준 · 좌상단 원점)
var hs = WLHudLayoutSettings.Instance;
var rs = WLRunUiSettings.Instance;
var bs = WLBossUiSettings.Instance;
float barTop = hs != null ? hs.bossBarTopMarginPx : 190f;
float barBottom = hs != null ? barTop + hs.bossBarSizePx.y + hs.bossBarNameFontPx + hs.bossBarNameGapPx : 256f;
float barHalfW = hs != null ? hs.bossBarSizePx.x * 0.5f : 440f;
float runTop = rs != null ? rs.runHudTopMarginPx : 272f;
float runH = rs != null ? rs.timerFontPx + rs.zoneGaugeTopGapPx + rs.zoneCellHeightPx + rs.phaseTopGapPx + rs.phaseFontPx : 122f;
float runHalfW = rs != null ? rs.runHudWidthPx * 0.5f : 280f;
float banTop = bs != null ? bs.bannerTopMarginPx : 300f;
float banBottom = bs != null ? banTop + bs.bannerSizePx.y : 450f;
float banHalfW = bs != null ? bs.bannerSizePx.x * 0.5f : 430f;
var r0 = WLLookToggleUi.ButtonRectPx(0);
var r1 = WLLookToggleUi.ButtonRectPx(1);
L("");
L(" | 요소 | 소유 | 상단 y(px) | 하단 y(px) | 좌 x(px) | 우 x(px) |");
L(" |---|---|---|---|---|---|");
L(" | 상단 재화 바 | 프리팹(#729) | 130 | 176 | (프리팹) | (프리팹) |");
L(" | 보스 HP 바 | 813c3 | " + barTop + " | " + barBottom + " | " + (540f - barHalfW) + " | " + (540f + barHalfW) + " |");
L(" | 런 HUD | 813q | " + runTop + " | " + (runTop + runH) + " | " + (540f - runHalfW) + " | " + (540f + runHalfW) + " |");
L(" | 보스 배너(1.2 s) | 813tj | " + banTop + " | " + banBottom + " | " + (540f - banHalfW) + " | " + (540f + banHalfW) + " |");
L(" | **[카메라] 버튼** | **814d** | **" + r0.y + "** | **" + (r0.y + r0.height) + "** | **" + r0.x + "** | **" + (r0.x + r0.width) + "** |");
L(" | **[배경] 버튼** | **814d** | **" + r1.y + "** | **" + (r1.y + r1.height) + "** | **" + r1.x + "** | **" + (r1.x + r1.width) + "** |");
L("");
Chk(r0.width >= 96f && r0.height >= 48f, "버튼 크기 ≥ 96×48 px — 실측 " + r0.width + "×" + r0.height);
Chk(r0.y >= barBottom, "보스 HP 바(≤" + barBottom + ")와 겹침 0 · 여유 " + (r0.y - barBottom) + " px");
Chk(r0.y >= runTop + runH, "런 HUD(≤" + (runTop + runH) + ")와 겹침 0 · 여유 " + (r0.y - (runTop + runH)) + " px");
Chk(r0.y >= banBottom, "보스 배너(≤" + banBottom + ")와 겹침 0 · 여유 " + (r0.y - banBottom) + " px");
Chk(r0.x >= 540f + runHalfW, "런 HUD 폭(우 " + (540f + runHalfW) + ")보다 오른쪽 · 가로 여유 " + (r0.x - (540f + runHalfW)) + " px");
Chk(r1.y + r1.height <= 1920f, "두 버튼이 화면 안 — 아래끝 " + (r1.y + r1.height) + " px");
N("결과창(813q2 sortingOrder 40) · 부활 팝업(813z 60) 은 이 버튼(" + WLLookToggleUi.SortingOrder +
")보다 위 = 팝업이 뜨면 버튼이 가려진다(겹침 대신 덮임).");
// 프리팹 상단 노드 실측 덤프(참고)
L("");
L(" [참고] NewGameUI 캔버스 직속 노드(이름 · active):");
var canvasT = _uiRoot.transform;
for (int i = 0; i < canvasT.childCount; i++)
{
var c = canvasT.GetChild(i);
L(" - " + c.name + " (active=" + c.gameObject.activeSelf + " children=" + c.childCount + ")");
}
}
static Transform FindNode(Transform root, string name)
{
var all = root.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < all.Length; i++) if (all[i].name == name) return all[i];
return null;
}
// ─────────────────────────────────────────── D. 모드 순환
static void D_ModeCycle()
{
H("D. 모드 0→1→2→3→0 — 리그 · arr_cCamData · 원본 카메라 · 버튼 라벨");
var s = WLLookModeSettings.Instance;
if (s == null || _srcCam == null || _realCam == null) { Chk(false, "전제 준비 실패"); return; }
LookModeHub.ResetAll();
CameraFramingOverride.ResetDiagnostics();
WLLookToggleUi.ResetState();
WLLookToggleUi.Tick();
L("");
L(" | 모드 | 이름 | 리그조립 | 원본 ortho | orthoSize | RT | arr[0] h/min/max/up | 라이브 h/max/up | 내림각 | 스택 | 카메라 라벨 | 배경 라벨 |");
L(" |---|---|---|---|---|---|---|---|---|---|---|---|");
for (int step = 0; step <= 4; step++)
{
int want = step % 4;
if (step > 0) WLLookToggleUi.ClickCamera(); // 실제 버튼 onClick 경로
Chk(LookModeHub.CameraMode == want, "step " + step + " → 모드 " + want + " (실측 " + LookModeHub.CameraMode + ")");
// 811o 는 폴링으로 적용한다 — 에디트 모드에는 러너가 없으므로 경계 1회를 강제한다(런타임과 같은 Apply 경로).
CameraFramingOverride.ProbeApply(_realCam);
WLLookToggleUi.Tick();
var p = s.Preset(want);
var d = _realCam.arr_cCamData[0];
var srcData = _srcCam.GetUniversalAdditionalCameraData();
int stackN = srcData != null && srcData.cameraStack != null ? srcData.cameraStack.Count : -1;
var mgr = PixelCameraRig.Manager;
string rt = mgr != null ? mgr.GameResolution.x + "×" + mgr.GameResolution.y : "—";
L(" | " + want + " | " + p.name + " | " + (PixelCameraRig.IsAssembled ? "O" : "-") +
" | " + _srcCam.orthographic + " | " + _srcCam.orthographicSize.ToString("0.###") +
" | " + rt +
" | " + d.cameraHeight + "/" + d.minDistance + "/" + d.maxDistance + "/" + d.DistanceUp +
" | " + _realCam.cameraHeight + "/" + _realCam.maxDistance + "/" + _realCam.DistanceUp +
" | " + CameraFramingOverride.DownAngleDeg(d.cameraHeight, d.maxDistance, d.DistanceUp).ToString("0.0") + "°" +
" | " + stackN +
" | " + WLLookToggleUi.CameraLabel + " | " + WLLookToggleUi.EnvLabel + " |");
Chk(PixelCameraRig.IsAssembled == p.pixelRig,
"모드 " + want + " 리그 조립 = " + p.pixelRig + " (실측 " + PixelCameraRig.IsAssembled + ")");
Chk(_srcCam.orthographic == p.pixelRig,
"모드 " + want + " 원본 카메라 orthographic = " + p.pixelRig + " (실측 " + _srcCam.orthographic + ")");
if (p.pixelRig)
{
Chk(Mathf.Abs(_srcCam.orthographicSize - p.orthoSize) < 0.001f,
"모드 " + want + " orthographicSize = " + p.orthoSize + " (실측 " + _srcCam.orthographicSize.ToString("0.####") + ")");
Chk(mgr != null && mgr.GameResolution.y == p.pixelHeight,
"모드 " + want + " RT 세로 = pixelHeight " + p.pixelHeight + " (실측 " + (mgr != null ? mgr.GameResolution.y : -1) + ")");
Chk(Mathf.Abs(d.cameraHeight - p.cameraHeight) < 0.001f && Mathf.Abs(d.maxDistance - p.distance) < 0.001f &&
Mathf.Abs(d.DistanceUp - p.distanceUp) < 0.001f,
"모드 " + want + " 구도 = h " + p.cameraHeight + " · d " + p.distance + " · up " + p.distanceUp +
" (실측 " + d.cameraHeight + "/" + d.maxDistance + "/" + d.DistanceUp + ")");
Chk(stackN == 2, "모드 " + want + " URP 스택 = [ViewCamera, UICamera] 2개(UI 소실 0) — 실측 " + stackN);
Chk(srcData != null && srcData.cameraStack[stackN - 1] == _uiCam, "모드 " + want + " UICamera 가 스택 맨 뒤(UI 가 맨 위에 그려진다)");
}
else
{
var fr = WLCameraFramingSettings.Instance;
Chk(!CameraFramingOverride.RuntimePresetOn, "모드 0 런타임 프리셋 해제 = 811o SO 값으로 복귀");
Chk(fr != null && Mathf.Abs(d.cameraHeight - fr.cameraHeight) < 0.001f && Mathf.Abs(d.maxDistance - fr.distance) < 0.001f,
"모드 0 구도 = 811o SO(h " + (fr != null ? fr.cameraHeight : 0f) + " · d " + (fr != null ? fr.distance : 0f) +
") — 실측 " + d.cameraHeight + "/" + d.maxDistance);
Chk(stackN == 1, "모드 0 URP 스택 = [UICamera] 1개(리그 해제 후 원복) — 실측 " + stackN);
}
Chk(WLLookToggleUi.CameraLabel == string.Format(s.cameraLabelFormat, p.name),
"모드 " + want + " 카메라 라벨 = \"" + string.Format(s.cameraLabelFormat, p.name) + "\" (실측 \"" + WLLookToggleUi.CameraLabel + "\")");
Chk(_realCam.arr_cCamData[1].cameraHeight == _bossPreset,
"보스 프리셋 arr_cCamData[1] 무변경 — h " + _realCam.arr_cCamData[1].cameraHeight + "(시작 " + _bossPreset + ")");
}
// 배경 룩 버튼
H("D-2. 배경 룩 버튼(814c 연동 훅) — 플래그 + 이벤트만");
int evt = 0; bool last = false;
System.Action<bool> sub = v => { evt++; last = v; };
LookModeHub.EnvLookChanged += sub;
try
{
bool before = LookModeHub.EnvLookOn;
WLLookToggleUi.ClickEnv(); WLLookToggleUi.Tick();
Chk(LookModeHub.EnvLookOn != before, "[배경] 탭 → 플래그 뒤집힘 " + before + " → " + LookModeHub.EnvLookOn);
Chk(evt == 1 && last == LookModeHub.EnvLookOn, "EnvLookChanged 1회 발행(814c 구독 지점) — 실측 " + evt + "회 값=" + last);
Chk(WLLookToggleUi.EnvLabel == string.Format(WLLookModeSettings.Instance.envLabelFormat,
LookModeHub.EnvLookOn ? WLLookModeSettings.Instance.envOnText : WLLookModeSettings.Instance.envOffText),
"배경 라벨 = \"" + WLLookToggleUi.EnvLabel + "\"");
WLLookToggleUi.ClickEnv(); WLLookToggleUi.Tick();
Chk(LookModeHub.EnvLookOn == before && evt == 2, "다시 탭 → 원래대로 · 이벤트 2회");
N("클릭 카운트: 카메라 " + WLLookToggleUi.CameraClicks + " · 배경 " + WLLookToggleUi.EnvClicks +
" · 라벨 쓰기 " + WLLookToggleUi.LabelWrites + "회");
// envFollowsCameraMode — 도트 모드로 바꾸면 배경 룩이 따라 켜진다
evt = 0;
LookModeHub.SetCameraMode(0); CameraFramingOverride.ProbeApply(_realCam);
LookModeHub.SetCameraMode(1); CameraFramingOverride.ProbeApply(_realCam);
Chk(WLLookModeSettings.Instance.envFollowsCameraMode && LookModeHub.EnvLookOn,
"envFollowsCameraMode 1 → 도트 모드에서 배경 룩 자동 ON (실측 " + LookModeHub.EnvLookOn + " · 이벤트 " + evt + "회)");
LookModeHub.SetCameraMode(0); CameraFramingOverride.ProbeApply(_realCam);
Chk(!LookModeHub.EnvLookOn, "기본 모드로 돌아오면 배경 룩 자동 OFF (실측 " + LookModeHub.EnvLookOn + ")");
}
finally { LookModeHub.EnvLookChanged -= sub; }
}
// ─────────────────────────────────────────── E. 세션 유지 · 로비 복귀
static void E_Session()
{
H("E. 세션 유지 — 로비 강등 · 인게임 복원 · 맵 재진입 재조립");
if (_modeField == null || _info == null) { Chk(false, "가짜 InGameInfo 준비 실패 — 건너뜀"); return; }
var info = _info; var fi = _modeField;
fi.SetValue(info, eGameMode.Stage);
Chk(!LookModeHub.InLobby, "인게임(Stage) 판정");
LookModeHub.SetCameraMode(2);
LookModeHub.Tick(); CameraFramingOverride.ProbeApply(_realCam);
var d = _realCam.arr_cCamData[0];
Chk(LookModeHub.AppliedMode == 2 && PixelCameraRig.IsAssembled,
"인게임 모드 2 적용 — applied=" + LookModeHub.AppliedMode + " 리그=" + PixelCameraRig.IsAssembled +
" h=" + d.cameraHeight + " ortho=" + _srcCam.orthographicSize.ToString("0.###"));
// 로비로 — 고른 모드는 유지, 적용만 0
fi.SetValue(info, eGameMode.Lobby);
LookModeHub.Tick(); CameraFramingOverride.ProbeApply(_realCam);
d = _realCam.arr_cCamData[0];
Chk(LookModeHub.CameraMode == 2 && LookModeHub.AppliedMode == 0 && !PixelCameraRig.IsAssembled,
"로비 → 카메라 기본 복귀(적용 0 · 리그 해제) · 고른 모드 2 유지 — 실측 mode=" + LookModeHub.CameraMode +
" applied=" + LookModeHub.AppliedMode + " 리그=" + PixelCameraRig.IsAssembled +
" ortho=" + _srcCam.orthographic + " h=" + d.cameraHeight);
// 인게임 재진입 — 저절로 복원
fi.SetValue(info, eGameMode.Stage);
LookModeHub.Tick(); CameraFramingOverride.ProbeApply(_realCam);
d = _realCam.arr_cCamData[0];
var p2 = WLLookModeSettings.Instance.Preset(2);
Chk(LookModeHub.AppliedMode == 2 && PixelCameraRig.IsAssembled &&
Mathf.Abs(_srcCam.orthographicSize - p2.orthoSize) < 0.001f && Mathf.Abs(d.cameraHeight - p2.cameraHeight) < 0.001f,
"인게임 재진입 → 모드 2 자동 복원 — ortho=" + _srcCam.orthographicSize.ToString("0.###") +
" h=" + d.cameraHeight + " (로비강등 " + LookModeHub.LobbyDemotes + "회)");
// 맵 재진입(리그가 걷혔다가 다시 조립되는 경로) — 814b PollBoundary 가 하는 일을 손으로
PixelCameraRig.Teardown("맵 이탈(프로브)");
Chk(!PixelCameraRig.IsAssembled && PixelCameraRig.RuntimeOverrideOn,
"맵 이탈 → 리그 해제되지만 오버라이드 플래그는 유지(다음 진입에서 같은 값으로 재조립) — 실측 " +
PixelCameraRig.RuntimeOverrideOn);
Chk(PixelCameraRig.ProbeAssemble(_srcCam) && Mathf.Abs(_srcCam.orthographicSize - p2.orthoSize) < 0.001f,
"맵 재진입 → 같은 값으로 재조립 ortho=" + _srcCam.orthographicSize.ToString("0.###"));
N(LookModeHub.Dump());
}
// ─────────────────────────────────────────── F. GC
static void F_Gc()
{
H("F. GC — 전환 외 프레임당 할당 0");
LookModeHub.UiTick = null; // 러너 훅 대신 직접 부른다
WLLookToggleUi.Tick();
LookModeHub.Tick(); // 안정화 1회
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
long before = GC.GetTotalMemory(false);
int g0 = GC.CollectionCount(0);
int writes = WLLookToggleUi.LabelWrites, applies = LookModeHub.Applies;
for (int i = 0; i < 200; i++) { LookModeHub.Tick(); WLLookToggleUi.Tick(); }
long after = GC.GetTotalMemory(false);
Chk(after - before == 0, "허브 Tick + UI Tick ×200 → Δ" + (after - before) + " B (gen0 " + g0 + "→" + GC.CollectionCount(0) + ")");
Chk(WLLookToggleUi.LabelWrites == writes, "라벨 쓰기 0회(값이 그대로면 안 쓴다) — 실측 " + (WLLookToggleUi.LabelWrites - writes));
Chk(LookModeHub.Applies == applies, "적용 0회(모드가 그대로면 두 기능을 건드리지 않는다) — 실측 " + (LookModeHub.Applies - applies));
// 대조군 — 계기 감도 확인
long b2 = GC.GetTotalMemory(false);
var junk = new byte[16 * 1024 * 1024];
junk[0] = 1;
long a2 = GC.GetTotalMemory(false);
Chk(a2 - b2 > 15000000, "대조군 16 MB 할당 감지 Δ" + (a2 - b2) + " B = 계기 감도 확인");
junk = null;
}
// ─────────────────────────────────────────── G. C8
static void G_C8()
{
H("G. C8 — WLLookModeSettings 를 끄면 814b/811o 가 원래대로");
var fr = WLCameraFramingSettings.Instance;
LookModeHub.SetCameraMode(1);
LookModeHub.Tick(); CameraFramingOverride.ProbeApply(_realCam);
Chk(PixelCameraRig.RuntimeOverrideOn && CameraFramingOverride.RuntimePresetOn, "끄기 전 = 도트 모드 · 두 오버라이드 on");
WLLookModeSettings.RuntimeDisabled = true;
Chk(!WLLookModeSettings.Enabled, "WLLookModeSettings.Enabled = false");
LookModeHub.Tick();
CameraFramingOverride.ProbeApply(_realCam);
WLLookToggleUi.Tick();
var d = _realCam.arr_cCamData[0];
Chk(!PixelCameraRig.RuntimeOverrideOn && !PixelCameraRig.IsAssembled, "814b 리그 오버라이드 0 · 리그 해제 = SO(enabled 0) 원래 동작");
Chk(!CameraFramingOverride.RuntimePresetOn, "811o 런타임 프리셋 0 = SO 값 경로 그대로");
Chk(fr != null && Mathf.Abs(d.cameraHeight - fr.cameraHeight) < 0.001f && Mathf.Abs(d.maxDistance - fr.distance) < 0.001f &&
Mathf.Abs(d.DistanceUp - fr.distanceUp) < 0.001f,
"arr_cCamData[0] = 811o SO 값 h " + (fr != null ? fr.cameraHeight : 0f) + " / d " + (fr != null ? fr.distance : 0f) +
" / up " + (fr != null ? fr.distanceUp : 0f) + " — 실측 " + d.cameraHeight + "/" + d.maxDistance + "/" + d.DistanceUp);
Chk(!_srcCam.orthographic, "원본 카메라 원근 복귀 — orthographic=" + _srcCam.orthographic);
Chk(!WLLookToggleUi.Visible, "버튼 숨김 — visible=" + WLLookToggleUi.Visible);
Chk(LookModeHub.AppliedMode == 0, "적용 모드 0 고정 — 실측 " + LookModeHub.AppliedMode);
// showButtons 만 끄기 — 모드는 그대로 두고 버튼만 숨긴다(시연 뒤 off)
WLLookModeSettings.RuntimeDisabled = false;
var s = WLLookModeSettings.Instance;
bool saved = s.showButtons;
s.showButtons = false;
WLLookToggleUi.Tick();
Chk(!WLLookToggleUi.Visible, "showButtons = 0 → 버튼만 숨김(모드 전환은 그대로) — visible=" + WLLookToggleUi.Visible);
s.showButtons = saved;
WLLookToggleUi.Tick();
Chk(WLLookToggleUi.Visible, "되돌리면 다시 보인다");
}
// ─────────────────────────────────────────── Z. 정리
static void Z_Cleanup()
{
H("Z. 정리 — 오버라이드 해제 · 노드 파괴 · 프리팹 저장 0");
try
{
LookModeHub.ResetAll();
LookModeHub.UiTick = null;
PixelCameraRig.Teardown("probe end");
CameraFramingOverride.ProbeRestore();
CameraFramingOverride.ResetDiagnostics();
N("리그 조립=" + PixelCameraRig.IsAssembled + " 오버라이드=" + PixelCameraRig.RuntimeOverrideOn +
" · 구도 런타임프리셋=" + CameraFramingOverride.RuntimePresetOn + " 적용=" + CameraFramingOverride.IsApplied);
N(WLLookToggleUi.Teardown());
if (_uiRoot != null) { PrefabUtility.UnloadPrefabContents(_uiRoot); _uiRoot = null; }
if (_infoGo != null) { InGameInfo.Ins = null; UnityEngine.Object.DestroyImmediate(_infoGo); _infoGo = null; }
if (_uiCamGo != null) { UnityEngine.Object.DestroyImmediate(_uiCamGo); _uiCamGo = null; }
if (_camGo != null) { UnityEngine.Object.DestroyImmediate(_camGo); _camGo = null; }
var strays = UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Include, FindObjectsSortMode.None);
N("남은 카메라 " + strays.Length + "개 · InGameInfo.Ins=" + (InGameInfo.Ins != null));
N("NewGameUI.prefab 저장 0(UnloadPrefabContents(false) 기본) · 에셋 쓰기 0");
}
catch (Exception e) { N("정리 중 예외 " + e.GetType().Name + " " + e.Message); }
}
}