From 5bfe81facd43e234f1278b33906e09f1b46504d4 Mon Sep 17 00:00:00 2001
From: HungDK <>
Date: Sat, 18 Apr 2026 13:51:02 +0700
Subject: [PATCH] Fix unstable router, water and air route
---
.../Scripts/Managers/EC_HPWorkMove.cs | 294 ++++++++++++++----
1 file changed, 230 insertions(+), 64 deletions(-)
diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs
index 20f29cd05c..a090a4b0e1 100644
--- a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs
+++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkMove.cs
@@ -70,6 +70,15 @@ namespace BrewMonster.Scripts
protected bool m_bResetAutoPF;
+ ///
+ /// RMap marks deep water as blocked; AutoPF snaps goals to nearest land. If the real mission point
+ /// (m_vMoveDest) is still farther horizontally (e.g. in water), continue with DEST_2D after AutoPF ends.
+ ///
+ const float AutoPF_WorldDestContinueDistH = 4.0f;
+
+ /// Horizontal radius to treat DEST_2D as arrived (swim/fly overshoot logic often never fires).
+ const float Dest2D_ArrivalDistH = 2.5f;
+
public CECHPWorkMove(CECHPWorkMan pWorkMan) : base(Host_work_ID.WORK_MOVETOPOS, pWorkMan)
{
m_dwMask = Work_mask.MASK_MOVETOPOS;
@@ -111,15 +120,19 @@ namespace BrewMonster.Scripts
}
else if (iDestType == Types.DEST_2D || iDestType == Types.DEST_3D)
{
- m_vCurDir = vMoveDest - new A3DVECTOR3(m_pHost.transform.position.x, m_pHost.transform.position.y, m_pHost.transform.position.z);
+ m_vCurDir = vMoveDest - m_pHost.GetPos();
m_vCurDir.y = 0.0f;
- m_vCurDir.Normalize();
+ if (m_vCurDir.Normalize() > 1e-4f)
+ OrientHostHorizontal(m_vCurDir);
}
else if (IsAutoPF())
{
- m_vCurDir = CECIntelligentRoute.Instance().GetCurDest() - m_pHost.GetPos();
+ // Search() has not run yet — GetCurDest() is wrong here; use goal direction until first waypoint.
+ // Search() 尚未执行时 GetCurDest() 不可靠;先用目标方向,寻路成功后再对准第一个节点。
+ m_vCurDir = vMoveDest - m_pHost.GetPos();
m_vCurDir.y = 0.0f;
- m_vCurDir.Normalize();
+ if (m_vCurDir.Normalize() > 1e-4f)
+ OrientHostHorizontal(m_vCurDir);
if (m_bUseAutoMoveDialog)
{
// �˴����� m_bUseAutoMoveDialog���� SetUseAutoMoveDialog ��˵��
@@ -132,6 +145,13 @@ namespace BrewMonster.Scripts
}
}
+ // Swim/fly velocity carry-over skews the first AirWaterMove/GroundMove frame on new click.
+ if (iDestType == Types.DEST_2D || iDestType == Types.DEST_3D || iDestType == Types.DEST_AUTOPF)
+ {
+ m_pHost.m_vVelocity.x = 0f;
+ m_pHost.m_vVelocity.z = 0f;
+ }
+
// TO DO: fix later
//if (m_pHost.m_pMoveTargetGFX)
//{
@@ -161,18 +181,12 @@ namespace BrewMonster.Scripts
{
if (global::BrewMonster.Scripts.CECIntelligentRoute.Instance().IsIdle())
{
- // C++: AutoPF not ready yet, wait next tick.
- // C++:自动寻路未就绪,等待下一帧。
- return true;
- }
- if (m_pHost.IsFlying())
- {
- // C++: if flying, reset and switch back to DEST_2D.
- // C++:飞行状态下重置并切回 DEST_2D。
- global::BrewMonster.Scripts.CECIntelligentRoute.Instance().ResetSearch();
- m_bSwitchTo2D = true;
+ // C++: AutoPF not ready yet, wait next tick for SwitchTo2D (EC_HPWorkMove.cpp).
+ // C++:智能寻路模式未成功时,等待下个 Tick 切换到 DEST_2D 模式。
return true;
}
+ // Unity extension: do not clear AutoPF when flying — Tick_FlySwim follows waypoints in air/water.
+ // 原版 C++ 此处会 Reset+DEST_2D;空中/水中目标需要保留路径并在 Tick_FlySwim 沿路径移动。
}
#endif
@@ -411,12 +425,24 @@ namespace BrewMonster.Scripts
public void SetUseAutoMoveDialog(bool bUseAutoMoveDialog)
{
-
+ m_bUseAutoMoveDialog = bUseAutoMoveDialog;
}
+
public bool GetUseAutoMoveDialog() { return m_bUseAutoMoveDialog; }
+
+ /// Matches C++ CECHPWorkMove::GetAutoMove — must not always return true or input stays "auto" forever.
public bool GetAutoMove()
{
- return true;
+ if (m_bUseAutoMoveDialog)
+ return true;
+#if ENABLE_CEC_INTELLIGENT_ROUTE
+ if (IsAutoPF())
+ {
+ var r = global::BrewMonster.Scripts.CECIntelligentRoute.Instance();
+ return r.IsUsageMove() && !r.IsIdle();
+ }
+#endif
+ return false;
}
void SetAutoLand(bool bAutoLand) { m_bAutoLand = bAutoLand; }
@@ -475,6 +501,73 @@ namespace BrewMonster.Scripts
SetTaskNPCInfo(tid, taskid);
SetUseAutoMoveDialog(true);
}
+
+#if ENABLE_CEC_INTELLIGENT_ROUTE
+ /// True if switched to direct 2D leg — caller must not Finish() this frame.
+ bool TryContinueAutoPFToWorldDestDirect()
+ {
+ A3DVECTOR3 p = m_pHost.GetPos();
+ float dx = m_vMoveDest.x - p.x;
+ float dz = m_vMoveDest.z - p.z;
+ if (dx * dx + dz * dz <= AutoPF_WorldDestContinueDistH * AutoPF_WorldDestContinueDistH)
+ return false;
+
+ int tid = m_iNPCTempleId;
+ int taskid = m_iTaskId;
+ global::BrewMonster.Scripts.CECIntelligentRoute.Instance().ResetSearch();
+ SetDestination(DestTypes.DEST_2D, m_vMoveDest);
+ SetTaskNPCInfo(tid, taskid);
+ SetUseAutoMoveDialog(true);
+ return true;
+ }
+#endif
+
+ ///
+ /// Swim/fly DEST_2D often never satisfies the overshoot branch; ground can miss when vTPNormal is zero.
+ /// Finishes the move work so GetAutoMove() goes false and the player can steer again.
+ ///
+ bool TryFinishDest2DIfArrived(A3DVECTOR3 vCurPos, float stopSpeed, int iMoveMode, bool flySwimAddRun)
+ {
+ if (m_iDestType != DestTypes.DEST_2D)
+ return false;
+ float dx = m_vMoveDest.x - vCurPos.x;
+ float dz = m_vMoveDest.z - vCurPos.z;
+ if (dx * dx + dz * dz > Dest2D_ArrivalDistH * Dest2D_ArrivalDistH)
+ return false;
+ Finish();
+ int mode = flySwimAddRun ? (iMoveMode | (int)GPMoveMode.GP_MOVE_RUN) : iMoveMode;
+ m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), stopSpeed, mode);
+ return true;
+ }
+
+ void OrientHostHorizontal(A3DVECTOR3 dirHorizontal)
+ {
+ if (dirHorizontal.IsZero())
+ return;
+ Vector3 d = EC_Utility.ToVector3(dirHorizontal);
+ d.y = 0f;
+ if (d.sqrMagnitude < 1e-8f)
+ return;
+ d.Normalize();
+ m_pHost.SetDirAndUp(d, Vector3.up);
+ }
+
+#if ENABLE_CEC_INTELLIGENT_ROUTE
+ /// After Search(), face the first path node so swim/air/walk doesn't use previous heading.
+ void OrientHostToFirstAutoPFWaypoint()
+ {
+ var route = global::BrewMonster.Scripts.CECIntelligentRoute.Instance();
+ if (!route.IsMoveOn())
+ return;
+ A3DVECTOR3 d = route.GetCurDest() - m_pHost.GetPos();
+ d.y = 0f;
+ if (d.Normalize() < 1e-4f)
+ return;
+ m_vCurDir = d;
+ OrientHostHorizontal(d);
+ }
+#endif
+
// On first tick
protected override void OnFirstTick()
{
@@ -578,6 +671,9 @@ namespace BrewMonster.Scripts
m_iDestType = DestTypes.DEST_PUSH;
if (m_iDestType == DestTypes.DEST_2D)
{
+ if (TryFinishDest2DIfArrived(vCurPos, fSpeed, iMoveMode, false))
+ return true;
+
float fDist;
if (m_pHost.m_GndInfo.bOnGround)
{
@@ -786,18 +882,15 @@ namespace BrewMonster.Scripts
global::BrewMonster.Scripts.CECIntelligentRoute.Instance().OnPlayerPosChange(vCurPos);
if (global::BrewMonster.Scripts.CECIntelligentRoute.Instance().IsPathFinished())
{
+ if (TryContinueAutoPFToWorldDestDirect())
+ return true;
Finish();
m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), fSpeed, iMoveMode);
}
else
{
- // NOTE: Use Vector3 overload to avoid signature mismatch across ports.
- // 注意:使用 Vector3 重载以避免移植过程中签名不匹配。
- m_pHost.m_MoveCtrl.SendMoveCmd(
- EC_Utility.ToVector3(vCurPos),
- EC_Utility.ToVector3(cdr.vAbsVelocity),
- iMoveMode,
- false);
+ // C++: SendMoveCmd(vCurPos, 1, vCurDest, cdr.vAbsVelocity, iMoveMode)
+ m_pHost.m_MoveCtrl.SendMoveCmd(vCurPos, 1, vCurDest, cdr.vAbsVelocity, iMoveMode);
}
}
}
@@ -938,6 +1031,9 @@ namespace BrewMonster.Scripts
}
else if (m_iDestType == DestTypes.DEST_2D)
{
+ if (TryFinishDest2DIfArrived(vCurPos, fMaxSpeed, iMoveMode, true))
+ return true;
+
Vector3 vPushDir = Vector3.zero;
m_pHost.GetPushDir(ref vPushDir, (uint)CECHostPlayer.MOVE_DIR.MD_ALL, 0f);
vPushDir.x = vPushDir.z = 0.0f;
@@ -1227,6 +1323,110 @@ namespace BrewMonster.Scripts
}
else if (IsAutoPF())
{
+#if ENABLE_CEC_INTELLIGENT_ROUTE
+ var route = global::BrewMonster.Scripts.CECIntelligentRoute.Instance();
+ // Unity: ground AutoPF is in Tick_Walk; swim/fly use Tick_FlySwim. C++ resets here and forces DEST_2D,
+ // which breaks routes when the path or target is in water/air — follow path nodes like DEST_2D swim/fly.
+ bool envAirOrWater = m_pHost.m_iMoveEnv == (int)MoveEnvironment.MOVEENV_WATER
+ || m_pHost.m_iMoveEnv == (int)MoveEnvironment.MOVEENV_AIR;
+ if (envAirOrWater && route.IsUsageMove())
+ {
+ if (route.IsPathFinished())
+ {
+ if (TryContinueAutoPFToWorldDestDirect())
+ return true;
+ Finish();
+ m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), fMaxSpeed,
+ iMoveMode | (int)GPMoveMode.GP_MOVE_RUN);
+ return true;
+ }
+
+ if (!route.IsMoveOn())
+ {
+ route.ResetSearch();
+ m_bSwitchTo2D = true;
+ return true;
+ }
+
+ Vector3 vPushDir = Vector3.zero;
+ m_pHost.GetPushDir(ref vPushDir, (uint)CECHostPlayer.MOVE_DIR.MD_ALL, 0f);
+ vPushDir.x = vPushDir.z = 0.0f;
+
+ float fSpeed1H = m_pHost.m_vVelocity.MagnitudeH();
+ float fSpeed1V = m_pHost.m_vVelocity.y;
+
+ A3DVECTOR3 vCurDest = route.GetCurDest();
+ A3DVECTOR3 vMoveDirH = vCurDest - vCurPos;
+ vMoveDirH.y = 0.0f;
+ float fDistH = vMoveDirH.Normalize();
+
+ if (fDistH < 1e-4f)
+ {
+ route.OnPlayerPosChange(vCurPos);
+ if (route.IsPathFinished())
+ {
+ if (TryContinueAutoPFToWorldDestDirect())
+ return true;
+ Finish();
+ m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), fMaxSpeed,
+ iMoveMode | (int)GPMoveMode.GP_MOVE_RUN);
+ }
+
+ return true;
+ }
+
+ float pa = 0.0f;
+ float s = -0.5f * fSpeed1H * fSpeed1H / na;
+ if (fDistH > s - 0.01f)
+ pa = CECHostMove.EC_PUSH_ACCE;
+
+ float fSpeed2H = fSpeed1H + (pa + na) * fDeltaTime;
+ if (Math.Abs(pa - 0f) < float.Epsilon && fSpeed2H < 0.0f)
+ fSpeed2H = 0.0f;
+ else if (fSpeed2H > fMaxSpeed)
+ fSpeed2H = fMaxSpeed;
+
+ Glide(fDistH / Mathf.Max(fMaxSpeed, 0.01f), vMoveDirH, fDeltaTime, bInAir);
+
+ vMoveDirH = m_pHost.GetModelMoveDir();
+ vMoveDirH.y = 0;
+ vMoveDirH.Normalize();
+
+ float fSpeed2V = CalcFlySwimVertSpeed(fSpeed1V, vPushDir.y, CECHostMove.EC_PUSH_ACCE, fDeltaTime);
+ A3DVECTOR3 vVel2 = vMoveDirH * fSpeed2H + GPDataTypeHelper.g_vAxisY * fSpeed2V;
+
+ vCurPos = m_pHost.m_MoveCtrl.AirWaterMove(vVel2, fDeltaTime, bInAir);
+
+ if (m_pHost.m_MoveCtrl.MoveBlocked() >= 3)
+ {
+ vVel2.Clear();
+ Finish();
+ m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), fMaxSpeed,
+ iMoveMode | (int)GPMoveMode.GP_MOVE_RUN);
+ }
+ else
+ {
+ m_pHost.SetPos(EC_Utility.ToVector3(vCurPos));
+ m_pHost.m_vVelocity = vVel2;
+ route.OnPlayerPosChange(vCurPos);
+ if (route.IsPathFinished())
+ {
+ if (TryContinueAutoPFToWorldDestDirect())
+ return true;
+ Finish();
+ m_pHost.m_MoveCtrl.SendStopMoveCmd(EC_Utility.ToVector3(vCurPos), vVel2.Magnitude(),
+ iMoveMode | (int)GPMoveMode.GP_MOVE_RUN);
+ }
+ else
+ {
+ m_pHost.m_MoveCtrl.SendMoveCmd(vCurPos, 0, m_vMoveDest, vVel2,
+ iMoveMode | (int)GPMoveMode.GP_MOVE_RUN);
+ }
+ }
+
+ return true;
+ }
+#endif
CECIntelligentRoute.Instance().ResetSearch();
m_bSwitchTo2D = true;
}
@@ -1308,22 +1508,8 @@ namespace BrewMonster.Scripts
while (true)
{
- if (m_pHost.IsFlying())
- {
- if (CECUIManager.Instance != null)
- {
- // string message = $"Please deactive the fly mode to start the path finding.";
-
- // CECUIManager.Instance.ShowMessageBox(
- // "Fly Mode", // 飞行模式
- // message, // 消息
- // BrewMonster.MessageBoxType.YesButton
- // );
- // Finish();
- //return;
- }
- break;
- }
+ // C++ skips Search while flying (forces DEST_2D). Unity: allow Search so aerial targets get a 2D path.
+ // 原版飞行时不做 Search;此处允许寻路,使空中起点/空中目标也能得到 XZ 路径。
// Brush test is not fully ported yet; pass null for now (static movemap only).
// BrushTest 暂未完整移植;当前先传 null(仅静态 movemap)。
@@ -1334,34 +1520,13 @@ namespace BrewMonster.Scripts
if (ret == CECIntelligentRoute.SearchResult.enumSearchNoPath)
{
- // Calculate map coordinates from world coordinates
- // 从世界坐标计算地图坐标
- // Map coord formula: (world / 10) + offset (X: +400, Z: +550)
- // 地图坐标公式:(世界坐标 / 10) + 偏移量 (X: +400, Z: +550)
+ // C++ UpdateResetUseAutoPF: Search != success → break; bSwitchTo2D stays true (fallback straight line).
+ // Do not Finish() — same as EC_HPWorkMove.cpp (no path still switches to DEST_2D).
int mapX = Mathf.RoundToInt(m_vMoveDest.x / 10.0f) + 400;
int mapY = Mathf.RoundToInt(m_vMoveDest.y / 10.0f);
int mapZ = Mathf.RoundToInt(m_vMoveDest.z / 10.0f) + 550;
-
- // Show popup notification to player that path cannot be found
- // 显示弹窗通知玩家无法找到路径
- if (CECUIManager.Instance != null)
- {
- // string message = $"Cannot find path to target position.\nPlease move manually to target location.\n\nMap Coordinates: ({mapX}, {mapZ}, ↑{mapY})";
- // string messageCN = $"无法找到到目标位置的路径。\n请手动移动到目标位置。\n\n地图坐标: ({mapX}, {mapZ}, ↑{mapY})";
- //
- // // CECUIManager.Instance.ShowMessageBox(
- // // "Path Not Found", // 路径未找到
- // // message, // English message with map coordinates
- // // BrewMonster.MessageBoxType.YesButton
- // // );
- // CECUIManager.Instance.ShowMessageBoxYes("Path Not Found", message, null, null);
- }
- else
- {
- Debug.LogWarning($"[CECIntelligentRoute] Cannot find path to target position. Map Coordinates: ({mapX}, {mapZ}, ↑{mapY}). Please move manually.");
- }
-
- Finish();
+ Debug.LogWarning(
+ $"[CECIntelligentRoute] No path; switching to DEST_2D. Map coords: ({mapX}, {mapZ}, ↑{mapY}).");
break;
}
else if (ret != CECIntelligentRoute.SearchResult.enumSearchSuccess)
@@ -1369,6 +1534,7 @@ namespace BrewMonster.Scripts
break;
}
bSwitchTo2D = false;
+ OrientHostToFirstAutoPFWaypoint();
break;
}