Files
test/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs

450 lines
18 KiB
C#
Raw Permalink 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.
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
{
/// <summary>
/// Intelligent route (AutoPF) controller.
/// 智能寻路(AutoPF)控制器。
/// </summary>
public sealed class CECIntelligentRoute
{
private const bool DEBUG_AUTOPF = true;
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<RangedMoveAgent> m_moveAgents = new List<RangedMoveAgent>(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 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;
}
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<string> 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<string, byte[]> 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<TextAsset>(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;
}
m_state = RouteState.enumRouteMoving;
m_iCurDest = 0;
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})");
}
return SearchResult.enumSearchSuccess;
}
public A3DVECTOR3 GetCurDest()
{
if (m_state == RouteState.enumRouteIdle || m_iCurMoveAgent < 0) return m_end;
var agent = m_moveAgents[m_iCurMoveAgent].agent;
if (agent == null) return m_end;
int cnt = agent.GetPathCount();
if (cnt <= 0) return m_end;
int idx = Mathf.Clamp(m_iCurDest, 0, cnt - 1);
Vector3 v = agent.Get3DPathNode(idx);
return new A3DVECTOR3(v.x, v.y, v.z);
}
/// <summary>
/// Get full path for debug visualization.
/// 获取完整路径用于调试可视化。
/// </summary>
public System.Collections.Generic.List<Vector3> GetFullPath()
{
var path = new System.Collections.Generic.List<Vector3>();
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();
}
public void OnPlayerPosChange(A3DVECTOR3 pos)
{
if (m_state != RouteState.enumRouteMoving) return;
if (m_iCurMoveAgent < 0) return;
var agent = m_moveAgents[m_iCurMoveAgent].agent;
if (agent == null) return;
int cnt = agent.GetPathCount();
if (cnt <= 0)
{
m_state = RouteState.enumRoutePathFinished;
return;
}
// Advance nodes when close to current dest.
// 接近当前节点时推进到下一节点。
float reach = 1.0f;
while (m_iCurDest < cnt)
{
Vector3 d = agent.Get3DPathNode(m_iCurDest);
Vector3 delta = d - new Vector3(pos.x, pos.y, pos.z);
delta.y = 0.0f;
if (delta.sqrMagnitude <= reach * reach)
{
m_iCurDest++;
continue;
}
break;
}
if (m_iCurDest >= cnt)
{
m_state = RouteState.enumRoutePathFinished;
}
}
}
}