diff --git a/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.Time.cs b/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.Time.cs index f4ee1a2419..02dd7b84e4 100644 --- a/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.Time.cs +++ b/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.Time.cs @@ -10,6 +10,7 @@ namespace BrewMonster.Network private static int m_AbsTimeStart; private static int m_iTimeError; // 服务器与本机时间差(秒) // Time error in seconds private static int m_iTimeZoneBias; // 服务器时区偏移(秒) // Server timezone bias in seconds + private static bool m_bServerTimeInited; public static int GetTimeZoneBias() { return m_iTimeZoneBias; } // 设置时间误差 // Set time error public static void SetServerTime(int iSevTime, int iTimeZoneBias) @@ -37,11 +38,20 @@ namespace BrewMonster.Network // 初始化绝对时间参考点 // Initialize absolute time reference m_AbsTimeStart = iSevTime; m_AbsTickStart = (uint)(Time.realtimeSinceStartup * 1000.0f); + m_bServerTimeInited = true; Debug.Log($"timeGetTime(), TickStart = {m_AbsTickStart}"); } public static int GetServerAbsTime() { + // Fallback: if server time was never initialized (SetServerTime not called), + // return local unix time seconds so task timestamps (usually epoch seconds) still work. + // This makes wait-time/countdown and timetable logic behave correctly even before server sync. + if (!m_bServerTimeInited || m_AbsTimeStart == 0) + { + return (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + m_iTimeError; + } + uint curTick = (uint)(Time.realtimeSinceStartup * 1000.0f); if (curTick < m_AbsTickStart) diff --git a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs index 21b9f9f0e8..a5be7d4488 100644 --- a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs +++ b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs @@ -479,24 +479,7 @@ namespace BrewMonster.Scripts.Task public void CheckPQEnterWorldInit() { - return; - ActiveTaskList pList = GetActiveTaskList(); - List aEntries = new List(pList.m_TaskEntries); - - for(var i = 0; i < pList.m_uTaskCount; i++) - { - var CurEntry = aEntries[i]; - - if (CurEntry.m_ulTemplAddr == 0) - continue; - - ATaskTempl pTempl = CurEntry.GetTempl(); - if (pTempl == null || !pTempl.m_FixedData.m_bPQTask) - continue; - - pTempl.IncValidCount(); - // _notify_svr(this, TASK_CLT_NOTIFY_PQ_CHECK_INIT, CurEntry.m_ID); - } + // TODO: implement PQ enter-world init if needed } public static void WriteLog(int nPlayerId, int nTaskId, int nType, string szLog) @@ -507,7 +490,6 @@ namespace BrewMonster.Scripts.Task public bool IsDeliverLegal() { return !m_pHost.IsTrading() && m_pHost.GetBoothState() == 0 && !m_pHost.IsDead(); - return true; } public int GetCommonItemCount(uint ulCommonItem) @@ -1249,12 +1231,6 @@ namespace BrewMonster.Scripts.Task // return pTempl.CheckPrerequisite(this, static_cast(GetActiveTaskList()), GetCurTime(), true, true, false); return pTempl.CheckPrerequisite(this, GetActiveTaskList(), GetCurTime(), true, true, false); - - // if (!pTempl.CheckReachLevel(this)) return (uint)TaskInterfaceConstants.TASK_PREREQU_FAIL_BELOW_LEVEL; - // uint keyCheck = pTempl.CheckGlobalKeyValue(this, false); - // if (keyCheck != 0u) return keyCheck; - - return 0u; } public bool CanDeliverCommonItem(uint ulTypes) { @@ -1730,6 +1706,24 @@ namespace BrewMonster.Scripts.Task global::System.Buffer.BlockCopy(lst.m_Buf, 0, m_pFinishedListBuf, 0, m_pFinishedListBuf.Length); } } + + // Persist an updated FinishedTaskList back into the underlying buffer. + public void WriteFinishedTaskList(FinishedTaskList lst) + { + if (m_pFinishedListBuf == null) return; + if (lst.m_Buf == null || lst.m_Buf.Length != m_pFinishedListBuf.Length) return; + global::System.Buffer.BlockCopy(lst.m_Buf, 0, m_pFinishedListBuf, 0, m_pFinishedListBuf.Length); + } + + // Reset role-based finish counter for a task when period rolls over (used by CheckDeliverTime). + public void ResetRoleFinishCount(uint taskId) + { + if (m_pFinishedListBuf == null) return; + FinishedTaskList lst = new FinishedTaskList(); + lst.ReadFromBytes(m_pFinishedListBuf); + lst.ResetFinishCount(taskId); + WriteFinishedTaskList(lst); + } public int GetPlayerId() { diff --git a/Assets/PerfectWorld/Scripts/Task/TaskClient.cs b/Assets/PerfectWorld/Scripts/Task/TaskClient.cs index 85b745ea02..ee746bea45 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskClient.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskClient.cs @@ -17,6 +17,9 @@ namespace BrewMonster.Scripts.Task 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) { @@ -77,7 +80,16 @@ namespace BrewMonster.Scripts.Task // 绝对失效时间判断 // Absolute fail time check if (pTempl.m_FixedData.m_bAbsFail) { - // TODO: Time zone bias and 'task_tm.before' not ported; skipping precise comparison + // 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 @@ -245,8 +257,8 @@ namespace BrewMonster.Scripts.Task } else { - // TODO: UpdateTaskToConfirm not ported; implement confirmation UI/state if needed - UpdateTaskToConfirm(pTask, pTempl, bNeedServerCheck); + // Minimal behavior: for wait-time tasks, auto request server check when time is up. + UpdateTaskToConfirm(pTask, pTempl, CurEntry, bNeedServerCheck, ulCurTime); } } } @@ -295,10 +307,27 @@ namespace BrewMonster.Scripts.Task ATaskTempl._notify_svr(pTask, uReason, uTaskID); } - // 更新“待确认任务” // Update task to confirm - private static void UpdateTaskToConfirm(TaskInterface pTask, ATaskTempl pTempl, bool needServerCheck) + // 更新“待确认任务” / 最小实现:当客户端确认已满足完成条件时,发一次 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) { - // TODO: Implement confirmation queue/UI if required by design + 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 diff --git a/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs b/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs index bf2a66e203..71bed145dc 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs @@ -24,20 +24,21 @@ namespace BrewMonster.Scripts.Task [StructLayout( LayoutKind.Sequential, Pack = 1 )] public struct TaskFinishCountList { - public static ushort m_uCount; + public ushort m_uCount; - public static TaskFinishCountEntry[] m_aList = new TaskFinishCountEntry[(uint)TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN]; + public TaskFinishCountEntry[] m_aList; public uint Search(uint ulID, ref uint ulTime) { - for (ushort i = 0; i < TaskFinishCountList.m_uCount; i++) + if (m_aList == null) return 0u; + for (ushort i = 0; i < m_uCount; i++) { - if (TaskFinishCountList.m_aList[i].m_uTaskId == (ushort)ulID) + if (m_aList[i].m_uTaskId == (ushort)ulID) { - ulTime = TaskFinishCountList.m_aList[i].m_ulFinishTime; - return TaskFinishCountList.m_aList[i].m_ulFinishCount; + ulTime = m_aList[i].m_ulFinishTime; + return m_aList[i].m_ulFinishCount; } } @@ -47,12 +48,17 @@ namespace BrewMonster.Scripts.Task public void ResetAt(uint ulID) { for (ushort i = 0; i < m_uCount; i++) - if (m_aList[i].m_uTaskId == (ushort)ulID) - m_aList[i].m_ulFinishCount = 0; + { + if (m_aList != null && m_aList[i].m_uTaskId == (ushort)ulID) + m_aList[i].m_ulFinishCount = 0; + } } public void AddOrUpdate(uint ulID, uint ulFinishTime) { + if (m_aList == null || m_aList.Length != TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN) + m_aList = new TaskFinishCountEntry[TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN]; + for (ushort i = 0; i < m_uCount; i++) { if (m_aList[i].m_uTaskId == (ushort)ulID) @@ -74,7 +80,7 @@ namespace BrewMonster.Scripts.Task public void RemoveAll() { m_uCount = 0; - m_aList = new TaskFinishCountEntry[(uint)TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN]; + m_aList = new TaskFinishCountEntry[TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN]; } public bool IsValid() { return m_uCount <= TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN; } @@ -104,6 +110,31 @@ namespace BrewMonster.Scripts.Task return true; } + + // Persist list back into a buffer returned by TaskInterface.GetFinishedCntList(). + // Layout: ushort count + TASK_FINISH_COUNT_MAX_LEN * TaskFinishCountEntry bytes + public void WriteToBuffer(byte[] data) + { + if (data == null) return; + int entrySize = Marshal.SizeOf(); + int expectedSize = 2 + entrySize * TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN; + if (data.Length < expectedSize) return; + + Array.Copy(BitConverter.GetBytes(m_uCount), 0, data, 0, 2); + if (m_aList == null || m_aList.Length != TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN) + { + Array.Clear(data, 2, data.Length - 2); + return; + } + + for (int i = 0; i < TaskInterfaceConstants.TASK_FINISH_COUNT_MAX_LEN; i++) + { + int offset = 2 + i * entrySize; + // TaskFinishCountEntry is blittable with Pack=1, use helper + byte[] entryBytes = GPDataTypeHelper.ToBytes(m_aList[i]); + Array.Copy(entryBytes, 0, data, offset, Math.Min(entryBytes.Length, entrySize)); + } + } }; [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs index 338fbd68e8..9aade744ef 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs @@ -10,6 +10,21 @@ namespace BrewMonster.Scripts.Task { public partial class ATaskTempl { + // Some servers/packets may send cur_time in a different unit (e.g. ms tick) or a different epoch (uptime). + // We normalize to the same seconds-based epoch used by EC_Game.GetServerAbsTime() so wait-time/timers work. + private static uint NormalizePacketCurTime(TaskInterface pTask, uint packetTime) + { + if (pTask == null) return packetTime; + uint now = pTask.GetCurTime(); + if (packetTime == 0) return now; + + // If packet time is wildly different from our seconds-based time, trust our time. + // - ms vs sec: packetTime ~ now*1000 + // - uptime vs epoch: packetTime << now (or vice versa) + if (packetTime > now * 10u) return now; + if (now > packetTime * 10u) return now; + return packetTime; + } public uint GetID() { return m_FixedData.m_ID; @@ -543,6 +558,9 @@ namespace BrewMonster.Scripts.Task ref sub_tags ); + // Normalize packet time so active timers (wait-time, time-limit) count down correctly. + ulTime = NormalizePacketCurTime(pTask, ulTime); + // NOTE: Disabled for now due to an ambiguous call error being reported by the Unity compiler in this project setup. // TODO: Re-enable once the underlying duplicate/assembly ambiguity is resolved. // GetTaskTemplMan().RemoveActiveStorageTask(pStorage, m_FixedData.m_ID); @@ -578,6 +596,8 @@ namespace BrewMonster.Scripts.Task var TaskFinishTimeList = new TaskFinishTimeList(); TaskFinishTimeList.ReadFromBuffer(pTask.GetFinishedTimeList()); TaskFinishTimeList.AddOrUpdate(m_FixedData.m_ID, ulTime); + // Persist to underlying buffer so CheckDeliverTime can see it later. + TaskFinishTimeList.WriteToBuffer(pTask.GetFinishedTimeList()); } // TODO: Log new task acceptance @@ -658,6 +678,9 @@ namespace BrewMonster.Scripts.Task ref sub_tags ); + // Normalize packet time so award bookkeeping and time-based logic uses consistent timebase. + ulTime = NormalizePacketCurTime(pTask, ulTime); + pEntry.m_uState = (char)svr_task_complete.sub_tags.state; if (!pEntry.IsSuccess()) @@ -969,14 +992,57 @@ namespace BrewMonster.Scripts.Task // 检查任务可接时间表 // English: Check task timetable window public uint CheckTimetable(uint ulCurTime) { - if (m_FixedData.m_ulTimetable == 0) return 0; + // C++: + // if (!m_ulTimetable) return 0; + // for (i=0; i= TaskInterfaceConstants.TASK_FINISH_TIME_MAX_LEN) - return (uint)TaskInterfaceConstants.TASK_PREREQU_FAIL_FULL; - - // Search for task completion time - uint ulTaskTime = pTimeList.Search(m_FixedData.m_ID); - - // If task was never completed (Search returns 0), allow it - if (ulTaskTime == 0) - return 0u; - - // Task was completed - TODO: Check if within same period based on frequency - // For now, return error to prevent showing tasks that were already completed - // This is a temporary fix - proper implementation should check time periods - return (uint)TaskInterfaceConstants.TASK_PREREQU_FAIL_WRONG_TIME; + + long lastSec = lastMark - (long)(TaskInterface.GetTimeZoneBias() * 60); + if (lastSec < 0) lastSec = 0; + DateTime lastLocal = DateTimeOffset.FromUnixTimeSeconds(lastSec).UtcDateTime; + + // Same-period checks + bool samePeriod = false; + switch (freq) + { + case TaskAwardFreq.enumTAFEachDay: + samePeriod = curLocal.Year == lastLocal.Year && curLocal.Month == lastLocal.Month && curLocal.Day == lastLocal.Day; + break; + case TaskAwardFreq.enumTAFEachWeek: + { + int curDow = (int)curLocal.DayOfWeek; // Sunday=0 + int lastDow = (int)lastLocal.DayOfWeek; + int curDiff = (curDow == 0) ? 6 : (curDow - 1); // Monday-start + int lastDiff = (lastDow == 0) ? 6 : (lastDow - 1); + DateTime curWeekStart = curLocal.Date.AddDays(-curDiff); + DateTime lastWeekStart = lastLocal.Date.AddDays(-lastDiff); + samePeriod = curWeekStart == lastWeekStart; + break; + } + case TaskAwardFreq.enumTAFEachMonth: + samePeriod = curLocal.Year == lastLocal.Year && curLocal.Month == lastLocal.Month; + break; + case TaskAwardFreq.enumTAFEachYear: + samePeriod = curLocal.Year == lastLocal.Year; + break; + } + + if (!isLimitTask) + { + return samePeriod ? (uint)TaskInterfaceConstants.TASK_PREREQU_FAIL_WRONG_TIME : 0u; + } + + // Period-limit tasks: reset counters when entering a new period. + if (!samePeriod && pTask is CECTaskInterface cec) + { + if (m_FixedData.m_bAccountTaskLimit) + { + byte[] cntBuf = cec.GetFinishedCntList(); + if (cntBuf != null && cntBuf.Length > 0) + { + TaskFinishCountList cnt = new TaskFinishCountList(); + cnt.ReadFromBytes(cntBuf); + cnt.ResetAt(m_FixedData.m_ID); + cnt.WriteToBuffer(cntBuf); + } + } + else if (m_FixedData.m_bRoleTaskLimit) + { + cec.ResetRoleFinishCount(m_FixedData.m_ID); + } + } + + // Maintain/update period mark + timeList.AddOrUpdate(m_FixedData.m_ID, ulCurTime); + timeList.WriteToBuffer(finishedTimeListBuf); + return 0u; } // inline unsigned long ATaskTempl::CheckFnshLst(TaskInterface* pTask, unsigned long ulCurTime) const @@ -1913,6 +2041,48 @@ namespace BrewMonster.Scripts.Task cec.RecordFinishedTask(m_FixedData.m_ID, success); } + // Account/role period-limit bookkeeping (C++ RecursiveAward) + if (m_pParent == null && (m_FixedData.m_bAccountTaskLimit || m_FixedData.m_bRoleTaskLimit)) + { + // Check deliver time to reset counters when entering a new period. + CheckDeliverTime(pTask, ulCurtime); + + // "NotIncCntWhenFailed" gating: only increment counters when allowed. + if (!m_FixedData.m_bNotIncCntWhenFailed || (m_FixedData.m_bNotIncCntWhenFailed && pEntry.IsSuccess())) + { + if (pTask is CECTaskInterface cec) + { + // Maintain time mark for counting period (C++ also updates TaskFinishTimeList here for role limit). + byte[] timeBuf = cec.GetFinishedTimeList(); + if (timeBuf != null && timeBuf.Length > 0) + { + TaskFinishTimeList timeList = new TaskFinishTimeList(); + timeList.ReadFromBuffer(timeBuf); + timeList.AddOrUpdate(m_FixedData.m_ID, ulCurtime); + timeList.WriteToBuffer(timeBuf); + } + + if (m_FixedData.m_bAccountTaskLimit) + { + byte[] cntBuf = cec.GetFinishedCntList(); + if (cntBuf != null && cntBuf.Length > 0) + { + TaskFinishCountList cnt = new TaskFinishCountList(); + cnt.ReadFromBytes(cntBuf); + cnt.AddOrUpdate(m_FixedData.m_ID, ulCurtime); + cnt.WriteToBuffer(cntBuf); + } + } + else if (m_FixedData.m_bRoleTaskLimit) + { + FinishedTaskList fnsh = cec.GetFinishedTaskList(); + fnsh.AddForFinishCount(m_FixedData.m_ID, pEntry.IsSuccess()); + cec.WriteFinishedTaskList(fnsh); + } + } + } + } + // Mark empty + decrement count (C++ does this before realign / reuse) pEntry.m_ulTemplAddr = 0; pEntry.m_ID = 0; @@ -2717,28 +2887,41 @@ namespace BrewMonster.Scripts.Task ActiveTaskEntry pEntry, uint ulCurTime) { - - // TODO: implement full logic when ActiveTaskList/ActiveTaskEntry and TaskInterface APIs are available -// if (m_ulTimeLimit > 0 && pEntry.m_ulTaskTime + m_ulTimeLimit < ulCurTime) // ��ʱ -// pEntry.ClearSuccess(); -// -// // if (m_ulAbsFailTime && m_ulAbsFailTime < ulCurTime) // ��������ʧЧ���� -// // ��������ʧЧ���� -// if (m_bAbsFail) -// { -// tm cur = *localtime((long*)&ulCurTime); -// -// if(m_tmAbsFailTime.before(&cur)) -// { -// pEntry->ClearSuccess(); -// } -// } -// -// if (m_pParent && pEntry->m_ParentIndex != 0xff) -// { -// ActiveTaskEntry& ParentEntry = pList->m_TaskEntries[pEntry->m_ParentIndex]; -// m_pParent->RecursiveCheckTimeLimit(pTask, pList, &ParentEntry, ulCurTime); -// } + if (pTask == null || pList == null || pEntry == null) return; + + // Timeout (relative time limit) + if (m_FixedData.m_ulTimeLimit > 0 + && (ulong)pEntry.m_ulTaskTime + (ulong)m_FixedData.m_ulTimeLimit < (ulong)ulCurTime) + { + pEntry.ClearSuccess(); + } + + // Absolute fail time (task_tm) + if (m_FixedData.m_bAbsFail) + { + long sec = ulCurTime - (long)(TaskInterface.GetTimeZoneBias() * 60); + if (sec < 0) sec = 0; + DateTime cur = DateTimeOffset.FromUnixTimeSeconds(sec).UtcDateTime; + + if (m_FixedData.m_tmAbsFailTime.before(cur)) + { + pEntry.ClearSuccess(); + } + } + + // Recurse to parent (template + entry link) + if (m_pParent != null && pEntry.m_ParentIndex != 0xff) + { + int parentIndex = (byte)pEntry.m_ParentIndex; + if (parentIndex >= 0 && parentIndex < pList.m_TaskEntries.Length) + { + ActiveTaskEntry parentEntry = pList.m_TaskEntries[parentIndex]; + if (parentEntry != null) + { + m_pParent.RecursiveCheckTimeLimit(pTask, pList, parentEntry, ulCurTime); + } + } + } } diff --git a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs index 6dc02a28a2..8cc1939992 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs @@ -247,37 +247,52 @@ namespace BrewMonster.Scripts.Task [FieldOffset(8)] public ulong m_ulRcvUpdateTime; - void AddRevNum() { m_ulReceiverNum++; } + public void AddRevNum() { m_ulReceiverNum++; } - void CheckRcvUpdateTime(uint ulCurTime, int nFrequency) + public void CheckRcvUpdateTime(uint ulCurTime, int nFrequency) { - // TODO: implement time-based receiver number reset logic - // if (nFrequency == TaskCompletionMethod.enumTAFNormal || m_ulRcvUpdateTime == 0) - // return; - // - // tm tmCur = *localtime((time_t*)&ulCurTime); - // tm tmRcv = *localtime((time_t*)&m_ulRcvUpdateTime); - // - // if (nFrequency == enumTAFEachDay) - // { - // if (tmCur.tm_year != tmRcv.tm_year || tmCur.tm_yday != tmRcv.tm_yday) - // m_ulReceiverNum = 0; - // } - // else if (nFrequency == enumTAFEachWeek) - // { - // if (!_is_same_week(&tmCur, &tmRcv, ulCurTime, m_ulRcvUpdateTime)) - // m_ulReceiverNum = 0; - // } - // else if (nFrequency == enumTAFEachMonth) - // { - // if (tmCur.tm_year != tmRcv.tm_year || tmCur.tm_mon != tmRcv.tm_mon) - // m_ulReceiverNum = 0; - // } - // else if (nFrequency == enumTAFEachYear) - // { - // if (tmCur.tm_year != tmRcv.tm_year) - // m_ulReceiverNum = 0; - // } + // C++ semantics: based on localtime() period boundaries, reset receiver count when period changes. + // Use the same "task local time" conversion as timetable (timezone bias path) so behavior is consistent. + if (nFrequency == (int)TaskAwardFreq.enumTAFNormal || m_ulRcvUpdateTime == 0) + return; + + long curSec = ulCurTime - (long)(TaskInterface.GetTimeZoneBias() * 60); + if (curSec < 0) curSec = 0; + DateTime cur = DateTimeOffset.FromUnixTimeSeconds(curSec).UtcDateTime; + + long rcvSec = (long)m_ulRcvUpdateTime - (long)(TaskInterface.GetTimeZoneBias() * 60); + if (rcvSec < 0) rcvSec = 0; + DateTime rcv = DateTimeOffset.FromUnixTimeSeconds(rcvSec).UtcDateTime; + + bool reset = false; + if (nFrequency == (int)TaskAwardFreq.enumTAFEachDay) + { + reset = cur.Year != rcv.Year || cur.Month != rcv.Month || cur.Day != rcv.Day; + } + else if (nFrequency == (int)TaskAwardFreq.enumTAFEachWeek) + { + int curDow = (int)cur.DayOfWeek; // Sunday=0 + int rcvDow = (int)rcv.DayOfWeek; + int curDiff = (curDow == 0) ? 6 : (curDow - 1); // Monday-start week + int rcvDiff = (rcvDow == 0) ? 6 : (rcvDow - 1); + DateTime curWeekStart = cur.Date.AddDays(-curDiff); + DateTime rcvWeekStart = rcv.Date.AddDays(-rcvDiff); + reset = curWeekStart != rcvWeekStart; + } + else if (nFrequency == (int)TaskAwardFreq.enumTAFEachMonth) + { + reset = cur.Year != rcv.Year || cur.Month != rcv.Month; + } + else if (nFrequency == (int)TaskAwardFreq.enumTAFEachYear) + { + reset = cur.Year != rcv.Year; + } + + if (reset) + { + m_ulReceiverNum = 0; + m_ulRcvUpdateTime = ulCurTime; + } } } diff --git a/Assets/PerfectWorld/Scripts/Task/TaskTempl.cs b/Assets/PerfectWorld/Scripts/Task/TaskTempl.cs index d058997b76..efc156b8c6 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskTempl.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskTempl.cs @@ -873,6 +873,131 @@ namespace BrewMonster.Scripts.Task { return string.Format("{0}-{1}-{2} {3}:{4} (wday:{5})", year, month, day, hour, min, wday); } + + // ===== C++ task_tm time comparison helpers ===== + // [中文] 对齐 C++ 的 task_tm::after/before 等比较逻辑(含“月/周/日”模式) + // [English] Parity with C++ task_tm::after/before and per-month/week/day variants + + // C++: bool after(const tm* _tm) const + public bool after(DateTime t) + { + int ty = t.Year; + int tm = t.Month; + int td = t.Day; + int th = t.Hour; + int tmin = t.Minute; + + if (year < ty) return false; + if (year > ty) return true; + + if (month < tm) return false; + if (month > tm) return true; + + if (day < td) return false; + if (day > td) return true; + + if (hour < th) return false; + return hour > th || min > tmin; + } + + // C++: bool before(const tm* _tm) const + public bool before(DateTime t) + { + int ty = t.Year; + int tm = t.Month; + int td = t.Day; + int th = t.Hour; + int tmin = t.Minute; + + if (year > ty) return false; + if (year < ty) return true; + + if (month > tm) return false; + if (month < tm) return true; + + if (day > td) return false; + if (day < td) return true; + + if (hour > th) return false; + return hour < th || min <= tmin; + } + + // C++: bool after_per_month(const tm* _tm, bool bLastDay) const + public bool after_per_month(DateTime t, bool bLastDay) + { + int td = t.Day; + int th = t.Hour; + int tmin = t.Minute; + + if (day < td) return false; + if (!bLastDay && day > td) return true; + + if (hour < th) return false; + return hour > th || min > tmin; + } + + // C++: bool before_per_month(const tm* _tm, bool bLastDay) const + public bool before_per_month(DateTime t, bool bLastDay) + { + int td = t.Day; + int th = t.Hour; + int tmin = t.Minute; + + if (day < td) return true; + if (!bLastDay && day > td) return false; + + if (hour > th) return false; + return hour < th || min <= tmin; + } + + // task_week_map (C++): { 7,1,2,3,4,5,6 } with tm_wday Sunday=0 + private static readonly int[] s_task_week_map = { 7, 1, 2, 3, 4, 5, 6 }; + + // C++: bool after_per_week(const tm* _tm) const + public bool after_per_week(DateTime t) + { + int w = s_task_week_map[(int)t.DayOfWeek]; + + if (wday < w) return false; + if (wday > w) return true; + + int th = t.Hour; + int tmin = t.Minute; + if (hour < th) return false; + return hour > th || min > tmin; + } + + // C++: bool before_per_week(const tm* _tm) const + public bool before_per_week(DateTime t) + { + int w = s_task_week_map[(int)t.DayOfWeek]; + + if (wday > w) return false; + if (wday < w) return true; + + int th = t.Hour; + int tmin = t.Minute; + if (hour > th) return false; + return hour < th || min <= tmin; + } + + // C++: bool after_per_day(const tm* _tm) const + public bool after_per_day(DateTime t) + { + int th = t.Hour; + int tmin = t.Minute; + if (hour < th) return false; + return hour > th || min > tmin; + } + + // C++: bool before_per_day(const tm* _tm) const + public bool before_per_day(DateTime t) + { + int th = t.Hour; + int tmin = t.Minute; + if (hour > th) return false; + return hour < th || min <= tmin; + } } // Define task_team_member_info struct required by TEAM_MEM_WANTED @@ -1444,6 +1569,35 @@ namespace BrewMonster.Scripts.Task } } public bool IsValid() { return m_uCount <= TaskInterfaceConstants.TASK_FINISH_TIME_MAX_LEN; } + + // Persist this list back into the underlying buffer returned by TaskInterface.GetFinishedTimeList(). + // Buffer layout: ushort count + TASK_FINISH_TIME_MAX_LEN * (ushort taskId + uint timeMark) + public void WriteToBuffer(byte[] data) + { + if (data == null) return; + int entrySize = sizeof(ushort) + sizeof(uint); + int expected = sizeof(ushort) + entrySize * TaskInterfaceConstants.TASK_FINISH_TIME_MAX_LEN; + if (data.Length < expected) return; + + int offset = 0; + Array.Copy(BitConverter.GetBytes(m_uCount), 0, data, offset, sizeof(ushort)); + offset += sizeof(ushort); + + if (m_aList == null || m_aList.Length != TaskInterfaceConstants.TASK_FINISH_TIME_MAX_LEN) + { + // Keep buffer consistent even if list isn't initialized. + Array.Clear(data, offset, data.Length - offset); + return; + } + + for (int i = 0; i < TaskInterfaceConstants.TASK_FINISH_TIME_MAX_LEN; i++) + { + Array.Copy(BitConverter.GetBytes(m_aList[i].m_uTaskId), 0, data, offset, sizeof(ushort)); + offset += sizeof(ushort); + Array.Copy(BitConverter.GetBytes(m_aList[i].m_ulTimeMark), 0, data, offset, sizeof(uint)); + offset += sizeof(uint); + } + } }; @@ -1692,6 +1846,8 @@ namespace BrewMonster.Scripts.Task uint ulTopCount = 0; byte uBudget = 0; long lReputation = 0; + // Suppress unused warnings until RecursiveCalcAward is fully ported. + _ = ulCmnCount; _ = ulTskCount; _ = lReputation; // 任务屏蔽检查 // Task forbid check if (pTask.CheckTaskForbid(m_FixedData.m_ID)) return (uint)TaskInterfaceConstants.TASK_PREREQU_FAIL_TASK_FORBID; @@ -4478,7 +4634,7 @@ namespace BrewMonster.Scripts.Task return true; } - public uint GetType() { return m_FixedData.m_ulType; } + public new uint GetType() { return m_FixedData.m_ulType; } void Init() { diff --git a/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs b/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs index 7b04488f18..ea1baed163 100644 --- a/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs +++ b/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs @@ -135,6 +135,16 @@ namespace BrewMonster.Scripts.Task.UI // [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() @@ -357,6 +367,63 @@ namespace BrewMonster.Scripts.Task.UI // private bool Tick() { + // 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;