# 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.