Merge branch 'implement_task_UI' into feature/task-emote

This commit is contained in:
NguyenVanDat
2025-12-16 15:48:25 +07:00
7 changed files with 525 additions and 154 deletions
@@ -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}");
@@ -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()
{
+9 -12
View File
@@ -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<task_notify_base>() + Marshal.SizeOf<StorageTaskList>()) return;
// TODO: OnStorageData method not found in ATaskTemplMan; implement if needed
// ATaskTemplMan pMan = GetTaskTemplMan(pTask);
// if (pMan != null)
// {
// byte[] storageData = new byte[Marshal.SizeOf<StorageTaskList>()];
// Array.Copy(pBuf, Marshal.SizeOf<task_notify_base>(), storageData, 0, storageData.Length);
// pMan.OnStorageData(pTask, storageData);
// }
// TODO: UpdateTaskUI static method not found; implement UI update if needed
// TaskInterface.UpdateTaskUI(pNotify.task, pNotify.reason);
if (sz != Marshal.SizeOf<task_notify_base>() + Marshal.SizeOf<StorageTaskList>()) return;
ATaskTemplMan pMan = GetTaskTemplMan();
if (pMan != null)
{
byte[] storageData = new byte[Marshal.SizeOf<StorageTaskList>()];
Array.Copy(pBuf, Marshal.SizeOf<task_notify_base>(), storageData, 0, storageData.Length);
pMan.OnStorageData(pTask, storageData);
}
pTask.UpdateTaskUI(pNotify.task, pNotify.reason);
return;
}
// Handle special award notification
+98 -94
View File
@@ -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<TASK_ENTRY_FIXED_DATA>() ]; // 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<unsigned char>(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<unsigned char>(pInsert - pSrc);
// unsigned long i = 0;
//
// for (; i < static_cast<unsigned long>(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);
}
};
}
@@ -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<svr_new_task>(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<task_notify_base>() + 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 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<svr_new_task>(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<svr_new_task>(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<svr_task_complete>(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<task_notify_base>() + 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 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<svr_task_complete>(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<svr_task_complete>(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];
@@ -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<FnshedTaskEntry>(); // 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<FnshedTaskEntry>();
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<FnshedTaskEntry>();
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<FnshedTaskEntry>();
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()
+58 -12
View File
@@ -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);