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

This commit is contained in:
MinhHai
2025-12-17 10:49:35 +07:00
8 changed files with 611 additions and 126 deletions
@@ -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)
@@ -479,24 +479,7 @@ namespace BrewMonster.Scripts.Task
public void CheckPQEnterWorldInit()
{
return;
ActiveTaskList pList = GetActiveTaskList();
List<ActiveTaskEntry> aEntries = new List<ActiveTaskEntry>(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<ActiveTaskList*>(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()
{
+35 -6
View File
@@ -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<uint, uint> 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
@@ -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<TaskFinishCountEntry>();
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)]
@@ -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<m_ulTimetable; i++)
// if (judge_time_date(&m_tmStart[i], &m_tmEnd[i], ulCurTime, (task_tm_type)m_tmType[i])) return 0;
// return TASK_PREREQU_FAIL_WRONG_TIME;
if (m_FixedData.m_ulTimetable == 0) return 0u;
// TODO: Implement judge_time_date function to properly check time windows
// C++ logic: if ANY timetable entry matches current time, return 0; else return TASK_PREREQU_FAIL_WRONG_TIME
// For now, since judge_time_date is not implemented, we allow the task to pass
// This is a temporary workaround - proper implementation should check each timetable entry
// WARNING: This may allow tasks to show when they shouldn't, but prevents blocking valid tasks
return 0u;
// Defensive: if data missing, don't hard-block delivering the task (avoids regressions)
if (m_FixedData.m_tmStart == null || m_FixedData.m_tmEnd == null || m_FixedData.m_tmType == null)
return 0u;
int cnt = (int)m_FixedData.m_ulTimetable;
int safeCnt = Math.Min(cnt, Math.Min(m_FixedData.m_tmStart.Length, m_FixedData.m_tmEnd.Length));
safeCnt = Math.Min(safeCnt, m_FixedData.m_tmType.Length);
for (int i = 0; i < safeCnt; i++)
{
task_tm_type type = (task_tm_type)m_FixedData.m_tmType[i];
if (judge_time_date(m_FixedData.m_tmStart[i], m_FixedData.m_tmEnd[i], ulCurTime, type))
return 0u;
}
return (uint)TaskInterfaceConstants.TASK_PREREQU_FAIL_WRONG_TIME;
}
// inline bool judge_time_date(const task_tm* tmStart, const task_tm* tmEnd, unsigned long ulCurTime, task_tm_type tm_type)
// [中文] 对齐 C++ 的时间窗口判断(包含时区偏移、月/周/日模式)
// [English] C++ parity for timetable window checks (timezone bias + month/week/day modes)
private static bool judge_time_date(task_tm tmStart, task_tm tmEnd, uint ulCurTime, task_tm_type tm_type)
{
// C++ client path uses gmtime after subtracting timezone bias.
long t = ulCurTime - (long)(TaskInterface.GetTimeZoneBias() * 60);
if (t < 0) t = 0;
DateTime cur = DateTimeOffset.FromUnixTimeSeconds(t).UtcDateTime;
DateTime tomorrow = DateTimeOffset.FromUnixTimeSeconds(t + 24 * 3600).UtcDateTime;
bool last_day = (cur.Month != tomorrow.Month);
switch (tm_type)
{
case task_tm_type.enumTaskTimeDate:
return tmStart.before(cur) && tmEnd.after(cur);
case task_tm_type.enumTaskTimeMonth:
return tmStart.before_per_month(cur, last_day) && tmEnd.after_per_month(cur, last_day);
case task_tm_type.enumTaskTimeWeek:
return tmStart.before_per_week(cur) && tmEnd.after_per_week(cur);
case task_tm_type.enumTaskTimeDay:
return tmStart.before_per_day(cur) && tmEnd.after_per_day(cur);
default:
return false;
}
}
// inline unsigned long ATaskTempl::CheckDeliverTime(TaskInterface* pTask, unsigned long ulCurTime) const
@@ -986,37 +1052,99 @@ namespace BrewMonster.Scripts.Task
// C++: if (m_lAvailFrequency == enumTAFNormal) return 0;
if (m_FixedData.m_lAvailFrequency == (int)TaskAwardFreq.enumTAFNormal)
return 0u;
// Basic implementation: Check if task was never completed
// If task was never completed (Search returns 0), allow it
// TODO: Full implementation should also check time periods (daily/weekly/monthly/yearly)
// and compare last completion time with current time
// Deliver frequency is tracked via TaskFinishTimeList (time marks). For pure-frequency tasks,
// being in the same period means "can't receive again". For account/role limit tasks, the same
// frequency defines the *counting period*; those tasks should not be blocked by WRONG_TIME, but
// their counters should reset when the period changes (C++ calls CheckDeliverTime during award).
byte[] finishedTimeListBuf = pTask.GetFinishedTimeList();
if (finishedTimeListBuf == null || finishedTimeListBuf.Length == 0)
if (finishedTimeListBuf == null || finishedTimeListBuf.Length == 0) return 0u;
TaskFinishTimeList timeList = new TaskFinishTimeList();
timeList.ReadFromBuffer(finishedTimeListBuf);
if (!timeList.IsValid()) return 0u;
uint lastMark = timeList.Search(m_FixedData.m_ID);
bool isLimitTask = m_FixedData.m_bAccountTaskLimit || m_FixedData.m_bRoleTaskLimit;
// Convert server-abs time into "task local time" (same convention as timetable judge_time_date).
long curSec = ulCurTime - (long)(TaskInterface.GetTimeZoneBias() * 60);
if (curSec < 0) curSec = 0;
DateTime curLocal = DateTimeOffset.FromUnixTimeSeconds(curSec).UtcDateTime;
TaskAwardFreq freq = (TaskAwardFreq)m_FixedData.m_lAvailFrequency;
// No mark yet
if (lastMark == 0)
{
// No finished time list, task was never completed, allow it
if (isLimitTask)
{
timeList.AddOrUpdate(m_FixedData.m_ID, ulCurTime);
timeList.WriteToBuffer(finishedTimeListBuf);
}
return 0u;
}
TaskFinishTimeList pTimeList = new TaskFinishTimeList();
pTimeList.ReadFromBuffer(finishedTimeListBuf);
// Check if list is full
if (pTimeList.m_uCount >= 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);
}
}
}
}
@@ -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;
}
}
}
+157 -1
View File
@@ -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()
{
@@ -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;