From 28be65ada82c0a03a68eb2b1490c4758df671e47 Mon Sep 17 00:00:00 2001 From: swrring Date: Thu, 17 Sep 2026 00:18:43 +0900 Subject: [PATCH] =?UTF-8?q?[WL]=20=EB=8B=A4=EB=A6=AC=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC(=EC=9B=90=EB=B3=B8=20=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=20=EA=B2=BD=EC=9F=81=20=E2=86=92=20=ED=95=B4=EA=B8=88?= =?UTF-8?q?=C2=B7=EB=B9=84=ED=99=9C=EC=84=B1)=203=EA=B2=B9=20=EB=B0=A9?= =?UTF-8?q?=EC=96=B4=20=C2=B7=20=ED=99=95=EC=9E=A5=20=EA=B0=80=EA=B2=A9?= =?UTF-8?q?=ED=8C=90=200.5=EB=B0=B0=C2=B7=ED=8C=90=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B1=B0=C2=B7=ED=95=AD=EC=83=81=20=EC=9C=84?= =?UTF-8?q?=EC=97=90(WL/SpriteOverlay)=20=C2=B7=20EventSystem=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=20=ED=99=8D=EC=88=98=20=EC=B0=A8=EB=8B=A8=20=C2=B7=20?= =?UTF-8?q?WLErrorTrace=20(#816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WLDemoTerrain.Update: 활성화 첫 프레임에 IsUnlocked 선반영 · 활성화 중 꺼지면 재활성 · 해금·비활성 칸 5 s 스캔 복구(InitializeProps 리플렉션) - WLIslandFlowV2: PlacePurchasers 배율 expansionPadScale 0.5 · 트리거 expansionPadTriggerSize 3.4 m(십자 겹침 제거) · ApplyOverlay(스프라이트 WL/SpriteOverlay · TMP Overlay) — 우리 발판에도 - WLIslandUi: ReconcileEventSystems(원본 EventSystem 있으면 우리 것 삭제) · WLErrorTrace: 에러·예외·Coroutine 경고 최근 40건 보관 - 실측: 같은 프레임 땅+다리 활성화 오류 0 · 가격판 scale 0.50 · 상자 3.4 m · overlayApplied 9 · 콘솔 에러 0 Co-Authored-By: Claude Fable 5.1 --- .../Resources/WL/WLIslandSettings.asset | 3 + Assets/WL/Island/Shaders.meta | 8 +++ .../WL/Island/Shaders/WLSpriteOverlay.shader | 55 +++++++++++++++++ .../Shaders/WLSpriteOverlay.shader.meta | 9 +++ Assets/WL/Island/WLErrorTrace.cs | 61 +++++++++++++++++++ Assets/WL/Island/WLErrorTrace.cs.meta | 2 + Assets/WL/Island/WLIslandFlowV2.cs | 61 ++++++++++++++++++- Assets/WL/Island/WLIslandGateFlow.cs | 2 + Assets/WL/Island/WLIslandSettings.cs | 9 +++ Assets/WL/Island/WLIslandUi.cs | 19 ++++++ Assets/WL/Look/Farm/WLDemoTerrain.cs | 53 +++++++++++++--- 11 files changed, 273 insertions(+), 9 deletions(-) create mode 100644 Assets/WL/Island/Shaders.meta create mode 100644 Assets/WL/Island/Shaders/WLSpriteOverlay.shader create mode 100644 Assets/WL/Island/Shaders/WLSpriteOverlay.shader.meta create mode 100644 Assets/WL/Island/WLErrorTrace.cs create mode 100644 Assets/WL/Island/WLErrorTrace.cs.meta diff --git a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset index ae75f04ca..55d020ee1 100644 --- a/Assets/WL/Island/Resources/WL/WLIslandSettings.asset +++ b/Assets/WL/Island/Resources/WL/WLIslandSettings.asset @@ -97,6 +97,9 @@ MonoBehaviour: expansionPadPlace: 1 expansionPadInset: 2.2 expansionPadLift: 0.06 + expansionPadTriggerSize: 3.4 + expansionPadScale: 0.5 + expansionPadOverlay: 1 campDecorSnapToGround: 0 applyStartCoin: 1 startCoin: 10 diff --git a/Assets/WL/Island/Shaders.meta b/Assets/WL/Island/Shaders.meta new file mode 100644 index 000000000..371b1607e --- /dev/null +++ b/Assets/WL/Island/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 90beba3ec3bc4f0469656aa21524963a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Island/Shaders/WLSpriteOverlay.shader b/Assets/WL/Island/Shaders/WLSpriteOverlay.shader new file mode 100644 index 000000000..a74b60f57 --- /dev/null +++ b/Assets/WL/Island/Shaders/WLSpriteOverlay.shader @@ -0,0 +1,55 @@ +// WLSpriteOverlay.shader — 스프라이트를 **항상 위에**(깊이 무시) 그리는 언릿 · 알파 블렌드 셰이더 +// 2026-09-17 PD 「가격판이 풀·오브젝트에 가려지지 않게 상단에 보이게」 — 확장 가격판(FI Purchaser 의 Frame/Coin 스프라이트)에 런타임 복사본 재질로 물린다. +// SpriteRenderer 가 _MainTex 에 스프라이트 텍스처를 넣어 주고 정점색을 곱한다. 모바일: 텍스처 1장 · 조명 없음. +Shader "WL/SpriteOverlay" +{ + Properties + { + [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} + _Color ("Tint", Color) = (1,1,1,1) + } + SubShader + { + Tags { "Queue"="Transparent+100" "RenderType"="Transparent" "IgnoreProjector"="True" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } + Cull Off + Lighting Off + ZWrite Off + ZTest Always + Blend SrcAlpha OneMinusSrcAlpha + + Pass + { + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); + CBUFFER_START(UnityPerMaterial) + float4 _MainTex_ST; + half4 _Color; + CBUFFER_END + + struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; half4 color : COLOR; }; + struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; half4 color : COLOR; }; + + Varyings vert(Attributes v) + { + Varyings o; + o.positionHCS = TransformObjectToHClip(v.positionOS.xyz); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + o.color = v.color * _Color; + return o; + } + + half4 frag(Varyings i) : SV_Target + { + half4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv) * i.color; + c.rgb *= c.a; + return half4(c.rgb / max(c.a, 1e-4), c.a); + } + ENDHLSL + } + } + Fallback Off +} diff --git a/Assets/WL/Island/Shaders/WLSpriteOverlay.shader.meta b/Assets/WL/Island/Shaders/WLSpriteOverlay.shader.meta new file mode 100644 index 000000000..fdd3570d3 --- /dev/null +++ b/Assets/WL/Island/Shaders/WLSpriteOverlay.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: d6fd8c10b72144e4980dd1090d915bec +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/WL/Island/WLErrorTrace.cs b/Assets/WL/Island/WLErrorTrace.cs new file mode 100644 index 000000000..a5edabccb --- /dev/null +++ b/Assets/WL/Island/WLErrorTrace.cs @@ -0,0 +1,61 @@ +// ───────────────────────────────────────────────────────────────────────────── +// WLErrorTrace.cs — 런타임 에러·예외·「Coroutine couldn't be started」 경고를 **메모리에 보관**한다(최근 40건 · 스택 포함). +// +// 2026-09-17 PD 「다리가 생성될 때 에러」 실측: 화면 팝업은 보였는데 에디터 콘솔(파이프라인 조회)은 비어 있어 원문·스택을 못 잡았다 +// (Play 재시작·Clear 로 사라짐). 이후로는 Lead 프로브가 `WLErrorTrace.Dump()` 로 언제든 읽는다. 원본 코드 0줄 · 화면 출력 없음. +// ───────────────────────────────────────────────────────────────────────────── +using System.Collections.Generic; +using UnityEngine; + +namespace WL.Island +{ + public static class WLErrorTrace + { + public struct Entry { public float time; public LogType type; public string message; public string stack; } + + static readonly List s_entries = new List(64); + static bool s_hooked; + public static int Total; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + static void Hook() + { + if (s_hooked) return; + s_hooked = true; + Application.logMessageReceived += OnLog; + } + + static void OnLog(string message, string stack, LogType type) + { + bool keep = type == LogType.Error || type == LogType.Exception || type == LogType.Assert + || (type == LogType.Warning && message != null && message.IndexOf("Coroutine couldn't be started", System.StringComparison.Ordinal) >= 0); + if (!keep) return; + Total++; + if (s_entries.Count >= 40) s_entries.RemoveAt(0); + s_entries.Add(new Entry { time = Time.realtimeSinceStartup, type = type, message = message, stack = stack }); + } + + /// 최근 항목을 한 문자열로(프로브용). 같은 메시지는 횟수만 센다. + public static string Dump(int maxEach = 400) + { + var sb = new System.Text.StringBuilder(); + sb.Append("total=").Append(Total).Append(" kept=").Append(s_entries.Count); + var counts = new Dictionary(); + for (int i = 0; i < s_entries.Count; i++) + { + var e = s_entries[i]; + string key = e.message.Length > 100 ? e.message.Substring(0, 100) : e.message; + int c; counts.TryGetValue(key, out c); + counts[key] = c + 1; + if (c > 0) continue; + sb.Append("\n[").Append(e.time.ToString("F1")).Append("s ").Append(e.type).Append("] ") + .Append(e.message.Length > maxEach ? e.message.Substring(0, maxEach) : e.message); + if (!string.IsNullOrEmpty(e.stack)) sb.Append("\n ").Append(e.stack.Length > maxEach ? e.stack.Substring(0, maxEach) : e.stack); + } + foreach (var kv in counts) if (kv.Value > 1) sb.Append("\n x").Append(kv.Value).Append(" ").Append(kv.Key); + return sb.ToString(); + } + + public static void Clear() { s_entries.Clear(); } + } +} diff --git a/Assets/WL/Island/WLErrorTrace.cs.meta b/Assets/WL/Island/WLErrorTrace.cs.meta new file mode 100644 index 000000000..f924e8e5b --- /dev/null +++ b/Assets/WL/Island/WLErrorTrace.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d5cabada8fb5b024a965629f583e026e \ No newline at end of file diff --git a/Assets/WL/Island/WLIslandFlowV2.cs b/Assets/WL/Island/WLIslandFlowV2.cs index c9177e482..948518ec7 100644 --- a/Assets/WL/Island/WLIslandFlowV2.cs +++ b/Assets/WL/Island/WLIslandFlowV2.cs @@ -501,8 +501,25 @@ namespace WL.Island dir = Mathf.Abs(dir.x) >= Mathf.Abs(dir.z) ? new Vector3(Mathf.Sign(dir.x), 0f, 0f) : new Vector3(0f, 0f, Mathf.Sign(dir.z)); var target = c + dir * (half - Mathf.Clamp(cfg.expansionPadInset, 0.5f, half)); float gy = PadGroundY(target, c.y); - float plateDrop = 0f; var frame = p.transform.Find("Frame"); if (frame != null) plateDrop = -frame.localPosition.y; // 판 자식들이 루트보다 아래(−0.49) - p.transform.position = new Vector3(target.x, gy + plateDrop + cfg.expansionPadLift, target.z); + float plateDropLocal = 0f; var frame = p.transform.Find("Frame"); if (frame != null) plateDropLocal = -frame.localPosition.y; // 판 자식들이 루트보다 아래(로컬 −0.49) + // 🔴 2026-09-17 PD 「확장 가격판이 너무 크다 → 캠프파이어 발판과 같은 크기」 = 우리 발판과 같은 배율(platformScale 0.5 = expansionPadScale) + float ps = cfg.expansionPadScale > 0.05f ? cfg.expansionPadScale : 1f; + p.transform.localScale = Vector3.one * ps; + p.transform.position = new Vector3(target.x, gy + plateDropLocal * ps + cfg.expansionPadLift, target.z); + // 🔴 2026-09-17 — 원본 트리거는 십자(10×5×5 두 개 · ±5 m)라 가장자리로 옮긴 판끼리 겹쳐 두 구매가 동시에 시작됐다(원본 활성화 경쟁 → 「Bridge (0) inactive」). + // 판 크기(expansionPadTriggerSize · 월드 m)만큼의 상자 하나로 줄인다 = 판을 밟아야 산다(우리 발판과 같은 규칙). 로컬 치수는 배율로 역보정. + if (cfg.expansionPadTriggerSize > 0.1f) + { + var boxes = p.GetComponents(); + float ts = cfg.expansionPadTriggerSize / ps; + for (int b = 0; b < boxes.Length; b++) + { + if (boxes[b] == null) continue; + boxes[b].size = new Vector3(ts, 4f / ps, ts); + boxes[b].center = new Vector3(0f, -plateDropLocal + 2f / ps, 0f); // 판 높이에서 위로 4 m(월드) + } + } + if (cfg.expansionPadOverlay != 0) ApplyOverlay(p.gameObject); // 풀·소품에 가려지지 않게 항상 위에 var mk = p.gameObject.AddComponent(); mk.original = pos; PurchasersPlaced++; LastPlaceLog = "확장 가격판 " + pos.ToString("F0") + " → " + p.transform.position.ToString("F2") + "(이웃 " + c.ToString("F0") + " 가장자리 안쪽 " + cfg.expansionPadInset.ToString("F1") + " m · 지면 " + gy.ToString("F2") + ")"; @@ -510,6 +527,46 @@ namespace WL.Island } } + // ───────────────────────────────────────────────────────────────── + // 🔴 2026-09-17 PD 「가격판이 풀·오브젝트에 가려지지 않게 상단에 보이게」 — 스프라이트(Frame·Coin)는 깊이 무시 셰이더 `WL/SpriteOverlay`, + // 가격 글자(TMP)는 TMP 의 Overlay 변형 셰이더로 바꾼 **런타임 복사본** 재질을 물린다(에셋 0줄 · 큐 3100 = 물·거품 뒤). + // ───────────────────────────────────────────────────────────────── + static Material s_spriteOverlay; + static readonly Dictionary s_tmpOverlay = new Dictionary(); + public static int OverlayApplied; + + public static void ApplyOverlay(GameObject root) + { + if (root == null) return; + var srs = root.GetComponentsInChildren(true); + if (srs.Length > 0) + { + if (s_spriteOverlay == null) + { + var sh = Shader.Find("WL/SpriteOverlay"); + if (sh != null) s_spriteOverlay = new Material(sh) { name = "WL_SpriteOverlay(runtime)", hideFlags = HideFlags.DontSave }; + } + if (s_spriteOverlay != null) + for (int i = 0; i < srs.Length; i++) if (srs[i] != null && srs[i].sharedMaterial != s_spriteOverlay) { srs[i].sharedMaterial = s_spriteOverlay; OverlayApplied++; } + } + var texts = root.GetComponentsInChildren(true); + for (int i = 0; i < texts.Length; i++) + { + var t = texts[i]; if (t == null) continue; + var src = t.fontSharedMaterial; if (src == null || src.shader == null) continue; + if (src.shader.name.IndexOf("Overlay", System.StringComparison.OrdinalIgnoreCase) >= 0) continue; + Material dst; + if (!s_tmpOverlay.TryGetValue(src, out dst) || dst == null) + { + var sh = Shader.Find("TextMeshPro/Mobile/Distance Field Overlay"); + if (sh == null) return; + dst = new Material(src) { name = src.name + "(WL overlay)", hideFlags = HideFlags.DontSave, shader = sh }; + s_tmpOverlay[src] = dst; + } + t.fontSharedMaterial = dst; OverlayApplied++; + } + } + static float PadGroundY(Vector3 p, float fallbackY) { var hits = Physics.RaycastAll(new Vector3(p.x, fallbackY + 5f, p.z), Vector3.down, 20f, ~0, QueryTriggerInteraction.Ignore); diff --git a/Assets/WL/Island/WLIslandGateFlow.cs b/Assets/WL/Island/WLIslandGateFlow.cs index af15cfc71..48a5fb738 100644 --- a/Assets/WL/Island/WLIslandGateFlow.cs +++ b/Assets/WL/Island/WLIslandGateFlow.cs @@ -554,6 +554,7 @@ namespace WL.Island WLIslandTileFenceStrip.Apply(cfg, scene); // 확장·재구성으로 타일 메시가 바뀌면 다시 잘라낸다(멱등) if (cfg.hideFarms != 0) { Vector3 _c; HideFarms(cfg, scene, out _c); } // 늦게 생긴 밭의 울타리·소품도 WLIslandExpansionGate.PlacePurchasers(cfg, scene); // 2026-09-16 — 확장 뒤 새로 생긴 FI 가격판도 이웃 칸 가장자리로(표식 있으면 비용 0) + WLIslandPrompt.ReconcileEventSystems(); // 2026-09-17 — 원본 EventSystem 이 올라오면 우리 것을 지운다(경고 홍수 차단) } } @@ -796,6 +797,7 @@ namespace WL.Island float ps = cfg.platformScale > 0.001f ? cfg.platformScale : 1f; AppliedScale = ps; if (ps != 1f) p.transform.localScale = p.transform.localScale * ps; + if (cfg.expansionPadOverlay != 0) WLIslandExpansionGate.ApplyOverlay(p.gameObject); // 2026-09-17 PD — 우리 발판도 풀·소품 위에 보이게(같은 규칙) // 🔴 FI 가격판은 프리팹 안에서 **루트보다 앞·아래**에 놓여 있다(실측 2026-09-13: // Frame/Price/Coin 이 전부 y = -0.49 · z = 루트+(-1/0/+1) — 잠긴 섬 자리의 **물 위**에 눕는 판이다). diff --git a/Assets/WL/Island/WLIslandSettings.cs b/Assets/WL/Island/WLIslandSettings.cs index 6aa4ab557..36fb56e94 100644 --- a/Assets/WL/Island/WLIslandSettings.cs +++ b/Assets/WL/Island/WLIslandSettings.cs @@ -570,6 +570,15 @@ namespace WL.Island [Tooltip("판을 지면보다 이만큼(m) 띄운다(z-fighting 방지).")] public float expansionPadLift = 0.06f; + [Tooltip("옮긴 가격판의 트리거 한 변(m). 원본 십자(±5 m)는 가장자리로 옮긴 판끼리 겹쳐 두 구매가 동시에 시작돼 원본 활성화 경쟁 오류가 났다(2026-09-17). 0 = 원본 트리거 그대로.")] + public float expansionPadTriggerSize = 3.4f; + + [Tooltip("옮긴 확장 가격판의 크기 배율 — 2026-09-17 PD 「캠프파이어 발판과 같은 크기」 = platformScale 과 같은 0.5.")] + public float expansionPadScale = 0.5f; + + [Tooltip("1 = 가격판(프레임·코인 스프라이트·가격 글자)을 깊이 무시 셰이더로 그려 풀·소품에 가려지지 않게 한다(2026-09-17 PD). 0 = 원본 재질.")] + public int expansionPadOverlay = 1; + [Tooltip("한 줄 = 한 단계. 위에서부터 차례로 발판이 선다.")] public WLFlowStepDef[] flowSteps = new WLFlowStepDef[] { diff --git a/Assets/WL/Island/WLIslandUi.cs b/Assets/WL/Island/WLIslandUi.cs index 63f643d8a..db098f26e 100644 --- a/Assets/WL/Island/WLIslandUi.cs +++ b/Assets/WL/Island/WLIslandUi.cs @@ -157,8 +157,26 @@ namespace WL.Island if (s_root != null) s_root.SetActive(false); } + static GameObject s_ownEventSystem; + public static int EventSystemsRemoved; + + /// + /// 🔴 2026-09-17 실측: 우리가 「EventSystem 없음」일 때 만든 `~WL_EventSystem` 이 나중에 올라온 원본 씬의 EventSystem 과 겹쳐 + /// 매 프레임 「There are 2 event systems」 경고가 콘솔을 가득 채웠다(에러 원문이 밀려남). 원본 것이 있으면 우리 것을 지운다. + /// + public static void ReconcileEventSystems() + { + if (s_ownEventSystem == null) return; + var all = Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); + if (all.Length <= 1) return; + Object.Destroy(s_ownEventSystem); + s_ownEventSystem = null; + EventSystemsRemoved++; + } + static void Ensure(WLIslandSettings cfg) { + ReconcileEventSystems(); if (s_root != null) return; s_root = new GameObject("~WL_IslandPrompt"); @@ -179,6 +197,7 @@ namespace WL.Island Object.DontDestroyOnLoad(es); es.AddComponent(); es.AddComponent(); + s_ownEventSystem = es; } // 버튼 diff --git a/Assets/WL/Look/Farm/WLDemoTerrain.cs b/Assets/WL/Look/Farm/WLDemoTerrain.cs index 2fdee8004..271ae3787 100644 --- a/Assets/WL/Look/Farm/WLDemoTerrain.cs +++ b/Assets/WL/Look/Farm/WLDemoTerrain.cs @@ -754,17 +754,20 @@ namespace WL.Look.Farm // 새 칸 둘레만 이전 높이 → 새 높이로 `expandRiseSeconds` 동안 보간(SetHeightsDelayLOD · 부분 사각형). 풀 인스턴서·거품은 다 솟은 뒤. // FI 의 OnActivated(1 s 뒤) → 우리 훅의 재굽기는 내용이 같아 생략된다(§6 비교). // ───────────────────────────────────────────────────────────────── - public static int Rises, RiseFrames; - public static string LastRiseLog = ""; + public static int Rises, RiseFrames, Reactivated, Recovered, EarlyUnlocked; + public static string LastRiseLog = "", LastGuardLog = ""; readonly List _rising = new List(4); + readonly List _activating = new List(4); // 활성화 애니 중인 칸(다리 포함) — 경쟁으로 꺼지면 다시 켠다 FIIsland[] _watch; float _watchNext; bool _deferInstancer, _riseCancel; Coroutine _riseCo; + static System.Reflection.MethodInfo s_initProps; void Update() { - if (cfg == null || cfg.enabled_ == 0 || cfg.demoTerrainEnabled == 0 || cfg.expandHideOriginal == 0) return; - if (_watch == null || Time.unscaledTime >= _watchNext) + if (cfg == null || cfg.enabled_ == 0 || cfg.demoTerrainEnabled == 0) return; + bool rescan = _watch == null || Time.unscaledTime >= _watchNext; + if (rescan) { _watch = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); _watchNext = Time.unscaledTime + 5f; @@ -772,16 +775,52 @@ namespace WL.Look.Farm for (int i = 0; i < _watch.Length; i++) { var isl = _watch[i]; - if (isl == null || isl.IsUnlocked || isl.IsBridge || !isl.gameObject.activeInHierarchy) continue; - if (isl.transform.localScale.x >= 0.999f) continue; // 잠긴 채 아직 비활성화 전(로드 직후)은 scale 1 → 해당 없음 - if (_rising.Contains(isl)) continue; + if (isl == null) continue; var mf = isl.GetComponent(); if (mf == null || mf.sharedMesh == null) continue; // 더미 제외 if (gameObject.scene.IsValid() && isl.gameObject.scene != gameObject.scene) continue; + // 🔴 2026-09-17 PD 화면 실측 「Coroutine couldn't be started because the game object 'Bridge (0)' is inactive!」 + // = 원본 경쟁: 칸 A 의 해금 갱신(OnIslandUnlocked → 잠긴 칸 전부 RefreshIsland → SetActive(false))이 + // 활성화 애니(1 s) 중인 칸 B 를 「잠김」으로 보고 꺼 버림 → B 의 애니 끝에 IsUnlocked·InitializeProps 코루틴이 꺼진 오브젝트에서 실패 + // → B 는 해금인데 비활성(안 보이고 못 걷는 다리). 가격판을 가장자리로 옮긴 뒤 두 판을 잇달아 밟기 쉬워져 드러났다. + // ① 이미 깨진 칸(해금인데 비활성) = 5 s 스캔 때 복구 ② 활성화 중인 칸을 기억해 두었다가 꺼지면 그 프레임에 다시 켠다 + if (rescan && isl.IsUnlocked && !isl.gameObject.activeSelf && isl.transform.localScale.x >= 0.999f) { Recover(isl, "스캔"); continue; } + if (_activating.Contains(isl) || isl.IsUnlocked) continue; + if (!isl.gameObject.activeInHierarchy || isl.transform.localScale.x >= 0.999f) continue; // 잠긴 채 아직 비활성화 전(로드 직후)은 scale 1 → 해당 없음 + _activating.Add(isl); + // ③ 🔴 해금 표식을 **애니 첫 프레임에 미리** 세운다(원본은 1 s 뒤 OnComplete 에서 세운다) → 그 사이 다른 칸의 해금 갱신이 이 칸을 + // 「잠김」으로 보고 끄는 일이 아예 없다(같은 프레임에 두 칸이 끝나는 경우도 방어). 원본이 1 s 뒤 같은 값을 다시 쓰고 OnActivated 를 낸다. + isl.IsUnlocked = true; EarlyUnlocked++; + if (cfg.expandHideOriginal == 0 || isl.IsBridge || _rising.Contains(isl)) continue; StartRise(isl); } + for (int i = _activating.Count - 1; i >= 0; i--) + { + var a = _activating[i]; + if (a == null) { _activating.RemoveAt(i); continue; } + bool done = a.transform.localScale.x >= 0.999f; // 원본 DOScale 완료 + if (!a.gameObject.activeSelf) { a.gameObject.SetActive(true); Reactivated++; LastGuardLog = "활성화 중 꺼진 칸 다시 켬 — " + a.name + "@" + a.transform.position.ToString("F0"); cfg.Log(LastGuardLog); } + if (done) _activating.RemoveAt(i); + } for (int i = _rising.Count - 1; i >= 0; i--) if (_rising[i] == null || _rising[i].IsUnlocked) _rising.RemoveAt(i); } + /// 해금인데 비활성으로 남은 칸을 살린다 — 켜고 원본 `InitializeProps(true)`(private · 리플렉션)를 돌려 소품·낚시터를 초기화한다. + void Recover(FIIsland isl, string why) + { + isl.gameObject.SetActive(true); + isl.transform.localScale = Vector3.one; + if (s_initProps == null) s_initProps = typeof(FIIsland).GetMethod("InitializeProps", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + if (s_initProps != null) + { + var co = s_initProps.Invoke(isl, new object[] { true }) as IEnumerator; + if (co != null) isl.StartCoroutine(co); + } + Recovered++; + LastGuardLog = "해금·비활성 칸 복구(" + why + ") — " + isl.name + "@" + isl.transform.position.ToString("F0") + (isl.IsBridge ? " 다리" : ""); + cfg.Log(LastGuardLog); + _dirty = true; Rebuild(); + } + void StartRise(FIIsland isl) { _rising.Add(isl);