1107 lines
32 KiB
Markdown
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.
|