diff --git a/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md new file mode 100644 index 0000000000..82a8271619 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md @@ -0,0 +1,1106 @@ +# Perfect World Navigation System - Complete Guide + +## Table of Contents +1. [System Overview](#system-overview) +2. [Architecture](#architecture) +3. [Complete Flow: UI Click to Movement](#complete-flow-ui-click-to-movement) +4. [Force Navigate System (Bezier-Based)](#force-navigate-system-bezier-based) +5. [Normal Auto-Move System](#normal-auto-move-system) +6. [Map Loading from StreamingAssets](#map-loading-from-streamingassets) +7. [Bezier Curve Loading (navigate.dat)](#bezier-curve-loading-navigatedat) +8. [Coordinate Resolution System](#coordinate-resolution-system) +9. [Data Files Structure](#data-files-structure) +10. [Key Components Deep Dive](#key-components-deep-dive) +11. [Troubleshooting](#troubleshooting) + +--- + +## System Overview + +The Perfect World navigation system provides **two distinct movement modes**: + +1. **Force Navigate (Bezier-Based)**: Predefined cinematic paths using Bezier curves for scripted sequences +2. **Normal Auto-Move**: Pathfinding-based movement for general navigation + +### When Each Mode is Used + +- **Force Navigate**: Tasks with `enumTMSimpleClientTaskForceNavi` flag OR entries in `force_navigate.txt` +- **Normal Auto-Move**: All other cases (NPCs, monsters, items, general coordinates) + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User Interface Layer │ +│ DlgTask (Task UI) → OnEventLButtonDown_Txt_QuestItem │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Coordinate Resolution Layer │ +│ CECUIHelper.FollowCoord() → GetTaskObjectCoordinates() │ +│ - Live objects (NPCs/Players/Matters) │ +│ - Template ID lookup (spawned NPCs) │ +│ - task_npc.data table │ +│ - coord_data.txt │ +│ - Task regions (fallback) │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ┌─────────────┴─────────────┐ + │ │ + ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ +│ Force Navigate │ │ Normal Auto-Move │ +│ (Bezier Path) │ │ (Pathfinding) │ +└──────────────────┘ └──────────────────┘ +``` + +--- + +## Complete Flow: UI Click to Movement + +### Step 1: User Clicks NPC/Monster Name in Task UI + +**File**: `Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs` + +When a user clicks a link in the task description (e.g., NPC name, monster name, item name): + +```csharp +private void OnEventLButtonDown_Txt_QuestItem(PointerEventData eventData) +{ + // 1. Detect clicked link using TextMeshPro + int linkIndex = TMP_TextUtilities.FindIntersectingLink( + m_pTxt_QuestItem, eventData.position, camera); + + // 2. Extract entity ID from link (format: "coord_{entityID}") + string linkID = m_pTxt_QuestItem.textInfo.linkInfo[linkIndex].GetLinkID(); + int entityID = int.Parse(linkID.Substring(6)); // Remove "coord_" prefix + + // 3. Trigger navigation + CECUIHelper.FollowCoord(entityID, currentTaskID); +} +``` + +**Key Points**: +- Links are formatted as `{name}` in rich text +- `TMP_TextUtilities.FindIntersectingLink` detects which link was clicked +- Camera must be `null` for `ScreenSpaceOverlay` canvases + +### Step 2: Coordinate Resolution + +**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs` + +The system attempts to find coordinates in this **priority order**: + +```csharp +public static A3DVECTOR3 GetTaskObjectCoordinates(int id, ref bool in_table) +{ + // Priority 1: Live runtime objects (spawned NPCs, players, matters) + var world = EC_Game.GetGameRun()?.GetWorld(); + if (world != null && isLikelyRuntimeObjectId) + { + var obj = world.GetObject(id, 0); + if (obj != null) + return EC_Utility.ToA3DVECTOR3(obj.transform.position); + } + + // Priority 2: Live NPC/Monster by template ID + var npcMan = EC_ManMessageMono.Instance?.CECNPCMan; + if (npcMan != null) + { + var npc = npcMan.FindNPCByTemplateID(id); + if (npc != null) + return EC_Utility.ToA3DVECTOR3(npc.transform.position); + } + + // Priority 3: task_npc.data table + ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan(); + if (pMan != null && pMan.TryGetTaskNPCInfo((uint)id, out NPC_INFO info)) + { + return new A3DVECTOR3(info.x, info.z, info.y); // Note: y/z swap + } + + // Priority 4: coord_data.txt + if (EC_Game.TryGetFirstObjectCoord(id.ToString(), out var pos, out var mapName)) + { + return new A3DVECTOR3(pos.x, pos.y, pos.z); + } + + // Priority 5: Task regions (fallback) + // Uses delivery zone, reach-site, enter/leave regions + return new A3DVECTOR3(0); +} +``` + +### Step 3: Navigation Mode Decision + +**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs` + +```csharp +public static bool FollowCoord(int id, int taskId) +{ + // Check if task uses Force Navigate + bool shouldForceNavigate = false; + + // Method 1: Task template flag + var templ = taskMan.GetTaskTemplByID((uint)taskId); + if (templ != null && + templ.m_FixedData.m_enumMethod == + (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) + { + shouldForceNavigate = true; + } + + // Method 2: force_navigate.txt mapping + if (!shouldForceNavigate) + { + var ctrl = hostPlayer.GetNavigatePlayer()?.GetNavigateCtrl(); + if (ctrl != null && ctrl.GetNavigateInfo(taskId, ref tmp)) + { + shouldForceNavigate = true; + } + } + + if (shouldForceNavigate) + { + // Use Bezier-based Force Navigate + hostPlayer.OnNaviageEvent(taskId, EM_PREPARE); + hostPlayer.OnNaviageEvent(taskId, EM_BEGIN); + return true; + } + + // Otherwise: Use normal auto-move with pathfinding + CECHPWorkMove work = wm.CreateWork(WORK_MOVETOPOS); + work.SetDestination(DEST_AUTOPF, vPos); + wm.StartWork_p2(work); + return true; +} +``` + +--- + +## Force Navigate System (Bezier-Based) + +### Configuration File: `force_navigate.txt` + +**Location**: `Assets/Addressable/force_navigate.txt` + +**Format**: CSV with 6 fields per line +``` +taskID,bezierID,speed,angle,dir,modelPath +32220,-1001,30,30.0,1,Models\NPC\xxx.ecm +``` + +**Fields**: +- `taskID`: Task ID that triggers this navigation +- `bezierID`: Global ID of Bezier curve in `navigate.dat` (negative = force navigate) +- `speed`: Movement speed (units per second) +- `angle`: Camera angle adjustment (degrees) +- `dir`: `1` = use Bezier direction, `0` = fixed horizontal +- `modelPath`: Model file path for navigation (optional) + +**Loading**: +```csharp +// Auto-loaded when CECNavigateCtrl is created +public CECNavigateCtrl(CECHostPlayer pHost) +{ + EnsureDefaultConfigLoaded(); // Loads from Addressables +} +``` + +### Phase 1: Prepare (`EM_PREPARE`) + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +```csharp +public void OnPrepareNavigate(int task) +{ + m_bForceNavigateState = true; + m_taskID = task; + + // Load navigation config for this task + INFO naviInfo = new INFO(); + if (GetNavigateInfo(task, ref naviInfo)) + { + m_curNavigateInfo = naviInfo; + + // Set up navigate player with model file + CECHostNavigatePlayer player = m_pHost.GetNavigatePlayer(); + if (player != null) + { + player.SetNavigateModelFile(naviInfo.strModelPath); + player.Init(); + } + } + else + { + // No config = give up task + m_pHost.GetTaskInterface().GiveUpTask((uint)m_taskID); + } +} +``` + +**What Happens**: +- Sets force navigate state flag +- Loads navigation config from `force_navigate.txt` +- Initializes navigate player with model file +- **If config missing → gives up task** (strict C++ behavior) + +### Phase 2: Begin (`EM_BEGIN`) + +```csharp +public void OnBeginNavigate() +{ + // Load Bezier curve from navigate.dat by globalID + CECBezier pBezier = CECBezierNavigateLoader.GetBezierObjectByGlobalID( + m_curNavigateInfo.bezierID); + + if (pBezier == null) + { + // No Bezier = give up task (strict C++ behavior) + m_pHost.GetTaskInterface().GiveUpTask((uint)m_taskID); + return; + } + + // Create Bezier walker + if (m_pBezierWalker == null) + m_pBezierWalker = new CECBezierWalker(); + + // Bind Bezier curve and configure + m_pBezierWalker.BindBezier(pBezier); + m_pBezierWalker.SetSpeed(m_curNavigateInfo.speed); + m_pBezierWalker.StartWalk(false, true); // no loop, forward + + // Create work system entry + CECHPWorkNavigate pWork = m_pHost.GetWorkMan() + .CreateWork(WORK_FORCENAVIGATEMOVE) as CECHPWorkNavigate; + pWork.BeginNavigate(); + m_pHost.GetWorkMan().StartWork_p2(pWork); +} +``` + +**What Happens**: +- Loads Bezier curve from `navigate.dat` using `bezierID` +- Creates `CECBezierWalker` to follow the curve +- Sets speed and starts walking +- Creates `CECHPWorkNavigate` work to drive movement each frame + +### Phase 3: Movement Loop (`Tick`) + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +Each frame, `CECHPWorkNavigate.Tick()` is called: + +```csharp +public override bool Tick(float dwDeltaTime) +{ + CECBezierWalker pBezierWalker = /* get from navigate ctrl */; + + if (!m_bMove || pBezierWalker == null) return true; + + // Get frame delta time in milliseconds + int dwRealTime = (int)EC_Game.GetRealTickTime(); + + if (pBezierWalker.IsWalking()) + { + // Update Bezier walker position along curve + pBezierWalker.Tick(dwRealTime); + + // Get position and direction from Bezier curve + A3DVECTOR3 vCurPos = pBezierWalker.GetPos(); + A3DVECTOR3 vDir = pBezierWalker.GetDir(); + vDir.Normalize(); + + // Calculate rotation vectors + A3DVECTOR3 vRight = A3DVECTOR3.CrossProduct(g_vAxisY, vDir); + A3DVECTOR3 vUp = A3DVECTOR3.CrossProduct(vDir, vRight); + A3DVECTOR3 vDirH = A3DVECTOR3.CrossProduct(vRight, g_vAxisY); + vUp.Normalize(); + + // Apply to player + CECHostNavigatePlayer pClone = m_pHost.GetNavigatePlayer(); + pClone.SetPos(vCurPos); + + // Use Bezier direction or fixed horizontal based on config + if (naviInfo.bezierDir) + pClone.ChangeModelMoveDirAndUp(vDir, vUp); + else + pClone.ChangeModelMoveDirAndUp(vDirH, g_vAxisY); + } + else + { + // Bezier walker finished + m_bMove = false; + m_pHost.GetTaskInterface().SetForceNavigateFinishFlag(true); + } + + return true; +} +``` + +**Key Points**: +- `GetRealTickTime()` returns **delta milliseconds** (not absolute time) +- Bezier walker accumulates time and calculates position along curve +- Position/direction come from Bezier curve calculations +- Rotation can follow Bezier tangent or stay horizontal + +### Phase 4: End (`EM_END`) + +```csharp +public void OnEndNavigate() +{ + if (!m_bForceNavigateState) return; + + m_taskID = 0; + + // Finish work + m_pHost.GetWorkMan() + .FinishRunningWork(WORK_FORCENAVIGATEMOVE); + + m_bForceNavigateState = false; +} +``` + +--- + +## Normal Auto-Move System + +When **not** using Force Navigate, the system uses pathfinding: + +```csharp +// In CECUIHelper.FollowCoord() +CECHPWorkMove work = wm.CreateWork(WORK_MOVETOPOS); +work.SetDestination(CECHPWorkMove.DestTypes.DEST_AUTOPF, vPos); + +// Store task NPC info for trace switching +if (taskId > 0) +{ + work.SetTaskNPCInfo(id, taskId); +} + +wm.StartWork_p2(work); +``` + +**What Happens**: +- Creates `CECHPWorkMove` work +- Uses **AutoPF** (intelligent pathfinding) instead of straight-line +- Can switch to `WORK_TRACEOBJECT` when near NPCs +- Uses pathfinding maps from `StreamingAssets/maps/{mapName}/movemap/` + +--- + +## Map Loading from StreamingAssets + +### Directory Structure + +``` +Assets/StreamingAssets/maps/ +├── a61/ # Map name (from CECInstance.GetPath()) +│ ├── movemap/ # Pathfinding data +│ │ ├── r1_1-c1_2-l0.cfg +│ │ ├── r1_1-c1_2-l0.prmap +│ │ ├── r1_1-c1_2-l0.pdhmap +│ │ └── r1_1-c1_2-l0.mlu +│ └── navigate.dat # Bezier curves for force navigate (C++ loads this) +└── world/ + └── ... +``` + +### How Maps Are Loaded + +**File**: `Assets/PerfectWorld/Scripts/World/EC_Instance.cs` + +```csharp +public class CECInstance +{ + string m_strPath; // e.g., "a61" + int m_iRowNum = 3; // Number of map rows + int m_iColNum = 4; // Number of map columns + List m_routeFiles; // e.g., ["r1_1-c1_2-l0"] + + public string GetPath() { return m_strPath; } + public List GetRouteFiles() { return m_routeFiles; } +} +``` + +**File**: `Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs` + +When the world instance changes, pathfinding maps are loaded: + +```csharp +public void ChangeWorldInstance(int idInstance) +{ + CECInstance pInst = EC_Game.GetGameRun()?.GetInstance(idInstance); + if (pInst == null) return; + + // Get route files from instance (e.g., ["r1_1-c1_2-l0"]) + List files = pInst.GetRouteFiles(); + + foreach (var fileBase in files) + { + // Parse range: "r1_1-c1_2-l0" → rowFrom=1, rowTo=1, colFrom=1, colTo=2 + int rowFrom, rowTo, colFrom, colTo; + TryParseRouteFileRange(fileBase, out rowFrom, out rowTo, + out colFrom, out colTo); + + // Build path: "maps/a61/movemap/r1_1-c1_2-l0" + string basePath = $"maps/{pInst.GetPath()}/movemap/{fileBase}"; + + // Load pathfinding agent + var agent = new CMoveAgent(); + agent.Load(basePath, ResolveStreamingBytes, origin); + + m_moveAgents.Add(new RangedMoveAgent { agent = agent, ... }); + } +} + +private static byte[] ResolveStreamingBytes(string key) +{ + // Resolves "maps/a61/movemap/r1_1-c1_2-l0.cfg" to: + // Application.streamingAssetsPath + "/maps/a61/movemap/r1_1-c1_2-l0.cfg" + string full = Path.Combine( + Application.streamingAssetsPath, + key.Replace('/', Path.DirectorySeparatorChar)); + + return File.Exists(full) ? File.ReadAllBytes(full) : null; +} +``` + +**Key Points**: +- Maps are loaded from `StreamingAssets/maps/{mapName}/movemap/` +- Route files follow pattern: `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0` +- Files: `.cfg`, `.prmap`, `.pdhmap`, `.mlu` +- Loaded via `CMoveAgent.Load()` which reads from `StreamingAssets` + +--- + +## Bezier Curve Loading (navigate.dat) + +### File Location + +**Original C++**: `maps/{mapName}/navigate.dat` (loaded per-map) +**Unity Version**: `Assets/Addressable/navigate.dat` (single global file) + +**Note**: The Unity version currently loads a single global `navigate.dat` from Addressables. In the original C++, each map has its own `navigate.dat` file loaded when the map loads. + +### Binary File Format + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +```csharp +public static class CECBezierNavigateLoader +{ + private struct NAVIGATEBEZIERFILEHEADER + { + public int iVersion; // File version + public int iNumBezier; // Number of Bezier curves + } + + public static bool LoadBezierNavigate(string address) + { + // Load from Addressables + var ta = Addressables.LoadAssetAsync(address) + .WaitForCompletion(); + + using var ms = new MemoryStream(ta.bytes, false); + using var br = new BinaryReader(ms); + + // Read header + NAVIGATEBEZIERFILEHEADER header; + header.iVersion = br.ReadInt32(); + header.iNumBezier = br.ReadInt32(); + + // Read each Bezier curve + for (int i = 0; i < header.iNumBezier; i++) + { + var pBezier = new CECBezier(); + pBezier.Load(br); // Loads curve data + + // Store by GlobalID + s_beziers[pBezier.GetGlobalID()] = pBezier; + } + } +} +``` + +### Bezier Curve Binary Format + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +```csharp +public bool Load(BinaryReader br) +{ + // Header + uint dwVersion = br.ReadUInt32(); // BEZIER_FILE_VERSION (3) + m_nObjectID = br.ReadInt32(); + + // Version 2+: Global IDs + if (dwVersion >= 2) + { + m_iGlobalID = br.ReadInt32(); // Used for lookup + m_iNextGlobalID = br.ReadInt32(); // Next curve in chain + } + + // Version 3+: Action name + if (dwVersion >= 3) + { + int len = br.ReadInt32(); + if (len > 0) + { + byte[] buf = br.ReadBytes(len); + m_strActName = Encoding.GetEncoding(936).GetString(buf); + } + } + + // Points array + int ptNum = br.ReadInt32(); + m_pListPoint = new CECBezierPoint[ptNum]; + for (int i = 0; i < ptNum; i++) + { + A3DVECTOR3 vPos = new A3DVECTOR3( + br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + A3DVECTOR3 vDir = new A3DVECTOR3( + br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + m_pListPoint[i] = new CECBezierPoint(); + m_pListPoint[i].SetPos(vPos); + m_pListPoint[i].SetDir(vDir); + } + + // Segments array + int segNum = br.ReadInt32(); + m_pListSeg = new CECBezierSeg[segNum]; + for (int i = 0; i < segNum; i++) + { + A3DVECTOR3 vAnchorHead = new A3DVECTOR3( + br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + A3DVECTOR3 vAnchorTail = new A3DVECTOR3( + br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + int headIndex = br.ReadInt32(); + int tailIndex = br.ReadInt32(); + float length = br.ReadSingle(); + + m_pListSeg[i] = new CECBezierSeg(); + m_pListSeg[i].Init(m_pListPoint); + m_pListSeg[i].SetAnchorHead(vAnchorHead); + m_pListSeg[i].SetAnchorTail(vAnchorTail); + m_pListSeg[i].SetHeadPoint(headIndex); + m_pListSeg[i].SetTailPoint(tailIndex); + m_pListSeg[i].SetSegLength(length); + } + + return true; +} +``` + +**Binary Structure**: +``` +[Header] + int32: version (3) + int32: objectID + +[Version 2+] + int32: globalID + int32: nextGlobalID + +[Version 3+] + int32: actionNameLength + byte[]: actionName (CP936 encoding) + +[Points Array] + int32: pointCount + for each point: + float32[3]: position (x, y, z) + float32[3]: direction (x, y, z) + +[Segments Array] + int32: segmentCount + for each segment: + float32[3]: anchorHead (x, y, z) + float32[3]: anchorTail (x, y, z) + int32: headPointIndex + int32: tailPointIndex + float32: segmentLength +``` + +### How Bezier Curves Are Looked Up + +```csharp +// When OnBeginNavigate() is called: +CECBezier pBezier = CECBezierNavigateLoader.GetBezierObjectByGlobalID( + m_curNavigateInfo.bezierID); + +// GetBezierObjectByGlobalID: +public static CECBezier GetBezierObjectByGlobalID(int iGlobalID) +{ + if (!s_loaded) + { + LoadBezierNavigate("Assets/Addressable/navigate.dat"); + } + + return s_beziers.TryGetValue(iGlobalID, out var bezier) + ? bezier : null; +} +``` + +**Key Points**: +- Bezier curves are stored in a `Dictionary` keyed by `GlobalID` +- `bezierID` from `force_navigate.txt` is the `GlobalID` used for lookup +- Negative `bezierID` values (e.g., `-1001`) indicate force navigate routes +- Loaded **once** on first access (lazy loading) + +--- + +## Coordinate Resolution System + +### Priority Order + +**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs` + +```csharp +public static A3DVECTOR3 GetTaskObjectCoordinates(int id, ref bool in_table) +{ + // Priority 1: Live runtime objects + // Check if ID is a runtime object ID (NPC, Player, Matter) + bool isLikelyRuntimeObjectId = + GPDataTypeHelper.ISNPCID(id) || + GPDataTypeHelper.ISMATTERID(id) || + (id > 100000000); // Player IDs are huge + + if (isLikelyRuntimeObjectId) + { + var obj = world.GetObject(id, 0); + if (obj != null) + return EC_Utility.ToA3DVECTOR3(obj.transform.position); + } + + // Priority 2: Live NPC/Monster by template ID + var npc = npcMan.FindNPCByTemplateID(id); + if (npc != null) + return EC_Utility.ToA3DVECTOR3(npc.transform.position); + + // Priority 3: task_npc.data table + if (pMan.TryGetTaskNPCInfo((uint)id, out NPC_INFO info)) + { + return new A3DVECTOR3(info.x, info.z, info.y); // Note: y/z swap + } + + // Priority 4: coord_data.txt + if (EC_Game.TryGetFirstObjectCoord(id.ToString(), out var pos, out var mapName)) + { + return new A3DVECTOR3(pos.x, pos.y, pos.z); + } + + // Priority 5: Task regions (fallback) + // Uses delivery zone, reach-site, enter/leave regions + return new A3DVECTOR3(0); +} +``` + +### Data Sources + +#### 1. Live Runtime Objects + +**Source**: `CECWorld.GetObject(id, 0)` + +- Spawned NPCs, players, matters in the current world +- Uses runtime object IDs (not template IDs) +- **Fastest** - direct world lookup + +#### 2. Live NPCs by Template ID + +**Source**: `CECNPCMan.FindNPCByTemplateID(id)` + +- Searches spawned NPCs/monsters matching template ID +- Checks both active NPCs and disappeared NPCs (may still have valid positions) +- **Good for** clicking NPC names that are currently spawned + +#### 3. task_npc.data Table + +**Source**: `ATaskTemplMan.TryGetTaskNPCInfo(uint id, out NPC_INFO info)` + +- Static NPC coordinates from task data +- Loaded from binary `task_npc.data` file +- **Note**: Coordinates use `(x, z, y)` mapping (y/z swapped) + +#### 4. coord_data.txt + +**Source**: `EC_Game.TryGetFirstObjectCoord(string targetId, out Vector3 pos, out string mapName)` + +**File**: `Assets/Addressable/coord_data.txt` + +**Format**: +``` +ID MapName X Y Z +44393 a61 33.37 25.08 244.49 +44618 a61 -440.90 21.00 102.72 +``` + +**Loading**: +```csharp +// In EC_Game.Init() +LoadObjectCoord(); // Loads coord_data.txt from Addressables + +// Parsing: +private static void ParseCoordDataText(string text) +{ + // Skip header line + // Parse: key mapName x y z + string key = parts[0]; // e.g., "44393" + string map = parts[1]; // e.g., "a61" + float x = float.Parse(parts[2]); + float y = float.Parse(parts[3]); + float z = float.Parse(parts[4]); + + m_CoordTab[key] = new List { + new OBJECT_COORD { strMap = map, vPos = new Vector3(x, y, z) } + }; +} +``` + +**Lookup**: +```csharp +public static bool TryGetFirstObjectCoord(string targetId, + out Vector3 pos, out string map) +{ + if (!m_CoordTab.TryGetValue(targetId, out var list) || + list == null || list.Count == 0) + { + return false; + } + + pos = list[0].vPos; + map = list[0].strMap; + return true; +} +``` + +#### 5. Task Regions (Fallback) + +**Source**: `ATaskTempl` region data + +If no specific coordinates found, uses task region centers: + +- **Delivery zone**: `m_pDelvRegion` (where quest giver is) +- **Reach-site**: `m_pReachSite` (for reach-site tasks) +- **Enter region**: `m_pEnterRegion` (where objectives happen) +- **Leave region**: `m_pLeaveRegion` (exit zones) + +```csharp +// Calculate region center +float cx = (r.zvMin.x + r.zvMax.x) * 0.5f; +float cy = (r.zvMin.y + r.zvMax.y) * 0.5f; +float cz = (r.zvMin.z + r.zvMax.z) * 0.5f; +``` + +--- + +## Data Files Structure + +### 1. force_navigate.txt + +**Location**: `Assets/Addressable/force_navigate.txt` + +**Format**: CSV (comma-separated, may be quoted) +``` +"32220,-1001,30,30.0,1,Models\NPC\xxx.ecm" +"32221,-1002,25,45.0,0,Models\NPC\yyy.ecm" +``` + +**Fields**: +- `taskID`: Task ID +- `bezierID`: Global ID in `navigate.dat` (negative = force navigate) +- `speed`: Movement speed +- `angle`: Camera angle (degrees) +- `dir`: `1` = Bezier dir, `0` = fixed horizontal +- `modelPath`: Model file path + +**Loading**: Auto-loaded when `CECNavigateCtrl` is created + +### 2. navigate.dat + +**Location**: `Assets/Addressable/navigate.dat` (Unity) +**Original**: `maps/{mapName}/navigate.dat` (C++) + +**Format**: Binary file with Bezier curves + +**Structure**: +``` +[Header: 8 bytes] + int32: version (3) + int32: numBezier + +[For each Bezier curve:] + [Bezier Header] + uint32: version + int32: objectID + int32: globalID (version >= 2) + int32: nextGlobalID (version >= 2) + int32: actionNameLength (version >= 3) + byte[]: actionName (CP936, if length > 0) + + [Points Array] + int32: pointCount + for each point: + float32[3]: position + float32[3]: direction + + [Segments Array] + int32: segmentCount + for each segment: + float32[3]: anchorHead + float32[3]: anchorTail + int32: headPointIndex + int32: tailPointIndex + float32: segmentLength +``` + +**Loading**: Lazy-loaded on first `GetBezierObjectByGlobalID()` call + +### 3. coord_data.txt + +**Location**: `Assets/Addressable/coord_data.txt` + +**Format**: Tab/space-separated text +``` +ID MapName X Y Z +44393 a61 33.37 25.08 244.49 +44618 a61 -440.90 21.00 102.72 +``` + +**Loading**: Loaded in `EC_Game.Init()` via `LoadObjectCoord()` + +**Lookup**: `EC_Game.TryGetFirstObjectCoord(string id, ...)` + +### 4. task_npc.data + +**Location**: Binary file loaded by `ATaskTemplMan` + +**Format**: Binary (NPC_INFO structs) + +**Lookup**: `ATaskTemplMan.TryGetTaskNPCInfo(uint id, out NPC_INFO info)` + +**Note**: Coordinates use `(x, z, y)` mapping (y/z swapped from Unity) + +### 5. Map Pathfinding Files + +**Location**: `Assets/StreamingAssets/maps/{mapName}/movemap/` + +**Files**: +- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.cfg` - Configuration +- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.prmap` - Pathfinding map +- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.pdhmap` - Height map +- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.mlu` - Movement lookup + +**Loading**: Loaded by `CECIntelligentRoute.ChangeWorldInstance()` when map changes + +--- + +## Key Components Deep Dive + +### CECNavigateCtrl + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +**Responsibilities**: +- Loads `force_navigate.txt` config +- Manages force navigate state +- Coordinates navigation events (PREPARE, BEGIN, END) +- Creates and manages `CECBezierWalker` + +**Key Methods**: +- `LoadConfig()` / `LoadConfigAddressable()` - Load config file +- `GetNavigateInfo()` - Lookup config by task ID +- `OnPrepareNavigate()` - Phase 1: Setup +- `OnBeginNavigate()` - Phase 2: Start movement +- `OnEndNavigate()` - Phase 3: Cleanup + +### CECBezierWalker + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +**Responsibilities**: +- Follows Bezier curves over time +- Calculates position/direction along curve +- Handles segment transitions and looping + +**Key Methods**: +- `BindBezier()` - Attach Bezier curve +- `SetSpeed()` - Set movement speed +- `StartWalk()` - Begin walking along curve +- `Tick()` - Update position based on time +- `GetPos()` - Get current position on curve +- `GetDir()` - Get current direction on curve + +**Time Calculation**: +```csharp +// Time accumulates in milliseconds +m_iTimeCnt += iDeltaTime; + +// Calculate parameter u [0,1] for current segment +float u = (float)(m_iTimeCnt - m_iPassSegTime) / m_iCurSegTime; + +// Get position from Bezier curve +A3DVECTOR3 pos = segment.Bezier(u, bForward); +``` + +### CECHPWorkNavigate + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +**Responsibilities**: +- Work system entry point for force navigate +- Ticks each frame to update player position +- Applies Bezier walker results to player + +**Key Methods**: +- `BeginNavigate()` - Start movement flag +- `OnFirstTick()` - Initial camera setup +- `Tick()` - Main movement loop + +### CECHostNavigatePlayer + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +**Responsibilities**: +- Wrapper that applies position/rotation to host player +- Manages navigate control instance + +**Key Methods**: +- `SetPos()` - Apply position to host player +- `ChangeModelMoveDirAndUp()` - Apply rotation to host player +- `GetNavigateCtrl()` - Access navigation control + +### CECBezier / CECBezierSeg / CECBezierPoint + +**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs` + +**Bezier Curve Structure**: +- `CECBezier`: Container for multiple segments +- `CECBezierSeg`: Single cubic Bezier segment +- `CECBezierPoint`: Control point with position and direction + +**Bezier Calculation**: +```csharp +// Cubic Bezier: B(u) = (1-u)³P₀ + 3(1-u)²uP₁ + 3(1-u)u²P₂ + u³P₃ +public A3DVECTOR3 Bezier(float u, bool bForward) +{ + A3DVECTOR3 pt1, pt2, control1, control2; + + if (bForward) + { + pt1 = m_pListPoint[m_nHeadPoint].GetPos(); + control1 = m_vAnchorHead; + control2 = m_vAnchorTail; + pt2 = m_pListPoint[m_nTailPoint].GetPos(); + } + else + { + // Reverse direction + pt1 = m_pListPoint[m_nTailPoint].GetPos(); + control1 = m_vAnchorTail; + control2 = m_vAnchorHead; + pt2 = m_pListPoint[m_nHeadPoint].GetPos(); + } + + // Calculate cubic Bezier + float u2 = u * u; + float u3 = u2 * u; + float oneMinusU = 1.0f - u; + float oneMinusU2 = oneMinusU * oneMinusU; + float oneMinusU3 = oneMinusU2 * oneMinusU; + + A3DVECTOR3 pos; + pos.x = oneMinusU3 * pt1.x + 3.0f * oneMinusU2 * u * control1.x + + 3.0f * oneMinusU * u2 * control2.x + u3 * pt2.x; + // ... (y, z similar) + + return pos; +} +``` + +### CECUIHelper + +**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs` + +**Responsibilities**: +- Entry point for navigation +- Coordinate resolution +- Navigation mode decision + +**Key Methods**: +- `FollowCoord()` - Main entry point +- `GetTaskObjectCoordinates()` - Multi-source coordinate lookup + +--- + +## Troubleshooting + +### Player Not Moving + +**Check**: +1. Is coordinate resolution working? Check logs for `GetTaskObjectCoordinates` results +2. Is Force Navigate config loaded? Check `force_navigate.txt` parsing +3. Is Bezier curve found? Check `GetBezierObjectByGlobalID` returns non-null +4. Is work system running? Check `CECHPWorkNavigate.Tick()` is being called + +### Bezier Curve Not Found + +**Check**: +1. Is `navigate.dat` loaded? Check `CECBezierNavigateLoader.s_loaded` +2. Does `bezierID` match `GlobalID` in `navigate.dat`? +3. Is `bezierID` negative? (Force navigate routes use negative IDs) + +### Coordinates Not Found + +**Check**: +1. Is entity spawned? Check live object lookup +2. Is template ID correct? Check `FindNPCByTemplateID` +3. Is entry in `task_npc.data`? Check `TryGetTaskNPCInfo` +4. Is entry in `coord_data.txt`? Check `TryGetFirstObjectCoord` +5. Are task regions available? Check fallback region lookup + +### Link Click Not Detected + +**Check**: +1. Is `raycastTarget` enabled on `TMP_Text`? +2. Is camera correct? `ScreenSpaceOverlay` needs `camera = null` +3. Are links formatted correctly? Must be `...` + +### Time Calculation Issues + +**Check**: +1. Is `GetRealTickTime()` returning delta milliseconds? (not absolute seconds) +2. Is time accumulating correctly? Check `m_iTimeCnt` in `CECBezierWalker` +3. Are segment times calculated correctly? `segmentLength * (1000.0f / speed)` + +--- + +## Summary + +The navigation system provides two movement modes: + +1. **Force Navigate**: Bezier-based cinematic movement for scripted sequences + - Configured in `force_navigate.txt` + - Uses Bezier curves from `navigate.dat` + - Time-based progression along curves + +2. **Normal Auto-Move**: Pathfinding-based movement for general navigation + - Uses AutoPF pathfinding + - Reads pathfinding maps from `StreamingAssets/maps/{mapName}/movemap/` + - Can switch to trace mode near NPCs + +**Coordinate Resolution** uses multiple fallback sources: +1. Live runtime objects +2. Spawned NPCs by template ID +3. `task_npc.data` table +4. `coord_data.txt` lookup +5. Task region centers + +**Map Loading**: +- Maps loaded from `StreamingAssets/maps/{mapName}/` +- Pathfinding maps: `movemap/r{row}_{row}-c{col}_{col}-l0.*` +- Bezier curves: `navigate.dat` (currently global, originally per-map) + +The system is designed to match the original C++ behavior exactly, with strict error handling (missing config = give up task) and time-based Bezier progression. diff --git a/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md.meta b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md.meta new file mode 100644 index 0000000000..6304c64005 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2d08d7d0debb5e44e8bd42943353c125 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: