using System; using System.Reflection; using System.Runtime.InteropServices; using BrewMonster.Network; using BrewMonster.Scripts.Task; using BrewMonster.UI; using CSNetwork.GPDataType; using PerfectWorld.Scripts.Task; using UnityEngine; namespace BrewMonster.Scripts.Task { // provide some global methods public class TaskClient { #if _TASK_CLIENT private const uint FINISH_DLG_SHOWN_TIME = 3000; // TODO: Confirm correct value private static uint s_finishDlgShownTime = 0; // Throttle CHECK_FINISH notifications per task to avoid spamming the server every tick. private static readonly System.Collections.Generic.Dictionary s_lastCheckFinishAt = new(); public static void OnTaskCheckStatus(TaskInterface pTask) { // 版本与交付合法性检查 // Version and deliver legality check // CheckVersion not exposed on TaskInterface; skipping version check if (pTask == null || !pTask.CheckVersion() || !pTask.IsDeliverLegal()) return; // 读取激活任务列表 // Read active task list ActiveTaskList pLst = TryGetActiveList(pTask); if (pLst == null) return; ActiveTaskEntry[] aEntries = pLst.m_TaskEntries; uint ulCurTime = GetCurTime(); // 遍历所有激活任务 // Iterate active tasks for (int i = 0; i < pLst.m_uTaskCount; i++) { ActiveTaskEntry CurEntry = aEntries[i]; if (CurEntry.m_ulTemplAddr == 0) { // assert(false) // English: unexpected empty template continue; } ATaskTempl pTempl = CurEntry.GetTempl(); if (pTempl == null) continue; // IsValidState from C++ not found in managed port; skip validity-state check if (!pTempl.IsValidState()) continue; // PQ子任务 // PQ subtask if (pTempl.m_FixedData.m_bPQSubTask) { // CheckGlobalPQKeyValue(true) not ported; if implemented and returns 0, notify server then continue if(pTempl.CheckGlobalPQKeyValue(true) == 0) { pTempl.IncValidCount(); _notify_svr(pTask, ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, CurEntry.m_ID); continue; } } // 超时判断 // Timeout check if (pTempl.m_FixedData.m_ulTimeLimit != 0 && CurEntry.m_ulTaskTime + pTempl.m_FixedData.m_ulTimeLimit < ulCurTime) { pTempl.IncValidCount(); _notify_svr(pTask, (int)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, CurEntry.m_ID); continue; } // 绝对失效时间判断 // Absolute fail time check if (pTempl.m_FixedData.m_bAbsFail) { // Mirror C++: if abs-fail time has passed, ask server to check/finish (will mark fail if needed). long sec = ulCurTime - (long)(TaskInterface.GetTimeZoneBias() * 60); if (sec < 0) sec = 0; DateTime cur = DateTimeOffset.FromUnixTimeSeconds(sec).UtcDateTime; if (pTempl.m_FixedData.m_tmAbsFailTime.before(cur)) { pTempl.IncValidCount(); _notify_svr(pTask, (int)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, CurEntry.m_ID); continue; } } // 进入或离开区域导致失败 // Entering or leaving region causes failure { float[] pos = new float[3]; uint ulWorldId = (uint)pTask.GetPos(pos); // 进入区域失败 // Enter region fail if (pTempl.m_FixedData.m_bEnterRegionFail && ulWorldId == pTempl.m_FixedData.m_ulEnterRegionWorld) { for (uint iRegion = 0; iRegion < pTempl.m_FixedData.m_ulEnterRegionCnt; iRegion++) { Task_Region t = pTempl.m_FixedData.m_pEnterRegion[(int)iRegion]; if (IsInZone(t.zvMin, t.zvMax, pos)) { pTempl.IncValidCount(); _notify_svr(pTask, (int)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, CurEntry.m_ID); break; } } } // 离开区域失败 // Leave region fail if (pTempl.m_FixedData.m_bLeaveRegionFail) { bool bLeaveRegion = false; if (ulWorldId != pTempl.m_FixedData.m_ulLeaveRegionWorld) bLeaveRegion = true; else { uint iRegion = 0; for (; iRegion < pTempl.m_FixedData.m_ulLeaveRegionCnt; iRegion++) { Task_Region t = pTempl.m_FixedData.m_pLeaveRegion[(int)iRegion]; if (IsInZone(t.zvMin, t.zvMax, pos)) break; } if (iRegion >= pTempl.m_FixedData.m_ulLeaveRegionCnt) bLeaveRegion = true; } if (bLeaveRegion) { pTempl.IncValidCount(); _notify_svr(pTask, (int)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, CurEntry.m_ID); } } } // 离开家族失败 // Leave faction fail if (!pTask.IsAtCrossServer() && pTempl.m_FixedData.m_bLeaveFactionFail && !pTask.IsInFaction(1)) { pTempl.IncValidCount(); _notify_svr(pTask, (int)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, CurEntry.m_ID); continue; } // 对话/NPC完成类任务跳过本轮 // Skip talk-to-NPC or NPC-finish tasks here if ((TaskCompletionMethod)pTempl.m_FixedData.m_enumMethod == TaskCompletionMethod.enumTMTalkToNPC || pTempl.m_FixedData.m_bMarriage || (TaskFinishType)pTempl.m_FixedData.m_enumFinishType == TaskFinishType.enumTFTNPC) { continue; } // 判断未完成的直接完成判定 // Check direct-finish for unfinished tasks if (!CurEntry.IsFinished()) { // 到达地点直接完成 // Reach-site direct finish if ((TaskCompletionMethod)pTempl.m_FixedData.m_enumMethod == TaskCompletionMethod.enumTMReachSite && (TaskFinishType)pTempl.m_FixedData.m_enumFinishType == TaskFinishType.enumTFTDirect) { if (ulCurTime - s_finishDlgShownTime < FINISH_DLG_SHOWN_TIME) continue; float[] pos = new float[3]; uint ulWorldId = (uint)pTask.GetPos(pos); if (ulWorldId == pTempl.m_FixedData.m_ulReachSiteId) { for (uint iRegion = 0; iRegion < pTempl.m_FixedData.m_ulReachSiteCnt; iRegion++) { Task_Region t = pTempl.m_FixedData.m_pReachSite[(int)iRegion]; if (IsInZone(t.zvMin, t.zvMax, pos)) { var pTalk = pTempl.m_AwardTalk; // If num_window == 1 but windows is null/empty, treat as no options (send notification) bool shouldNotifyDirectly = false; if (pTalk.num_window == 0) { shouldNotifyDirectly = true; } else if (pTalk.num_window == 1) { if (pTalk.windows == null || pTalk.windows.Length == 0) { // Invalid state: num_window == 1 but windows is null/empty - treat as no options shouldNotifyDirectly = true; } else if (pTalk.windows[0].num_option == 0) { shouldNotifyDirectly = true; } } if (shouldNotifyDirectly) { pTempl.IncValidCount(); _notify_svr(pTask, (byte)ClientNotificationConstants.TASK_CLT_NOTIFY_REACH_SITE, (ushort)pTempl.GetID()); } else { // 弹出任务完成对话框(奖励对话有选项) // Popup finish dialog when award talk has options var uiMan = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan(); if (uiMan != null) { // check if this is the main thread uiMan.PopupTaskFinishDialog(pTempl.GetID(), pTalk); s_finishDlgShownTime = ulCurTime; } } break; } } } continue; } // 离开地点直接完成 // Leave-site direct finish if ((TaskCompletionMethod)pTempl.m_FixedData.m_enumMethod == TaskCompletionMethod.enumTMLeaveSite && (TaskFinishType)pTempl.m_FixedData.m_enumFinishType == TaskFinishType.enumTFTDirect) { if (ulCurTime - s_finishDlgShownTime < FINISH_DLG_SHOWN_TIME) continue; float[] leavePos = new float[3]; uint leaveWorldId = (uint)pTask.GetPos(leavePos); bool regRet = false; if (leaveWorldId == pTempl.m_FixedData.m_ulLeaveSiteId) { for (uint iRegion = 0; iRegion < pTempl.m_FixedData.m_ulLeaveSiteCnt; iRegion++) { Task_Region t = pTempl.m_FixedData.m_pLeaveSite[(int)iRegion]; if (IsInZone(t.zvMin, t.zvMax, leavePos)) { regRet = true; break; } } } if (!regRet) { var pTalk = pTempl.m_AwardTalk; // If num_window == 1 but windows is null/empty, treat as no options (send notification) bool shouldNotifyDirectly = false; if (pTalk.num_window == 0) { shouldNotifyDirectly = true; } else if (pTalk.num_window == 1) { if (pTalk.windows == null || pTalk.windows.Length == 0) { // Invalid state: num_window == 1 but windows is null/empty - treat as no options shouldNotifyDirectly = true; } else if (pTalk.windows[0].num_option == 0) { shouldNotifyDirectly = true; } } if (shouldNotifyDirectly) { pTempl.IncValidCount(); _notify_svr(pTask, (byte)ClientNotificationConstants.TASK_CLT_NOTIFY_LEAVE_SITE, (ushort)pTempl.GetID()); } else { // 弹出任务完成对话框(奖励对话有选项) // Popup finish dialog when award talk has options var uiMan = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan(); if (uiMan != null) { uiMan.PopupTaskFinishDialog(pTempl.GetID(), pTalk); s_finishDlgShownTime = ulCurTime; } } } continue; } } // 非子任务:检查奖励条件并按需标记/通知 // If no children, check award conditions and update if (pTempl != null && pTempl.m_pFirstChild == null) { bool bNeedServerCheck = pTempl.RecursiveCheckAward(pTask, pLst, CurEntry, ulCurTime, -1) == 0 && pTempl.CanFinishTask(pTask, CurEntry, ulCurTime); if (pTempl.m_FixedData.m_bDisplayInExclusiveUI && pTempl.m_FixedData.m_bAutoDeliver && (TaskFinishType)pTempl.m_FixedData.m_enumFinishType == TaskFinishType.enumTFTDirect) { // TODO: Hook game UI and update auto-deliver countdown; no UI manager available here uint ulRemainTime = 0; if ((TaskCompletionMethod)pTempl.m_FixedData.m_enumMethod == TaskCompletionMethod.enumTMWaitTime) { uint ultime = CurEntry.m_ulTaskTime + pTempl.m_FixedData.m_ulWaitTime; if (ultime > ulCurTime) ulRemainTime = ultime - ulCurTime; } // TODO: pTempl.m_bReadyToNotifyServer/ResetAutoDelTask workflow may need UI support if (pTempl.m_FixedData.m_bReadyToNotifyServer && bNeedServerCheck) { pTempl.IncValidCount(); // TODO: pTempl.ResetAutoDelTask() not exposed; skip _notify_svr(pTask, (int)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, (ushort)CurEntry.m_ID); } } else { // Minimal behavior: for wait-time tasks, auto request server check when time is up. UpdateTaskToConfirm(pTask, pTempl, CurEntry, bNeedServerCheck, ulCurTime); } } } // ATaskTemplMan.UpdateStatus(pTask) not found in C# port; skipping GetTaskTemplMan().UpdateStatus(pTask); } // ===== Helpers ===== // 取当前时间(服务器绝对时间) // Get current time (server absolute) private static uint GetCurTime() { return (uint)EC_Game.GetServerAbsTime(); } // 反射读取激活任务列表 // Read active task list via reflection private static ActiveTaskList TryGetActiveList(TaskInterface pTask) { // Try to get private method GetActiveTaskList on CECTaskInterface MethodInfo mi = pTask.GetType().GetMethod("GetActiveTaskList", BindingFlags.Instance | BindingFlags.NonPublic); if (mi != null) { try { return mi.Invoke(pTask, null) as ActiveTaskList; } catch { } } // Fallback to private field m_pActiveListBuf FieldInfo fi = pTask.GetType().GetField("m_pActiveListBuf", BindingFlags.Instance | BindingFlags.NonPublic); if (fi != null) { try { return fi.GetValue(pTask) as ActiveTaskList; } catch { } } return null; } // 区域内检测(AABB) // In-zone check (AABB) private static bool IsInZone(ZONE_VERT min, ZONE_VERT max, float[] pos) { if (pos == null || pos.Length < 3) return false; return pos[0] >= min.x && pos[0] <= max.x && pos[1] >= min.y && pos[1] <= max.y && pos[2] >= min.z && pos[2] <= max.z; } public static void _notify_svr(TaskInterface pTask, byte uReason, ushort uTaskID) { ATaskTempl._notify_svr(pTask, uReason, uTaskID); } // 更新“待确认任务” / 最小实现:当客户端确认已满足完成条件时,发一次 CHECK_FINISH 给服务器(节流) // English: Minimal port: when conditions are met client-side, send CHECK_FINISH once (throttled). private static void UpdateTaskToConfirm(TaskInterface pTask, ATaskTempl pTempl, ActiveTaskEntry entry, bool needServerCheck, uint ulCurTime) { if (!needServerCheck || pTask == null || pTempl == null || entry == null) return; // Auto-check for wait-time, simple client, and force-navigation tasks (path + optional wait timer). // 等待时间、简单客户端、强制导航任务:条件满足时通知服务器校验完成。 // Without force-nav here, bezier/wait quests never send CHECK_FINISH and may only hit timeout / fail paths. TaskCompletionMethod method = (TaskCompletionMethod)pTempl.m_FixedData.m_enumMethod; if (method != TaskCompletionMethod.enumTMWaitTime && method != TaskCompletionMethod.enumTMSimpleClientTask && method != TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) return; if (entry.IsFinished()) return; uint id = entry.m_ID; if (id == 0) return; if (s_lastCheckFinishAt.TryGetValue(id, out uint last) && ulCurTime <= last + 1) return; s_lastCheckFinishAt[id] = ulCurTime; pTempl.IncValidCount(); _notify_svr(pTask, (int)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_FINISH, (ushort)id); } // Handle server notification for task updates public static void OnServerNotify(TaskInterface pTask, byte[] pBuf, uint sz) { // Check version validity // CheckVersion not exposed on TaskInterface; skipping version check if (!pTask.CheckVersion()) return; // Validate buffer size for base notification structure if (sz < (uint)Marshal.SizeOf()) return; // Marshal base notification structure from buffer task_notify_base pNotify = GPDataTypeHelper.FromBytes(pBuf); //BMLogger.Log($"[MH Task] TaskClient.OnServerNotify: reason={pNotify.reason}, task={pNotify.task}"); ATaskTempl pTempl = null; ActiveTaskEntry pEntry = null; // Handle error code notification if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_ERROR_CODE) { // TODO: svr_task_err_code struct not defined; need to define or use alternative approach // if (sz != Marshal.SizeOf()) return; #if _ELEMENTCLIENT // TODO: GetEntry method not implemented in ActiveTaskList; need to implement or use alternative // ActiveTaskList pLst = TryGetActiveList(pTask); // if (pLst != null) // { // pEntry = GetEntry(pLst, pNotify.task); // if (pEntry != null) pEntry.SetErrReported(); // } // TODO: TaskShowErrMessage not found; implement error message display // TaskShowErrMessage(...); #endif return; } // Handle forget skill notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_FORGET_SKILL) { // OnForgetLivingSkill method not found in ATaskTemplMan; implement if needed ATaskTemplMan pMan = GetTaskTemplMan(); if (pMan != null) pMan.OnForgetLivingSkill(pTask); return; } // Handle new task notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_NEW) { ATaskTemplMan pMan = GetTaskTemplMan(); if (pMan != null) pTempl = pMan.GetTopTaskByID(pNotify.task); } // Handle dynamic task time mark notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_DYN_TIME_MARK) { // TODO: svr_task_dyn_time_mark struct not defined; need to define or use alternative if (sz != Marshal.SizeOf()) return; // TODO: OnDynTasksTimeMark method not found in ATaskTemplMan; implement if needed ATaskTemplMan pMan = GetTaskTemplMan(); if (pMan != null) { svr_task_dyn_time_mark dynMark = GPDataTypeHelper.FromBytes(pBuf); pMan.OnDynTasksTimeMark(pTask, dynMark.time_mark, dynMark.version); } return; } // Handle dynamic task data notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_DYN_DATA) { if (sz <= (uint)Marshal.SizeOf()) return; // TODO: OnDynTasksData method not found in ATaskTemplMan; implement if needed ATaskTemplMan pMan = GetTaskTemplMan(); if (pMan != null) { byte[] dynData = new byte[sz - Marshal.SizeOf()]; Array.Copy(pBuf, Marshal.SizeOf(), dynData, 0, dynData.Length); pMan.OnDynTasksData(pTask, dynData, dynData.Length, pNotify.task != 0); } return; } // Handle storage data notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_STORAGE) { if (sz != Marshal.SizeOf() + Marshal.SizeOf()) return; ATaskTemplMan pMan = GetTaskTemplMan(); if (pMan != null) { byte[] storageData = new byte[Marshal.SizeOf()]; Array.Copy(pBuf, Marshal.SizeOf(), storageData, 0, storageData.Length); pMan.OnStorageData(pTask, storageData); } pTask.UpdateTaskUI(pNotify.task, pNotify.reason); return; } // Handle special award notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_SPECIAL_AWARD) { // Tsvr_task_special_award and special_award structs not defined; need to define if (sz != Marshal.SizeOf()) return; ATaskTemplMan pMan = GetTaskTemplMan(); if (pMan != null) { svr_task_special_award awardNotify = GPDataTypeHelper.FromBytes(pBuf); pMan.OnSpecialAward(awardNotify.sa, pTask); if (awardNotify.sa.id1 == 0) { // ID is 0 means no storage space, show newbie gift reminder // TODO: CECGameUIMan and PopupNewbieGiftRemind not found; implement UI if needed // CECGameUIMan* pGameUI = g_pGame->GetGameRun()->GetUIManager()->GetInGameUIMan(); // pGameUI->PopupNewbieGiftRemind(); } } return; } // Handle task limit increase notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_SET_TASK_LIMIT) { ActiveTaskList pLst = TryGetActiveList(pTask); if (pLst != null) { // ExpandMaxSimultaneousCount method not implemented; implement if needed pLst.ExpandMaxSimultaneousCount(); } // PopChatMessage static method and FIXMSG_TASK_LIMIT_INCREASED constant not found pTask.PopChatMessage((int)FixedMsg.FIXMSG_TASK_LIMIT_INCREASED); return; } // Search for task entry in active task list else { ActiveTaskList pLst = TryGetActiveList(pTask); if (pLst != null) { for (byte i = 0; i < pLst.m_uTaskCount; i++) { ActiveTaskEntry CurEntry = pLst.m_TaskEntries[i]; if (CurEntry == null) continue; if (CurEntry.m_ID != pNotify.task || CurEntry.m_ulTemplAddr == 0) continue; pTempl = CurEntry.GetTempl(); pEntry = CurEntry; break; } } } // Handle player killed notification if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_PLAYER_KILLED) { // TODO: CECUIHelper.OnTaskProcessUpdated not found; implement UI update if needed // CECUIHelper.OnTaskProcessUpdated(pNotify.task); } // Handle monster killed notification if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_MONSTER_KILLED) { // Monster kill count >= 2 triggers auto team // TODO: svr_monster_killed struct not defined; need to define or use alternative if (sz == Marshal.SizeOf()) { svr_monster_killed pKilled = GPDataTypeHelper.FromBytes(pBuf) ;//Marshal.PtrToStructure(pNotify.AddrOfPinnedObject()); if (pKilled.monster_num >= 2) { // CECAutoTeam pAutoTeam = EC_Game.GetGameRun().GetHostPlayer().GetAutoTeam(); // pAutoTeam.DoAutoTeam((int)CECAutoTeam.AutoTeamType.TYPE_TASK, pNotify.task); } } // TODO: CECUIHelper.OnTaskProcessUpdated not found; implement UI update if needed // CECUIHelper.OnTaskProcessUpdated(pNotify.task); } // Handle task completion or give up notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_COMPLETE ) { // TODO: CECUIHelper.OnTaskCompleted not found; implement UI update if needed // CECUIHelper.OnTaskCompleted(pNotify.task); } else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_GIVE_UP) { ActiveTaskList pLst = TryGetActiveList(pTask); if (pLst != null) { pLst.ClearTask(pTask, pEntry, false); } pLst.ClearTask(pTask, pEntry, false); if (pTempl.m_FixedData.m_bDisplayInTitleTaskUI) pTask.UpdateTaskUI(pTempl.m_FixedData.m_ID, TaskTemplConstants.TASK_SVR_NOTIFY_GIVE_UP); //if ((pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTask) && pTempl.m_FixedData.m_uiEmotion > 0) //pTask.UpdateTaskUI(pTempl.m_FixedData.m_ID, TaskTemplConstants.TASK_SVR_NOTIFY_GIVE_UP); pTask.OnGiveupTask((int)pTempl.m_FixedData.m_ID); } // Validate template was found if (pTempl == null) { // TODO: Replace assert with appropriate error handling Debug.Assert(false, "Task template not found"); return; } // Clear valid count and process server notification pTempl.ClearValidCount(); // OnServerNotify method signature may need adjustment for C# (ref/out parameters) pTempl.OnServerNotify(pTask, pEntry, pNotify, sz, pBuf); // TASK_SVR_NOTIFY_COMPLETE (reason 2): re-expand trace for this quest line (subtask chain advance). // TASK_SVR_NOTIFY_COMPLETE(reason=2):子任务推进后重新挂接追踪链。 if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_COMPLETE) { var gameUi = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan() as CECGameUIMan; gameUi?.RetraceTaskAfterServerNotifyComplete(pNotify.task); } } // Helper method to get task template manager private static ATaskTemplMan GetTaskTemplMan() { return EC_Game.GetTaskTemplateMan(); } #endif } }