// ───────────────────────────────────────────────────────────────────────────── // WLIslandNavMesh.cs — WL 쪽 변경(해변 걷기 콜라이더 · 상인 대역 캐릭터) 뒤 FI 섬 내비메시를 다시 굽는다 (2026-10-15) // // ■ 왜 — FI `IslandManager` 는 자기 변경(확장·소품)에서만 `NavMeshSurface.UpdateNavMesh` 를 부른다. // WL 이 런타임에 깐 콜라이더·NavMeshModifierVolume 은 다음 FI 갱신까지 내비메시에 안 들어간다. // ■ 해변 걷기(PD 「섬의 가장자리까지 조금 더 많이 이동」) — FI 에이전트 설정(경사 30° · 반지름 0.5)으로는 해변 경사(실측 28~46°)에서 끊긴다. // 그래서 **같은 소스·같은 에이전트 종류 ID** 로 경사·반지름만 바꾼 설정(`WLIslandLookSettings.beachWalkNav*`)으로 통째로 다시 굽고 바꿔 끼운다 // (실측 16 m 섬 4~8 ms · 끼우는 동안 에이전트는 메시 위에 그대로). // FI 가 나중에 자기 설정으로 갱신하면(확장 등) 바뀐 칸만 30°·0.5 로 돌아가므로 → FI 갱신이 끝나는 순간을 지켜보다가(`Co_Watch`) 다시 굽는다. // ■ 소스 수집·범위는 AI Navigation 2.0.14 `NavMeshSurface` 의 비공개 `CollectSources`·`CalculateWorldBounds` 를 그대로 부른다(동작 동일 보장). // 못 찾으면(패키지 변경) FI 기본 갱신(`UpdateNavMesh`)으로 돌아간다. FI 코드 0줄. // 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(ErrorLogHookManager 팝업). // ───────────────────────────────────────────────────────────────────────────── using System.Collections; using System.Collections.Generic; using System.Reflection; using Unity.AI.Navigation; using UnityEngine; using UnityEngine.AI; using FIIslandManager = CryingSnow.FarmingIsland.IslandManager; namespace WL.Island { public static class WLIslandNavMesh { // ── 진단(프로브가 읽는다) public static int Requests, Updates, Skipped, CustomBuilds, FiReapplies; public static string LastLog = "", LastBuildLog = ""; static bool s_pending, s_watching; static string s_why = ""; static NavMeshData s_ours; // 마지막으로 이쪽이 구워 끼운 데이터 static MethodInfo s_collect, s_bounds; static FieldInfo s_fiBusy; // FI IslandManager.navMeshUpdateCoroutine(비공개) — FI 갱신 진행 중이면 null 아님 static bool s_reflTried; /// FI 섬 내비메시 다시 굽기를 예약한다(2프레임 뒤 · 같은 사이의 요청은 합친다). public static void RequestUpdate(string why) { Requests++; s_why = string.IsNullOrEmpty(s_why) ? why : s_why + " + " + why; EnsureWatch(); if (s_pending) return; var runner = WLIslandBridge.Runner; if (runner == null) { Run(); return; } s_pending = true; runner.StartCoroutine(Co_Run()); } static IEnumerator Co_Run() { yield return null; yield return null; s_pending = false; Run(); } static void Run() { string why = s_why; s_why = ""; var im = FIIslandManager.Instance; var surf = im != null ? im.GetComponent() : null; if (surf == null || surf.navMeshData == null) { Skipped++; LastLog = "건너뜀(FI 내비메시 첫 베이크 전) — " + why; return; } Updates++; if (TryCustomBuild(surf)) { LastLog = "섬 내비메시 다시 굽기 #" + Updates + " — " + why + " · " + LastBuildLog; return; } surf.UpdateNavMesh(surf.navMeshData); LastLog = "FI 섬 내비메시 갱신 #" + Updates + " — " + why + (string.IsNullOrEmpty(LastBuildLog) ? "" : " (" + LastBuildLog + ")"); } static bool CustomEnabled(out float slope, out float radius) { var cfg = WL.Look.Farm.WLIslandLookSettings.Instance; slope = cfg != null ? cfg.beachWalkNavSlope : 0f; radius = cfg != null ? cfg.beachWalkNavRadius : 0f; return cfg != null && cfg.enabled_ != 0 && cfg.beachWalkEnabled != 0 && (slope > 0f || radius > 0f); } static bool Reflect() { if (!s_reflTried) { s_reflTried = true; var bf = BindingFlags.Instance | BindingFlags.NonPublic; s_collect = typeof(NavMeshSurface).GetMethod("CollectSources", bf, null, System.Type.EmptyTypes, null); s_bounds = typeof(NavMeshSurface).GetMethod("CalculateWorldBounds", bf, null, new[] { typeof(List) }, null); s_fiBusy = typeof(FIIslandManager).GetField("navMeshUpdateCoroutine", bf); } return s_collect != null && s_bounds != null; } /// 같은 소스·같은 에이전트 종류로 경사·반지름만 바꿔 통째로 굽고 바꿔 끼운다. 못 하면 false(→ FI 기본 갱신). static bool TryCustomBuild(NavMeshSurface surf) { float slope, radius; if (!CustomEnabled(out slope, out radius)) { LastBuildLog = ""; return false; } if (!Reflect()) { LastBuildLog = "다시 굽기 불가(NavMeshSurface 비공개 메서드 없음 · 패키지 변경?) → FI 기본 갱신"; return false; } List sources; Bounds bounds; try { sources = (List)s_collect.Invoke(surf, null); if (surf.collectObjects != CollectObjects.Volume) bounds = (Bounds)s_bounds.Invoke(surf, new object[] { sources }); else bounds = new Bounds(surf.center, new Vector3(Mathf.Abs(surf.size.x), Mathf.Abs(surf.size.y), Mathf.Abs(surf.size.z))); } catch (System.Exception ex) { LastBuildLog = "소스 수집 실패 — " + ex.Message + " → FI 기본 갱신"; return false; } var st = surf.GetBuildSettings(); float slope0 = st.agentSlope, radius0 = st.agentRadius; if (slope > 0f) st.agentSlope = slope; if (radius > 0f) st.agentRadius = radius; var sw = System.Diagnostics.Stopwatch.StartNew(); var data = NavMeshBuilder.BuildNavMeshData(st, sources, bounds, surf.transform.position, surf.transform.rotation); float ms = (float)sw.Elapsed.TotalMilliseconds; if (data == null) { LastBuildLog = "다시 굽기 실패(null) → FI 기본 갱신"; return false; } data.name = surf.gameObject.name; var old = surf.navMeshData; surf.RemoveData(); surf.navMeshData = data; surf.AddData(); if (old != null && old != data && old.GetInstanceID() < 0) Object.Destroy(old); // 런타임에 구운 옛 데이터만 버린다(에셋은 건드리지 않음) s_ours = data; CustomBuilds++; LastBuildLog = "경사 " + slope0.ToString("F0") + "→" + st.agentSlope.ToString("F0") + "° · 반지름 " + radius0.ToString("F2") + "→" + st.agentRadius.ToString("F2") + " m · 소스 " + sources.Count + " · " + ms.ToString("F1") + " ms"; return true; } // ── FI 가 자기 설정으로 다시 구운 뒤 → 우리 설정으로 되돌린다 ───────────────── static void EnsureWatch() { if (s_watching) return; var runner = WLIslandBridge.Runner; if (runner == null) return; s_watching = true; runner.StartCoroutine(Co_Watch()); } static IEnumerator Co_Watch() { bool wasBusy = false; while (true) { yield return null; var im = FIIslandManager.Instance; if (im == null) { wasBusy = false; continue; } float slope, radius; if (!CustomEnabled(out slope, out radius) || !Reflect()) { wasBusy = false; continue; } var surf = im.GetComponent(); if (surf == null || surf.navMeshData == null) continue; bool busy = s_fiBusy != null && s_fiBusy.GetValue(im) != null; if (wasBusy && !busy) { FiReapplies++; RequestUpdate("FI 갱신 뒤 재적용"); } else if (!busy && !s_pending && s_ours != surf.navMeshData) { FiReapplies++; RequestUpdate("FI 새 내비메시"); } wasBusy = busy; } } } }