Merge branch 'develop' of https://git.brew.monster/Unity/perfect-world-unity into feature/attach_mesh

This commit is contained in:
VuNgocHaiC7
2026-01-09 15:32:18 +07:00
10 changed files with 2095 additions and 97 deletions
@@ -4,6 +4,7 @@ using CSNetwork;
using ModelRenderer.Scripts.GameData;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEngine;
using UnityEngine.AddressableAssets;
@@ -114,6 +115,10 @@ namespace BrewMonster.Network
GetGameRun().Init();
InitializeStringTables();
// Load coord_data.txt (C++: Configs/Coord_data.txt) for clickable task links auto-move.
// 加载 coord_data.txtC++Configs/Coord_data.txt)用于任务可点击链接的自动移动。
LoadObjectCoord();
return true;
}
public static CECConfigs GetConfigs() { return m_pConfigs; }
@@ -262,6 +267,140 @@ namespace BrewMonster.Network
long unixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return (int)unixTime + m_iTimeError;
}
////////////////////////////////////////////////////////////////////////////////
//
// Coord_data.txt support (C++: Configs/Coord_data.txt, CECGame::LoadObjectCoord/GetObjectCoord)
//
////////////////////////////////////////////////////////////////////////////////
private struct OBJECT_COORD
{
public string strMap;
public Vector3 vPos;
}
private static readonly Dictionary<string, List<OBJECT_COORD>> m_CoordTab =
new Dictionary<string, List<OBJECT_COORD>>(StringComparer.OrdinalIgnoreCase);
private static bool m_bCoordLoaded = false;
public static bool LoadObjectCoord()
{
if (m_bCoordLoaded)
{
return true;
}
try
{
Addressables.InitializeAsync().WaitForCompletion();
var ta = Addressables.LoadAssetAsync<TextAsset>("Assets/Addressable/coord_data.txt").WaitForCompletion();
if (ta == null)
{
Debug.LogError("[EC_Game] LoadObjectCoord: failed to load Addressable 'Assets/Addressable/coord_data.txt'");
return false;
}
ParseCoordDataText(ta.text);
m_bCoordLoaded = true;
Debug.Log($"[EC_Game] LoadObjectCoord: loaded {m_CoordTab.Count} coord keys from coord_data.txt");
return true;
}
catch (Exception ex)
{
Debug.LogError($"[EC_Game] LoadObjectCoord exception: {ex}");
return false;
}
}
private static void ParseCoordDataText(string text)
{
m_CoordTab.Clear();
if (string.IsNullOrEmpty(text))
{
return;
}
using var sr = new StringReader(text);
string line;
int lineNo = 0;
while ((line = sr.ReadLine()) != null)
{
lineNo++;
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
line = line.Trim();
if (line.StartsWith("#", StringComparison.Ordinal) || line.StartsWith("//", StringComparison.Ordinal))
{
continue;
}
if (lineNo == 1 && line.StartsWith("ID", StringComparison.OrdinalIgnoreCase))
{
continue;
}
string[] parts = line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 5)
{
continue;
}
string key = parts[0];
string map = parts[1];
if (!float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x) ||
!float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out float y) ||
!float.TryParse(parts[4], NumberStyles.Float, CultureInfo.InvariantCulture, out float z))
{
continue;
}
var coord = new OBJECT_COORD
{
strMap = map,
vPos = new Vector3(x, y, z),
};
if (!m_CoordTab.TryGetValue(key, out var list))
{
list = new List<OBJECT_COORD>(1);
m_CoordTab[key] = list;
}
list.Add(coord);
}
}
public static bool TryGetFirstObjectCoord(string targetId, out Vector3 pos, out string map)
{
pos = default;
map = null;
if (string.IsNullOrWhiteSpace(targetId))
{
return false;
}
if (!m_bCoordLoaded)
{
LoadObjectCoord();
}
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;
}
#endregion
}
}
@@ -502,6 +502,25 @@ public class CECNPCMan : IMsgHandler
return npc;
}
// Find first NPC/Monster by template id (tid). Used by UI auto-move coordinate resolving.
// 通过模板ID(tid)查找第一个NPC/怪物。用于UI自动寻路坐标解析。
public CECNPC FindNPCByTemplateID(int tid)
{
if (tid == 0) return null;
foreach (var npc in m_NPCTab.Values)
{
if (!npc) continue;
var info = npc.GetNPCInfo();
if (info.tid == tid || info.vis_tid == tid)
{
return npc;
}
}
return null;
}
public CECNPC GetNPCFromAll(int nid)
{
CECNPC pNPC = GetNPC(nid);
@@ -586,7 +586,7 @@ namespace BrewMonster.Scripts
case CECHPWork.Host_work_ID.WORK_PASSIVEMOVE: pWork = new CECHPWorkPassiveMove(this); break;
//case CECHPWork.Host_work_ID.WORK_CONGREGATE: pWork = new CECHPWorkCongregate(this); break;
//case CECHPWork.Host_work_ID.WORK_SKILLSTATEACT: pWork = new CECHPWorkSkillStateAction(this); break;
//case CECHPWork.Host_work_ID.WORK_FORCENAVIGATEMOVE: pWork = new CECHPWorkNavigate(this); break;
case CECHPWork.Host_work_ID.WORK_FORCENAVIGATEMOVE: pWork = new CECHPWorkNavigate(this); break;
default:
return null;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c3491aaddacef1a418865bbcd31fb975
@@ -68,6 +68,13 @@ namespace BrewMonster.Scripts.Task
uint m_ulNPCInfoTimeMark;
private Dictionary<uint, NPC_INFO> m_NPCInfoMap = new();
// Lookup NPC/task object coordinates info by template id (loaded from task_npc pack)
// 通过模板ID查找NPC/任务对象坐标信息(从task_npc包加载)
public bool TryGetTaskNPCInfo(uint id, out NPC_INFO info)
{
return m_NPCInfoMap.TryGetValue(id, out info);
}
private TaskTemplContainerSO _taskTemplContainerSO;
#if _TASK_CLIENT
@@ -11,6 +11,7 @@ using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
using System;
using BrewMonster.Scripts; // For CECNavigateCtrl
namespace BrewMonster.Scripts.Task
{
@@ -1625,19 +1626,25 @@ namespace BrewMonster.Scripts.Task
{
return m_pFinishedCountListBuf;
}
void SetForceNavigateFinishFlag(bool bFinish) { m_bForceNavigateFinish = bFinish;} //
public void OnNewTask(int iTaskID)
public void SetForceNavigateFinishFlag(bool bFinish) { m_bForceNavigateFinish = bFinish;} // Set force navigate finish flag // 设置强制导航完成标志
public void OnNewTask(int iTaskID)
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnNewTask: TaskID={iTaskID}");
ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID);
if (pTempl != null
&& pTempl.m_FixedData.m_enumMethod== (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
{
ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID);
if (pTempl != null
&& pTempl.m_FixedData.m_enumMethod== (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
{
SetForceNavigateFinishFlag(false);
// TODO: trigger navigation event
// m_pHost.OnNaviageEvent(iTaskID,CECNavigateCtrl::EM_PREPARE);
}
UnityEngine.Debug.Log($"[CECTaskInterface] OnNewTask: Task {iTaskID} is force navigate task, triggering EM_PREPARE");
SetForceNavigateFinishFlag(false);
// Trigger navigation event // 触发导航事件
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_PREPARE);
}
else
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnNewTask: Task {iTaskID} is not a force navigate task (pTempl={pTempl != null}, method={pTempl?.m_FixedData.m_enumMethod})");
}
}
public void OnTaskConfirmUpdate()
{
@@ -1653,34 +1660,99 @@ namespace BrewMonster.Scripts.Task
OnTaskConfirmUpdate();
}
public void OnCompleteTask(int iTaskID)
public void OnCompleteTask(int iTaskID)
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnCompleteTask: TaskID={iTaskID}");
ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID);
if (pTempl != null &&
pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
{
ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID);
if (pTempl != null &&
pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
{
//TODO: trigger navigation end event
// m_pHost.OnNaviageEvent(iTaskID,CECNavigateCtrl::EM_END);
SetForceNavigateFinishFlag(false);
}
UnityEngine.Debug.Log($"[CECTaskInterface] OnCompleteTask: Task {iTaskID} is force navigate task, triggering EM_END");
// Trigger navigation end event // 触发导航结束事件
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_END);
SetForceNavigateFinishFlag(false);
}
else
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnCompleteTask: Task {iTaskID} is not a force navigate task");
}
}
public void TakeAwayCommonItem(uint ulTemplId, uint ulNum) {}
public void TakeAwayTaskItem(uint ulTemplId, uint ulNum) {}
public void TakeAwayGold(uint ulNum) {}
public void TakeAwayFactionConsumeContrib(int ulNum){}
public void OnGiveupTask(int iTaskID)
public void OnGiveupTask(int iTaskID)
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnGiveupTask: TaskID={iTaskID}");
ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID);
if (pTempl != null &&
pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
{
ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID);
if (pTempl != null &&
pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
UnityEngine.Debug.Log($"[CECTaskInterface] OnGiveupTask: Task {iTaskID} is force navigate task, triggering EM_END");
// Trigger navigation end event // 触发导航结束事件
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_END);
SetForceNavigateFinishFlag(false);
}
else
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnGiveupTask: Task {iTaskID} is not a force navigate task");
}
}
// Handle task text click in UI - trigger navigation if it's a force navigate task // 处理任务UI文本点击 - 如果是强制导航任务则触发导航
// This is called when user clicks on task name/link in the task UI // 当用户在任务UI中点击任务名称/链接时调用
public void OnTaskTextClick(int iTaskID)
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: TaskID={iTaskID}");
// Check if task exists and is a force navigate task // 检查任务是否存在且为强制导航任务
ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID);
if (pTempl != null &&
pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Task {iTaskID} is force navigate task, triggering navigation");
// Check if navigation is already prepared // 检查导航是否已准备
CECHostNavigatePlayer pNavigatePlayer = m_pHost.GetNavigatePlayer();
if (pNavigatePlayer != null && pNavigatePlayer.GetNavigateCtrl() != null)
{
// TODO: trigger navigation end event
// m_pHost.OnNaviageEvent(iTaskID,CECNavigateCtrl::EM_END);
SetForceNavigateFinishFlag(false);
// If already prepared, trigger begin // 如果已准备,触发开始
if (pNavigatePlayer.GetNavigateCtrl().IsInForceNavigateState())
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Navigation already prepared, triggering EM_BEGIN");
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_BEGIN);
}
else
{
// Prepare first, then begin // 先准备,然后开始
UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Preparing navigation first, then beginning");
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_PREPARE);
// Note: EM_BEGIN will be triggered after preparation is complete
// 注意:EM_BEGIN 将在准备完成后触发
// For now, trigger it immediately after a short delay
// 目前,在短暂延迟后立即触发
// TODO: Implement proper async handling if needed
// TODO: 如果需要,实现适当的异步处理
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_BEGIN);
}
}
else
{
// Prepare and begin navigation // 准备并开始导航
UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Preparing and beginning navigation");
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_PREPARE);
m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_BEGIN);
}
}
else
{
UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Task {iTaskID} is not a force navigate task, will use normal auto-move");
// TODO: Implement normal auto-move behavior here
// TODO: 在此处实现正常的自动移动行为
}
}
public void UpdateTaskUI(uint idTask, int reason)
{
+240 -68
View File
@@ -15,6 +15,7 @@ using NUnit.Framework;
using PerfectWorld.Scripts.Task;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
using Unity.VisualScripting;
@@ -166,10 +167,171 @@ namespace BrewMonster.Scripts.Task.UI
m_pBtn_SearchQuest.onClick.AddListener(OnCommand_searchquest);
m_pBtn_Abandon.onClick.AddListener(OnCommand_abandon);
// Convert exactly like C++ OnEventLButtonDown_Txt_QuestItem // 完全按照C++ OnEventLButtonDown_Txt_QuestItem转换
// C++ uses WM_LBUTTONDOWN event, we use EventTrigger PointerClick // C++使用WM_LBUTTONDOWN事件,我们使用EventTrigger PointerClick
if (m_pTxt_QuestItem != null)
{
// Enable raycast for TMP link detection (like C++ GetItemLinkItemOn needs link info) // 为TMP链接检测启用射线投射(如C++ GetItemLinkItemOn需要链接信息)
m_pTxt_QuestItem.raycastTarget = true;
// Add EventTrigger for PointerClick (like WM_LBUTTONDOWN in C++) // 为PointerClick添加EventTrigger(如C++中的WM_LBUTTONDOWN
EventTrigger trigger = m_pTxt_QuestItem.GetComponent<EventTrigger>();
if (trigger == null)
{
trigger = m_pTxt_QuestItem.gameObject.AddComponent<EventTrigger>();
}
// Clear existing triggers // 清除现有触发器
trigger.triggers.Clear();
// Add PointerClick event (like WM_LBUTTONDOWN) // 添加PointerClick事件(如WM_LBUTTONDOWN
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = EventTriggerType.PointerClick;
entry.callback.AddListener((data) => {
PointerEventData pointerData = (PointerEventData)data;
OnEventLButtonDown_Txt_QuestItem(pointerData);
});
trigger.triggers.Add(entry);
UnityEngine.Debug.Log($"[DlgTask] Awake: Added EventTrigger for PointerClick to m_pTxt_QuestItem (like C++ WM_LBUTTONDOWN)");
}
OnInitDialog();
}
// Convert exactly like C++ OnEventLButtonDown_Txt_QuestItem // 完全按照C++ OnEventLButtonDown_Txt_QuestItem转换
// C++: void CDlgTask::OnEventLButtonDown_Txt_QuestItem(WPARAM wParam, LPARAM lParam, AUIObject *pObj)
private void OnEventLButtonDown_Txt_QuestItem(PointerEventData eventData)
{
if (m_pTxt_QuestItem == null) return;
const string LINK_CLICK_VER = "DlgTaskLinkClickCamFix_v2";
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: {LINK_CLICK_VER} pressEventCamera={(eventData.pressEventCamera != null ? eventData.pressEventCamera.name : "null")}");
// C++: int x = GET_X_LPARAM(lParam); int y = GET_Y_LPARAM(lParam); // C++: int x = GET_X_LPARAM(lParam); int y = GET_Y_LPARAM(lParam);
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
m_pTxt_QuestItem.rectTransform,
eventData.position,
eventData.pressEventCamera,
out localPoint);
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Click at localPoint={localPoint}, screenPos={eventData.position}");
// C++: GetItemLinkItemOn(x, y, pObj, &Item); // C++: GetItemLinkItemOn(x, y, pObj, &Item);
// Find which link was clicked (like C++ GetItemLinkItemOn checks vecItemLink[i].rc.PtInRect) // 查找点击了哪个链接(如C++ GetItemLinkItemOn检查vecItemLink[i].rc.PtInRect
m_pTxt_QuestItem.ForceMeshUpdate();
private void Update()
// Debug: verify TMP parsed <link> tags and we have linkInfo
int linkCount = m_pTxt_QuestItem.textInfo?.linkCount ?? -1;
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: TMP linkCount={linkCount}, raycastTarget={m_pTxt_QuestItem.raycastTarget}, richText={m_pTxt_QuestItem.richText}");
if (linkCount <= 0)
{
string text = m_pTxt_QuestItem.text ?? string.Empty;
string preview = text.Substring(0, Mathf.Min(300, text.Length));
UnityEngine.Debug.LogWarning($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: No links parsed. Text preview: {preview}");
return;
}
// Get camera for link detection // 获取用于链接检测的相机
Camera camera = null;
Canvas canvas = m_pTxt_QuestItem.GetComponentInParent<Canvas>();
if (canvas != null)
{
// IMPORTANT: TMP_TextUtilities.FindIntersectingLink expects camera=null for ScreenSpaceOverlay.
// 重要:ScreenSpaceOverlay 必须传 camera=null,否则会算错导致 linkIndex = -1。
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Canvas renderMode={canvas.renderMode}, worldCamera={(canvas.worldCamera != null ? canvas.worldCamera.name : "null")}, pressEventCamera={(eventData.pressEventCamera != null ? eventData.pressEventCamera.name : "null")}");
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
camera = null;
}
else
{
// ScreenSpaceCamera / WorldSpace
camera = eventData.pressEventCamera != null ? eventData.pressEventCamera : canvas.worldCamera;
}
}
else
{
UnityEngine.Debug.LogWarning($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: {LINK_CLICK_VER} Canvas not found in parents of m_pTxt_QuestItem!");
// No canvas found; fall back to pressEventCamera (may be null) then main
camera = eventData.pressEventCamera != null ? eventData.pressEventCamera : Camera.main;
}
// Find intersecting link (like C++ checks vecItemLink[i].rc.PtInRect(x, y)) // 查找相交的链接(如C++检查vecItemLink[i].rc.PtInRect(x, y)
int linkIndexCam = TMP_TextUtilities.FindIntersectingLink(m_pTxt_QuestItem, eventData.position, camera);
int linkIndexNull = TMP_TextUtilities.FindIntersectingLink(m_pTxt_QuestItem, eventData.position, null);
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: {LINK_CLICK_VER} FindIntersectingLink(cam={(camera != null ? camera.name : "null")})={linkIndexCam}, FindIntersectingLink(null)={linkIndexNull}, screenPos={eventData.position}");
int linkIndex = linkIndexCam != -1 ? linkIndexCam : linkIndexNull;
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: FindIntersectingLink => linkIndex={linkIndex}, camera={(camera != null ? camera.name : "null")}, screenPos={eventData.position}");
// Dump all links for debugging
for (int i = 0; i < m_pTxt_QuestItem.textInfo.linkCount; i++)
{
TMP_LinkInfo li = m_pTxt_QuestItem.textInfo.linkInfo[i];
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Link[{i}] id={li.GetLinkID()} text={li.GetLinkText()}");
}
// C++: if( Item.m_pItem != NULL ) // C++: if( Item.m_pItem != NULL )
if (linkIndex != -1 && linkIndex < m_pTxt_QuestItem.textInfo.linkCount)
{
TMP_LinkInfo linkInfo = m_pTxt_QuestItem.textInfo.linkInfo[linkIndex];
string linkID = linkInfo.GetLinkID();
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Found link - linkID={linkID}");
// C++: if( Item.m_pItem->GetType() == enumEICoord ) // C++: if( Item.m_pItem->GetType() == enumEICoord )
// Check if this is a coordinate link (NPC, monster, item, target - all use enumEICoord in C++) // 检查这是否是坐标链接(NPC、怪物、物品、目标 - 在C++中都使用enumEICoord
if (linkID.StartsWith("coord_"))
{
// C++: if (IsTreasureMapSelected()){ OnCommand_TreasureMap(NULL); } // C++: if (IsTreasureMapSelected()){ OnCommand_TreasureMap(NULL); }
// TODO: Implement IsTreasureMapSelected check if needed // TODO: 如果需要,实现IsTreasureMapSelected检查
// C++: else { CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask); } // C++: else { CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask); }
// Extract entity ID from link (NPC, monster, item, target - all handled the same) // 从链接中提取实体ID(NPC、怪物、物品、目标 - 处理方式相同)
string entityIdStr = linkID.Substring(6); // Remove "coord_" prefix
if (int.TryParse(entityIdStr, out int entityID))
{
// Use currently selected task ID (like m_idSelTask in C++ FollowCoord call) // 使用当前选中的任务ID(如C++ FollowCoord调用中的m_idSelTask
int currentTaskID = m_idSelTask;
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Coordinate link clicked, EntityID={entityID} (NPC/Monster/Item/Target), task={currentTaskID}");
// C++: CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask); // C++: CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask);
// FollowCoord triggers auto-move to the coordinates (for NPC, monster, item, target) // FollowCoord触发自动移动到坐标(适用于NPC、怪物、物品、目标)
// This works for all entity types - NPC names, monster names, items, targets // 这适用于所有实体类型 - NPC名称、怪物名称、物品、目标
if (currentTaskID > 0)
{
CECHostPlayer hostPlayer = GetHostPlayer();
if (hostPlayer != null)
{
CECTaskInterface taskInterface = hostPlayer.GetTaskInterface();
if (taskInterface != null)
{
// This matches C++: CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask)
// 这匹配C++CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask)
bool ok = CECUIHelper.FollowCoord(entityID, currentTaskID);
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: FollowCoord(entity={entityID}, task={currentTaskID}) => {ok}");
}
}
}
else
{
// Even without a task, still follow coord
// 即使没有任务,也照样跟随坐标
bool ok = CECUIHelper.FollowCoord(entityID, 0);
UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: FollowCoord(entity={entityID}, task=0) => {ok}");
}
}
}
}
// C++: ChangeFocus(NULL); // C++: ChangeFocus(NULL);
// TODO: Implement ChangeFocus if needed // TODO: 如果需要,实现ChangeFocus
}
private new void Update()
{
Tick();
}
@@ -545,7 +707,7 @@ namespace BrewMonster.Scripts.Task.UI
// Award NPC
int nANPC = (int)pTemp.GetAwardNPC();
UpdateAwardNPC(ref strNewTextItem, nANPC);
UpdateAwardNPC(ref strNewTextItem, nANPC, idTask);
// Complete condition - always refresh to show updated progress
UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi);
@@ -668,11 +830,11 @@ namespace BrewMonster.Scripts.Task.UI
UpdateTaskBaseDesc(ref strNewTextItem, tsi);
// Append: deliver NPC
UpdateDeliverNPC(ref strNewTextItem, (int)pTemp.GetDeliverNPC());
UpdateDeliverNPC(ref strNewTextItem, (int)pTemp.GetDeliverNPC(), idTask);
// Append: award NPC
int nANPC = (int)pTemp.GetAwardNPC();
UpdateAwardNPC(ref strNewTextItem, nANPC);
UpdateAwardNPC(ref strNewTextItem, nANPC, idTask);
// Append: completion conditions
UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi);
@@ -1216,7 +1378,7 @@ namespace BrewMonster.Scripts.Task.UI
}
private void UpdateDeliverNPC(ref string strText, int nDNPC)
private void UpdateDeliverNPC(ref string strText, int nDNPC, int idTask = 0)
{
// [中文] 交付NPC
// [English] Deliver NPC
@@ -1241,60 +1403,59 @@ namespace BrewMonster.Scripts.Task.UI
}
if (string.IsNullOrEmpty(npcName)) npcName = nDNPC.ToString();
// [中文] 追加到内容文本
// [English] Append to content text
// [中文] 追加到内容文本,如果找到坐标则添加可点击链接(如C++中的enumEICoord
// [English] Append to content text, add clickable link if coordinates found (like enumEICoord in C++)
var sb = new System.Text.StringBuilder();
sb.Append(GetStringFromTable(7620));
sb.Append(npcName);
// Always create clickable link (even if coords aren't known yet).
// 总是创建可点击链接(即使暂时不知道坐标)。
sb.Append($"<link=\"coord_{nDNPC}\"><color=#00FF00>{npcName}</color></link>");
sb.Append("\n");
// if (m_pTxt_QuestItem != null)
// {
// Debug.Log($"UpdateDeliverNPC: {sb.ToString()}");
// m_pTxt_QuestItem.text += sb.ToString();
// }
strText += sb.ToString();
}
private A3DVECTOR3 UpdateAwardNPC(ref string strText, int nANPC)
private A3DVECTOR3 UpdateAwardNPC(ref string strText, int nANPC, int idTask = 0)
{
A3DVECTOR3 ret = new A3DVECTOR3(0f);
// Award NPC
if (nANPC == 0)
{
A3DVECTOR3 ret = new A3DVECTOR3(0f);
// Award NPC
if (nANPC == 0)
{
return ret;
}
// Lookup NPC name from element data
string npcName = string.Empty;
var edm = BrewMonster.ElementDataManProvider.GetElementDataMan();
if (edm != null)
{
if (edm.essence_id_data_type_map.TryGetValue((uint)nANPC, out var dtype)
&& dtype == DATA_TYPE.DT_NPC_ESSENCE
&& edm.essence_id_data_map.TryGetValue((uint)nANPC, out var obj)
&& obj is NPC_ESSENCE npc)
{
npcName = npc.Name;
}
}
if (string.IsNullOrEmpty(npcName)) npcName = nANPC.ToString();
// Append to content
var sb = new System.Text.StringBuilder();
sb.Append(GetStringFromTable(7621));
sb.Append(npcName);
sb.Append("\n");
// if (m_pTxt_Content != null)
// {
// Debug.Log($"Award NPC: {sb.ToString()}");
// m_pTxt_QuestItem.text += sb.ToString();
// }
strText += sb.ToString();
return ret;
}
// Lookup NPC name from element data
string npcName = string.Empty;
var edm = BrewMonster.ElementDataManProvider.GetElementDataMan();
if (edm != null)
{
if (edm.essence_id_data_type_map.TryGetValue((uint)nANPC, out var dtype)
&& dtype == DATA_TYPE.DT_NPC_ESSENCE
&& edm.essence_id_data_map.TryGetValue((uint)nANPC, out var obj)
&& obj is NPC_ESSENCE npc)
{
npcName = npc.Name;
}
}
if (string.IsNullOrEmpty(npcName)) npcName = nANPC.ToString();
// Append to content, add clickable link if coordinates found (like enumEICoord in C++) // 追加到内容,如果找到坐标则添加可点击链接(如C++中的enumEICoord
var sb = new System.Text.StringBuilder();
sb.Append(GetStringFromTable(7621));
// Add NPC name as clickable link if coordinates found (like enumEICoord in C++) // 如果找到坐标,将NPC名称添加为可点击链接(如C++中的enumEICoord
// In C++, it always creates a clickable link if coordinates are found // 在C++中,如果找到坐标,它总是创建一个可点击链接
// For force navigate tasks, we'll trigger navigation when clicked // 对于强制导航任务,点击时将触发导航
// Always create clickable link (even if coords aren't known yet).
// 总是创建可点击链接(即使暂时不知道坐标)。
sb.Append($"<link=\"coord_{nANPC}\"><color=#00FF00>{npcName}</color></link>");
sb.Append("\n");
strText += sb.ToString();
return ret;
}
// Update completion conditions (monsters, players, gold, level/reincarnation/realm)
private void UpdateCompleteCondition(ref string strText, ref string strHint, Task_State_info tsi)
{
@@ -1322,24 +1483,10 @@ namespace BrewMonster.Scripts.Task.UI
{
// strName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(me.name);
bool bFind = false;
A3DVECTOR3 vPos = CECUIHelper.GetTaskObjectCoordinates((int)id, ref bFind);
// TODO: serialize position info if found
// ACHAR szPos[100];
// EditBoxItemBase item(enumEICoord);
// item.SetName(pMonster->name);
if (bFind)
{
// a_sprintf(szPos, _AL("%f %f %f %d"), vPos.x, vPos.y, vPos.z, id);
// item.SetInfo(szPos);
// item.SetColor(A3DCOLORRGB(0, 255, 0));
// strName = (ACHAR)AUICOMMON_ITEM_CODE_START + item.Serialize();
}
else
{
strName = ByteToStringUtils.UshortArrayToUnicodeString(pMonster.name);
}
// Always create clickable link (even if coords aren't known yet).
// 总是创建可点击链接(即使暂时不知道坐标)。
string monsterName = ByteToStringUtils.UshortArrayToUnicodeString(pMonster.name);
strName = $"<link=\"coord_{id}\"><color=#00FF00>{monsterName}</color></link>";
}
// Build description for this monster requirement
@@ -1468,9 +1615,34 @@ namespace BrewMonster.Scripts.Task.UI
string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(itemTid);
if (string.IsNullOrEmpty(itemName)) itemName = $"Item {itemTid}";
// Find coordinates for item (like C++ GetTaskObjectCoordinates) // 查找物品的坐标(如C++ GetTaskObjectCoordinates
int search_id = 0;
if (pTempl.m_FixedData.m_enumMethod != (uint)TaskCompletionMethod.enumTMKillPlayer)
{
int id = (int)tsi.m_ItemsWanted[i].m_ulMonsterId;
if (id > 0)
{
search_id = id;
}
else
{
// TODO: Search for mine essence if needed // TODO: 如果需要,搜索矿点精华
// const MINE_ESSENCE* pMine = SearchTaskMine(idTask);
// if(pMine) search_id = pMine->id;
}
}
// Always create clickable link for the target (search_id).
// 总是为目标(search_id)创建可点击链接。
string displayName = itemName;
if (search_id > 0)
{
displayName = $"<link=\"coord_{search_id}\"><color=#00FF00>{itemName}</color></link>";
}
// Compose line: name and progress (gained/toGet)
// 组合文本:名称与进度(已获得/所需)
string strTemp = Format(GetStringFromTable(7625), itemName,
string strTemp = Format(GetStringFromTable(7625), displayName,
tsi.m_ItemsWanted[i].m_ulItemsGained,
tsi.m_ItemsWanted[i].m_ulItemsToGet);
+158 -1
View File
@@ -1,4 +1,7 @@
using BrewMonster.Network;
using BrewMonster.Managers;
using BrewMonster.Scripts.Task;
using BrewMonster.Scripts;
using CSNetwork.GPDataType;
namespace BrewMonster.Scripts.UI
@@ -10,7 +13,65 @@ namespace BrewMonster.Scripts.UI
public static A3DVECTOR3 GetTaskObjectCoordinates(int id, ref bool in_table)
{
in_table = false;
return new A3DVECTOR3(0);
// 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
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;
return new A3DVECTOR3(objPos.x, objPos.y, objPos.z);
}
}
// 1) Try live NPC/Monster in scene by template id (best match to "click name -> go to that entity")
// 1) 先尝试在场景中按模板ID查找活体NPC/怪物(最符合“点名字就去找它”)
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;
return new A3DVECTOR3(npcPos.x, npcPos.y, npcPos.z);
}
}
// 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;
return new A3DVECTOR3(info.x, info.z, info.y);
}
// Fallback to coord_data.txt (C++: Configs/Coord_data.txt via CECGame::GetObjectCoord)
// 回退到 coord_data.txtC++Configs/Coord_data.txt,通过 CECGame::GetObjectCoord
if (BrewMonster.Network.EC_Game.TryGetFirstObjectCoord(id.ToString(), out var coordPos, out var mapName))
{
UnityEngine.Debug.Log($"[CECUIHelper] GetTaskObjectCoordinates: Resolved id={id} via coord_data.txt map={mapName} pos=({coordPos.x:F2},{coordPos.y:F2},{coordPos.z:F2})");
in_table = true;
return new A3DVECTOR3(coordPos.x, coordPos.y, coordPos.z);
}
UnityEngine.Debug.LogWarning($"[CECUIHelper] GetTaskObjectCoordinates: Not found for id={id} (isLikelyRuntimeObjectId={isLikelyRuntimeObjectId}).");
return new A3DVECTOR3(0);
// TODO: Implement this method properly
// A3DVECTOR3 ret(0.f);
@@ -28,5 +89,101 @@ namespace BrewMonster.Scripts.UI
// }
// return ret;
}
// Follow coord like C++ CECUIHelper::FollowCoord(enumEICoord, taskId)
// 像C++的CECUIHelper::FollowCoord(enumEICoord, taskId)一样跟随坐标
public static bool FollowCoord(int id, int taskId)
{
// Resolve coordinates
bool inTable = false;
A3DVECTOR3 vPos = GetTaskObjectCoordinates(id, ref 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);
UnityEngine.Debug.Log($"[CECUIHelper] FollowCoord: Fallback {tag} region center=({cx:F2},{cy:F2},{cz:F2}) worldId={worldId} curWorldId={curWorldId}");
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;
}
work.SetDestination(CECHPWorkMove.DestTypes.DEST_2D, vPos);
wm.StartWork_p2(work);
UnityEngine.Debug.Log($"[CECUIHelper] FollowCoord: Started auto-move to ({vPos.x},{vPos.y},{vPos.z}) for id={id}, taskId={taskId}");
return true;
}
}
}
+45
View File
@@ -191,6 +191,51 @@ namespace BrewMonster
return m_taskInventory;
}
// Get work manager // 获取工作管理器
public CECHPWorkMan GetWorkMan()
{
return m_pWorkMan;
}
// Get navigate player // 获取导航玩家
private CECHostNavigatePlayer m_pNavigatePlayer = null;
public CECHostNavigatePlayer GetNavigatePlayer()
{
if (m_pNavigatePlayer == null)
{
// TODO: Implement proper creation of navigate player
// m_pNavigatePlayer = CreateNavigatePlayer();
}
return m_pNavigatePlayer;
}
// Check if in force navigate state // 检查是否在强制导航状态
public bool IsInForceNavigateState()
{
CECHostNavigatePlayer pNavigatePlayer = GetNavigatePlayer();
if (pNavigatePlayer != null && pNavigatePlayer.GetNavigateCtrl() != null)
{
return pNavigatePlayer.GetNavigateCtrl().IsInForceNavigateState();
}
return false;
}
// Handle navigation event // 处理导航事件
public void OnNaviageEvent(int task, int e)
{
UnityEngine.Debug.Log($"[CECHostPlayer] OnNaviageEvent: Task={task}, Event={e} ({(BrewMonster.Scripts.CECNavigateCtrl.NavigateEvent)e})");
CECHostNavigatePlayer pNavigatePlayer = GetNavigatePlayer();
if (pNavigatePlayer != null)
{
UnityEngine.Debug.Log($"[CECHostPlayer] OnNaviageEvent: Forwarding to NavigatePlayer");
pNavigatePlayer.OnNavigateEvent(task, e);
}
else
{
UnityEngine.Debug.LogWarning($"[CECHostPlayer] OnNaviageEvent: NavigatePlayer is null");
}
}
public EC_Inventory GetInventory(byte byPackage)
{
switch (byPackage)