Files
test/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs
2026-04-13 16:59:20 +07:00

2547 lines
95 KiB
C#
Raw Permalink 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.Runtime.InteropServices;
using System.Text.RegularExpressions;
using BrewMonster.Managers;
using BrewMonster.Scripts.Managers;
using BrewMonster.Network;
using BrewMonster.Scripts.Task;
using BrewMonster.Scripts.UI;
using BrewMonster.UI;
using CSNetwork.GPDataType;
using ModelRenderer.Scripts.Common;
using ModelRenderer.Scripts.GameData;
using NUnit.Framework;
using PerfectWorld.Scripts.Task;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
namespace BrewMonster.Scripts.Task.UI
{
/// <summary>
/// This is DlgTask.cpp
/// </summary>
public class DlgTask : AUIDialog
{
#if UNITY_EDITOR
[ContextMenu("Generate Tasks")]
public void TestUpdateTask()
{
UpdateTask(-1);
}
#endif
// Keep original macro as constant for array sizing
public const int CDLGTASK_AWARDITEM_MAX = 8;
public const int TickRate = 1000;
// ===== Nested structs (converted from C++), keep naming, public with explicit layout =====
// [中文] 任务目标位置
// [English] Task object position
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct TASK_OBJECT_POS
{
public int x;
public int y;
public int z;
public int mapid;
}
// [中文] 任务完成时间
// [English] Task finished time
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct TASK_FINISHED_TIME
{
public int iTaskID;
public uint dwTime; // DWORD -> uint
}
// [中文]
// [English] Type grouping node
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct TypeNode
{
public uint type; // DWORD -> uint
public GameObject item; // P_AUITREEVIEW_ITEM -> GameObject
}
// [中文] ȼ
// [English] Level priority node
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct LevelNode
{
public int level;
public GameObject item; // P_AUITREEVIEW_ITEM -> GameObject
}
// ===== Converted member variables (keep original naming) =====
// protected:
protected int m_idLastTask;
protected int m_idSelTask;
protected bool m_bTraceNew;
protected bool m_bShowTrace;
protected int m_iType;
[SerializeField] protected TMP_Text _nameTaskText;
[SerializeField] protected TMP_Text m_pTxt_QuestNO; // PAUILABEL -> TMP_Text
[SerializeField] protected TaskTreeView m_pTv_Quest; // PAUITREEVIEW -> GameObject container
[SerializeField] protected TMP_Text m_pTxt_Content; // PAUITEXTAREA -> TMP_Text
[SerializeField] protected TMP_Text m_pTxt_QuestItem; // PAUITEXTAREA -> TMP_Text
[SerializeField] protected Button m_pBtn_Abandon; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected Button m_pBtn_MainQuest; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected Button m_pBtn_NormalQuest; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected Button m_pBtn_SearchQuest; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected Button m_pBtn_HaveQuest; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected Toggle m_pTog_bShowTrace; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected Button m_pBtn_FinishTask; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected TMP_Text m_pTxt_BaseAward; // PAUILABEL -> TMP_Text
[Space(10)]
[SerializeField] protected Button Btn_TreasureMap;
[SerializeField] protected Button Btn_Focus;
[SerializeField] protected GameObject Lab_QuestNO; // the title label of m_pTxt_QuestNO
// PAUIIMAGEPICTURE m_pImg_Item[CDLGTASK_AWARDITEM_MAX];
// Use fixed-size array semantics via initialization length
// [中文] 奖励物品图片数组
// [English] Award item images array
[MarshalAs(UnmanagedType.ByValArray, SizeConst = CDLGTASK_AWARDITEM_MAX)]
[SerializeField] protected Image[] m_pImg_Item = new Image[CDLGTASK_AWARDITEM_MAX];
protected uint m_ImgCount => (uint)m_pImg_Item.Length; // unsigned int -> uint
[SerializeField] protected Button m_pBtn_GotoNPC; // PAUISTILLIMAGEBUTTON -> Button
[SerializeField] protected GameObject m_pQuickBuyTrigger; // CECQuickBuyPopActivityTrigger* -> GameObject
// private:
private static List<int> m_vecTasksUnFinish = new List<int>();
private static List<int> m_vecTasksCanFinish = new List<int>();
private static List<int> m_vecTasksCanGet = new List<int>();
// [中文] 目标坐标集合
// [English] Target coordinates collection
private static List<OBJECT_COORD> m_TargetCoord = new List<OBJECT_COORD>();
private static string m_strTraceName = string.Empty; // ACString -> string
// [中文] 任务相关矿点映射
// [English] Mine map related to tasks
private static Dictionary<int, object> m_TaskMines = new Dictionary<int, object>(); // MINE_ESSENCE* -> object
// [中文] 任务跟踪计时器
// [English] Task trace counter
private CECCounter m_TaskTraceCounter = new (); // CECCounter -> object placeholder
// ===== Time-gated task UI refresh (search list) =====
// Timetable/time-window tasks become available/unavailable as server time moves.
// The original C++ client periodically re-evaluates prerequisites; in this port we refresh the search list
// at a low frequency while the Search view is open so players can see time-gated tasks appear/disappear.
private uint _lastSearchRefreshMinuteKey = uint.MaxValue;
private uint _pendingReselectTaskId = 0;
// Active-task timer refresh (wait-time / time-limit / protect-time)
private float _nextActiveTimerUiRefreshAt = 0f;
#region Unity METHODS
private new void OnEnable()
{
OnShowDialog();
OnCommand_havequest();
}
private new void OnDisable()
{
OnHideDialog();
}
private new void Awake()
{
EventBus.Subscribe<TaskItemClickEvent>(evt =>
{
OnEventLButtonDown_Tv_Quest(evt.Data);
});
m_pBtn_HaveQuest.onClick.AddListener(OnCommand_havequest);
m_pBtn_SearchQuest.onClick.AddListener(OnCommand_searchquest);
m_pBtn_Abandon.onClick.AddListener(OnCommand_abandon);
Btn_Focus.onClick.AddListener(() => OnCommand_focus());
if (m_pTog_bShowTrace)
{
m_pTog_bShowTrace.onValueChanged.AddListener((state) => OnCommand_showtrace(null,state));
}
// 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);
}
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();
// 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
}
public static void SetTracePosition(List<OBJECT_COORD> targetPos, string targetName)
{
CECUIHelper.FollowCoord(targetPos, targetName);
}
public static void SetTraceNpc(int entityID, int taskID)
{
CECUIHelper.FollowCoord(entityID, taskID);
}
#endregion
#region PUBLIC METHODS
public void OnCommand_searchquest()
{
// if (m_szName != "Win_Quest") return;
m_iType = 1;
// TODO
// PAUIOBJECT pObj = GetDlgItem("Img_New");
// if (pObj && IsShow()) pObj->Show(false);
// if(GetDlgItem("Lab_Trace")) GetDlgItem("Lab_Trace")->Show(false);
m_pBtn_Abandon.gameObject.SetActive(false);
Btn_Focus.gameObject.SetActive(false);
Lab_QuestNO.gameObject.SetActive(false);
m_pTxt_QuestNO.gameObject.SetActive(false);
m_pBtn_SearchQuest.interactable = false;
m_pBtn_HaveQuest.interactable = true;
SearchForTask();
}
public void OnCommand_havequest()
{
m_iType = 0;
// TODO: if(GetDlgItem("Lab_Trace")) GetDlgItem("Lab_Trace")->Show(true);
m_pBtn_Abandon.gameObject.SetActive(true);
Btn_Focus.gameObject.SetActive(true);
Lab_QuestNO.gameObject.SetActive(true);
m_pTxt_QuestNO.gameObject.SetActive(true);
m_pBtn_SearchQuest.interactable = true;
m_pBtn_HaveQuest.interactable = false;
UpdateTask();
RefreshVecTasksCanGet();
}
public void OnCommand_showtrace(string szCommand,bool state) {
m_bShowTrace = state;
Debug.Log($"[DlgTask] OnCommand_showtrace: state={m_bShowTrace}");
if (state)
RebuildAcceptableQuestCache();
ShowTraceDialog();
}
public void TraceTask(uint idTask)
{
if (IsPQTaskOrSubTask((int)idTask))
return;
m_bShowTrace = true;
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
bool bFinishedTask = pTask.CanFinishTask(idTask);
List<int> vec_task = bFinishedTask ? m_vecTasksCanFinish : m_vecTasksUnFinish;
if (IsTaskTraceable(idTask))
{
if (!vec_task.Contains((int)idTask))
vec_task.Add((int)idTask);
else
{
vec_task.Remove((int)idTask);
}
}
Debug.Log($"[DlgTask] TraceTask: idTask={idTask}, task name={pTask.GetTaskTemplMan().GetTaskTemplByID((uint)idTask).GetName()}");
//get the position of idTask in pLst
int position = pTask.GetFirstSubTaskPosition(idTask);
while(position != -1)
{
int idSub = pTask.GetNextSub(ref position);
TraceTask((uint)idSub);
}
ShowTraceDialog();
}
/// <summary>
/// Add task to trace lists if traceable (no toggle). Used when seeding trace after TASK_DATA init; mirrors TraceTask minus remove/toggle.
/// 若可追踪则加入追踪列表(无切换)。在 TASK_DATA 初始化后填充追踪时用;逻辑对齐 TraceTask 但不移除/切换。
/// </summary>
private void AddTaskToTraceListsNonToggle(uint idTask)
{
if (IsPQTaskOrSubTask((int)idTask))
return;
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
if (pTask == null)
return;
bool bFinishedTask = pTask.CanFinishTask(idTask);
List<int> vec_task = bFinishedTask ? m_vecTasksCanFinish : m_vecTasksUnFinish;
if (IsTaskTraceable(idTask))
{
if (!vec_task.Contains((int)idTask))
vec_task.Add((int)idTask);
}
int position = pTask.GetFirstSubTaskPosition(idTask);
while (position != -1)
{
int idSub = pTask.GetNextSub(ref position);
AddTaskToTraceListsNonToggle((uint)idSub);
}
}
/// <summary>
/// After server task pack and templates are loaded (CECTaskInterface.Init finished), apply trace like USER_LAYOUT from server:
/// SyncTrace with a full dwTraceMask for the first 32 top-level slots, then add any further top-level tasks (mask is only 32 bits).
/// 服务端任务包与模板加载完成后调用:用满掩码对前 32 个顶层槽位 SyncTrace,再为更多顶层任务补充追踪(掩码仅 32 位)。
/// </summary>
public void SyncTraceAfterTaskDataInit(CECTaskInterface pTask)
{
if (pTask == null || GetHostPlayer()?.GetTaskInterface() != pTask)
return;
var ul = new USER_LAYOUT { bTraceAll = true };
int n = (int)pTask.GetTaskCount();
if (n > 0)
{
int bits = Mathf.Min(n, 32);
ul.dwTraceMask = bits >= 32 ? uint.MaxValue : ((1u << bits) - 1u);
}
SyncTrace(ul, true);
for (int i = 32; i < n; i++)
AddTaskToTraceListsNonToggle(pTask.GetTaskId((uint)i));
RefreshVecTasksCanGet(pTask);
RefreshTaskTrace(ul.bTraceAll);
}
public void ShowTraceDialog()
{
Debug.Log($"[DlgTask] ShowTraceDialog: m_bShowTrace={m_bShowTrace}");
if(m_bShowTrace)
{
EventBus.Publish(new UIEvent(UIEventType.ShowTrace));
}
else
{
EventBus.Publish(new UIEvent(UIEventType.HideTrace));
}
}
/// <summary>
/// TASK_SVR_NOTIFY_COMPLETE (reason 2): re-expand the quest trace from the top task (add-only, same subchain walk as TraceTask).
/// Using TraceTask() here would toggle entries off/on incorrectly; this re-seeds the active subtask chain after server advance.
/// 服务端 reason=2 任务推进/完成后,从顶层任务重新挂接追踪子链(与 TraceTask 同 walk,但只添加不切换)。
/// </summary>
public void RetraceAfterTaskServerNotifyComplete(uint notifyTaskId, bool bShowTrace)
{
var pIface = GetHostPlayer()?.GetTaskInterface();
if (pIface == null)
return;
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
ATaskTempl t = pMan?.GetTaskTemplByID(notifyTaskId);
if (t == null)
return;
uint rootId = t.GetTopTask().GetID();
if (rootId == 0)
return;
if (pIface.HasTask(rootId))
AddTaskToTraceListsNonToggle(rootId);
RefreshTaskTrace(bShowTrace);
}
public void OnCommand_focus(string szCommand="") {
var pTree = m_pTv_Quest;
var pItem = pTree?.GetSelectedItem();
if (pItem == null)
{
BMLogger.LogWarning("Cannot focus task: No task is currently selected");
return;
}
var idTask = pTree?.GetItemData(pItem);
if (idTask == 0)
{
BMLogger.LogWarning("Cannot focus task: Selected item has no valid task ID");
return;
}
m_pTog_bShowTrace.onValueChanged.Invoke(true);
TraceTask((uint)idTask);
//SwitchTaskTrace((int)idTask);
}
bool IsPQTaskOrSubTask(int idTask)
{
var pMan = EC_Game.GetTaskTemplateMan();
var pTemp = pMan?.GetTaskTemplByID((uint)idTask);
return (pTemp!=null && (pTemp.m_FixedData.m_bPQTask || pTemp.m_FixedData.m_bPQSubTask));
}
public void OnCommand_abandon()
{
// [中文] 放弃任务:发送通知到服务器 // [English] Abandon task: send notification to server
// Get the currently selected task from the tree view
var pTree = m_pTv_Quest;
var pSelectedItem = pTree?.GetSelectedItem();
if (pSelectedItem == null)
{
BMLogger.LogWarning("Cannot abandon task: No task is currently selected");
return;
}
// Get the task ID from the selected item
uint selectedTaskId = pTree.GetItemData(pSelectedItem);
if (selectedTaskId == 0)
{
BMLogger.LogWarning("Cannot abandon task: Selected item has no task ID");
return;
}
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
if (pTask == null)
{
BMLogger.LogError("Cannot abandon task: TaskInterface is null");
return;
}
// Check if the task is actually active before allowing abandon
// This prevents trying to abandon a task that's already been abandoned
if (!pTask.HasTask(selectedTaskId))
{
BMLogger.LogWarning($"Cannot abandon task: Task {selectedTaskId} is not active");
return;
}
// Get the task template to find the top-level task
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
ATaskTempl pTempl = pMan?.GetTaskTemplByID(selectedTaskId);
if (pTempl == null)
{
BMLogger.LogError($"Cannot abandon task: Task template {selectedTaskId} not found");
return;
}
// Get the top-level task ID (tasks can have subtasks)
ATaskTempl pTopTask = pTempl.GetTopTask();
uint topTaskId = pTopTask != null ? pTopTask.GetID() : selectedTaskId;
// Verify the top-level task is also active
if (!pTask.HasTask(topTaskId))
{
BMLogger.LogWarning($"Cannot abandon task: Top-level task {topTaskId} is not active");
return;
}
// Remove task from trace lists immediately (optimistic update)
// This prevents the abandoned task from showing in the task trace UI
int taskIdInt = (int)topTaskId;
m_vecTasksCanFinish.Remove(taskIdInt);
m_vecTasksUnFinish.Remove(taskIdInt);
RefreshVecTasksCanGet();
// Send notification to server to abandon the currently selected task
TaskClient._notify_svr(pTask, (byte)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_GIVEUP, (ushort)topTaskId);
// Refresh UI immediately to reflect the change
// The server confirmation will trigger another refresh, but this gives immediate feedback
RefreshTaskTrace(true);
// Clear selection if the abandoned task was selected
if (m_idSelTask == (int)topTaskId)
{
m_idSelTask = 0;
}
}
public void OnCommand_CANCEL(string szCommand) {}
public void OnCommand_TreasureMap(string szCommand) {}
public void OnCommand_FinishTask(string szCommand) {}
public void OnCommand_GotoNPC(string szCommand) {}
public void OnEventLButtonDown_Tv_Quest(uint itemData)
{
// POINT ptPos = pObj->GetPos();
// A3DVIEWPORTPARAM *p = m_pA3DEngine->GetActiveViewport()->GetParam();
// int x = GET_X_LPARAM(lParam) - ptPos.x - p->X;
// int y = GET_Y_LPARAM(lParam) - ptPos.y - p->Y;
// PAUITREEVIEW pTree = (PAUITREEVIEW)pObj;
//
// if( AUI_PRESS(VK_SHIFT) && m_iType == 0)
// {
// P_AUITREEVIEW_ITEM pItem = pTree->HitTest(x, y);
//
// if( pItem ) OnCommand_focus("focus");
// }
// P_AUITREEVIEW_ITEM pItem = pTree->GetSelectedItem();
// var pItem = m_pTv_Quest.GetItemByData(itemData);
int idTask = (int)itemData;
// int idTask(0);
// if( pItem && pTree->GetParentItem(pItem) != pTree->GetRootItem())
// idTask = pTree->GetItemData(pItem);
//
if (idTask == 0) return;
//
// if (m_szName == "Win_Quest" && CDlgAutoHelp::IsAutoHelp())
// {
// if(pTree->GetHitArea(x,y) == AUITREEVIEW_RECT_FRAME)
// CDlgWikiShortcut::PopQuestWiki(GetGameUIMan(),idTask);
// }
m_idSelTask = idTask;
}
// void OnEventMouseMove_Txt_QuestItem(WPARAM wParam, LPARAM lParam, AUIObject *pObj);
// void OnEventLButtonDown_Txt_QuestItem(WPARAM wParam, LPARAM lParam, AUIObject *pObj);
// void OnEventLButtonDown_Award_Item(WPARAM wParam, LPARAM lParam, AUIObject *pObj);
//
// void GetItemLinkItemOn(int x, int y, PAUIOBJECT pObj, AUITEXTAREA_EDITBOX_ITEM *pLink);
//
// // get formatted data
// static ACString FormatTaskText(const ACHAR* szText, A3DCOLOR background);
Color GetTaskColor(int idType)
{
// TODO: Map task type to color. Default white.
if (idType < (int)ENUM_TASK_TYPE.enumTTDaily || idType >= (int)ENUM_TASK_TYPE.enumTTEnd) {
// ASSERT(false && "wrong task type");
return Color.white;
}
Color result;
if (!EC_Utility.STRING_TO_A3DCOLOR(GetStringFromTable(idType - (int)ENUM_TASK_TYPE.enumTTDaily + 3121), out result))
{
// 解析颜色失败,返回白色
// [English] Failed to parse color, return white
result = Color.white;
}
return result;
}
// static A3DCOLOR GetTaskColor(const ATaskTempl *pTempl);
// static ACString FormatTime(int nSec, const ACString& desc, int timeLimit);
private string GetTaskNameWithColor(ATaskTempl pTempl)
{
if (pTempl == null) return string.Empty;
var type = (ENUM_TASK_TYPE)pTempl.m_FixedData.m_ulType;
string rawName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(pTempl.m_FixedData.m_szName);
if (type == ENUM_TASK_TYPE.enumTTQiShaList && !string.IsNullOrEmpty(rawName) && rawName[0] == '^')
{
// 如果是七杀榜任务且已经加了颜色,则颜色不变 // If QiShaList task already has color, keep it
return rawName;
}
string strTaskName = GetTaskNameWithOutColor(pTempl);
string strColorPreFix = A3DColorToString(GetTaskColor(pTempl));
return strColorPreFix + strTaskName;
}
private string GetTaskNameWithColor(ATaskTempl pTempl, out Color color)
{
if (pTempl == null) {
color = Color.white;
return string.Empty;
}
var type = (ENUM_TASK_TYPE)pTempl.m_FixedData.m_ulType;
string rawName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(pTempl.m_FixedData.m_szName);
if (type == ENUM_TASK_TYPE.enumTTQiShaList && !string.IsNullOrEmpty(rawName) && rawName[0] == '^')
{
// 如果是七杀榜任务且已经加了颜色,则颜色不变 // If QiShaList task already has color, keep it
color = GetTaskColor(pTempl);
return rawName;
}
string strTaskName = GetTaskNameWithOutColor(pTempl);
color = GetTaskColor(pTempl);
return strTaskName;
}
// static ACString GetTaskNameWithOutColor(const ATaskTempl* pTempl);
private static string GetTaskNameWithOutColor(ATaskTempl pTempl)
{
if (pTempl == null) return string.Empty;
string name = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(pTempl.m_FixedData.m_szName);
if (!string.IsNullOrEmpty(name) && name[0] == '^')
{
// 去掉颜色前缀(假设格式为 ^RRGGBB // Strip color prefix (assume ^RRGGBB)
if (name.Length > 7) return name.Substring(7);
return string.Empty;
}
return name;
}
private Color GetTaskColor(ATaskTempl pTempl)
{
// TODO: Map task type/flags to color. Default white.
// [English] Map task type/flags to color. Default white.
if (pTempl == null) return UnityEngine.Color.white;
int idType = (int)(ENUM_TASK_TYPE)pTempl.m_FixedData.m_ulType;
if (idType < (int)ENUM_TASK_TYPE.enumTTDaily || idType >= (int)ENUM_TASK_TYPE.enumTTEnd) {
// ASSERT(false && "wrong task type")
// [English] wrong task type
return UnityEngine.Color.white;
}
Color result;
EC_Utility.STRING_TO_A3DCOLOR(GetStringFromTable(idType - (int)ENUM_TASK_TYPE.enumTTDaily + 3121), out result);
return result;
// return UnityEngine.Color.white;
}
private static string A3DColorToString(UnityEngine.Color c)
{
// 原代码将颜色转换为字符串前缀,这里返回空前缀以保持UI简洁 // Return empty prefix for TMP rich text compatibility
return string.Empty;
}
//
public bool Tick()
{
//RefreshTaskTrace();
// Time-window task refresh: while in Search view, refresh the list when server time crosses a minute boundary.
// This is throttled to avoid rebuilding large task lists every frame.
if (m_iType == 1)
{
var host = GetHostPlayer();
var task = host != null ? host.GetTaskInterface() : null;
if (task != null)
{
uint now = task.GetCurTime();
uint minuteKey = now / 60u;
if (minuteKey != _lastSearchRefreshMinuteKey)
{
_lastSearchRefreshMinuteKey = minuteKey;
// Preserve current selection if any, so refreshing doesn't feel disruptive.
var curItem = m_pTv_Quest != null ? m_pTv_Quest.GetSelectedItem() : null;
_pendingReselectTaskId = (curItem != null) ? m_pTv_Quest.GetItemData(curItem) : 0u;
// Rebuild available task list according to current time-based prerequisites.
SearchForTask(-1);
// Restore selection best-effort (TaskTreeView selection is driven by EventBus).
if (_pendingReselectTaskId != 0u)
{
EventBus.Publish(new TaskItemClickEvent { Data = _pendingReselectTaskId });
}
}
}
}
// Active view: refresh selected task detail periodically so countdown UI updates in real time.
// (Wait-time tasks depend on m_ulTimePassed which changes with server time; without this, UI looks "stuck".)
if (m_iType == 0 && Time.unscaledTime >= _nextActiveTimerUiRefreshAt)
{
_nextActiveTimerUiRefreshAt = Time.unscaledTime + 0.5f; // 2 Hz is plenty for countdown text
var pTree = m_pTv_Quest;
var pItem = pTree != null ? pTree.GetSelectedItem() : null;
if (pItem != null && pTree.transform != pItem.transform.parent)
{
uint selectedTaskId = pTree.GetItemData(pItem);
if (selectedTaskId > 0)
{
var task = GetHostPlayer()?.GetTaskInterface();
if (task != null)
{
Task_State_info tsi = default;
task.GetTaskStateInfo(selectedTaskId, ref tsi, true);
if (tsi.m_ulWaitTime > 0 || tsi.m_ulTimeLimit > 0 || tsi.m_ulProtectTime > 0)
{
UpdateTask((int)selectedTaskId);
}
}
}
}
}
// if( m_szName == "Win_Quest" && IsShow() )
{
var pTree = m_pTv_Quest;
var pItem = pTree.GetSelectedItem();
if( pItem )
{
for( int i = 0; i < m_ImgCount; i++ )
m_pImg_Item[i].gameObject.SetActive(false);
m_pTxt_BaseAward.gameObject.SetActive(false);
// if( pTree->GetParentItem(pItem) != pTree->GetRootItem() )
if( pTree.transform != pItem.transform.parent )
{
if (m_iType == 0)
{
UpdateTask((int)pTree.GetItemData(pItem));
}
else if (m_iType == 1)
{
SearchForTask((int)pTree.GetItemData(pItem));
}
}
else
{
m_idLastTask = -2;
m_pTxt_Content.SetText("");
m_pTxt_QuestItem.SetText("");
_nameTaskText.SetText("");
m_pBtn_Abandon.interactable = false;
UpdateTaskConfirm(0, false);
Btn_TreasureMap.interactable = false;
}
}
// TODO
// UpdateGotoNPC();
}
// return CDlgBase::Tick();
return true;
}
public void RefreshTaskTrace(bool bShowTrace)
{
m_bShowTrace = bShowTrace;
// Get AUIManager, fallback to GetGameUIMan() if m_pAUIManager is not set yet
// 获取AUIManager,如果m_pAUIManager尚未设置则回退到GetGameUIMan()
AUIManager auiManager = GetAUIManager();
if (auiManager == null) {
CECGameUIMan gameUIMan = GetGameUIMan();
if (gameUIMan == null) {
return;
}
auiManager = gameUIMan;
}
DlgTaskTrace pDlgTaskTrace = auiManager.GetDialog("Win_QuestMinion") as DlgTaskTrace;
if (pDlgTaskTrace == null)
return;
if (!m_bShowTrace)
{
pDlgTaskTrace.Show(false);
return;
}
ShowType showType = pDlgTaskTrace.GetShowType();
// bool bShow = showType == ShowType.ST_TRACED || showType == ShowType.ST_TITLE;
bool bShow =true;//for test
if (bShow)
{
CECHostPlayer host = GetHostPlayer();
CECTaskInterface pTask = host.GetTaskInterface();
ShowType2 showType2 = pDlgTaskTrace.GetShowType2();
List<int> tasks = new List<int>();
if(showType2 == ShowType2.ST_RECIEVED)
{
if( m_vecTasksCanFinish.Count > 0 )
{
for(int i = 0; i < m_vecTasksCanFinish.Count; i++ )
{
int idTask = m_vecTasksCanFinish[i];
bool bCanFinish = pTask.CanFinishTask((uint)idTask);
bool bHasTask = pTask.HasTask((uint)idTask);
if(!bHasTask || !bCanFinish)
{
// 任务未完成,状态变化要删除
m_vecTasksCanFinish.RemoveAt(i);
i--;
if (bHasTask && !bCanFinish) {
if (!m_vecTasksUnFinish.Contains(idTask))
m_vecTasksUnFinish.Add(idTask);
}
}
}
}
if( m_vecTasksUnFinish.Count > 0 )
{
for(int i = 0; i < m_vecTasksUnFinish.Count; i++ )
{
int idTask = m_vecTasksUnFinish[i];
bool bCanFinish = pTask.CanFinishTask((uint)idTask);
bool bHasTask = pTask.HasTask((uint)idTask);
if(!bHasTask || bCanFinish)
{
// ûˣ״̬仯ҪƳ
m_vecTasksUnFinish.RemoveAt(i);
i--;
if (bHasTask && bCanFinish) {
if (!m_vecTasksCanFinish.Contains(idTask))
m_vecTasksCanFinish.Add(idTask);
}
}
}
}
tasks.Capacity = m_vecTasksCanFinish.Count + m_vecTasksUnFinish.Count;
tasks.AddRange(m_vecTasksCanFinish);
tasks.AddRange(m_vecTasksUnFinish);
}
else if(showType2 == ShowType2.ST_NOT_RECIEVED)
{
// Can-get list is refreshed only on level-up, quest state change (UpdateQuestView), trace toggle on, or minimion "可接" tab — not every tick.
// 可接列表仅在升级、任务变更、打开追踪、小窗切到可接时刷新,避免每帧扫全表拖垮 FPS。
tasks.Capacity = m_vecTasksCanGet.Count;
tasks.AddRange(m_vecTasksCanGet);
}
List<ATaskTempl> titlle_list = new List<ATaskTempl>();
List<int> titletask_list = new List<int>();
//Dictionary<int, int> title_map = new Dictionary<int, int>();
if (pDlgTaskTrace.GetShowType() == ShowType.ST_TITLE)
{
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
//pMan.GetTitleTasks(pTask, titlle_list);
for (int i = 0; i < titlle_list.Count; i++) {
if (IsTaskTraceable(titlle_list[i].GetID()))
titletask_list.Add((int)titlle_list[i].GetID());
}
}
// Always refresh task trace, even when lists are empty, to clear/update the display
if (!(GetHostPlayer().IsDead() || GetHostPlayer().IsTrading() || 0 != GetHostPlayer().GetBoothState()))
{
pDlgTaskTrace.RefreshTaskTrace(tasks.ToArray(), tasks.Count, titletask_list.ToArray(), titletask_list.Count, true);
}
} else if (showType == ShowType.ST_CONTRIBUTION)
{
pDlgTaskTrace.UpdateContributionTask();
}
}
public bool IsTaskTraceable(uint idTask)
{
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
ATaskTempl pTemp = pMan.GetTaskTemplByID(idTask);
// ׷
if (IsQuestionTask(pTemp)) {
return false;
}
Task_State_info tsi = new Task_State_info();
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
pTask.GetTaskStateInfo(idTask, ref tsi);
bool bTrace = tsi.m_ulTimeLimit > 0 ||
tsi.m_ulProtectTime > 0 ||
tsi.m_ulNPCToProtect > 0 ||
tsi.m_MonsterWanted[0].m_ulMonstersToKill > 0 ||
tsi.m_PlayerWanted[0].m_ulPlayersToKill > 0 ||
tsi.m_ItemsWanted[0].m_ulItemId > 0 ||
tsi.m_ulWaitTime > 0 ||
tsi.m_TaskCharArr._finish > 0 ||
tsi.m_ulReachLevel > 0 ||
tsi.m_ulReachRealm > 0 ||
tsi.m_ulReachReincarnation > 0;
// check the condition from template
if(!bTrace)
{
if (pTemp.m_FixedData.m_ulReachSiteCnt > 0 ||
(pTemp.m_FixedData.m_ulAwardNPC > 0 && pTask.CanFinishTask((uint)idTask))){
bTrace = true;
}
}
return bTrace;
}
public bool IsQuestionTask(ATaskTempl pTemp){
return pTemp.m_FixedData.m_ulType == (uint)ENUM_TASK_TYPE.enumTTQuestion;
}
public bool UpdateTask(int idTask = -1)
{
// Only rebuild the list if viewing "Have Quest" (m_iType == 0)
// But always allow updating specific task details regardless of view type
if (idTask < 0 && m_iType != 0)
{
return true;
}
// ATaskTemplMan *pMan = GetGame()->GetTaskTemplateMan();
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
// Host player can be null during early UI bootstrap (before InitCharacter/self-info).
// In that case just no-op; quest UI will populate once the player exists and the dialog is reopened/refreshed.
var host = EC_Game.GetGameRun()?.GetHostPlayer();
if (host == null) return true;
CECTaskInterface pTask = host.GetTaskInterface();
if (pTask == null)
{
BMLogger.LogError("No CECTaskInterface found !!!");
return false;
}
// PAUITEXTAREA pTextDesc = m_pTxt_Content;
var pTextDesc = m_pTxt_Content;
// PAUITEXTAREA pTextItem = m_pTxt_QuestItem;
var pTextItem = m_pTxt_QuestItem;
string strNewTextItem = "";
string strNewHintItem = "";
bool bLastTaskChanged = false;
// PAUIOBJECT pObj = GetDlgItem("Txt_Contribution");
// if (pObj) {
// ACString strText;
// strText.Format(_AL("%d"), GetHostPlayer()->GetWorldContribution());
// pObj->SetText(strText);
// }
if ( idTask >= 0)
{
ATaskTempl pTemp = pMan.GetTaskTemplByID((uint)idTask);
if (pTemp != null)
{
// Only update description and name if task changed
if( idTask != m_idLastTask )
{
_nameTaskText.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(GetTaskNameWithColor(pTemp))));
//pTextDesc->SetText(FormatTaskText(pTemp->GetDescription(), pTextDesc->GetColor()));
pTextDesc.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(pTemp.GetDescription())));
m_idLastTask = idTask;
bLastTaskChanged = true;
}
m_pBtn_Abandon.interactable = pMan.CanGiveUpTask((uint)idTask);
// Always refresh task state info to get latest progress data
// This ensures real-time updates when task progress changes
// When viewing "Have Quest" (m_iType == 0), tasks are active, so pass true to read kill counts
Task_State_info tsi = new Task_State_info();
pTask.GetTaskStateInfo((uint)idTask, ref tsi, m_iType == 0);
// Clear first
strNewTextItem = "";
// Base desc
UpdateTaskBaseDesc(ref strNewTextItem, tsi);
// Award NPC
int nANPC = (int)pTemp.GetAwardNPC();
UpdateAwardNPC(ref strNewTextItem, nANPC, idTask);
// Complete condition - always refresh to show updated progress
UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi);
// Wanted Item - always refresh to show updated item counts
UpdateItemWanted(ref strNewTextItem, tsi, idTask);
// Treasure Map
UpdateTreasureMap(ref strNewTextItem);
// Task Confirm - always refresh to update button state
UpdateTaskConfirm(idTask, pTemp.m_FixedData.m_enumFinishType == (uint)TaskFinishType.enumTFTConfirm);
// Award - always refresh to show updated award preview
Task_Award_Preview award = default;
pTask.GetTaskAwardPreview((uint)idTask, ref award);
UpdateBaseAward(award);
UpdateItemAward(award);
// GameObject pObj = GetDlgItem<GameObject>("Btn_TreasureMap");
if (Btn_TreasureMap != null)
{
Btn_TreasureMap.gameObject.
SetActive(pTemp.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMReachTreasureZone);
}
}
else
{
Debug.LogError($"Task {idTask} not found ATaskTempl !!!");
}
}
else
{
ClearContent(true);
if (m_pBtn_FinishTask)
m_pBtn_FinishTask.gameObject.SetActive(false);
for (int i = 0; i < pTask.GetTaskCount(); i++)
{
int id = (int)pTask.GetTaskId((uint)i);
AddTaskNode(id);
}
SortTaskNodeByType();
// Resolve all prefabs after quest loading is complete (after SetLastItem() calls are done)
// 在任务加载完成后解析所有预制体(在SetLastItem()调用完成后)
m_pTv_Quest.ResolveAllPrefabs();
string strTemp;
int iMaxTaskCount = TaskInterfaceConstants.TASK_ACTIVE_LIST_MAX_LEN;
strTemp = $"{pTask.GetTaskCount()}/{iMaxTaskCount}";
if (m_pTxt_QuestNO != null) m_pTxt_QuestNO.text = strTemp;
}
// GetGameUIMan()->ReplaceColor(&strNewTextItem, A3DCOLORRGB(255, 255, 255), pTextItem->GetColor());
SetTextItemText(strNewTextItem, pMan.GetTaskTemplByID((uint)idTask) != null && !bLastTaskChanged, strNewHintItem);
return true;
}
//
// Guard: only handle search list when current UI type is 1 (search)
public bool SearchForTask(int idTask = -1)
{
// Only process search list when in search view (m_iType == 1)
// This prevents clearing the wrong list when updating
if (m_iType != 1)
{
return true;
}
// Setup managers and UI references
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
var pTextDesc = m_pTxt_Content;
var pTextItem = m_pTxt_QuestItem;
// Track composed text buffers and change flag
string strNewTextItem = "";
string strNewHintItem = "";
bool bLastTaskChanged = false;
// Simplified: assume we are in quest UI context if tree exists
bool bQuestUI = m_pTv_Quest != null;
// When a concrete task id is provided
if (idTask >= 0)
{
ATaskTempl pTemp = pMan != null ? pMan.GetTaskTemplByID((uint)idTask) : null;
if (pTemp != null)
{
// Update description when the selected task changes
if (idTask != m_idLastTask)
{
_nameTaskText.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(GetTaskNameWithColor(pTemp))));
Debug.Log("[DlgTask] SearchForTask name task: " + _nameTaskText.text);
if (pTextDesc != null) pTextDesc.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(pTemp.GetDescription())));
m_idLastTask = idTask;
bLastTaskChanged = true;
}
// Optional: update tree item text if needed (skip if API not available)
if (bQuestUI)
{
var pItem = m_pTv_Quest.GetSelectedItem();
if (pItem != null)
{
uint id = m_pTv_Quest.GetItemData(pItem);
// NOTE: ATaskTemplMan.IsTaskToPush may be conditionally compiled; avoid hard dependency
// If needed, uncomment when method is available:
// if (pMan.IsTaskToPush((int)id)) m_pTv_Quest.SetItemText(pItem, GetTaskNameWithColor(pTemp));
}
}
// Get task state info
Task_State_info tsi = default;
if (pTask != null) pTask.GetTaskStateInfo((uint)idTask, ref tsi, false);
// Reset composed text buffer
strNewTextItem = "";
// Append: base description
UpdateTaskBaseDesc(ref strNewTextItem, tsi);
// Append: deliver NPC
UpdateDeliverNPC(ref strNewTextItem, (int)pTemp.GetDeliverNPC(), idTask);
// Append: award NPC
int nANPC = (int)pTemp.GetAwardNPC();
UpdateAwardNPC(ref strNewTextItem, nANPC, idTask);
// Append: completion conditions
UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi);
// Append: wanted items
UpdateItemWanted(ref strNewTextItem, tsi, idTask);
// Preview and show awards
Task_Award_Preview award = default;
if (pTask != null) pTask.GetTaskAwardPreview((uint)idTask, ref award);
UpdateBaseAward(award);
UpdateItemAward(award);
}
else
{
// No template found for id: clear content
m_idLastTask = -2;
if (m_pTxt_Content != null) m_pTxt_Content.SetText("");
if (m_pTxt_QuestItem != null) m_pTxt_QuestItem.SetText("");
}
}
else
{
// zhangyitian 20140521 先将可接任务列表清空,再判断是否有可接任务
// zhangyitian 20140521 First clear the available tasks list, then check if there are available tasks
// 修正了原先没有可接任务时,可接任务列表显示已接任务的问题
// Fix: prevent accepted tasks from showing when there are no available tasks
ClearContent(false);
// TaskTemplLst ttl;
List<ATaskTempl> ttl = new List<ATaskTempl>(); // TaskTemplLst -> List<ATaskTempl>
pMan.GetAvailableTasks(pTask, ttl);
// Mirror available-to-accept IDs for trace / other systems (same set as tree nodes).
// 与可接任务树节点一致,填充可接任务 ID 列表供追踪等系统使用。
m_vecTasksCanGet.Clear();
for (int j = 0; j < ttl.Count; j++)
m_vecTasksCanGet.Add((int)ttl[j].GetID());
if( ttl.Count <= 0 ) return true;
for(int i = 0; i < ttl.Count; i++ )
{
int id = (int)ttl[i].GetID();
AddTaskNode(id);
}
SortTaskNodeByType();
// Resolve all prefabs after quest loading is complete (after SetLastItem() calls are done)
// 在任务加载完成后解析所有预制体(在SetLastItem()调用完成后)
m_pTv_Quest.ResolveAllPrefabs();
// string strTemp;
// ActiveTaskList pLst = (ActiveTaskList)pTask.GetActiveTaskList();
// int iMaxTaskCount = pLst->GetMaxSimultaneousCount();
// strTemp.Format(_AL("%d/%d"), pTask->GetTaskCount(), iMaxTaskCount);
// m_pTxt_QuestNO->SetText(strTemp);
}
// Apply colors and set composed text into UI
bool hasTempl = idTask >= 0 && pMan != null && pMan.GetTaskTemplByID((uint)idTask) != null;
SetTextItemText(strNewTextItem, hasTempl && !bLastTaskChanged, strNewHintItem);
// Done
return true;
}
//
// //бɽѽѽ zhangyitian
// When task updates, the available task list also needs to be updated, otherwise the available task list won't update
public bool UpdateQuestView()
{
// Refresh the list for the current view type
// This ensures that when tasks are taken/completed/abandoned, the visible list is updated
bool result = true;
if (m_iType == 0)
{
// Refresh "Have Quest" list (taken tasks)
result = UpdateTask(-1);
}
else if (m_iType == 1)
{
// Refresh "Search Quest" list (available tasks)
result = SearchForTask(-1);
}
// Refresh the currently selected task details if one is selected
// This ensures task progress updates are reflected in real-time
var pTree = m_pTv_Quest;
var pItem = pTree?.GetSelectedItem();
if (pItem != null && pTree.transform != pItem.transform.parent)
{
uint selectedTaskId = pTree.GetItemData(pItem);
if (selectedTaskId > 0)
{
if (m_iType == 0)
{
// Refresh the selected task's details to show updated progress
UpdateTask((int)selectedTaskId);
}
else if (m_iType == 1)
{
// For search view, refresh the selected task
SearchForTask((int)selectedTaskId);
}
}
}
// Have-quest tab: rebuild can-get cache for trace minimion. Search tab: SearchForTask(-1) already filled m_vecTasksCanGet.
// 已接任务页:重建可接缓存。搜索页:SearchForTask 已写 m_vecTasksCanGet,勿二次全表扫描。
if (m_iType == 0)
RefreshVecTasksCanGet();
return result;
}
//
// bool IsPQTaskOrSubTask(int idTask);
// bool IsTreasureMapTask(int idTask);
//
// bool TraceTask(int idTask);
public void SyncTrace(object pData, bool fromServer)
{
USER_LAYOUT pul = (USER_LAYOUT)pData;
if(fromServer)
{
m_vecTasksUnFinish.Clear();
m_vecTasksCanFinish.Clear();
uint dwTraceMask = pul.dwTraceMask;
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
for(int i = 0; i < (int)pTask.GetTaskCount(); i++ )
{
if( (dwTraceMask & (1 << i)) != 0 )
{
TraceTask(pTask.GetTaskId((uint)i));
}
}
// store m_bShowTrace flag instead of bTraceAll flag
m_bShowTrace = pul.bTraceAll;
RefreshVecTasksCanGet();
}
else
{
pul.bTraceAll = m_bShowTrace;
uint dwTraceMask = 0;
int i = 0;
for(i=0;i<m_vecTasksCanFinish.Count;i++)
{
int index = GetTaskIndex(m_vecTasksCanFinish[i]);
if (index >= 0)
{
dwTraceMask |= (uint)(1 << index);
}
}
for(i=0;i<m_vecTasksUnFinish.Count;i++)
{
int index = GetTaskIndex(m_vecTasksUnFinish[i]);
if (index >= 0)
{
dwTraceMask |= (uint)(1 << index);
}
}
pul.dwTraceMask = dwTraceMask;
}
}
// bool IsShowTrace(){return m_bShowTrace;}
//
// typedef CECGame::ObjectCoords ObjectCoords;
// static const ObjectCoords& GetObjectCoords() { return m_TargetCoord; }
// static const ACString& GetTraceName() { return m_strTraceName; }
// static void SetTraceObjects(const ObjectCoords& objs, const ACString& name);
// static const MINE_ESSENCE* SearchTaskMine(int idTask);
//
// ACString GetKillPlayerRequirements(const Task_State_info& tsi,int iIndex);
//
// void SwitchTaskTrace(int idTask);
// void OnTaskPush(); // µĿɽ
// void OnTaskProcessUpdated(int idTask); // ѽҪǰʾ
// void OnTaskItemGained(int idItem);
public static MINE_ESSENCE SearchTaskMine(int idTask, out bool found)
{
if(idTask <= 0)
{
found = false;
return default;
}
if (m_TaskMines.Count == 0)
{
var pDataMan = ElementDataManProvider.GetElementDataMan();
foreach (var kv in pDataMan.essence_id_data_type_map)
{
if (kv.Value == DATA_TYPE.DT_MINE_ESSENCE && kv.Key != 0)
{
DATA_TYPE dt = DATA_TYPE.DT_INVALID;
var pMine = (MINE_ESSENCE)pDataMan.get_data_ptr(kv.Key, ID_SPACE.ID_SPACE_ESSENCE, ref dt);
m_TaskMines[(int)pMine.task_in] = pMine;
}
}
// avoid duplicated init
m_TaskMines[0] = null;
}
var itr = m_TaskMines.TryGetValue(idTask, out var value);
found =itr;
return itr ? (MINE_ESSENCE)value : default;
}
#endregion
#region PRIVATE METHODS
/// <summary>
/// Rebuild m_vecTasksCanGet from ATaskTemplMan.GetAvailableTasks (same source as Search quest tree).
/// 用 GetAvailableTasks 重建可接任务 ID 列表,与可接任务搜索树数据源一致。
/// </summary>
private void RefreshVecTasksCanGet(CECTaskInterface pTaskIfKnown = null)
{
RebuildAcceptableQuestCache(pTaskIfKnown);
}
/// <summary>
/// Rebuilds the cached list of quests the host may accept (trace minimion "可接", etc.).
/// Safe without a DlgTask instance — call from UI manager on level-up and similar.
/// 重建玩家当前可接任务 ID 缓存;可无对话框实例,由 UI 管理器在升级等时机调用。
/// </summary>
public static void RebuildAcceptableQuestCache(CECTaskInterface pTaskIfKnown = null)
{
m_vecTasksCanGet.Clear();
CECTaskInterface pTask = pTaskIfKnown;
if (pTask == null)
{
var run = EC_Game.GetGameRun();
pTask = run?.GetHostPlayer()?.GetTaskInterface();
}
if (pTask == null) return;
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
if (pMan == null) return;
List<ATaskTempl> ttl = new List<ATaskTempl>();
pMan.GetAvailableTasks(pTask, ttl);
for (int i = 0; i < ttl.Count; i++)
m_vecTasksCanGet.Add((int)ttl[i].GetID());
}
private bool OnInitDialog()
{
// m_pTxt_QuestNO = (PAUILABEL)GetDlgItem("Txt_QuestNO");
// m_pTv_Quest = (PAUITREEVIEW)GetDlgItem("Tv_Quest");
// m_pTxt_Content = dynamic_cast<PAUITEXTAREA>(GetDlgItem("Txt_Content"));
// m_pTxt_QuestItem = dynamic_cast<PAUITEXTAREA>(GetDlgItem("Txt_QuestItem"));
// m_pBtn_Abandon = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_Abandon");
// m_pTxt_BaseAward = (PAUILABEL)GetDlgItem("Txt_BaseAward");
// m_pBtn_SearchQuest = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_SearchQuest");
// m_pBtn_HaveQuest = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_HaveQuest");
// m_pBtn_bShowTrace = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_ShowTrace");
// m_pBtn_FinishTask = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_FinishTask");
// m_pBtn_GotoNPC = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_GotoNPC");
if (m_pBtn_FinishTask) m_pBtn_FinishTask.gameObject.SetActive(false);
if (m_pBtn_GotoNPC) m_pBtn_GotoNPC.gameObject.SetActive(false);
// TODO: Set button pushed state
// if (m_pBtn_HaveQuest != null) { /* set pushed state if needed */ }
GameObject pObj = GetDlgItem<GameObject>("Btn_TreasureMap");
if (pObj != null)
{
pObj.SetActive(false);
}
pObj = GetDlgItem<GameObject>("Img_New");
if (pObj != null)
{
pObj.SetActive(false);
}
if (m_pTxt_QuestNO != null)
{
m_pTxt_QuestNO.text = "0";
}
m_TaskTraceCounter.SetPeriod(950);
return true;
}
private T GetDlgItem<T>(string name)
{
var t = transform.Find(name);
if (t != null) return t.GetComponent<T>();
return default(T);
}
public new CECHostPlayer GetHostPlayer()
{
if(EC_Game.GetGameRun() == null)
{
BMLogger.LogError("EC_Game.GetGameRun() is null !!!");
return null;
}
if (EC_Game.GetGameRun().GetHostPlayer() == null)
{
BMLogger.LogError("EC_Game.GetHostPlayer() is null !!!");
return null;
}
return EC_Game.GetGameRun().GetHostPlayer();
}
//
// virtual bool OnInitDialog();
void OnShowDialog()
{
AUIManager auiManager = GetAUIManager();
if (m_idSelTask != 0)
UpdateTask();
if(auiManager != null)
{
m_pTog_bShowTrace.isOn = auiManager.IsDialogShow("Win_QuestMinion");
}
}
void OnHideDialog()
{
if (m_szName != "Win_Quest") return;
var pTree = m_pTv_Quest;
var pItem = pTree?.GetSelectedItem();
if( pItem )
m_idSelTask = (int)pTree.GetItemData(pItem);
else m_idSelTask = 0;
}
// virtual void OnHideDialog();
// virtual bool OnChangeLayout(PAUIOBJECT pMine, PAUIOBJECT pTheir);
// virtual void OnChangeLayoutEnd(bool bAllDone);
//
private void InsertTaskChildren(TaskTreeViewHolder pRoot, uint idTask, bool bExpand, bool bKey)
{
var pTreeTask = m_pTv_Quest;
var pMan = EC_Game.GetTaskTemplateMan();
var pTask = GetHostPlayer()?.GetTaskInterface();
if (pTreeTask == null || pMan == null || pTask == null) return;
ATaskTempl parentTempl = pMan.GetTaskTemplByID(idTask);
if (parentTempl == null) return;
ATaskTempl child = parentTempl.m_pFirstChild;
while (child != null)
{
uint id = child.m_FixedData.m_ID;
Color disPlayColor=Color.white;
string text = GetTaskNameWithColor(child,out disPlayColor);
var pItem = pTreeTask.InsertItem(text, pRoot, null);
pItem.SetItemTextColor(disPlayColor);
if (pItem != null)
{
pTreeTask.SetItemData(pItem, id);
if ((int)id == m_idSelTask)
{
if (m_pBtn_Abandon != null) m_pBtn_Abandon.interactable = true;
UpdateTask((int)id);
}
// Optional: colorize key tasks if UI supports it
}
InsertTaskChildren(pItem, id, bExpand, bKey);
child = child.m_pNextSibling;
}
}
// [中文] 仅插入“已接任务(Active)”中的子任务(基于 ActiveTaskList 的 Child/NextSbl 索引)
// [English] Insert only active subtasks (based on ActiveTaskList Child/NextSbl indices)
private void InsertActiveTaskChildren(TaskTreeViewHolder pRoot, uint idTask)
{
var pTreeTask = m_pTv_Quest;
var pMan = EC_Game.GetTaskTemplateMan();
var pTask = GetHostPlayer()?.GetTaskInterface();
if (pTreeTask == null || pMan == null || pTask == null) return;
ActiveTaskList pList = pTask.GetActiveTaskList();
if (pList == null) return;
ActiveTaskEntry parentEntry = pList.GetEntry(idTask);
if (parentEntry == null) return;
char idx = parentEntry.m_ChildIndex;
while (idx != (char)0xff)
{
int childIndex = (byte)idx;
if (childIndex < 0 || childIndex >= TaskInterfaceConstants.TASK_ACTIVE_LIST_MAX_LEN) break;
ActiveTaskEntry childEntry = pList.m_TaskEntries[childIndex];
if (childEntry == null || childEntry.m_ID == 0) break;
uint childId = childEntry.m_ID;
ATaskTempl childTempl = pMan.GetTaskTemplByID(childId);
Color disPlayColor=Color.white;
string text = childTempl != null ? GetTaskNameWithColor(childTempl,out disPlayColor) : $"Task {childId}";
var pItem = pTreeTask.InsertItem(text, pRoot, null);
pItem.SetItemTextColor(disPlayColor);
if (pItem != null)
{
pTreeTask.SetItemData(pItem, childId);
if ((int)childId == m_idSelTask)
{
if (m_pBtn_Abandon != null) m_pBtn_Abandon.interactable = true;
UpdateTask((int)childId);
}
// recurse into active children
InsertActiveTaskChildren(pItem, childId);
}
idx = childEntry.m_NextSblIndex;
}
}
private void SetTextItemText(string strNewTextItem, bool keepScrollPos, string strNewHintItem)
{
var pTextItem = m_pTxt_QuestItem;
if (pTextItem == null) return;
// Preserve scroll position if inside a ScrollRect
// UnityEngine.UI.ScrollRect scrollRect = pTextItem.GetComponentInParent<UnityEngine.UI.ScrollRect>();
// float oldNormPos = scrollRect != null ? scrollRect.verticalNormalizedPosition : 0f;
string formatted = EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(strNewTextItem ?? string.Empty));
if (!string.Equals(formatted, pTextItem.text))
{
pTextItem.text = formatted;
// TODO: apply hint to a tooltip UI if available (strNewHintItem)
}
// if (keepScrollPos && scrollRect != null)
// {
// // Restore previous scroll position
// scrollRect.verticalNormalizedPosition = oldNormPos;
// }
}
// void SetTaskText(PAUIOBJECT pObj, ACString* pStr);
int GetTaskIndex(int idTask)
{
// find the task index in task list
if(idTask <= 0)
{
return -1;
}
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
for(int i = 0; i < (int)pTask.GetTaskCount(); i++ )
{
int curTask = pTask.GetTaskId(i);
if(idTask == curTask || pTask.CheckParent((uint)curTask, (uint)idTask))
return i;
}
return -1;
}
//
// bool IsQuest()const;
// bool IsShowHaveQuest()const;
// int GetSelectedTaskFromUI();
// bool IsTreasureMapSelected();
//
// // update task content in dialog
// update task content in dialog (converted from C++)
private void UpdateBaseAward(Task_Award_Preview award)
{
// Ported from original C++: strTemp.Format(GetStringFromTable(3201), award.m_ulGold);
var sb = new System.Text.StringBuilder();
int colCount = 0;
const int col = 3;
string strTemp;
if (award.m_ulGold > 0)
{
strTemp = Format(GetStringFromTable(3201), award.m_ulGold);
sb.Append(strTemp);
if ((++colCount) % col == 0) sb.Append("\n");
}
if (award.m_ulExp > 0)
{
strTemp = Format(GetStringFromTable(3202), award.m_ulExp);
sb.Append(strTemp);
if ((++colCount) % col == 0) sb.Append("\n");
}
if (award.m_ulSP > 0)
{
strTemp = Format(GetStringFromTable(3203), award.m_ulSP);
sb.Append(strTemp);
if ((++colCount) % col == 0) sb.Append("\n");
}
if (award.m_ulRealmExp > 0)
{
strTemp = Format(GetStringFromTable(3207), award.m_ulRealmExp);
sb.Append(strTemp);
if ((++colCount) % col == 0) sb.Append("\n");
}
if (award.m_iForceContrib > 0)
{
strTemp = Format(GetStringFromTable(3205), award.m_iForceContrib);
sb.Append(strTemp);
if ((++colCount) % col == 0) sb.Append("\n");
}
if (award.m_iForceRepu > 0)
{
strTemp = Format(GetStringFromTable(3206), award.m_iForceRepu);
sb.Append(strTemp);
if ((++colCount) % col == 0) sb.Append("\n");
}
if (sb.Length > 0 && m_pTxt_BaseAward != null)
{
m_pTxt_BaseAward.text = EC_Utility.FormatForTextMeshPro(sb.ToString());
m_pTxt_BaseAward.gameObject.SetActive(true);
}
}
private void UpdateItemAward(Task_Award_Preview award)
{
bool bShowItem = false;
if (award.m_bHasItem)
{
if (!award.m_bItemKnown)
{
if (m_pTxt_BaseAward != null && m_pTxt_BaseAward.gameObject.activeSelf)
{
string strAward = (GetStringFromTable(3204) ?? string.Empty) + "\n" + (m_pTxt_BaseAward.text ?? string.Empty);
m_pTxt_BaseAward.text = EC_Utility.FormatForTextMeshPro(strAward);
}
else if (m_pTxt_BaseAward != null)
{
m_pTxt_BaseAward.text = EC_Utility.FormatForTextMeshPro(GetStringFromTable(3204) ?? string.Empty);
m_pTxt_BaseAward.gameObject.SetActive(true);
}
}
else
{
int max = m_pImg_Item != null ? m_pImg_Item.Length : 0;
for (int i = 0; i < max; i++)
{
if (i < award.m_ulItemTypes)
{
var img = m_pImg_Item[i];
if (img == null) continue;
var sprite = EC_IvtrItemUtils.Instance.ResolveItemIconSprite((int)award.m_ItemsId[i]);
if (sprite != null) img.sprite = sprite;
img.color = Color.white;
img.gameObject.SetActive(true);
var countLabel = img.GetComponentInChildren<TMP_Text>(true);
if (countLabel != null) countLabel.text = award.m_ItemsNum[i].ToString();
bShowItem = true;
}
else if (m_pImg_Item[i] != null)
{
m_pImg_Item[i].gameObject.SetActive(false);
}
}
}
}
// adjust the label position relative to item icons (approximate)
if (m_pTxt_BaseAward != null && m_pImg_Item != null && m_pImg_Item.Length > 0 && m_pImg_Item[0] != null)
{
var txtRT = m_pTxt_BaseAward.rectTransform;
var imgRT = m_pImg_Item[0].rectTransform;
var pos = imgRT.anchoredPosition;
var sz = imgRT.sizeDelta;
float margin = 2f;
if (bShowItem) txtRT.anchoredPosition = new Vector2(pos.x, pos.y - (sz.y + margin));
else txtRT.anchoredPosition = pos;
}
}
private void UpdateTaskBaseDesc(ref string strText, Task_State_info tsi)
{
// Build the base description text from task state
var sb = new System.Text.StringBuilder();
// NOTE: Original appended each entry in tsi.m_TaskCharArr (vector<wchar_t*>)
// In C#, this array is not directly available; content is already localized elsewhere.
// Append error message if any
if (tsi.m_ulErrCode != 0)
{
string szMsg = GetFixedMsg(tsi.m_ulErrCode);
if (!string.IsNullOrEmpty(szMsg))
{
sb.Append("<color=#ff0000>");
sb.Append(szMsg);
string strTemp;
if (tsi.m_ulErrCode == TaskInterfaceConstants.TASK_AWARD_FAIL_LEVEL_CHECK)
strTemp = Format(GetStringFromTable(7637), tsi.m_ulPremLevelMin);
else
strTemp = GetStringFromTable(807);
sb.Append(strTemp);
sb.AppendLine("</color>");
}
}
// Time limit and remaining time
if (tsi.m_ulTimeLimit > 0)
{
int nSec = (int)tsi.m_ulTimeLimit;
sb.Append(FormatTime(nSec, GetStringFromTable(245), 0));
int remain = System.Math.Max(0, (int)tsi.m_ulTimeLimit - (int)tsi.m_ulTimePassed);
sb.Append(FormatTime(remain, GetStringFromTable(246), 0));
}
// Wait time
if (tsi.m_ulWaitTime > 0)
{
int nSec = System.Math.Max(0, (int)tsi.m_ulWaitTime - (int)tsi.m_ulTimePassed);
sb.Append(FormatTime(nSec, GetStringFromTable(199), 0));
}
// Protect NPC info and timers
if (tsi.m_ulNPCToProtect > 0)
{
// Fallback text with NPC id; detailed name lookup omitted
sb.Append(Format(GetStringFromTable(257) ?? "Protect NPC: %d", tsi.m_ulNPCToProtect));
sb.Append(FormatTime((int)tsi.m_ulProtectTime, GetStringFromTable(258), 0));
int remain = System.Math.Max(0, (int)tsi.m_ulProtectTime - (int)tsi.m_ulTimePassed);
sb.Append(FormatTime(remain, GetStringFromTable(259), 0));
}
// Apply to content text
// if (m_pTxt_Content != null)
// {
// m_pTxt_Content.text += sb.ToString();
// }
strText += sb.ToString();
}
public static string FormatTime(int nSec, string desc, int timeLimit)
{
int total = System.Math.Max(0, nSec);
int h = total / 3600;
int m = (total % 3600) / 60;
int s = total % 60;
// Matches table printf "%d:%02d:%02d" (hours, zero-padded min/sec); works for durations over 24h.
string timeStr = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}:{1:D2}:{2:D2}", h, m, s);
if (string.IsNullOrEmpty(desc))
return timeStr;
const string kPrintfTime = "%d:%02d:%02d";
if (desc.IndexOf(kPrintfTime, System.StringComparison.Ordinal) >= 0)
return desc.Replace(kPrintfTime, timeStr);
return desc + timeStr;
}
// private static string GetStringFromTable(int id)
// {
// // TODO: return AUIManager.GetStringFromTable(id);
//
// // HARD CODED STRINGS
// switch (id)
// {
// case 3101:
// return "Hàng ngày";
// case 3102:
// return "Tu chân";
// case 3103:
// return "Chủ tuyến";
// case 3104:
// return "Phụ tuyến";
// case 3105:
// return "Event";
// case 3106:
// return "7 Killer List";
// case 3107:
// return "Bang hội";
// case 3108:
// return "Management";
// case 3109:
// return "Huyền thoại";
// case 3110:
// return "Câu hỏi";
// case 3201:
// return "Gold:";
// case 3202:
// return "EXP:";
// case 3203:
// return "SP:";
// case 3207:
// return "Realm EXP:";
// case 3205:
// return "Contribution:";
// case 3206:
// return "Reputation:";
// case 7621:
// return "NPC giao nhiệm vụ: ";
// default:
// return $"UnKnown_{id} ";
// }
// }
private string GetStringFromTable(uint id)
{
// return AUIManager.GetStringFromTable(id);
return GetStringFromTable((int)id);
}
private static string GetFixedMsg(uint id)
{
return BrewMonster.Network.EC_Game.GetFixedMsgs()?.GetWideString((int)id) ?? string.Empty;
}
private void UpdateDeliverNPC(ref string strText, int nDNPC, int idTask = 0)
{
// [中文] 交付NPC
// [English] Deliver NPC
if (nDNPC == 0)
{
return;
}
// [中文] 从元素数据中查找NPC
// [English] Lookup NPC from element data
string npcName = string.Empty;
var edm = BrewMonster.ElementDataManProvider.GetElementDataMan();
if (edm != null)
{
if (edm.essence_id_data_type_map.TryGetValue((uint)nDNPC, out var dtype)
&& dtype == DATA_TYPE.DT_NPC_ESSENCE
&& edm.essence_id_data_map.TryGetValue((uint)nDNPC, out var obj)
&& obj is NPC_ESSENCE npc)
{
npcName = npc.Name;
}
}
if (string.IsNullOrEmpty(npcName)) npcName = nDNPC.ToString();
// [中文] 追加到内容文本,如果找到坐标则添加可点击链接(如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));
// Always create clickable link (even if coords aren't known yet).
// 总是创建可点击链接(即使暂时不知道坐标)。
sb.Append($"<link=\"coord_{nDNPC}\"><color=#00FF00>{npcName}</color></link>");
sb.Append("\n");
strText += sb.ToString();
}
private A3DVECTOR3 UpdateAwardNPC(ref string strText, int nANPC, int idTask = 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, 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)
{
// Setup host reference
// 设置宿主引用
var pHost = GetHostPlayer();
// Monster kill requirements
// 怪物击杀条件
for (int i = 0; i < TaskInterfaceConstants.MAX_MONSTER_WANTED; i++)
{
if (tsi.m_MonsterWanted[i].m_ulMonsterId == 0) break;
uint id = tsi.m_MonsterWanted[i].m_ulMonsterId;
if (tsi.m_MonsterWanted[i].m_ulMonstersKilled > 0 || tsi.m_MonsterWanted[i].m_ulMonstersToKill > 0)
{
// Resolve monster name
// 解析怪物名称
string strName = "<color=#00FF00>????<color=#FFFFFF>";
var edm = BrewMonster.ElementDataManProvider.GetElementDataMan();
if (edm != null && edm.essence_id_data_type_map.TryGetValue(id, out var dtype)
&& dtype == DATA_TYPE.DT_MONSTER_ESSENCE
&& edm.essence_id_data_map.TryGetValue(id, out var obj)
&& obj is MONSTER_ESSENCE pMonster)
{
// strName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(me.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
// 构建该怪物需求描述
string strTemp = "";
if (tsi.m_MonsterWanted[i].m_ulMonstersToKill > 0)
{
strTemp += Format(GetStringFromTable(7624), strName,
tsi.m_MonsterWanted[i].m_ulMonstersKilled, tsi.m_MonsterWanted[i].m_ulMonstersToKill);
}
else
{
strTemp += Format(GetStringFromTable(256), tsi.m_MonsterWanted[i].m_ulMonstersKilled);
}
// Prefix label for first/next item
// 首项/后续项的前缀标签
strText += (i == 0) ? GetStringFromTable(7622) : GetStringFromTable(7626);
strText += strTemp;
}
}
// Player kill requirements
// 击杀玩家条件
for (int i = 0; i < TaskInterfaceConstants.MAX_PLAYER_WANTED; i++)
{
if (tsi.m_PlayerWanted[i].m_ulPlayersToKill == 0) break;
if (tsi.m_ItemsWanted[i].m_ulItemId > 0) continue;
strText += (i == 0) ? GetStringFromTable(7630) : GetStringFromTable(7626);
strText += GetKillPlayerRequirements(tsi, i);
}
// Gold requirement
// 金币需求
if (tsi.m_ulGoldWanted != 0)
{
string strTemp = Format(GetStringFromTable(7636), tsi.m_ulGoldWanted);
strText += strTemp;
}
// Reincarnation requirement
// 转生次数需求
if (tsi.m_ulReachReincarnation != 0)
{
int iLevel = GetReincarnationCount(pHost);
string strColor = (iLevel < (int)tsi.m_ulReachReincarnation) ? "<color=#ff0000>" : "<color=#00ff00>";
if (iLevel < (int)tsi.m_ulReachReincarnation)
{
strHint += Format(GetStringFromTable(11144), iLevel);
}
strText += strColor;
strText += Format(GetStringFromTable(11141), tsi.m_ulReachReincarnation);
}
// Level requirement
// 等级需求
if (tsi.m_ulReachLevel != 0 && pHost != null)
{
int iLevel = pHost.GetBasicProps().iLevel;
string strColor = (iLevel < (int)tsi.m_ulReachLevel) ? "<color=#ff0000>" : "<color=#00ff00>";
if (iLevel < (int)tsi.m_ulReachLevel)
{
strHint += Format(GetStringFromTable(11143), iLevel);
}
strText += strColor;
strText += Format(GetStringFromTable(11140), tsi.m_ulReachLevel);
}
// Realm requirement
// 境界等级需求
if (tsi.m_ulReachRealm != 0 && pHost != null)
{
int iLevel = GetRealmLevel(pHost);
string strColor = (iLevel < (int)tsi.m_ulReachRealm) ? "<color=#ff0000>" : "<color=#00ff00>";
if (iLevel < (int)tsi.m_ulReachRealm)
{
strHint += Format(GetStringFromTable(11145), iLevel);
}
strText += strColor;
strText += Format(GetStringFromTable(11142), (int)tsi.m_ulReachRealm);
}
}
// Build text for player kill requirements
// 构建击杀玩家需求的文本
private string GetKillPlayerRequirements(Task_State_info tsi, int index)
{
uint killed = tsi.m_PlayerWanted[index].m_ulPlayersKilled;
uint toKill = tsi.m_PlayerWanted[index].m_ulPlayersToKill;
return $" {killed}/{toKill}\n";
}
// Get host reincarnation count (fallback implementation)
// 获取宿主转生次数(回退实现)
private int GetReincarnationCount(CECHostPlayer host)
{
return 0; // TODO: Replace with actual value when available
}
// Get host realm level via basic props level2
// 通过二级等级获取宿主境界等级
private int GetRealmLevel(CECHostPlayer host)
{
return host.GetBasicProps().iLevel2;
}
// Update wanted items section
// 更新需要的物品部分
private void UpdateItemWanted(ref string strText, Task_State_info tsi, int idTask)
{
// Resolve task template
// 获取任务模板
var pMan = EC_Game.GetTaskTemplateMan();
var pTempl = pMan != null ? pMan.GetTaskTemplByID((uint)idTask) : null;
if (pTempl == null) return;
// Iterate wanted items
// 遍历需要的物品
for (int i = 0; i < TaskInterfaceConstants.MAX_ITEM_WANTED; i++)
{
if (tsi.m_ItemsWanted[i].m_ulItemId == 0) break;
// Resolve item name
// 解析物品名称
int itemTid = unchecked((int)tsi.m_ItemsWanted[i].m_ulItemId);
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), displayName,
tsi.m_ItemsWanted[i].m_ulItemsGained,
tsi.m_ItemsWanted[i].m_ulItemsToGet);
// Prefix for first or subsequent entries
// 首项或后续项前缀
strText += (i == 0) ? GetStringFromTable(7623) : GetStringFromTable(7626);
strText += strTemp;
// If task is KillPlayer, also add player requirements line
// 若为击杀玩家任务,同时追加玩家需求行
if (pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMKillPlayer && i < TaskInterfaceConstants.MAX_PLAYER_WANTED)
{
strText += GetKillPlayerRequirements(tsi, i);
}
}
}
private void UpdateTreasureMap(ref string strText)
{
// if (IsTreasureMapSelected())
// {
// EditBoxItemBase item(enumEICoord);
// item.SetName(GetStringFromTable(7629));
// item.SetInfo(GetStringFromTable(7629));
// item.SetColor(A3DCOLORRGB(0, 255, 0));
//
// strText += (ACHAR)AUICOMMON_ITEM_CODE_START + item.Serialize();
// }
}
private void UpdateTaskConfirm(int idTask, bool bFinishType)
{
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
if (m_pBtn_FinishTask != null && pTask != null && bFinishType)
{
m_pBtn_FinishTask.gameObject.SetActive(true);
// TODO: Enable/disable based on task readiness
// m_pBtn_FinishTask->Enable(pTask->IsTaskReadyToConfirm(idTask));
m_pBtn_FinishTask.interactable = pTask.IsTaskReadyToConfirm(idTask);
}
else m_pBtn_FinishTask.gameObject.SetActive(false);
}
// void UpdateGotoNPC();
// void ClearGotoNPC();
//
// clear the task content in dialog
void ClearContent(bool clearNPC)
{
m_idLastTask = -2;
m_pTxt_Content.SetText("");
m_pTxt_BaseAward.gameObject.SetActive(false);
for( int j = 0; j < m_ImgCount; j++ )
{
m_pImg_Item[j].gameObject.SetActive(false);
// TODO: Clear image data
// m_pImg_Item[j]->SetData(0);
}
// TODO: Clear Tree quest view
m_pTv_Quest.DeleteAllItems();
}
// // add node to task tree
void AddTaskNode(int id)
{
var pTreeTask = m_pTv_Quest;
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
ATaskTempl pTemp = pMan.GetTaskTemplByID((uint)id);
if( pTemp == null )
{
return;
}
uint nTaskType = pTemp.m_FixedData.m_ulType;
if (pTemp.m_FixedData.m_DynTaskType != 0) nTaskType = (uint)ENUM_TASK_TYPE.enumTTEvent;
uint nAfterType = 0;
TaskTreeViewHolder pAfter = null, pParent = null;
// P_AUITREEVIEW_ITEM pItem = pTreeTask->GetFirstChildItem(pTreeTask->GetRootItem());
// var pItem = pTreeTask.transform.parent.GetChild(0).GetComponent<TaskTreeViewItem>();
var pItem = pTreeTask.GetFirstChild();
while( pItem )
{
uint nType = pItem.GetItemData();
if( nType == nTaskType ){
pParent = pItem;
break;
}
else if (nType < nTaskType && nType > nAfterType){
nAfterType = nType;
pAfter = pItem;
}
pItem = pTreeTask.GetNextSiblingItem(pItem);
}
// add Biggest node if not exist
string strItem = GetTaskNameWithColor(pTemp, out Color color);
if(pParent ==null)
{
pParent = pTreeTask.InsertItem(GetStringFromTable(3101 + nTaskType - 100), null, pAfter);
pParent.SetItemTextColor(color);
// TODO: Expand tree node
// pTreeTask.Expand(pParent, AUITREEVIEW_EXPAND_EXPAND);
pTreeTask.SetItemData(pParent, nTaskType);
//if(nTaskType == enumTTLevel2)
// pTreeTask.SetItemTextColor(pParent, GetTaskColor(pTemp));
}
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
if (pTask == null) return;
bool bTaskPushed = pMan.IsTaskToPush(id) && !pTask.HasTask((uint)id);
if (bTaskPushed) {
strItem += GetStringFromTable(3100);
}
pItem = pTreeTask.InsertItem(strItem, pParent, null);
pItem.SetItemTextColor(color);
if( pTemp.IsKeyTask() )
{
pItem.SetItemTextColor(GetTaskColor((int)ENUM_TASK_TYPE.enumTTLevel2));
}
pTreeTask.SetItemData(pItem, (uint)id);
// HaveQuest view: children should reflect ActiveTaskList, not template tree (otherwise they never disappear on completion)
if (m_iType == 0)
InsertActiveTaskChildren(pItem, (uint)id);
else
InsertTaskChildren(pItem, (uint)id, true, pTemp.IsKeyTask());
if( (int)id == m_idSelTask )
{
// TODO : select the item in UI
// pTreeTask.SelectItem(pItem);
// m_pBtn_Abandon->Enable(true);
UpdateTask(id);
}
}
private void SortTaskNodeByType()
{
var pTreeTask = m_pTv_Quest;
if (pTreeTask == null) return;
// Collect direct children under the tree root (this component's transform)
int childCount = pTreeTask.transform.childCount;
var items = new List<(uint type, TaskTreeViewHolder item)>(childCount);
for (int i = 0; i < childCount; i++)
{
var child = pTreeTask.transform.GetChild(i).GetComponent<TaskTreeViewHolder>();
if (child == null) continue;
uint nType = pTreeTask.GetItemData(child);
items.Add((nType, child));
}
// Sort by type ascending
items.Sort((a, b) => a.type.CompareTo(b.type));
// Reorder siblings to match sorted order
for (int i = 0; i < items.Count; i++)
{
items[i].item.transform.SetSiblingIndex(i);
}
}
// // whether the task can be traced
// bool IsTaskTraceable(int idTask);
string Format( string formatStr, params object[] args )
{
// Ported from original C++ ACString::Format() which uses vsprintf/vswprintf
// Original implementation: AString& AString::Format(const char* szFormat, ...) { vsprintf(m_pStr, szFormat, argList); }
// This converts printf-style format specifiers to C# format specifiers
if (string.IsNullOrEmpty(formatStr))
return formatStr;
// Process format string character by character to convert %d, %s, etc. to {0}, {1}, etc.
// This matches the original vsprintf behavior exactly
var sb = new System.Text.StringBuilder();
int paramIndex = 0;
for (int i = 0; i < formatStr.Length; i++)
{
if (formatStr[i] == '%' && i + 1 < formatStr.Length)
{
// Check for %% (literal %)
if (formatStr[i + 1] == '%')
{
sb.Append('%');
i++; // Skip the second %
continue;
}
// Parse format specifier: %[flags][width][.precision][length]type
int startPos = i;
i++; // Skip the %
// Track flags
bool hasMinus = false;
bool hasPlus = false;
bool hasZero = false;
// Parse flags: +, -, 0, space
while (i < formatStr.Length && (formatStr[i] == '+' || formatStr[i] == '-' || formatStr[i] == '0' || formatStr[i] == ' '))
{
if (formatStr[i] == '-') hasMinus = true;
if (formatStr[i] == '+') hasPlus = true;
if (formatStr[i] == '0') hasZero = true;
i++;
}
// Parse width: digits
int width = 0;
int widthStart = i;
while (i < formatStr.Length && char.IsDigit(formatStr[i]))
{
i++;
}
if (i > widthStart)
{
int.TryParse(formatStr.Substring(widthStart, i - widthStart), out width);
}
// Skip precision: .digits
if (i < formatStr.Length && formatStr[i] == '.')
{
i++;
while (i < formatStr.Length && char.IsDigit(formatStr[i]))
{
i++;
}
}
// Skip length modifier: h, l, L, etc.
while (i < formatStr.Length && (formatStr[i] == 'h' || formatStr[i] == 'l' || formatStr[i] == 'L'))
{
i++;
}
// Get type specifier
if (i < formatStr.Length)
{
char typeChar = formatStr[i];
// Common types: d, i, u, o, x, X, f, e, E, g, G, c, s, p, n
if ("diouxXeEfFgGaAcspn".IndexOf(typeChar) >= 0)
{
// Extract the full format specifier for special handling
string fullSpec = formatStr.Substring(startPos, i - startPos + 1);
// Check for special formats
string csharpFormatSpec = "";
// Handle %-10d, %-5d, etc. (left-aligned with width) - check this FIRST
// Also handle %-d (left-aligned without explicit width, but width might be 0)
if (hasMinus && (typeChar == 'd' || typeChar == 'i' || typeChar == 'u' || typeChar == 's' || typeChar == 'c'))
{
if (width > 0)
{
// C# left alignment with width: {0,-10} (comma for alignment, negative for left-align)
csharpFormatSpec = $",-{width}";
}
else
{
// Just left alignment flag, no width specified - use default
csharpFormatSpec = "";
}
}
// Handle %02d, %03d, etc. (zero-padded integers) - check before regular width
else if (hasZero && width > 0 && (typeChar == 'd' || typeChar == 'i' || typeChar == 'u'))
{
csharpFormatSpec = $":D{width}";
}
// Handle %10d, %5d, etc. (right-aligned with width, no zero-padding)
else if (width > 0 && (typeChar == 'd' || typeChar == 'i' || typeChar == 'u' || typeChar == 's' || typeChar == 'c'))
{
// C# right alignment: {0,10} (comma for alignment)
csharpFormatSpec = $",{width}";
}
// Handle %+d (signed format)
else if (hasPlus && (typeChar == 'd' || typeChar == 'i'))
{
csharpFormatSpec = ":+0;-0";
}
// Handle %.2f, %.3f, etc. (float precision)
else if (fullSpec.Contains(".") && (typeChar == 'f' || typeChar == 'F' || typeChar == 'e' || typeChar == 'E' || typeChar == 'g' || typeChar == 'G'))
{
int dotPos = fullSpec.IndexOf('.');
if (dotPos >= 0 && dotPos + 1 < fullSpec.Length)
{
int precStart = dotPos + 1;
int precEnd = precStart;
while (precEnd < fullSpec.Length - 1 && char.IsDigit(fullSpec[precEnd]))
{
precEnd++;
}
if (precEnd > precStart && int.TryParse(fullSpec.Substring(precStart, precEnd - precStart), out int precNum))
{
if (typeChar == 'f' || typeChar == 'F')
{
csharpFormatSpec = $":F{precNum}";
}
else if (typeChar == 'e' || typeChar == 'E')
{
csharpFormatSpec = $":E{precNum}";
}
else if (typeChar == 'g' || typeChar == 'G')
{
csharpFormatSpec = $":G{precNum}";
}
}
}
}
// Build replacement - always replace recognized format specifiers
// C# format: {index,alignment} for width, {index:format} for format specifiers
if (csharpFormatSpec.Length > 0)
{
// If format spec starts with comma (alignment) or colon (format), use it directly
// Otherwise, assume it's a format specifier and add colon
if (csharpFormatSpec.StartsWith(",") || csharpFormatSpec.StartsWith(":"))
{
sb.Append($"{{{paramIndex}{csharpFormatSpec}}}");
}
else
{
sb.Append($"{{{paramIndex}:{csharpFormatSpec}}}");
}
}
else
{
sb.Append($"{{{paramIndex}}}");
}
paramIndex++;
// Note: i currently points to the type character (e.g., 'd' in "%-10d")
// The for loop will increment i, so we've consumed the entire format specifier
}
else
{
// Not a recognized format specifier, keep as-is
sb.Append(formatStr[startPos]);
i = startPos; // Reset to process next character
}
}
else
{
// Incomplete format specifier, keep the %
sb.Append('%');
i = startPos; // Reset to process next character
}
}
else
{
sb.Append(formatStr[i]);
}
}
// Use C#'s string.Format (equivalent to original vsprintf)
// Handle case where we might have more format specifiers than arguments
try
{
return string.Format(sb.ToString(), args);
}
catch (System.FormatException)
{
// If format fails (e.g., not enough arguments), return the original format string
// This can happen if GetStringFromTable returns an unexpected format
BMLogger.LogWarning($"Format failed for string: {formatStr}, expected {paramIndex} args, got {args.Length}");
return formatStr;
}
}
#endregion
}
}