diff --git a/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs b/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs index dcb0c371cc..2bb38036cf 100644 --- a/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs +++ b/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs @@ -754,7 +754,25 @@ namespace BrewMonster.Scripts.Task m_ulDynTasksDataSize = 0; } } - // void OnStorageData(TaskInterface* pTask, const void* data); + + // 处理存储任务数据 // Handle storage task data + public void OnStorageData(TaskInterface pTask, byte[] data) + { + // Copy data directly to the storage buffer (equivalent to C++ memcpy operations) + // 直接将数据复制到存储缓冲区(等同于C++的memcpy操作) + if (pTask is CECTaskInterface cecTask) + { + cecTask.SetStorageTaskListBuffer(data); + } + else + { + // Fallback: read into struct (but won't persist without writing back) + // 后备方案:读取到结构体(但不写回则不会持久化) + StorageTaskList pLst = pTask.GetStorageTaskList(); + pLst.ReadByte(data); + } + } + // void OnSpecialAward(const special_award* p,TaskInterface* pTask); // void VerifyDynTasksPack(const char* szPath); // const special_award* GetSpecialAward() const { return &m_SpecialAward; } @@ -902,7 +920,9 @@ namespace BrewMonster.Scripts.Task return false; } - if (header.pack_size != data_size) + // Only check pack_size when reading full data, not when header_only is true + // When header_only is true, we only read the header, so pack_size will be larger than data_size + if (!header_only && header.pack_size != data_size) { // TaskInterface::WriteLog(0, 0, 0, "UnmarshalDynTasks, wrong header"); BMLogger.LogError($" [ATaskTemplMan] UnmarshalDynTasks, wrong header: pack_size {header.pack_size} != data_size {data_size}"); diff --git a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs index 56b9ab0385..0cec42c345 100644 --- a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs +++ b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using BrewMonster.UI; using CSNetwork; using UnityEngine; +using System; namespace BrewMonster.Scripts.Task { @@ -1292,8 +1293,7 @@ namespace BrewMonster.Scripts.Task ActiveTaskList pLst = GetActiveTaskList(); uint ulCurTime = GetCurTime(); // 当前时间 // current time ATaskTempl pTempl = null; - ActiveTaskEntry foundEntry = default; - bool hasFoundEntry = false; + int foundEntryIndex = -1; // Keep track of entry index like C++ code does if (bActiveTask) { @@ -1304,8 +1304,7 @@ namespace BrewMonster.Scripts.Task if (CurEntry.m_ID != ulTaskId || CurEntry.m_ulTemplAddr == 0) continue; pTempl = CurEntry.GetTempl(); - foundEntry = CurEntry; - hasFoundEntry = true; + foundEntryIndex = i; // Store index to access entry later // 检查任务是否可以完成 // Check if task can be completed if (pTempl != null && pTempl.CanFinishTask(this, CurEntry, ulCurTime)) @@ -1380,9 +1379,11 @@ namespace BrewMonster.Scripts.Task pInfo.m_MonsterWanted[ulMonsterCount].m_ulMonsterId = mw.m_ulMonsterTemplId; pInfo.m_MonsterWanted[ulMonsterCount].m_ulMonstersToKill = mw.m_ulMonsterNum; - if (bActiveTask && hasFoundEntry) + // Access the entry directly from the list using stored index, like C++ code does + if (bActiveTask && foundEntryIndex >= 0) { - pInfo.m_MonsterWanted[ulMonsterCount].m_ulMonstersKilled = foundEntry.m_wMonsterNum[j]; + ActiveTaskEntry CurEntry = pLst.m_TaskEntries[foundEntryIndex]; + pInfo.m_MonsterWanted[ulMonsterCount].m_ulMonstersKilled = CurEntry.m_wMonsterNum[j]; } ulMonsterCount++; @@ -1409,9 +1410,11 @@ namespace BrewMonster.Scripts.Task { pInfo.m_PlayerWanted[ulPlayerCount].m_ulPlayersToKill = pw.m_ulPlayerNum; - if (bActiveTask && hasFoundEntry) + // Access the entry directly from the list using stored index, like C++ code does + if (bActiveTask && foundEntryIndex >= 0) { - pInfo.m_PlayerWanted[ulPlayerCount].m_ulPlayersKilled = foundEntry.m_wMonsterNum[j]; + ActiveTaskEntry CurEntry = pLst.m_TaskEntries[foundEntryIndex]; + pInfo.m_PlayerWanted[ulPlayerCount].m_ulPlayersKilled = CurEntry.m_wMonsterNum[j]; } pInfo.m_PlayerWanted[ulPlayerCount].m_Requirements = pw.m_Requirements; @@ -1564,7 +1567,16 @@ namespace BrewMonster.Scripts.Task public StorageTaskList GetStorageTaskList() { StorageTaskList ret = new StorageTaskList(); - ret.ReadByte(m_pStorageTaskListBuf); + // Initialize arrays before use + ret.EnsureInitialized(); + + // Check if buffer is initialized before reading + if (m_pStorageTaskListBuf != null) + { + ret.ReadByte(m_pStorageTaskListBuf); + } + // If buffer is null, return empty/default StorageTaskList + // This can happen if Init() hasn't been called yet or failed return ret; } @@ -1670,6 +1682,21 @@ namespace BrewMonster.Scripts.Task ret.ReadFromBytes(m_pFinishedListBuf); return ret; } + + // 记录任务完成/失败到已完成列表(用于前置任务/可重复接任务判断) + // English: Record task finish/fail into FinishedTaskList (used by prerequisite checks) + public void RecordFinishedTask(uint taskId, bool success) + { + if (m_pFinishedListBuf == null) return; + FinishedTaskList lst = new FinishedTaskList(); + lst.ReadFromBytes(m_pFinishedListBuf); + lst.AddOneTask(taskId, success); + // Persist back into buffer + if (lst.m_Buf != null && lst.m_Buf.Length == m_pFinishedListBuf.Length) + { + global::System.Buffer.BlockCopy(lst.m_Buf, 0, m_pFinishedListBuf, 0, m_pFinishedListBuf.Length); + } + } public int GetPlayerId() { diff --git a/Assets/PerfectWorld/Scripts/Task/TaskClient.cs b/Assets/PerfectWorld/Scripts/Task/TaskClient.cs index c808d31d69..85b745ea02 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskClient.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskClient.cs @@ -383,18 +383,15 @@ namespace BrewMonster.Scripts.Task // Handle storage data notification else if (pNotify.reason == TaskTemplConstants.TASK_SVR_NOTIFY_STORAGE) { - // TODO: StorageTaskList struct not defined; need to define or use alternative - // if (sz != Marshal.SizeOf() + Marshal.SizeOf()) return; - // TODO: OnStorageData method not found in ATaskTemplMan; implement if needed - // ATaskTemplMan pMan = GetTaskTemplMan(pTask); - // if (pMan != null) - // { - // byte[] storageData = new byte[Marshal.SizeOf()]; - // Array.Copy(pBuf, Marshal.SizeOf(), storageData, 0, storageData.Length); - // pMan.OnStorageData(pTask, storageData); - // } - // TODO: UpdateTaskUI static method not found; implement UI update if needed - // TaskInterface.UpdateTaskUI(pNotify.task, pNotify.reason); + 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 diff --git a/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs b/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs index 550ad1e9a1..a12c9dbf0c 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskProcess.cs @@ -148,7 +148,9 @@ namespace BrewMonster.Scripts.Task // unsigned char m_BufData[TASK_DATA_BUF_MAX_LEN-sizeof(TASK_ENTRY_FIXED_DATA)]; public byte[] m_BufData = new byte[TaskInterfaceConstants.TASK_DATA_BUF_MAX_LEN - Marshal.SizeOf() ]; // Raw data buffer // nsigned short m_wMonsterNum[MAX_MONSTER_WANTED]; - public ushort[] m_wMonsterNum // Monster numbers + // 注意:这个属性返回的是拷贝数组,不能用 m_wMonsterNum[i] = x 来写入(不会回写到底层缓冲) + // English: This property returns a COPY. Do not mutate via m_wMonsterNum[i] = x (it won't persist). + public ushort[] m_wMonsterNum // Monster numbers (copy) { get { @@ -170,6 +172,21 @@ namespace BrewMonster.Scripts.Task } } + // 读取/写入怪物计数(直接回写到底层缓冲) // English: Get/set monster count (writes through to backing buffer) + public ushort GetMonsterNum(int index) + { + if (index < 0 || index >= TaskInterfaceConstants.MAX_MONSTER_WANTED) return 0; + return BitConverter.ToUInt16(m_BufData, index * 2); + } + + public void SetMonsterNum(int index, ushort value) + { + if (index < 0 || index >= TaskInterfaceConstants.MAX_MONSTER_WANTED) return; + byte[] bytes = BitConverter.GetBytes(value); + m_BufData[index * 2] = bytes[0]; + m_BufData[index * 2 + 1] = bytes[1]; + } + public int m_iUsefulData1 { get => BitConverter.ToInt32(m_BufData, TaskInterfaceConstants.MAX_MONSTER_WANTED * 2); @@ -239,6 +256,7 @@ namespace BrewMonster.Scripts.Task public bool IsContributionFinish() => (m_uState & (byte)TaskState.TASK_STATE_CONTRIBUTION_FINISH) != 0; public void SetFinished() { m_uState |= (char)TaskState.TASK_STATE_FINISHED; } + public void SetSuccess() { m_uState |= (char)TaskState.TASK_STATE_SUCCESS; } // 设置成功标志 // English: Mark success flag // void ClearFinished() { m_uState &= ~TASK_STATE_FINISHED; } // void SetSuccess() { m_uState |= TASK_STATE_SUCCESS; } public void ClearSuccess() { m_uState &= (char)~TaskState.TASK_STATE_SUCCESS; } @@ -259,8 +277,15 @@ namespace BrewMonster.Scripts.Task var man = BrewMonster.Network.EC_Game.GetTaskTemplateMan(); if (man != null) { - var templ = man.GetTaskTemplByID(m_ID); - if (templ != null) return templ; + // NOTE: Some project configurations report an "ambiguous call" error for direct calls + // into ATaskTemplMan methods (likely due to duplicate symbols/assemblies). Use reflection + // here to keep compilation stable while still using the manager at runtime. + var mi = man.GetType().GetMethod("GetTaskTemplByID", new[] { typeof(uint) }); + if (mi != null) + { + var templObj = mi.Invoke(man, new object[] { (uint)m_ID }); + if (templObj is ATaskTempl templ) return templ; + } } } catch { } @@ -426,98 +451,65 @@ namespace BrewMonster.Scripts.Task } void RealignTask(ActiveTaskEntry pEntry, byte uReserve) { - // TODO: implement RealignTask logic - // // unsigned char uCurIndex = static_cast(pEntry - m_TaskEntries); - // byte uCurIndex = (byte)Array.IndexOf(m_TaskEntries, pEntry); + // 简化实现:压缩数组,移除空洞(m_ID==0 或 null) + // English: Simplified implementation: compact array, remove holes (m_ID==0 or null). // - // uint ulCount = (uint)m_uTaskCount - uCurIndex; // ʣ��������� - // - // if (ulCount == 0) return; // ���һ������ - // - // byte uEmptyCount = 0; - // for (int uEmpty = uCurIndex; uEmpty < TaskInterfaceConstants.TASK_ACTIVE_LIST_MAX_LEN; uEmpty++) - // { - // if (m_TaskEntries[uEmpty].m_ID == 0) - // uEmptyCount++; - // else - // break; - // } - // - // if (uReserve == uEmptyCount) return; - // - // // ActiveTaskEntry* pSrc = pEntry + uEmptyCount; - // int pSrcIndex = uCurIndex + uEmptyCount; - // ActiveTaskEntry[] pSrc = new ActiveTaskEntry[ulCount]; - // Array.Copy(m_TaskEntries, pSrcIndex, pSrc, 0, ulCount); - // - // // ActiveTaskEntry* pInsert = pEntry + uReserve; - // int pInsertIndex = uCurIndex + uReserve; - // ActiveTaskEntry[] pInsert = new ActiveTaskEntry[ulCount]; - // Array.Copy(m_TaskEntries, pInsertIndex, pInsert, 0, ulCount); - // - // // move it - // // memmove(pInsert, pSrc, sizeof(ActiveTaskEntry) * ulCount); - // Array.Copy(pSrc, 0, m_TaskEntries, 0, ulCount); - // - // // clear reserve part - // ActiveTaskEntry[] pClearStart, pClearEnd; - // int pClearStartIndex = 0, pClearEndIndex = 0; - // - // // if (pInsert > pSrc) // C++ pointer compare - // if (pInsertIndex > pSrcIndex) // C# index compare - // { - // pClearStart = pSrc; - // pClearEnd = pInsert; - // } - // else - // { - // // pClearStart = pInsert + ulCount; - // pClearStartIndex = pInsertIndex + (int)ulCount; - // // pClearEnd = pSrc + ulCount; - // pClearEndIndex = pSrcIndex + (int)ulCount; - // } - // - // // while (pClearStart < pClearEnd) - // while (pClearStartIndex < pClearEndIndex) - // { - // pClearStart.m_ulTemplAddr = 0; - // pClearStart.m_ID = 0; - // pClearStart++; - // } - // - // // calc gap - // unsigned char uGap = static_cast(pInsert - pSrc); - // unsigned long i = 0; - // - // for (; i < static_cast(uCurIndex); i++) - // { - // // Parent, PrevС��uCurIndex - // ActiveTaskEntry& CurEntry = m_TaskEntries[i]; - // - // if(!CurEntry.m_ID) - // continue; - // - // if (CurEntry.m_ChildIndex != 0xff && CurEntry.m_ChildIndex >= uCurIndex) - // CurEntry.m_ChildIndex += uGap; - // if (CurEntry.m_NextSblIndex != 0xff && CurEntry.m_NextSblIndex >= uCurIndex) - // CurEntry.m_NextSblIndex += uGap; - // } - // - // for (i = 0; i < ulCount; i++) - // { - // ActiveTaskEntry& CurEntry = *(pInsert + i); - // if(!CurEntry.m_ID) - // continue; - // - // if (CurEntry.m_ParentIndex != 0xff && CurEntry.m_ParentIndex >= uCurIndex) - // CurEntry.m_ParentIndex += uGap; - // if (CurEntry.m_PrevSblIndex != 0xff && CurEntry.m_PrevSblIndex >= uCurIndex) - // CurEntry.m_PrevSblIndex += uGap; - // if (CurEntry.m_ChildIndex != 0xff) - // CurEntry.m_ChildIndex += uGap; - // if (CurEntry.m_NextSblIndex != 0xff) - // CurEntry.m_NextSblIndex += uGap; - // } + // NOTE: This does not preserve complex parent/child/sibling linkage yet. It is enough to keep + // top-level task list stable for UI refresh when taking/completing/abandoning tasks. + int write = 0; + // Scan full storage, because legacy clear logic mutates m_uTaskCount before compaction. + // English: Scan full storage since legacy clear logic may change m_uTaskCount before compaction. + int count = m_TaskEntries.Length; + for (int read = 0; read < count; read++) + { + ActiveTaskEntry e = m_TaskEntries[read]; + if (e == null || e.m_ID == 0) continue; + if (write != read) m_TaskEntries[write] = e; + write++; + } + + for (int i = write; i < m_TaskEntries.Length; i++) + m_TaskEntries[i] = null; + + m_uTaskCount = (byte)write; + + // Reset linkage to "top-level only" defaults + for (int i = 0; i < m_uTaskCount; i++) + { + var e = m_TaskEntries[i]; + e.m_ParentIndex = (char)0xff; + e.m_PrevSblIndex = (char)0xff; + e.m_NextSblIndex = (char)0xff; + e.m_ChildIndex = (char)0xff; + } + + RecountTaskCounters(); + } + + // 重新统计顶部任务计数与使用量 // English: Recount top task counters and used count + public void RecountTaskCounters() + { + m_uTopShowTaskCount = 0; + m_uTopHideTaskCount = 0; + m_uTitleTaskCount = 0; + m_uUsedCount = 0; + + for (int i = 0; i < m_uTaskCount; i++) + { + var e = m_TaskEntries[i]; + if (e == null || e.m_ID == 0) continue; + var templ = e.GetTempl(); + if (templ == null) continue; + if (templ.m_pParent != null) continue; + + if (templ.m_FixedData.m_bHidden) m_uTopHideTaskCount++; + else if (templ.m_FixedData.m_bDisplayInTitleTaskUI) m_uTitleTaskCount++; + else m_uTopShowTaskCount++; + + // used count is an 8-bit field in the original packed header; clamp to byte range + int used = m_uUsedCount + templ.m_uDepth; + m_uUsedCount = (byte)Math.Clamp(used, 0, byte.MaxValue); + } } public void ClearTask(TaskInterface pTask, ActiveTaskEntry pEntry, bool bRemoveItem) { @@ -644,5 +636,17 @@ namespace BrewMonster.Scripts.Task { m_uMaxSimultaneousCount = true; } + + // 从列表中移除指定条目(并压缩列表) // English: Remove an entry from the list (and compact) + public void RemoveEntry(ActiveTaskEntry entry) + { + if (entry == null) return; + // Mark as empty + entry.m_ulTemplAddr = 0; + entry.m_ID = 0; + + // Compact list (RealignTask is our compaction implementation) + RealignTask(entry, 0); + } }; } \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs index 2acfb49e7a..f8162e1b3d 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Method.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using UnityEngine; using BrewMonster.Network; using BrewMonster.Scripts.Task; using CSNetwork.GPDataType; @@ -394,6 +395,28 @@ namespace BrewMonster.Scripts.Task } } // bool CanShowInExclusiveUI (TaskInterface* pTask, unsigned long ulCurTime) const; +#if !TASK_TEMPL_EDITOR + // RecursiveCheckPunchMonster(const ATaskTempl* pTempl) + // 递归检查“沙包怪”并弹提示 // English: Recursively check "punch bag" monster and show prompt + // NOTE: The original C++ checks monster essence switches (MCS_SUMMONER_ATTACK_ONLY + MCS_RECORD_DPS_RANK) + // and calls ShowPunchBagMessage(). The Unity port doesn't currently expose elementdataman/monster essence, + // so this is a safe no-op placeholder that preserves call sites and recursion shape. + private static void RecursiveCheckPunchMonster(ATaskTempl pTempl) + { + if (pTempl == null) return; + + // TODO: Port elementdataman lookup + MONSTER_ESSENCE combined_switch checks and call: + // pTask.ShowPunchBagMessage(false,false,monsterId,0,0); + + var child = pTempl.m_pFirstChild; + while (child != null) + { + RecursiveCheckPunchMonster(child); + child = child.m_pNextSibling; + } + } +#endif + public void OnServerNotify( TaskInterface pTask, ActiveTaskEntry pEntry, @@ -424,7 +447,7 @@ namespace BrewMonster.Scripts.Task int iIndex = pKilledPlayer.index; if (iIndex < TaskInterfaceConstants.MAX_MONSTER_WANTED) { - pEntry.m_wMonsterNum[iIndex] = pKilledPlayer.player_num; + pEntry.SetMonsterNum(iIndex, pKilledPlayer.player_num); } } break; @@ -447,7 +470,7 @@ namespace BrewMonster.Scripts.Task if (mw.m_ulMonsterTemplId == pKilled.monster_id) { - pEntry.m_wMonsterNum[i] = pKilled.monster_num; + pEntry.SetMonsterNum((int)i, pKilled.monster_num); if (pKilled.dps > 0 && pKilled.dph > 0) { @@ -459,12 +482,60 @@ namespace BrewMonster.Scripts.Task break; case TaskTemplConstants.TASK_SVR_NOTIFY_NEW: - var svr_new_task = GPDataTypeHelper.FromBytes(pBuf); - if (svr_new_task.valid_size((int)sz)) + // Follow C++ logic: valid_size checks the buffer size before deserializing + // In C++, pNotify is a pointer to the structure, so valid_size can read sub_tags.sz directly + // In C#, we need to read sub_tags.sz from the buffer first, then validate + + // Calculate base size: task_notify_base (3) + cur_time (4) + cap_task (4) = 11 + int baseSzNew = Marshal.SizeOf() + 8; + if (sz <= (uint)baseSzNew) { - BMLogger.LogError($" [TASK_SVR_NOTIFY_NEW] the size of byte not meet !!!"); + Debug.Log($" [ATaskTempl] TASK_SVR_NOTIFY_NEW: buffer too small, need more than {baseSzNew} bytes, got {sz}"); break; } + + // Read sub_tags.sz from buffer to validate size (matching C++ valid_size logic) + // Offset to sub_tags: baseSzNew = 11 + // sub_tags layout: sub_task/state (2 bytes) + sz (1 byte) = 3 bytes minimum + if ((int)sz < baseSzNew + 3) + { + Debug.Log($" [ATaskTempl] TASK_SVR_NOTIFY_NEW: buffer too small to read sub_tags.sz"); + break; + } + + byte subTagsSzNew = pBuf[baseSzNew + 2]; // Read sz field from sub_tags (after sub_task/state which is 2 bytes) + + // Manually implement valid_size check (matching C++ logic) + // C++: return sub_tags.valid_size(sz - base_sz); + // sub_tags.valid_size checks: get_size() == _sz, where get_size() = sz + 3 + int subTagsSize = subTagsSzNew + 3; // get_size() = sz + 3 + if (subTagsSize != (int)sz - baseSzNew) + { + Debug.Log($" [TASK_SVR_NOTIFY_NEW] the size of byte not meet !!! expected sub_tags size {subTagsSize}, got {sz - baseSzNew}"); + break; + } + + // Now safe to deserialize - buffer size matches expected size + // Marshal.SizeOf returns size with full MAX_SUB_TAGS array, but actual data is smaller + int fixedSizeNew = Marshal.SizeOf(); + svr_new_task svr_new_task; + + if (sz < (uint)fixedSizeNew) + { + // Buffer is smaller than fixed structure size, create padded buffer for safe deserialization + byte[] paddedBufNew = new byte[fixedSizeNew]; + Array.Copy(pBuf, paddedBufNew, (int)sz); + // Zero-fill the rest (tags array will be zero-filled, which is safe) + + svr_new_task = GPDataTypeHelper.FromBytes(paddedBufNew); + // Restore the original sub_tags.sz value since padding might have corrupted it + svr_new_task.sub_tags.sz = subTagsSzNew; + } + else + { + // Buffer is large enough for fixed structure size + svr_new_task = GPDataTypeHelper.FromBytes(pBuf); + } pLst = pTask.GetActiveTaskList(); svr_new_task.get_data( ref ulTime, @@ -472,7 +543,9 @@ namespace BrewMonster.Scripts.Task ref sub_tags ); - GetTaskTemplMan().RemoveActiveStorageTask(pStorage, m_FixedData.m_ID); + // 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); if (sub_tags.sub_task > 0) { @@ -494,7 +567,7 @@ namespace BrewMonster.Scripts.Task pSub, ref sub_tags, new TaskGlobalData(), - 0); + 0xFF); if (m_FixedData.m_lAvailFrequency != (int)TaskAwardFreq.enumTAFNormal && !m_FixedData.m_bAccountTaskLimit && !m_FixedData.m_bRoleTaskLimit) @@ -526,8 +599,60 @@ namespace BrewMonster.Scripts.Task break; case TaskTemplConstants.TASK_SVR_NOTIFY_COMPLETE: - var svr_task_complete = GPDataTypeHelper.FromBytes(pBuf); - if (svr_task_complete.valid_size((int)sz)) break; + // Follow C++ logic: valid_size checks the buffer size before deserializing + // In C++, pNotify is a pointer to the structure, so valid_size can read sub_tags.sz directly + // In C#, we need to read sub_tags.sz from the buffer first, then validate + + // Calculate base size: task_notify_base (3) + cur_time (4) = 7 + int baseSz = Marshal.SizeOf() + 4; + if (sz <= (uint)baseSz) + { + Debug.Log($" [ATaskTempl] TASK_SVR_NOTIFY_COMPLETE: buffer too small, need more than {baseSz} bytes, got {sz}"); + break; + } + + // Read sub_tags.sz from buffer to validate size (matching C++ valid_size logic) + // Offset to sub_tags: baseSz = 7 + // sub_tags layout: sub_task/state (2 bytes) + sz (1 byte) = 3 bytes minimum + if ((int)sz < baseSz + 3) + { + Debug.Log($" [ATaskTempl] TASK_SVR_NOTIFY_COMPLETE: buffer too small to read sub_tags.sz"); + break; + } + + byte subTagsSz = pBuf[baseSz + 2]; // Read sz field from sub_tags (after sub_task/state which is 2 bytes) + + // Manually implement valid_size check (matching C++ logic) + // C++: return sub_tags.valid_size(sz - base_sz); + // sub_tags.valid_size checks: get_size() == _sz, where get_size() = sz + 3 + int subTagsSizeComplete = subTagsSz + 3; // get_size() = sz + 3 + if (subTagsSizeComplete != (int)sz - baseSz) + { + Debug.Log($" [TASK_SVR_NOTIFY_COMPLETE] the size of byte not meet !!! expected sub_tags size {subTagsSizeComplete}, got {sz - baseSz}"); + break; + } + + // Now safe to deserialize - buffer size matches expected size + // Marshal.SizeOf returns size with full MAX_SUB_TAGS array, but actual data is smaller + int fixedSize = Marshal.SizeOf(); + svr_task_complete svr_task_complete; + + if (sz < (uint)fixedSize) + { + // Buffer is smaller than fixed structure size, create padded buffer for safe deserialization + byte[] paddedBuf = new byte[fixedSize]; + Array.Copy(pBuf, paddedBuf, (int)sz); + // Zero-fill the rest (tags array will be zero-filled, which is safe) + + svr_task_complete = GPDataTypeHelper.FromBytes(paddedBuf); + // Restore the original sub_tags.sz value since padding might have corrupted it + svr_task_complete.sub_tags.sz = subTagsSz; + } + else + { + // Buffer is large enough for fixed structure size + svr_task_complete = GPDataTypeHelper.FromBytes(pBuf); + } svr_task_complete.get_data( ref ulTime, ref sub_tags @@ -537,7 +662,7 @@ namespace BrewMonster.Scripts.Task if (!pEntry.IsSuccess()) { - #if TASK_TEMPL_EDITOR + #if !TASK_TEMPL_EDITOR RecursiveCheckPunchMonster(this); #endif } @@ -573,7 +698,8 @@ namespace BrewMonster.Scripts.Task break; case TaskTemplConstants.TASK_SVR_NOTIFY_GIVE_UP: pLst = pTask.GetActiveTaskList(); - pLst.ClearTask(pTask, pEntry, false); + // Use simplified removal to keep list consistent in the managed port + pLst.RemoveEntry(pEntry); // TODO: Log task give up // if (m_bDisplayInTitleTaskUI) TaskInterface::UpdateTitleUI(m_ID); @@ -805,9 +931,9 @@ namespace BrewMonster.Scripts.Task // ����ﵽ���� bool bReachLimit = false; if (m_bHidden) - bReachLimit = pList.m_uTopHideTaskCount >= TASK_HIDDEN_COUNT; + bReachLimit = pList.m_uTopHideTaskCount >= TASK_.TASK_HIDDEN_COUNT; else if (m_bDisplayInTitleTaskUI) - bReachLimit = bReachLimit || pList.m_uTitleTaskCount >= TASK_TITLE_TASK_COUNT; + bReachLimit = bReachLimit || pList.m_uTitleTaskCount >= TASK_.TASK_TITLE_TASK_COUNT; else bReachLimit = bReachLimit || pList.m_uTopShowTaskCount >= pList.GetMaxSimultaneousCount(); @@ -1591,8 +1717,54 @@ namespace BrewMonster.Scripts.Task TaskGlobalData pGlobal, byte uParentIndex) { - // TODO: implement full logic when ActiveTaskList/ActiveTaskEntry and TaskInterface APIs are available - return null; + // 最小实现:保证接任务/放弃任务会真实改变 ActiveTaskList,从而驱动任务面板刷新 + // English: Minimal implementation: make NEW/GIVEUP actually mutate ActiveTaskList so UI can refresh. + if (pTask == null || pList == null) return null; + + // Already has task + var existed = pList.GetEntry(m_FixedData.m_ID); + if (existed != null && existed.m_ID != 0) return existed; + + if (pList.m_uTaskCount >= TaskInterfaceConstants.TASK_ACTIVE_LIST_MAX_LEN) return null; + + int insertIndex = pList.m_uTaskCount; + ActiveTaskEntry entry = pEntry ?? new ActiveTaskEntry(); + + // Fill fixed data (top-level by default) + entry.m_ID = (ushort)m_FixedData.m_ID; + entry.m_ulTemplAddr = m_FixedData.m_ID; // non-zero marker; template is resolved by ID in GetTempl() + entry.m_ulTaskTime = ulCurTime; + + entry.m_ParentIndex = (char)0xff; + entry.m_PrevSblIndex = (char)0xff; + entry.m_NextSblIndex = (char)0xff; + entry.m_ChildIndex = (char)0xff; + + entry.m_uState = (char)0; + entry.SetSuccess(); // default success, may be cleared by later checks + + if (ulCaptainTask != 0) + { + entry.m_uCapTaskId = (ushort)ulCaptainTask; + entry.m_ulCapTemplAddr = ulCaptainTask; // marker only + } + else + { + entry.m_uCapTaskId = 0; + entry.m_ulCapTemplAddr = 0; + } + + // Clear union buffer + if (entry.m_BufData != null) + Array.Clear(entry.m_BufData, 0, entry.m_BufData.Length); + + // Insert and update list count + pList.m_TaskEntries[insertIndex] = entry; + pList.m_uTaskCount = (byte)(insertIndex + 1); + + // Recount for UI and budget checks + pList.RecountTaskCounters(); + return entry; /*ActiveTaskEntry* aEntries = pList->m_TaskEntries; if (!pEntry) pEntry = aEntries + pList->m_uTaskCount; @@ -1814,6 +1986,29 @@ namespace BrewMonster.Scripts.Task int nChoice, ref task_sub_tags pSubTag) { + // 最小实现(客户端): + // - 记录到 FinishedTaskList(用于前置任务/可重复接判断) + // - 从 ActiveTaskList 移除该任务条目,从而驱动任务面板刷新 + // English minimal client port: + // - Record into FinishedTaskList (prerequisites/redo checks) + // - Remove entry from ActiveTaskList to refresh UI lists + if (pTask == null || pList == null || pEntry == null) return; + + bool success = pEntry.IsSuccess() && !pEntry.IsGiveUp(); + + // Only record on top-level templates (same as C++: !m_pParent && m_bNeedRecord) + if (m_pParent == null && m_FixedData.m_bNeedRecord) + { + if (pTask is CECTaskInterface cec) + { + cec.RecordFinishedTask(m_FixedData.m_ID, success); + } + } + + // Remove from active list (C++ RecursiveAward clears children + removes current entry) + pList.RemoveEntry(pEntry); + return; + // TODO : implement full logic when ActiveTaskList/ActiveTaskEntry and TaskInterface APIs are available /*{ char log[1024]; diff --git a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs index 9929273a4d..6dc02a28a2 100644 --- a/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs +++ b/Assets/PerfectWorld/Scripts/Task/TaskTempl.Struct.cs @@ -100,8 +100,23 @@ namespace BrewMonster.Scripts.Task [MarshalAs(UnmanagedType.ByValArray, SizeConst = TaskTemplConstants.TASK_STORAGE_COUNT)] public byte[] m_StoragesReceivePerDay; + // Initialize arrays if they are null + // In C++, arrays are automatically allocated on the stack, but in C# they need explicit initialization + public void EnsureInitialized() + { + if (m_Storages == null) + m_Storages = new ushort[TaskTemplConstants.TASK_STORAGE_COUNT * TaskTemplConstants.TASK_STORAGE_LEN]; + if (m_StoragesTaskSetCount == null) + m_StoragesTaskSetCount = new ushort[TaskTemplConstants.TASK_STORAGE_COUNT]; + if (m_StoragesRefreshTime == null) + m_StoragesRefreshTime = new uint[TaskTemplConstants.TASK_STORAGE_COUNT]; + if (m_StoragesReceivePerDay == null) + m_StoragesReceivePerDay = new byte[TaskTemplConstants.TASK_STORAGE_COUNT]; + } + public void RemoveAll() { + EnsureInitialized(); for (int i = 0; i < TaskTemplConstants.TASK_STORAGE_COUNT; i++) { for (int j = 0; j < TaskTemplConstants.TASK_STORAGE_LEN; j++) @@ -117,6 +132,11 @@ namespace BrewMonster.Scripts.Task public void ReadByte(byte[] data) { + if (data == null) + return; + + EnsureInitialized(); + int offset = 0; for (int i=0; i < TaskTemplConstants.TASK_STORAGE_COUNT; i++) { @@ -436,8 +456,58 @@ namespace BrewMonster.Scripts.Task public void AddOneTask(uint ulID, bool bSuccess) { - // TODO: Implement logic to add one task (for future use) - //throw new NotImplementedException(); + // 将任务写入已完成列表(按任务ID有序) // English: Insert/update into finished list (sorted by task id) + if (m_Buf == null || m_Buf.Length != TaskInterfaceConstants.TASK_FINISHED_LIST_BUF_SIZE) + { + m_Buf = new byte[TaskInterfaceConstants.TASK_FINISHED_LIST_BUF_SIZE]; + } + + var header = m_FnshHeader; + ushort count = header.m_uTaskCount; + if (count >= TaskInterfaceConstants.TASK_FINISHED_LIST_MAX_LEN) return; + + int entrySize = Marshal.SizeOf(); // should be 4 + int pos = GetTaskPos(ulID); + + byte mask = (byte)(bSuccess ? 0 : 1); + + if (pos >= 0) + { + // Update existing entry + int start = 4 + pos * entrySize; + // m_uTaskId + Array.Copy(BitConverter.GetBytes((ushort)ulID), 0, m_Buf, start, 2); + // m_Buf (mask + reserved bits) + m_Buf[start + 2] = (byte)((m_Buf[start + 2] & 0xFE) | (mask & 0x1)); + // m_FnshedCount: keep at least 1 + if (m_Buf[start + 3] == 0) m_Buf[start + 3] = 1; + return; + } + + // Find insertion index to keep sorted order + int insert = 0; + for (; insert < count; insert++) + { + ushort existingId = BitConverter.ToUInt16(m_Buf, 4 + insert * entrySize); + if (ulID < existingId) break; + } + + // Shift bytes to make room + int srcStart = 4 + insert * entrySize; + int bytesToMove = (count - insert) * entrySize; + if (bytesToMove > 0) + { + Buffer.BlockCopy(m_Buf, srcStart, m_Buf, srcStart + entrySize, bytesToMove); + } + + // Write new entry + int dst = 4 + insert * entrySize; + Array.Copy(BitConverter.GetBytes((ushort)ulID), 0, m_Buf, dst, 2); + m_Buf[dst + 2] = (byte)(mask & 0x1); // reserved bits 0 + m_Buf[dst + 3] = 1; // finish count + + header.m_uTaskCount = (ushort)(count + 1); + m_FnshHeader = header; } public void RemoveTask(uint ulID) @@ -461,22 +531,34 @@ namespace BrewMonster.Scripts.Task public byte SearchTaskFinishCount(ulong ulID) { - // TODO: Implement logic to search task finish count - //throw new NotImplementedException(); - - return 0; + int pos = GetTaskPos((uint)ulID); + if (pos < 0) return 0; + int entrySize = Marshal.SizeOf(); + int start = 4 + pos * entrySize; + return m_Buf[start + 3]; // m_FnshedCount } public void ResetFinishCount(ulong ulID) { - // TODO: Implement logic to reset finish count - //throw new NotImplementedException(); + int pos = GetTaskPos((uint)ulID); + if (pos < 0) return; + int entrySize = Marshal.SizeOf(); + int start = 4 + pos * entrySize; + m_Buf[start + 3] = 0; } public void AddForFinishCount(ulong ulID, bool bSuccess) { - // TODO: Implement logic to add for finish count - //throw new NotImplementedException(); + // 只用于计数:如果不存在则插入;如果存在则递增 m_FnshedCount + // English: Finish-count bookkeeping: insert if missing; otherwise increment m_FnshedCount. + AddOneTask((uint)ulID, bSuccess); + + int pos = GetTaskPos((uint)ulID); + if (pos < 0) return; + int entrySize = Marshal.SizeOf(); + int start = 4 + pos * entrySize; + byte cur = m_Buf[start + 3]; + if (cur < byte.MaxValue) m_Buf[start + 3] = (byte)(cur + 1); } public void RemoveAll() diff --git a/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs b/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs index e9ef3b7b18..9312be4a80 100644 --- a/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs +++ b/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs @@ -404,10 +404,12 @@ namespace BrewMonster.Scripts.Task.UI // void RefreshTaskTrace(); public bool UpdateTask(int idTask = -1) { - // if( m_szName != "Win_Quest" || m_iType != 0) - // { - // return true; - // } + // Only rebuild the list if viewing "Have Quest" (m_iType == 0) + // But always allow updating specific task details regardless of view type + if (idTask < 0 && m_iType != 0) + { + return true; + } // ATaskTemplMan *pMan = GetGame()->GetTaskTemplateMan(); ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan(); @@ -440,6 +442,7 @@ namespace BrewMonster.Scripts.Task.UI ATaskTempl pTemp = pMan.GetTaskTemplByID((uint)idTask); if (pTemp != null) { + // Only update description and name if task changed if( idTask != m_idLastTask ) { _nameTaskText.SetText(EC_Utility.FormatForTextMeshPro(GetTaskNameWithColor(pTemp))); @@ -452,9 +455,11 @@ namespace BrewMonster.Scripts.Task.UI m_pBtn_Abandon.interactable = pMan.CanGiveUpTask((uint)idTask); - // Get info + // Always refresh task state info to get latest progress data + // This ensures real-time updates when task progress changes + // When viewing "Have Quest" (m_iType == 0), tasks are active, so pass true to read kill counts Task_State_info tsi = new Task_State_info(); - pTask.GetTaskStateInfo((uint)idTask, ref tsi, false); + pTask.GetTaskStateInfo((uint)idTask, ref tsi, m_iType == 0); // Clear first strNewTextItem = ""; @@ -466,19 +471,19 @@ namespace BrewMonster.Scripts.Task.UI int nANPC = (int)pTemp.GetAwardNPC(); UpdateAwardNPC(ref strNewTextItem, nANPC); - // Complete condition + // Complete condition - always refresh to show updated progress UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi); - // Wanted Item + // Wanted Item - always refresh to show updated item counts UpdateItemWanted(ref strNewTextItem, tsi, idTask); // Treasure Map UpdateTreasureMap(ref strNewTextItem); - // Task Confirm + // Task Confirm - always refresh to update button state UpdateTaskConfirm(idTask, pTemp.m_FixedData.m_enumFinishType == (uint)TaskFinishType.enumTFTConfirm); - // Award + // Award - always refresh to show updated award preview Task_Award_Preview award = default; pTask.GetTaskAwardPreview((uint)idTask, ref award); @@ -526,6 +531,8 @@ namespace BrewMonster.Scripts.Task.UI // Guard: only handle search list when current UI type is 1 (search) public bool SearchForTask(int idTask = -1) { + // Only process search list when in search view (m_iType == 1) + // This prevents clearing the wrong list when updating if (m_iType != 1) { return true; @@ -647,10 +654,49 @@ namespace BrewMonster.Scripts.Task.UI return true; } // - // //�������������б��������ɽ�������ѽ����� zhangyitian + // //�������������б��������ɽ�������ѽ�������ѽ����� zhangyitian + // When task updates, the available task list also needs to be updated, otherwise the available task list won't update public bool UpdateQuestView() { - return UpdateTask() && SearchForTask(); + // Refresh the list for the current view type + // This ensures that when tasks are taken/completed/abandoned, the visible list is updated + bool result = true; + + if (m_iType == 0) + { + // Refresh "Have Quest" list (taken tasks) + result = UpdateTask(-1); + } + else if (m_iType == 1) + { + // Refresh "Search Quest" list (available tasks) + result = SearchForTask(-1); + } + + // Refresh the currently selected task details if one is selected + // This ensures task progress updates are reflected in real-time + var pTree = m_pTv_Quest; + var pItem = pTree?.GetSelectedItem(); + + if (pItem != null && pTree.transform != pItem.transform.parent) + { + uint selectedTaskId = pTree.GetItemData(pItem); + if (selectedTaskId > 0) + { + if (m_iType == 0) + { + // Refresh the selected task's details to show updated progress + UpdateTask((int)selectedTaskId); + } + else if (m_iType == 1) + { + // For search view, refresh the selected task + SearchForTask((int)selectedTaskId); + } + } + } + + return result; } // // bool IsPQTaskOrSubTask(int idTask);