Files
test/Documentation/AutoMoveImp/NAVIGATION_SYSTEM_GUIDE.md
2026-01-12 10:08:58 +07:00

1107 lines
32 KiB
Markdown

# 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 `<link="coord_{entityID}">{name}</link>` 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<string> m_routeFiles; // e.g., ["r1_1-c1_2-l0"]
public string GetPath() { return m_strPath; }
public List<string> 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<string> 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<TextAsset>(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<int, CECBezier>` 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<OBJECT_COORD> {
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 `<link="coord_{id}">...</link>`
### 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.