Files
test/Assets/PerfectWorld/Scripts/Task/TaskClient.cs
T

593 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.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<uint, uint> 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)
{
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;
// Only auto-check for wait-time tasks (the reported broken case).
if ((TaskCompletionMethod)pTempl.m_FixedData.m_enumMethod != TaskCompletionMethod.enumTMWaitTime)
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<task_notify_base>()) return;
// Marshal base notification structure from buffer
task_notify_base pNotify = GPDataTypeHelper.FromBytes<task_notify_base>(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<svr_task_err_code>()) 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<svr_task_dyn_time_mark>()) 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<svr_task_dyn_time_mark>(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<task_notify_base>()) return;
// TODO: OnDynTasksData method not found in ATaskTemplMan; implement if needed
ATaskTemplMan pMan = GetTaskTemplMan();
if (pMan != null)
{
byte[] dynData = new byte[sz - Marshal.SizeOf<task_notify_base>()];
Array.Copy(pBuf, Marshal.SizeOf<task_notify_base>(), 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<task_notify_base>() + Marshal.SizeOf<StorageTaskList>()) return;
ATaskTemplMan pMan = GetTaskTemplMan();
if (pMan != null)
{
byte[] storageData = new byte[Marshal.SizeOf<StorageTaskList>()];
Array.Copy(pBuf, Marshal.SizeOf<task_notify_base>(), 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<svr_task_special_award>()) return;
ATaskTemplMan pMan = GetTaskTemplMan();
if (pMan != null)
{
svr_task_special_award awardNotify = GPDataTypeHelper.FromBytes<svr_task_special_award>(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>())
{
svr_monster_killed pKilled = GPDataTypeHelper.FromBytes<svr_monster_killed>(pBuf) ;//Marshal.PtrToStructure<svr_monster_killed>(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);
}
// Helper method to get task template manager
private static ATaskTemplMan GetTaskTemplMan()
{
return EC_Game.GetTaskTemplateMan();
}
#endif
}
}