using System; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.AddressableAssets; using AutoMove; using BrewMonster.Network; using CSNetwork.GPDataType; // Filename : CECIntelligentRoute.cs // Creator : ported (simplified) from C++ EC_IntelligentRoute.* // Date : 2026/01/09 namespace BrewMonster.Scripts { /// /// Intelligent route (AutoPF) controller. /// 智能寻路(AutoPF)控制器。 /// public sealed class CECIntelligentRoute { private const bool DEBUG_AUTOPF = true; /// cos(5°) for FindFarthestNode — same as C++ EC_IntelligentRoute.cpp. private static readonly float s_cos5Deg = Mathf.Cos(5f * Mathf.PI / 180f); public enum SearchResult { enumSearchSuccess, // 寻路成功 // Search success enumSearchStartEndCoincide, // 起点终点相同 // Start==End enumSearchUnInitialized, // 未初始化 // Uninitialized enumSearchStartInvalid, // 起点无效 // Start invalid enumSearchEndInvalid, // 终点无效 // End invalid enumSearchStartEndInvalid, // 起点终点都无效 // Start & End invalid enumSearchNoPath, // 未找到路径 // No path enumSearchExceedMaxExpand, // 超过最大扩展 // Exceed max expand } public enum RouteState { enumRouteIdle, // 未进行寻路 // Idle enumRouteMoving, // 路径行进中 // Moving enumRoutePathFinished, // 到达终点 // Finished } public enum Usage { enumUsageNone, enumUsageWorkMove, enumUsageWorkTrace, } private struct RangedMoveAgent { public RectInt rect; // map rect (in map pixels, 1024 blocks) // 地图范围(按 map 像素/块) public Vector3 origin; // origin override // 原点覆盖 public CMoveAgent agent; // agent // 寻路器 public bool Contain(Vector3 pos) { return agent != null && agent.IsContain(pos); } } private static readonly CECIntelligentRoute s_inst = new CECIntelligentRoute(); public static CECIntelligentRoute Instance() => s_inst; private CECInstance m_pInst; private readonly List m_moveAgents = new List(8); private RouteState m_state = RouteState.enumRouteIdle; private A3DVECTOR3 m_start; private A3DVECTOR3 m_end; private int m_iCurMoveAgent = -1; private int m_iCurDest = -1; private Usage m_usage = Usage.enumUsageNone; private A3DVECTOR3 m_lastPos; // ÉÏ´ÎÒÆ¶¯µ½µÄλÖ㬵±ÓÐÐÂλÖÃʱ£¬ÓÃÓÚ¼ì²âÊÇ·ñÓ¦ÒÆÍùÏÂÒ»²½ private float m_lastMove; // ÉÏ´ÎÒÆ¶¯¾àÀ룬ÓÃÓÚ¹À¼ÆÏÂÒ»²½½«Òƶ¯µÄ¾àÀ룬´Ó¶øÅжÏÊÇ·ñÓ¦ÒÆÍùÏÂÒ»²½ private float m_dist2CurDest; // µ½´ï m_iCurDest »¹ÐèÒÆ¶¯µÄ¾àÀ룬ÓÃÓÚÅжÏÊÇ·ñÓ¦ÒÆÍùÏÂÒ»²½ private CECIntelligentRoute() { } public Usage GetUsage() => m_usage; public void SetUsage(Usage usage) => m_usage = usage; public bool IsUsageMove() => m_usage == Usage.enumUsageWorkMove; public bool IsUsageTrace() => m_usage == Usage.enumUsageWorkTrace; public RouteState GetState() => m_state; public bool IsIdle() => m_state == RouteState.enumRouteIdle; public bool IsMoveOn() => m_state == RouteState.enumRouteMoving; public bool IsPathFinished() => m_state == RouteState.enumRoutePathFinished; public A3DVECTOR3 GetDest() => m_end; public void Release() { m_pInst = null; m_moveAgents.Clear(); ResetSearch(); m_usage = Usage.enumUsageNone; } public void ResetSearch() { //m_state = RouteState.enumRouteIdle; //m_start = new A3DVECTOR3(0); //m_end = new A3DVECTOR3(0); //m_iCurMoveAgent = -1; //m_iCurDest = -1; if (IsIdle()) { // µ±Ç°²»ÔÚËÑË÷״̬ return; } RangedMoveAgent? pCurAgent = GetCurAgent(); if (pCurAgent != null) { pCurAgent.Value.agent.ResetSearch(); } m_state = RouteState.enumRouteIdle; m_start.Clear(); m_end.Clear(); m_iCurMoveAgent = -1; m_iCurDest = -1; m_lastPos.Clear(); m_lastMove = 0.0f; m_dist2CurDest = 0.0f; } public void ChangeWorldInstance(int idInstance) { CECInstance pInst = EC_Game.GetGameRun()?.GetInstance(idInstance); if (pInst == null) { Release(); return; } if (ReferenceEquals(pInst, m_pInst)) { return; } Release(); m_pInst = pInst; List files = pInst.GetRouteFiles(); if (files == null || files.Count == 0) { return; } // Equivalent to C++ CECIntelligentRoute::ChangeWorldInstance file parsing. // 等价于 C++ 的 CECIntelligentRoute::ChangeWorldInstance 文件解析。 foreach (var fileBase in files) { // parse "r{rowFrom}_{rowTo}-c{colFrom}_{colTo}" // 解析 "r{rowFrom}_{rowTo}-c{colFrom}_{colTo}" int rowFrom, rowTo, colFrom, colTo; if (!TryParseRouteFileRange(fileBase, out rowFrom, out rowTo, out colFrom, out colTo)) { continue; } if (rowFrom < 0 || rowTo >= pInst.GetRowNum() || colFrom < 0 || colTo >= pInst.GetColNum()) { continue; } // Build rect/origin (1024 units per tile) like original. // 按原版构建 rect/origin(每格 1024)。 var rect = new RectInt( colFrom * 1024, (pInst.GetRowNum() - rowTo - 1) * 1024, (colTo - colFrom + 1) * 1024, (rowTo - rowFrom + 1) * 1024 ); Vector3 origin = Vector3.zero; origin.x = rect.xMin - pInst.GetColNum() * 1024 * 0.5f; origin.z = rect.yMin - pInst.GetRowNum() * 1024 * 0.5f; string basePath = $"maps/{pInst.GetPath()}/movemap/{fileBase}"; var agent = new CMoveAgent(); // Create resolver that captures the basePath context for relative file references // 创建捕获 basePath 上下文的解析器,用于相对文件引用 Func resolver = (key) => ResolveAddressableBytes(key, basePath); if (!agent.Load(basePath, resolver, origin)) { continue; } if (!agent.IsReady()) { continue; } m_moveAgents.Add(new RangedMoveAgent { rect = rect, origin = origin, agent = agent, }); } } private static bool TryParseRouteFileRange(string fileBase, out int rowFrom, out int rowTo, out int colFrom, out int colTo) { rowFrom = rowTo = colFrom = colTo = -1; if (string.IsNullOrEmpty(fileBase)) return false; // Expected prefix: r{a}_{b}-c{c}_{d} // 期望前缀:r{a}_{b}-c{c}_{d} // We ignore "-l0" suffix if present. // 忽略可能存在的 "-l0" 后缀。 string s = fileBase; int l0 = s.IndexOf("-l", StringComparison.OrdinalIgnoreCase); if (l0 >= 0) s = s.Substring(0, l0); // Example: r1_1-c1_2 // 例:r1_1-c1_2 try { if (!s.StartsWith("r", StringComparison.OrdinalIgnoreCase)) return false; int dash = s.IndexOf("-c", StringComparison.OrdinalIgnoreCase); if (dash < 0) return false; string rPart = s.Substring(1, dash - 1); string cPart = s.Substring(dash + 2); string[] r = rPart.Split('_'); string[] c = cPart.Split('_'); if (r.Length != 2 || c.Length != 2) return false; rowFrom = int.Parse(r[0]); rowTo = int.Parse(r[1]); colFrom = int.Parse(c[0]); colTo = int.Parse(c[1]); return rowFrom <= rowTo && colFrom <= colTo; } catch { return false; } } private static byte[] ResolveAddressableBytes(string key, string basePath) { // key is like "maps/a61/movemap/r1_1-c1_2-l0.cfg" or "r1_1-c1_2-l0.prmap" // key 类似 "maps/a61/movemap/r1_1-c1_2-l0.cfg" 或 "r1_1-c1_2-l0.prmap" // basePath is like "maps/a61/movemap/r1_1-c1_2-l0" (without extension) // basePath 类似 "maps/a61/movemap/r1_1-c1_2-l0"(无扩展名) try { // Normalize key to use forward slashes for Addressables // 规范化 key 以使用 Addressables 的正斜杠 string addressableKey = key.Replace('\\', '/'); // If key doesn't start with "maps/", it's a relative reference from cfg file // 如果 key 不以 "maps/" 开头,则是 cfg 文件中的相对引用 if (!addressableKey.StartsWith("maps/", StringComparison.OrdinalIgnoreCase)) { // Construct full path from basePath directory + key // 从 basePath 目录 + key 构建完整路径 // basePath is "maps/a61/movemap/r1_1-c1_2-l0", so directory is "maps/a61/movemap/" // basePath 是 "maps/a61/movemap/r1_1-c1_2-l0",所以目录是 "maps/a61/movemap/" int lastSlash = basePath.LastIndexOf('/'); if (lastSlash >= 0) { string baseDir = basePath.Substring(0, lastSlash + 1); addressableKey = baseDir + addressableKey; } else { // Fallback: assume maps/{mapName}/movemap/ structure // 回退:假设 maps/{mapName}/movemap/ 结构 BMLogger.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes: cannot resolve relative key '{key}' with basePath '{basePath}'"); return null; } } // Append .txt extension for Addressables (all map files are renamed to .txt) // 为 Addressables 追加 .txt 扩展名(所有地图文件已重命名为 .txt) if (!addressableKey.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) { addressableKey += ".txt"; } // Build Addressables path: Assets/Addressable/{key} // 构建 Addressables 路径:Assets/Addressable/{key} string address = $"Assets/Addressable/{addressableKey}"; // Load synchronously (similar to navigate.txt loading pattern) // 同步加载(类似于 navigate.txt 的加载模式) Addressables.InitializeAsync().WaitForCompletion(); try { var ta = Addressables.LoadAssetAsync(address).WaitForCompletion(); if (ta != null && ta.bytes != null) { return ta.bytes; } } catch (UnityEngine.AddressableAssets.InvalidKeyException ex) { // If the asset exists but is registered as DefaultAsset (folder) instead of TextAsset, // it means the files need to be properly configured in Addressables. // 如果资产存在但注册为 DefaultAsset(文件夹)而不是 TextAsset, // 这意味着文件需要在 Addressables 中正确配置。 BMLogger.LogError($"[CECIntelligentRoute] ResolveAddressableBytes: Asset '{address}' is registered as DefaultAsset (folder) instead of TextAsset. " + $"Please ensure the file is properly imported as a TextAsset in Unity and marked as Addressable. " + $"You may need to: 1) Select the file in Unity, 2) Set Import Type to 'Text' in Inspector, 3) Mark as Addressable. Error: {ex.Message}"); return null; } BMLogger.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes: failed to load '{address}' (asset is null or has no bytes)"); return null; } catch (Exception ex) { BMLogger.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes failed for '{key}': {ex.Message}"); return null; } } private RangedMoveAgent? GetCurAgent() { if (m_iCurMoveAgent >= 0 && m_iCurMoveAgent < m_moveAgents.Count) { return m_moveAgents[m_iCurMoveAgent]; } return null; } public SearchResult Search(A3DVECTOR3 start, A3DVECTOR3 end, CMoveAgent.BrushTest brushTest = null, int nMaxExpand = -1) { ResetSearch(); if (m_moveAgents.Count == 0) { if (DEBUG_AUTOPF) BMLogger.LogWarning("[CECIntelligentRoute] Search: no moveAgents (uninitialized)"); return SearchResult.enumSearchUnInitialized; } m_start = start; m_end = end; // Find an agent that contains both start & end. // 找到同时包含起点和终点的 agent。 bool bStartContained = false, bEndContained = false; int idx = -1; for (int i = 0; i < m_moveAgents.Count; i++) { var a = m_moveAgents[i]; if (a.Contain(new Vector3(start.x, start.y, start.z))) { bStartContained = true; if (a.Contain(new Vector3(end.x, end.y, end.z))) { bEndContained = true; idx = i; break; } } else if (a.Contain(new Vector3(end.x, end.y, end.z))) { bEndContained = true; } } if (idx < 0) { if (bStartContained) return SearchResult.enumSearchEndInvalid; if (bEndContained) return SearchResult.enumSearchStartInvalid; return SearchResult.enumSearchStartEndInvalid; } m_iCurMoveAgent = idx; var agent = m_moveAgents[idx].agent; if (agent == null) return SearchResult.enumSearchUnInitialized; // Determine layers (minimal: layer0 or invalid). // 确定层(最小实现:层0或无效)。 float dist; int startLayer = agent.WhichLayer(new Vector3(start.x, start.y, start.z), 0.0f, out dist); int endLayer = agent.WhichLayer(new Vector3(end.x, end.y, end.z), 0.0f, out dist); if (startLayer < 0) startLayer = 0; if (endLayer < 0) endLayer = 0; agent.SetStartEnd(new Vector3(start.x, start.y, start.z), startLayer, new Vector3(end.x, end.y, end.z), endLayer, brushTest); bool ok = agent.Search(nMaxExpand); if (!ok) { if (DEBUG_AUTOPF) { BMLogger.Log($"[CECIntelligentRoute] Search: no path found, ignoring. End position=({end.x:F2},{end.y:F2},{end.z:F2})"); } // Don't call ResetSearch() again - it was already called at the start of Search() // This prevents the idle state deadlock that blocks all movement // 不再调用 ResetSearch() - 已在 Search() 开始时调用 // 这防止了阻止所有移动的空闲状态死锁 return SearchResult.enumSearchNoPath; } // EC_IntelligentRoute.cpp: single-node 2D path → start/goal coincide. if (agent.GetPathCount() == 1) { agent.ResetSearch(); if (DEBUG_AUTOPF) BMLogger.Log("[CECIntelligentRoute] Search: path size 1 (start/end coincide)."); return SearchResult.enumSearchStartEndCoincide; } m_state = RouteState.enumRouteMoving; m_lastPos = m_start; m_lastMove = 0.0f; m_iCurDest = FindNextNode(start, 0); m_dist2CurDest = (GetCurDest() - m_start).MagnitudeH(); if (DEBUG_AUTOPF) { BMLogger.Log($"[CECIntelligentRoute] Search: success pathCount={agent.GetPathCount()} start=({start.x:F2},{start.z:F2}) end=({end.x:F2},{end.z:F2}) curDestIdx={m_iCurDest}"); } return SearchResult.enumSearchSuccess; } public A3DVECTOR3 GetCurDest() { if (IsIdle() || m_iCurMoveAgent < 0) return m_end; return GetNodePos(m_iCurDest); } /// /// Get full path for debug visualization. /// 获取完整路径用于调试可视化。 /// public System.Collections.Generic.List GetFullPath() { var path = new System.Collections.Generic.List(); if (m_state == RouteState.enumRouteIdle || m_iCurMoveAgent < 0) return path; var agent = m_moveAgents[m_iCurMoveAgent].agent; if (agent == null) return path; return agent.GetFullPath(); } /// Ported from C++ CECIntelligentRoute::OnPlayerPosChange (EC_IntelligentRoute.cpp). public void OnPlayerPosChange(A3DVECTOR3 pos) { if (!IsMoveOn()) return; if (CanFinishPath(pos)) { m_state = RouteState.enumRoutePathFinished; return; } if (!CanMoveToNext(pos)) return; RangedMoveAgent? pCur = GetCurAgent(); if (pCur == null || pCur.Value.agent == null) return; CMoveAgent agent = pCur.Value.agent; int pathCount = agent.GetPathCount(); if (m_iCurDest == pathCount - 1) { m_state = RouteState.enumRoutePathFinished; return; } m_iCurDest = FindNextNode(pos, m_iCurDest + 1); m_dist2CurDest = (GetCurDest() - pos).MagnitudeH(); // C++ calls agent->Optimize(m_iCurDest) here; Unity CMoveAgent has no path optimizer yet. } A3DVECTOR3 GetNodePos(int iNode) { RangedMoveAgent? pCurAgent = GetCurAgent(); if (pCurAgent == null || pCurAgent.Value.agent == null) return new A3DVECTOR3(0f, 0f, 0f); CMoveAgent ag = pCurAgent.Value.agent; int nPathCount = ag.GetPathCount(); if (iNode >= 0 && iNode < nPathCount) return GetNodePosNoCheck(iNode); return new A3DVECTOR3(0f, 0f, 0f); } A3DVECTOR3 GetNodePosNoCheck(int iNode) { if (m_iCurMoveAgent < 0 || m_iCurMoveAgent >= m_moveAgents.Count) return new A3DVECTOR3(0f, 0f, 0f); CMoveAgent agent = m_moveAgents[m_iCurMoveAgent].agent; if (agent == null) return new A3DVECTOR3(0f, 0f, 0f); Vector3 v = agent.Get3DPathNode(iNode); // C++ adds GetTerrainHeight to Y; Unity path nodes use movemap/world height from loader. return new A3DVECTOR3(v.x, v.y, v.z); } A3DVECTOR3 GetNodePosXZ(int iNode) { if (IsIdle()) return new A3DVECTOR3(0f, 0f, 0f); A3DVECTOR3 p = GetNodePosNoCheck(iNode); p.y = 0f; return p; } int FindNearestNode(A3DVECTOR3 curPos, int iNodeFrom) { if (!IsMoveOn()) return -1; RangedMoveAgent? pCur = GetCurAgent(); if (pCur == null || pCur.Value.agent == null) return -1; CMoveAgent agent = pCur.Value.agent; int pathCount = agent.GetPathCount(); if (iNodeFrom < 0 || iNodeFrom >= pathCount) return -1; if (iNodeFrom == pathCount - 1) return iNodeFrom; int maxCheckIndex = iNodeFrom + agent.GetOptimizeCatchCount(); maxCheckIndex = Mathf.Min(maxCheckIndex, pathCount - 1); int bestIndex = -1; double bestDist2 = double.MaxValue; for (int i = iNodeFrom; i <= maxCheckIndex; ++i) { A3DVECTOR3 testPos = GetNodePosXZ(i); testPos -= curPos; testPos.y = 0f; double dist2 = testPos.x * testPos.x + testPos.z * testPos.z; if (dist2 < bestDist2) { bestIndex = i; bestDist2 = dist2; } } return bestIndex >= 0 ? bestIndex : -1; } int FindFarthestNode(A3DVECTOR3 curPos, int iNodeFrom) { if (iNodeFrom < 0) return iNodeFrom; RangedMoveAgent? pCur = GetCurAgent(); if (pCur == null || pCur.Value.agent == null) return iNodeFrom; CMoveAgent agent = pCur.Value.agent; int pathCount = agent.GetPathCount(); if (iNodeFrom == pathCount - 1) return iNodeFrom; A3DVECTOR3 moveDir = GetNodePosXZ(iNodeFrom); moveDir -= curPos; moveDir.y = 0f; if (moveDir.Normalize() < 1e-4f) return iNodeFrom; int maxCheckIndex = iNodeFrom + agent.GetOptimizeCatchCount(); maxCheckIndex = Mathf.Min(maxCheckIndex, pathCount - 1); for (int i = iNodeFrom + 1; i <= maxCheckIndex; ++i) { A3DVECTOR3 testDir = GetNodePosXZ(i); testDir -= curPos; testDir.y = 0f; if (testDir.Normalize() < 1e-4f) break; float dtp = A3DVECTOR3.DotProduct(moveDir, testDir); if (dtp < s_cos5Deg) break; iNodeFrom = i; } return iNodeFrom; } int FindNextNode(A3DVECTOR3 curPos, int iNodeFrom) { int iCandidate = FindNearestNode(curPos, iNodeFrom); if (iCandidate < 0) return Mathf.Max(0, iNodeFrom); return FindFarthestNode(curPos, iCandidate); } bool CanFinishPath(A3DVECTOR3 pos) { if (!IsMoveOn()) return false; A3DVECTOR3 delta = m_end - pos; return delta.Magnitude() <= 0.5f; } bool CanMoveToNext(A3DVECTOR3 pos) { float fMove = (pos - m_lastPos).MagnitudeH(); m_lastPos = pos; float lastMove = m_lastMove; m_lastMove = fMove; float dist2CurDest = m_dist2CurDest; A3DVECTOR3 vCurDest = GetCurDest(); m_dist2CurDest = (vCurDest - pos).MagnitudeH(); if (fMove >= dist2CurDest) return true; if (fMove + 0.1f >= dist2CurDest) return true; if (lastMove * 0.5f >= m_dist2CurDest) return true; return false; } } }