Files
test/Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs
T
2026-04-13 12:31:49 +07:00

545 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Text;
using BrewMonster.Network;
using BrewMonster.Managers;
using ModelRenderer.Scripts.Common;
using ModelRenderer.Scripts.GameData;
using BrewMonster.Scripts.Task;
using BrewMonster.Scripts;
using BrewMonster.Scripts.Chat;
using CSNetwork.GPDataType;
using CSNetwork;
namespace BrewMonster.Scripts.UI
{
public class CECUIHelper
{
public static string DlgTaskName = "Win_Quest";
public static A3DVECTOR3 GetTaskObjectCoordinates(int id, ref bool in_table)
{
in_table = false;
// 0) If the id is actually a runtime object id (npc/player/matter), fetch it from world directly.
// IMPORTANT: GPDataTypeHelper.ISPLAYERID returns true for ANY positive int, so we must not use it to
// decide whether something is an "object id". Use a heuristic instead.
// 0) 如果这个id其实是运行时对象ID(NPC/玩家/物品),直接从World取坐标。
// 重要:ISPLAYERID 对任何正整数都为 true,不能用它判断“是否对象ID”,这里用启发式判断。
bool isLikelyRuntimeObjectId =
GPDataTypeHelper.ISNPCID(id) ||
GPDataTypeHelper.ISMATTERID(id) ||
(id > 100000000); // player ids are typically huge; template ids are usually small
A3DVECTOR3 ret = new A3DVECTOR3(0);
var world = EC_Game.GetGameRun()?.GetWorld();
if (world != null && isLikelyRuntimeObjectId)
{
var obj = world.GetObject(id, 0);
if (obj != null)
{
var objPos = obj.transform.position;
in_table = true;
BMLogger.Log(
$"[CECUIHelper] GetTaskObjectCoordinates isLikelyRuntimeObjectId: objPos={objPos.x},{objPos.y},{objPos.z}, inTable={in_table}");
ret.Set(objPos.x, objPos.y, objPos.z);
return ret;
}
}
List<OBJECT_COORD> TargetTemp = new List<OBJECT_COORD>();
ret = EC_Game.GetGameRun()?.GetHostPlayer().GetObjectCoordinates(id, out TargetTemp, ref in_table) ??
new A3DVECTOR3(0);
// 1) Try live NPC/Monster in scene by template id (best match to "click name -> go to that entity")
// 1) 先尝试在场景中按模板ID查找活体NPC/怪物(最符合“点名字就去找它”)
//This only work in Major map not A61. Skip for now.
// if(!in_table)
// {
// var npcMan = EC_ManMessageMono.Instance != null ? EC_ManMessageMono.Instance.CECNPCMan : null;
// if (npcMan != null)
// {
// var npc = npcMan.FindNPCByTemplateID(id);
// if (npc != null)
// {
// UnityEngine.Vector3 npcPos = npc.transform.position;
// in_table = true;
// BMLogger.Log($"[CECUIHelper] GetTaskObjectCoordinates npcMan: npcPos={npcPos.x},{npcPos.y},{npcPos.z}, inTable={in_table}");
// ret.Set(npcPos.x, npcPos.y, npcPos.z);
// }
// }
// }
// else
// {
// BMLogger.Log($"[CECUIHelper] GetTaskObjectCoordinates in_table: ret={ret.x},{ret.y},{ret.z}, inTable={in_table}");
// }
if (ret.x != 0 && ret.y != 0 && ret.z != 0)
{
BMLogger.Log(
$"[CECUIHelper] GetHostPlayer GetObjectCoordinates True ret={ret.x},{ret.y},{ret.z}, inTable={in_table}");
return ret;
}
else
{
BMLogger.Log(
$"[CECUIHelper] GetHostPlayer TryGetFirstObjectCoord False ret={ret.x},{ret.y},{ret.z}, inTable={in_table}");
}
// Fallback to task_npc table (C++: ATaskTemplMan::GetTaskNPCInfo)
// 回退到task_npc表(C++ATaskTemplMan::GetTaskNPCInfo
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
if (pMan != null && pMan.TryGetTaskNPCInfo((uint)id, out NPC_INFO info))
{
// NOTE: Keep original PW coordinate mapping: ret.Set(x, z, y)
// 注意:保持原版坐标映射:ret.Set(x, z, y)
in_table = true;
BMLogger.Log(
$"[CECUIHelper] GetTaskObjectCoordinates TryGetTaskNPCInfo: info.x={info.x}, info.z={info.z}, info.y={info.y}, inTable={in_table}");
return new A3DVECTOR3(info.x, info.z, info.y);
}
else
{
BMLogger.Log($"[CECUIHelper] GetTaskObjectCoordinates TryGetTaskNPCInfo: not found for id={id}");
}
// Fallback to coord_data.txt (C++: Configs/Coord_data.txt via CECGame::GetObjectCoord)
// 回退到 coord_data.txtC++Configs/Coord_data.txt,通过 CECGame::GetObjectCoord
if (global::BrewMonster.Network.EC_Game.TryGetFirstObjectCoord(id.ToString(), out var coordPos, out var mapName))
{
in_table = true;
BMLogger.Log(
$"[CECUIHelper] GetTaskObjectCoordinates TryGetFirstObjectCoord: coordPos.x={coordPos.x}, coordPos.y={coordPos.y}, coordPos.z={coordPos.z}, inTable={in_table}");
return new A3DVECTOR3(coordPos.x, coordPos.y, coordPos.z);
}
UnityEngine.Debug.LogWarning(
$"[CECUIHelper] GetTaskObjectCoordinates: Not found for id={id} (isLikelyRuntimeObjectId={isLikelyRuntimeObjectId}).");
BMLogger.Log(
$"[CECUIHelper] GetTaskObjectCoordinates default return ret={ret.x},{ret.y},{ret.z}, inTable={in_table}");
return ret;
// TODO: Implement this method properly
// A3DVECTOR3 ret(0.f);
// in_table = false;
// CECGame::ObjectCoords TargetTemp;
// ret = g_pGame->GetGameRun()->GetHostPlayer()->GetObjectCoordinates(
// id, TargetTemp, in_table);
// if (!in_table && MAJOR_MAP == g_pGame->GetGameRun()->GetWorld()->GetInstanceID()) {
// ATaskTemplMan *pMan = g_pGame->GetTaskTemplateMan();
// const NPC_INFO* pInfo = pMan->GetTaskNPCInfo(id);
// if(pInfo) {
// ret.Set(pInfo->x, pInfo->z, pInfo->y);
// in_table = true;
// }
// }
// return ret;
}
// Follow coord like C++ CECUIHelper::FollowCoord(enumEICoord, taskId)
// 像C++的CECUIHelper::FollowCoord(enumEICoord, taskId)一样跟随坐标
public static bool FollowCoord(int id, int taskId)
{
// If this task is a force-navigate task (or has force_navigate mapping), use bezier-force navigate
// instead of normal auto-move (which looks like a straight line).
// 如果该任务是强制导航任务(或在 force_navigate 中有映射),则使用贝塞尔强制导航,
// 而不是普通自动移动(看起来像直线)。
if (taskId > 0)
{
CECHostPlayer hostPlayer = EC_Game.GetGameRun()?.GetHostPlayer();
if (hostPlayer != null)
{
// Force-navigate dispatch is currently disabled here (see commented-out block below).
// 强制导航的分发目前在这里被禁用(见下方注释块)。
// 2) Fallback: if force_navigate.txt contains this task, treat it as force-navigate even if template flag is missing.
// 2) 回退:如果 force_navigate.txt 中存在该任务映射,即使模板标记缺失,也按强制导航处理。
// if (!shouldForceNavigate)
// {
// var np = hostPlayer.GetNavigatePlayer();
// var ctrl = np != null ? np.GetNavigateCtrl() : null;
// if (ctrl != null)
// {
// var tmp = new BrewMonster.Scripts.CECNavigateCtrl.INFO();
// if (ctrl.GetNavigateInfo(taskId, ref tmp))
// {
// shouldForceNavigate = true;
// }
// }
// }
// if (shouldForceNavigate)
// {
// UnityEngine.Debug.Log(
// $"[CECUIHelper] FollowCoord: taskId={taskId} => force navigate (bezier) instead of normal auto-move");
// hostPlayer.OnNaviageEvent(taskId,
// (int)BrewMonster.Scripts.CECNavigateCtrl.NavigateEvent.EM_PREPARE);
// hostPlayer.OnNaviageEvent(taskId,
// (int)BrewMonster.Scripts.CECNavigateCtrl.NavigateEvent.EM_BEGIN);
// return true;
// }
}
}
// Resolve coordinates
bool inTable = false;
A3DVECTOR3 vPos = GetTaskObjectCoordinates(id, ref inTable);
BMLogger.Log($"[CECUIHelper] FollowCoord: vPos={vPos.x},{vPos.y},{vPos.z}, inTable={inTable}");
if (!inTable)
{
// Fallback: use task regions if available (move to the center of the first region)
// 回退:如果任务有区域信息,则移动到第一个区域的中心
if (taskId > 0)
{
var taskMan = EC_Game.GetTaskTemplateMan();
var templ = taskMan != null ? taskMan.GetTaskTemplByID((uint)taskId) : null;
var world = EC_Game.GetGameRun()?.GetWorld();
int curWorldId = world != null ? world.GetInstanceID() : 0;
if (templ != null)
{
// Helper local function: pick first region center if in current world
bool TryUseRegion(uint worldId, uint cnt, BrewMonster.Scripts.Task.Task_Region[] regions,
string tag, out A3DVECTOR3 pos)
{
pos = new A3DVECTOR3(0);
if (cnt == 0 || regions == null || regions.Length == 0) return false;
if (curWorldId != 0 && worldId != 0 && worldId != (uint)curWorldId) return false;
var r = regions[0];
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;
pos = new A3DVECTOR3(cx, cy, cz);
return true;
}
// 1) Deliver zone (often where quest giver is)
if (templ.m_FixedData.m_bDelvInZone &&
TryUseRegion(templ.m_FixedData.m_ulDelvWorld, templ.m_FixedData.m_ulDelvRegionCnt,
templ.m_FixedData.m_pDelvRegion, "DelvInZone", out vPos))
{
inTable = true;
}
// 2) Reach-site regions (for reach-site tasks / also can be used as guidance)
else if (TryUseRegion(templ.m_FixedData.m_ulReachSiteId, templ.m_FixedData.m_ulReachSiteCnt,
templ.m_FixedData.m_pReachSite, "ReachSite", out vPos))
{
inTable = true;
}
// 3) Enter region / leave region zones (some tasks use these to define where objectives happen)
else if (TryUseRegion(templ.m_FixedData.m_ulEnterRegionWorld,
templ.m_FixedData.m_ulEnterRegionCnt, templ.m_FixedData.m_pEnterRegion,
"EnterRegion", out vPos))
{
inTable = true;
}
else if (TryUseRegion(templ.m_FixedData.m_ulLeaveRegionWorld,
templ.m_FixedData.m_ulLeaveRegionCnt, templ.m_FixedData.m_pLeaveRegion,
"LeaveRegion", out vPos))
{
inTable = true;
}
}
}
if (!inTable)
{
UnityEngine.Debug.LogWarning(
$"[CECUIHelper] FollowCoord: No coordinates for id={id}, taskId={taskId} (will not move)");
return false;
}
}
// Start auto-move work to destination (this is what actually moves the player in this project)
// 启动自动移动工作(这才是本项目中真正驱动角色移动的系统)
CECHostPlayer host = EC_Game.GetGameRun()?.GetHostPlayer();
if (host == null)
{
UnityEngine.Debug.LogError($"[CECUIHelper] FollowCoord: Host player is null");
return false;
}
CECHPWorkMan wm = host.GetWorkMan();
if (wm == null)
{
UnityEngine.Debug.LogError($"[CECUIHelper] FollowCoord: WorkMan is null");
return false;
}
CECHPWorkMove work = wm.CreateWork(CECHPWork.Host_work_ID.WORK_MOVETOPOS) as CECHPWorkMove;
if (work == null)
{
UnityEngine.Debug.LogError($"[CECUIHelper] FollowCoord: Failed to create WORK_MOVETOPOS");
return false;
}
// Prefer AutoPF intelligent route (original PW behavior) instead of naive straight-line.
// 优先使用 AutoPF 智能寻路(原版 PW 行为),而不是简单直线移动。
work.SetDestination(CECHPWorkMove.DestTypes.DEST_AUTOPF, vPos);
// If this coord link is to an NPC template id, store it so WorkMove can switch to WorkTrace near the NPC.
// 如果该坐标链接指向 NPC 模板ID,保存下来,以便 WorkMove 接近 NPC 时切换到 WorkTrace。
if (taskId > 0)
{
work.SetTaskNPCInfo(id, taskId);
}
wm.StartWork_p2(work);
return true;
}
// Follow coord using target coordinate list and trace name
// 使用目标坐标列表和追踪名称跟随坐标
public static bool FollowCoord(List<OBJECT_COORD> m_TargetCoord, string m_strTraceName)
{
// Validate inputs
// 验证输入
if (m_TargetCoord == null || m_TargetCoord.Count == 0)
{
UnityEngine.Debug.LogWarning(
$"[CECUIHelper] FollowCoord: m_TargetCoord is null or empty, traceName={m_strTraceName} (will not move)");
return false;
}
// Use the first coordinate from the list
// 使用列表中的第一个坐标
OBJECT_COORD targetCoord = m_TargetCoord[0];
A3DVECTOR3 vPos = targetCoord.vPos;
// Log map information if available (for debugging)
// 如果可用,记录地图信息(用于调试)
if (!string.IsNullOrEmpty(targetCoord.strMap))
{
UnityEngine.Debug.Log(
$"[CECUIHelper] FollowCoord: Target map='{targetCoord.strMap}', traceName={m_strTraceName}");
}
// Start auto-move work to destination (this is what actually moves the player in this project)
// 启动自动移动工作(这才是本项目中真正驱动角色移动的系统)
CECHostPlayer host = EC_Game.GetGameRun()?.GetHostPlayer();
if (host == null)
{
UnityEngine.Debug.LogError(
$"[CECUIHelper] FollowCoord: Host player is null, traceName={m_strTraceName}");
return false;
}
CECHPWorkMan wm = host.GetWorkMan();
if (wm == null)
{
UnityEngine.Debug.LogError($"[CECUIHelper] FollowCoord: WorkMan is null, traceName={m_strTraceName}");
return false;
}
CECHPWorkMove work = wm.CreateWork(CECHPWork.Host_work_ID.WORK_MOVETOPOS) as CECHPWorkMove;
if (work == null)
{
UnityEngine.Debug.LogError(
$"[CECUIHelper] FollowCoord: Failed to create WORK_MOVETOPOS, traceName={m_strTraceName}");
return false;
}
// Prefer AutoPF intelligent route (original PW behavior) instead of naive straight-line.
// 优先使用 AutoPF 智能寻路(原版 PW 行为),而不是简单直线移动。
work.SetDestination(CECHPWorkMove.DestTypes.DEST_AUTOPF, vPos);
// If trace name is provided and there are multiple coordinates, we might want to trace through them
// 如果提供了追踪名称且有多个坐标,我们可能想要追踪它们
// Note: SetTaskNPCInfo might not be applicable here since we don't have taskId/id
// 注意:SetTaskNPCInfo 可能不适用于此,因为我们没有 taskId/id
wm.StartWork_p2(work);
UnityEngine.Debug.Log(
$"[CECUIHelper] FollowCoord: Started auto-move to ({vPos.x},{vPos.y},{vPos.z}) map={targetCoord.strMap}, traceName={m_strTraceName}, coordCount={m_TargetCoord.Count}");
return true;
}
public static void AutoMoveStartComplex(A3DVECTOR3 dst, int targetId = 0, int taskId = 0)
{
UnityEngine.Debug.Log(
$"[CECUIHelper] AutoMoveStartComplex: dst={dst}, targetId={targetId}, taskId={taskId}");
// TODO: Implement this method properly
// if( CECAutoPolicy.Instance.IsAutoPolicyEnabled() )
// return;
ECMSG msg = new ECMSG();
msg.iManager = 0;
msg.iSubID = 0;
msg.dwParam1 = (uint)dst.x;
msg.dwParam2 = (uint)dst.y;
msg.dwParam3 = (uint)dst.z;
msg.dwParam4 = new MsgDataAutoMove(0, targetId, taskId);
EC_ManMessage.PostMessage(0, 0, 0, msg);
}
/// <summary>
/// PW client: <c>CECUIHelper::FormatCoordText</c> — replaces <c>@essenceId@</c> (monster template) and
/// <c>$essenceId$</c> (NPC or mine template) with display names; when coordinates exist, wraps the name in a TMP
/// coord link like the main quest UI.
/// </summary>
public static string FormatCoordText(string szText)
{
if (string.IsNullOrEmpty(szText))
return string.Empty;
var sb = new StringBuilder(szText.Length + 48);
int len = szText.Length;
int i = 0;
elementdataman edm = global::BrewMonster.ElementDataManProvider.GetElementDataMan();
while (i < len)
{
int segStart = i;
while (i < len && szText[i] != '@' && szText[i] != '$')
i++;
sb.Append(szText, segStart, i - segStart);
if (i >= len)
break;
char open = szText[i];
i++;
int idStart = i;
while (i < len && szText[i] != '@' && szText[i] != '$')
i++;
if (i >= len)
{
sb.Append(open);
sb.Append(szText, idStart, i - idStart);
break;
}
char flag = szText[i];
string keyword = szText.Substring(idStart, i - idStart);
i++;
if (!uint.TryParse(keyword, out uint essenceId))
{
sb.Append(open);
sb.Append(keyword);
sb.Append(flag);
continue;
}
AppendEssencePlaceholder(sb, edm, essenceId, flag);
}
return sb.ToString();
}
static void AppendEssencePlaceholder(StringBuilder sb, elementdataman edm, uint essenceId, char flag)
{
string strName = null;
if (edm != null)
{
DATA_TYPE dt = default;
object p = edm.get_data_ptr(essenceId, ID_SPACE.ID_SPACE_ESSENCE, ref dt);
if (flag == '$')
{
if (dt == DATA_TYPE.DT_NPC_ESSENCE && p is NPC_ESSENCE npc)
strName = npc.Name;
else if (dt == DATA_TYPE.DT_MINE_ESSENCE && p is MINE_ESSENCE mine)
strName = ByteToStringUtils.UshortArrayToUnicodeString(mine.name);
}
else if (flag == '@')
{
if (dt == DATA_TYPE.DT_MONSTER_ESSENCE && p is MONSTER_ESSENCE mon)
strName = mon.Name;
}
}
if (string.IsNullOrEmpty(strName))
{
sb.Append("^00FF00?????^FFFFFF");
return;
}
bool inTable = false;
GetTaskObjectCoordinates((int)essenceId, ref inTable);
if (inTable)
sb.Append("<link=\"coord_").Append(essenceId).Append("\"><color=#00FF00>").Append(strName)
.Append("</color></link>");
else
sb.Append(strName);
}
public static string PolicySpecialCharReplace(
string szText,
CHAT_S2C.PolicyChatParameter pPolicyChatPara)
{
if (string.IsNullOrEmpty(szText))
return szText;
string result = szText;
//result = ReplaceNameInPolicyChat(result, pPolicyChatPara);
string subString;
string key;
string variable;
while (FindSpecialCharInPolicyChat(result, out subString, out key, out variable))
{
/*result = ReplaceSpecialCharInPolicyChat(
result,
subString,
key,
variable,
pPolicyChatPara);*/
}
return result;
}
public static bool FindSpecialCharInPolicyChat(
string srcPolicyChat,
out string subString,
out string keyInSubString,
out string variableInSubString)
{
subString = null;
keyInSubString = null;
variableInSubString = null;
if (string.IsNullOrEmpty(srcPolicyChat))
return false;
int posStart = srcPolicyChat.IndexOf("{");
if (posStart == -1)
return false;
int posEnd = srcPolicyChat.IndexOf("}", posStart);
if (posEnd == -1)
return false;
subString = srcPolicyChat.Substring(posStart, posEnd - posStart + 1);
// remove { }
string inner = subString.Substring(1, subString.Length - 2);
var parts = inner.Split(':');
if (parts.Length == 2)
{
keyInSubString = parts[0];
variableInSubString = parts[1];
return true;
}
return false;
}
public static void RemoveNameFlagFromNPCChat(string chat, out string conv)
{
if (string.IsNullOrEmpty(chat))
{
conv = string.Empty;
return;
}
conv = chat.Replace("&", "");
}
}
}