From 02007bee504cacf6f933ce5faad61f7e7a63f0cc Mon Sep 17 00:00:00 2001 From: NguyenVanDat Date: Wed, 19 Nov 2025 15:10:12 +0700 Subject: [PATCH 1/3] add mutiplier player plugin --- Packages/manifest.json | 1 + Packages/packages-lock.json | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Packages/manifest.json b/Packages/manifest.json index b49917561b..aa0ae46bb6 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -11,6 +11,7 @@ "com.unity.ide.visualstudio": "2.0.23", "com.unity.inputsystem": "1.14.2", "com.unity.multiplayer.center": "1.0.0", + "com.unity.multiplayer.playmode": "1.6.1", "com.unity.render-pipelines.universal": "17.0.4", "com.unity.test-framework": "1.5.1", "com.unity.timeline": "1.8.9", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 36f9eeeee2..3d1771b022 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -134,6 +134,15 @@ "com.unity.modules.uielements": "1.0.0" } }, + "com.unity.multiplayer.playmode": { + "version": "1.6.1", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "2.0.2" + }, + "url": "https://packages.unity.com" + }, "com.unity.nuget.mono-cecil": { "version": "1.11.4", "depth": 3, @@ -141,6 +150,13 @@ "dependencies": {}, "url": "https://packages.unity.com" }, + "com.unity.nuget.newtonsoft-json": { + "version": "3.2.1", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.profiling.core": { "version": "1.0.2", "depth": 1, From 5dd011df46518290203db2ec8d572ba344d80dfd Mon Sep 17 00:00:00 2001 From: hungdk Date: Thu, 20 Nov 2025 02:18:48 +0700 Subject: [PATCH 2/3] Refactor and fixing ref --- .../Scripts/Managers/CECInventory.cs | 798 ++++++++++++++++ .../Scripts/Managers/CECInventory.cs.meta | 2 + .../Scripts/Managers/EC_Inventory.cs | 393 -------- .../Scripts/Managers/EC_Inventory.cs.meta | 2 - .../Scripts/Managers/EC_InventoryUI.cs | 8 +- .../Scripts/Managers/EC_IvtrEquip.cs | 4 +- .../Scripts/Managers/EC_IvtrItem.cs | 849 +++++++++++++++++- .../Scripts/Task/UI/TaskWindow.cs | 4 +- .../PerfectWorld/Scripts/UI/ShopItemPanel.cs | 2 +- Assets/PerfectWorld/Scripts/UI/pickupItem.cs | 2 +- Assets/Scripts/CECHostPlayer.cs | 2 +- 11 files changed, 1621 insertions(+), 445 deletions(-) create mode 100644 Assets/PerfectWorld/Scripts/Managers/CECInventory.cs create mode 100644 Assets/PerfectWorld/Scripts/Managers/CECInventory.cs.meta delete mode 100644 Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs delete mode 100644 Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs.meta diff --git a/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs b/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs new file mode 100644 index 0000000000..107493f88f --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs @@ -0,0 +1,798 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BrewMonster.Scripts.Managers +{ + /// + /// C# mirror of C++ CECInventory (EC_Inventory.h / EC_Inventory.cpp). + /// This is an instance-based inventory using a fixed-size item array. + /// + public class CECInventory + { + // Item array: index is slot, null means empty. + private EC_IvtrItem[] m_aItems = Array.Empty(); + + public CECInventory() + { + } + + public bool Init(int iSize) + { + Resize(iSize); + return true; + } + + public void Release() + { + // In C++ this deletes all heap-allocated items. + // Here we simply clear references so GC can collect them. + RemoveAllItems(); + m_aItems = Array.Empty(); + } + + public void RemoveAllItems() + { + for (int i = 0; i < m_aItems.Length; i++) + { + m_aItems[i] = null; + } + } + + public void Resize(int iNewSize) + { + int oldSize = m_aItems.Length; + if (iNewSize < 0) iNewSize = 0; + + if (iNewSize == oldSize) + return; + + var newArray = iNewSize > 0 ? new EC_IvtrItem[iNewSize] : Array.Empty(); + + if (oldSize > 0 && iNewSize > 0) + { + Array.Copy(m_aItems, newArray, Math.Min(oldSize, iNewSize)); + } + + m_aItems = newArray; + } + + public EC_IvtrItem PutItem(int iSlot, EC_IvtrItem pItem) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return null; + } + + var old = m_aItems[iSlot]; + m_aItems[iSlot] = pItem; + return old; + } + + public void SetItem(int iSlot, EC_IvtrItem pItem) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return; + } + + m_aItems[iSlot] = pItem; + } + + public EC_IvtrItem GetItem(int iSlot, bool bRemove = false) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return null; + } + + var pItem = m_aItems[iSlot]; + if (bRemove) + m_aItems[iSlot] = null; + return pItem; + } + + public void ExchangeItem(int iSlot1, int iSlot2) + { + if (iSlot1 < 0 || iSlot1 >= m_aItems.Length || + iSlot2 < 0 || iSlot2 >= m_aItems.Length) + { + return; + } + + if (iSlot1 == iSlot2) + return; + + var tmp = m_aItems[iSlot1]; + m_aItems[iSlot1] = m_aItems[iSlot2]; + m_aItems[iSlot2] = tmp; + } + + public bool MergeItem(int tid, int iExpireDate, int iAmount, out int piLastSlot, out int piLastAmount) + { + piLastSlot = -1; + piLastAmount = 0; + + int firstEmpty = -1; + + for (int i = 0; i < m_aItems.Length; i++) + { + var slotItem = m_aItems[i]; + if (slotItem != null) + { + if (slotItem.GetTemplateID() != tid) + continue; + + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(tid)); + int canAdd = Math.Max(0, pileLimit - Math.Max(0, slotItem.GetCount())); + if (canAdd <= 0) continue; + + int add = Math.Min(canAdd, iAmount); + slotItem.AddAmount(add); + iAmount -= add; + + if (iAmount == 0) + { + piLastSlot = i; + piLastAmount = slotItem.GetCount(); + return true; + } + } + else if (firstEmpty < 0) + { + firstEmpty = i; + } + } + + if (firstEmpty < 0 || iAmount <= 0) + { + return false; + } + + var newItem = new EC_IvtrItem(tid, iExpireDate) + { + Slot = firstEmpty, + State = 0, + Crc = 0, + Content = null + }; + newItem.SetCount(iAmount); + + m_aItems[firstEmpty] = newItem; + piLastSlot = firstEmpty; + piLastAmount = iAmount; + return true; + } + + public bool MoveItem(int iSrc, int iDest, int iAmount) + { + if (iSrc < 0 || iSrc >= m_aItems.Length || + iDest < 0 || iDest >= m_aItems.Length) + { + return false; + } + + var pSrc = m_aItems[iSrc]; + var pDst = m_aItems[iDest]; + + if (pSrc == null) + return false; + + if (iAmount == 0) + return false; + + if (pDst == null) + { + var clone = new EC_IvtrItem(pSrc.GetTemplateID(), pSrc.GetExpireDate()) + { + Slot = iDest, + Package = pSrc.Package, + State = pSrc.State, + Crc = pSrc.Crc, + Content = pSrc.Content != null ? (byte[])pSrc.Content.Clone() : null + }; + clone.SetCount(iAmount); + m_aItems[iDest] = clone; + } + else + { + if (pSrc.GetTemplateID() != pDst.GetTemplateID()) + return false; + + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(pDst.GetTemplateID())); + int canAdd = Math.Max(0, pileLimit - Math.Max(0, pDst.GetCount())); + int add = Math.Min(canAdd, iAmount); + if (add <= 0) return false; + pDst.AddAmount(add); + iAmount = add; + } + + RemoveItem(iSrc, iAmount); + return true; + } + + public bool RemoveItem(int iSlot, int iAmount) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return false; + } + + var pItem = m_aItems[iSlot]; + if (pItem == null) + return true; + + int newCount = pItem.AddAmount(-Math.Max(0, iAmount)); + if (newCount <= 0) + m_aItems[iSlot] = null; + + return true; + } + + public int FindItem(int idItem, int baseIdx = 0) + { + if (baseIdx < 0) baseIdx = 0; + for (int i = baseIdx; i < m_aItems.Length; i++) + { + var pItem = m_aItems[i]; + if (pItem != null && pItem.GetTemplateID() == idItem) + return i; + } + return -1; + } + + public int GetItemTotalNum(int idItem) + { + int count = 0; + for (int i = 0; i < m_aItems.Length; i++) + { + var pItem = m_aItems[i]; + if (pItem != null && pItem.GetTemplateID() == idItem) + count += Math.Max(0, pItem.GetCount()); + } + return count; + } + + public int SearchEmpty() + { + for (int i = 0; i < m_aItems.Length; i++) + { + if (m_aItems[i] == null) + return i; + } + return -1; + } + + public int GetEmptySlotNum() + { + int count = 0; + for (int i = 0; i < m_aItems.Length; i++) + { + if (m_aItems[i] == null) + count++; + } + return count; + } + + public int CanAddItem(int idItem, int iAmount, bool tryPile) + { + int foundEmpty = -1; + for (int i = 0; i < m_aItems.Length; i++) + { + var pItem = m_aItems[i]; + if (pItem == null) + { + if (!tryPile) return i; + if (foundEmpty < 0) foundEmpty = i; + } + else if (pItem.GetTemplateID() == idItem) + { + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(idItem)); + if (pItem.GetCount() + iAmount <= pileLimit) + return i; + } + } + return foundEmpty; + } + + public int GetSize() + { + return m_aItems.Length; + } + } + + /// + /// Static inventory facade used by the current client code. + /// This implementation was originally in EC_Inventory.cs and is now colocated + /// with CECInventory so both static-style and instance-style code continue to work. + /// + public static class EC_Inventory + { + // We currently support exactly three inventory packs, matching legacy C++ semantics: + // 0 = normal pack, 1 = equip pack, 2 = task pack. + private const int PackageCount = 3; + + // Fixed-size arrays per package, mimicking C++ CECInventory::m_aItems (APtrArray). + // Index is the slot index; a null entry means the slot is empty. + private static readonly int[] _packSizeByPackage = new int[PackageCount]; + private static readonly EC_IvtrItem[][] _itemsByPackage = new EC_IvtrItem[PackageCount][]; + private const int MaxContentHexToLog = 64; + + // Package constants to mirror legacy C++ names + public const byte IVTRTYPE_PACK = 0; + public const byte IVTRTYPE_EQUIPPACK = 1; + public const byte IVTRTYPE_TASKPACK = 2; + + private static int GetPackageIndex(byte pkg) + { + switch (pkg) + { + case IVTRTYPE_PACK: + case IVTRTYPE_EQUIPPACK: + case IVTRTYPE_TASKPACK: + return pkg; + default: + // Only three legacy packages are currently supported; ignore others safely. + Debug.LogWarning($"[Inventory] Unsupported package id={pkg}, expected 0..2."); + return -1; + } + } + + private static int GetPackSize(byte byPackage) + { + int idx = GetPackageIndex(byPackage); + if (idx < 0) return 0; + return _packSizeByPackage[idx]; + } + + private static EC_IvtrItem[] EnsureSlots(byte byPackage) + { + int idx = GetPackageIndex(byPackage); + if (idx < 0) + return Array.Empty(); + + int size = Math.Max(0, _packSizeByPackage[idx]); + var slots = _itemsByPackage[idx]; + + if (slots == null || slots.Length != size) + { + var newSlots = size > 0 ? new EC_IvtrItem[size] : Array.Empty(); + + // Preserve items that still fit into the resized pack, similar to C++ Resize. + if (slots != null && slots.Length > 0 && newSlots.Length > 0) + { + Array.Copy(slots, newSlots, Math.Min(slots.Length, newSlots.Length)); + } + + _itemsByPackage[idx] = newSlots; + slots = newSlots; + } + + return slots; + } + + private static string GetPackageName(byte pkg) + { + switch (pkg) + { + case 0: return "IVTRTYPE_PACK"; + case 1: return "IVTRTYPE_EQUIPPACK"; + case 2: return "IVTRTYPE_TASKPACK"; + default: return "PACK_UNKNOWN"; + } + } + + private static string BytesToHex(byte[] bytes, int max) + { + if (bytes == null || bytes.Length == 0) return ""; + int len = Math.Min(bytes.Length, max); + string hex = BitConverter.ToString(bytes, 0, len); + if (bytes.Length > len) hex += "-..."; + return hex; + } + + // C++ has CECInventory per pack; this helper returns a snapshot Dictionary view + // of the current slots for convenience where a map-like API is easier to use. + public static Dictionary GetPack(byte byPackage) + { + var slots = EnsureSlots(byPackage); + var result = new Dictionary(slots.Length); + for (int i = 0; i < slots.Length; i++) + { + var it = slots[i]; + if (it != null) + result[i] = it; + } + return result; + } + + public static EC_IvtrItem GetItem(int iSlot, bool bRemove) + { + // Backward-compatible overload defaulting to inventory package + return GetItem(IVTRTYPE_PACK, iSlot, bRemove); + } + + public static EC_IvtrItem GetItem(byte byPackage, int slot, bool remove) + { + var slots = EnsureSlots(byPackage); + if (slot < 0 || slot >= slots.Length) + return null; + + var item = slots[slot]; + if (remove && slot >= 0 && slot < slots.Length) + slots[slot] = null; + return item; + } + + public static void Resize(byte byPackage, int newSize) + { + int idx = GetPackageIndex(byPackage); + if (idx < 0) + return; + + newSize = Math.Max(0, newSize); + int oldSize = _packSizeByPackage[idx]; + _packSizeByPackage[idx] = newSize; + + var oldSlots = _itemsByPackage[idx]; + + if (oldSlots == null) + { + _itemsByPackage[idx] = newSize > 0 ? new EC_IvtrItem[newSize] : Array.Empty(); + return; + } + + if (oldSize == newSize && oldSlots.Length == newSize) + return; + + var newSlots = newSize > 0 ? new EC_IvtrItem[newSize] : Array.Empty(); + if (oldSlots.Length > 0 && newSlots.Length > 0) + { + Array.Copy(oldSlots, newSlots, Math.Min(oldSlots.Length, newSlots.Length)); + } + _itemsByPackage[idx] = newSlots; + } + + public static EC_IvtrItem PutItem(byte byPackage, int slot, EC_IvtrItem item) + { + var slots = EnsureSlots(byPackage); + if (slot < 0 || slot >= slots.Length) + return null; + + var oldItem = slots[slot]; + slots[slot] = item; + return oldItem; + } + + public static void SetItem(byte byPackage, int slot, EC_IvtrItem item) + { + var slots = EnsureSlots(byPackage); + if (slot < 0 || slot >= slots.Length) + return; + slots[slot] = item; + } + + public static void ExchangeItem(byte byPackage, int slot1, int slot2) + { + if (slot1 == slot2) return; + var slots = EnsureSlots(byPackage); + if (slot1 < 0 || slot1 >= slots.Length || slot2 < 0 || slot2 >= slots.Length) + return; + + var i1 = slots[slot1]; + var i2 = slots[slot2]; + slots[slot1] = i2; + slots[slot2] = i1; + } + + public static bool RemoveItem(byte byPackage, int slot, int amount) + { + var slots = EnsureSlots(byPackage); + if (slot < 0 || slot >= slots.Length) + return false; + + var item = slots[slot]; + if (item == null) + return true; + + int newCount = Math.Max(0, item.m_iCount - Math.Max(0, amount)); + item.m_iCount = newCount; + if (newCount <= 0) + slots[slot] = null; + + return true; + } + + public static void RemoveAllItems(byte byPackage) + { + var slots = EnsureSlots(byPackage); + for (int i = 0; i < slots.Length; i++) + { + slots[i] = null; + } + } + + public static int SearchEmpty(byte byPackage) + { + var slots = EnsureSlots(byPackage); + for (int i = 0; i < slots.Length; i++) + { + if (slots[i] == null) + return i; + } + return -1; + } + + public static int GetEmptySlotNum(byte byPackage) + { + var slots = EnsureSlots(byPackage); + int empty = 0; + for (int i = 0; i < slots.Length; i++) + { + if (slots[i] == null) + empty++; + } + return empty; + } + + public static int FindItem(byte byPackage, int templateId, int baseIdx = 0) + { + var slots = EnsureSlots(byPackage); + if (baseIdx < 0) baseIdx = 0; + for (int i = baseIdx; i < slots.Length; i++) + { + var it = slots[i]; + if (it != null && it.m_tid == templateId) + return i; + } + return -1; + } + + public static int GetItemTotalNum(byte byPackage, int templateId) + { + int total = 0; + var slots = EnsureSlots(byPackage); + for (int i = 0; i < slots.Length; i++) + { + var it = slots[i]; + if (it != null && it.m_tid == templateId) + total += Math.Max(0, it.m_iCount); + } + return total; + } + + public static int GetItemCanPileCount(byte byPackage, int templateId) + { + int ret = 0; + var slots = EnsureSlots(byPackage); + for (int i = 0; i < slots.Length; i++) + { + var it = slots[i]; + if (it != null && it.m_tid == templateId) + { + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(templateId)); + ret += Math.Max(0, pileLimit - Math.Max(0, it.m_iCount)); + } + } + return ret; + } + + public static int CanAddItem(byte byPackage, int templateId, int amount, bool tryPile) + { + int firstEmpty = -1; + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(templateId)); + var slots = EnsureSlots(byPackage); + for (int i = 0; i < slots.Length; i++) + { + var it = slots[i]; + if (it == null) + { + // return first empty slot if not trying to pile item + if (!tryPile) return i; + if (firstEmpty < 0) firstEmpty = i; + } + else if (it.m_tid == templateId && it.m_iCount + amount <= pileLimit) + { + return i; + } + } + return firstEmpty; + } + + public static bool MergeItem(byte byPackage, int templateId, int expireDate, int amount, out int lastSlot, out int slotAmount) + { + lastSlot = -1; + slotAmount = 0; + var slots = EnsureSlots(byPackage); + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(templateId)); + int firstEmpty = -1; + + for (int i = 0; i < slots.Length && amount > 0; i++) + { + var slotItem = slots[i]; + if (slotItem == null) + { + if (firstEmpty < 0) firstEmpty = i; + continue; + } + if (slotItem.m_tid != templateId) continue; + int canAdd = Math.Max(0, pileLimit - Math.Max(0, slotItem.m_iCount)); + if (canAdd <= 0) continue; + int add = Math.Min(canAdd, amount); + slotItem.m_iCount += add; + amount -= add; + lastSlot = i; + slotAmount = slotItem.m_iCount; + } + if (amount <= 0) return true; + if (firstEmpty < 0) return false; + + var newItem = new EC_IvtrItem + { + Package = byPackage, + Slot = firstEmpty, + m_tid = templateId, + m_expire_date = expireDate, + State = 0, + m_iCount = amount, + Crc = 0, + Content = null + }; + slots[firstEmpty] = newItem; + lastSlot = firstEmpty; + slotAmount = amount; + return true; + } + + public static bool MoveItem(byte byPackage, int src, int dest, int amount) + { + if (src == dest) return false; + if (amount <= 0) return false; + + var slots = EnsureSlots(byPackage); + if (src < 0 || src >= slots.Length || dest < 0 || dest >= slots.Length) return false; + + var srcItem = slots[src]; + var dstItem = slots[dest]; + if (srcItem == null) return false; + + if (dstItem == null) + { + var clone = new EC_IvtrItem + { + Package = byPackage, + Slot = dest, + m_tid = srcItem.m_tid, + m_expire_date = srcItem.m_expire_date, + State = srcItem.State, + m_iCount = amount, + Crc = srcItem.Crc, + Content = srcItem.Content != null ? (byte[])srcItem.Content.Clone() : null + }; + slots[dest] = clone; + } + else + { + if (dstItem.m_tid != srcItem.m_tid) return false; + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(dstItem.m_tid)); + int canAdd = Math.Max(0, pileLimit - Math.Max(0, dstItem.m_iCount)); + int add = Math.Min(canAdd, amount); + if (add <= 0) return false; + dstItem.m_iCount += add; + amount = add; + } + RemoveItem(byPackage, src, amount); + return true; + } + + public static void UpdatePack(byte byPackage, int ivtrSize, IEnumerable items) + { + Resize(byPackage, ivtrSize); + var slots = EnsureSlots(byPackage); + + // Clear existing entries; keep size. + for (int i = 0; i < slots.Length; i++) + slots[i] = null; + + if (items != null) + { + foreach (var it in items) + { + if (it != null && it.Slot >= 0 && it.Slot < slots.Length) + { + slots[it.Slot] = it; + } + } + } + // Log this pack's items + LogPackInternal(byPackage, ivtrSize, slots); + } + + public static bool ResetWithDetailData(byte byPackage, int ivtrSize, byte[] data) + { + // Uses EC_IvtrItem.TryParseInventoryDetail format + if (data == null) + { + Resize(byPackage, ivtrSize); + RemoveAllItems(byPackage); + return true; + } + if (!EC_IvtrItemUtils.Instance.TryParseInventoryDetail(data, out var pkg, out var size, out var items)) + return false; + // Prefer header values when valid + byte finalPkg = byPackage; + if (pkg == byPackage) finalPkg = pkg; + int finalSize = ivtrSize > 0 ? ivtrSize : size; + UpdatePack(finalPkg, finalSize, items); + return true; + } + + private static void LogPackInternal(byte byPackage, int ivtrSize, EC_IvtrItem[] slots) + { + //Debug.Log($"[Inventory] === Pack {GetPackageName(byPackage)}({byPackage}) size={ivtrSize}, items={slots?.Length ?? 0} ==="); + if (slots == null || slots.Length == 0) + { + //Debug.Log("[Inventory] (empty)"); + return; + } + for (int i = 0; i < slots.Length; i++) + { + var it = slots[i]; + if (it == null) + continue; + + string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(it.m_tid); + string extraHex = it.Content != null && it.Content.Length > 0 ? EC_IvtrItemUtils.Instance.BytesToHex(it.Content, MaxContentHexToLog) : ""; + //int extraLen = it.Content?.Length ?? 0; + //Debug.Log( + // $"[Inventory] pkg={GetPackageName(it.Package)}({it.Package}) slot={it.Slot} tid={it.TemplateId}{(string.IsNullOrEmpty(itemName) ? "" : " \"" + itemName + "\"")} count={it.Count} state={it.State} expire={it.ExpireDate} crc={it.Crc} content_len={extraLen}{(extraLen > 0 ? ", content_hex=" + extraHex : "")}" + //); + } + } + + public static void LogInventoryPacket(string tag, byte[] buffer, int hostId) + { + if (buffer == null) + { + return; + } + + int index = 0; + if (buffer.Length < 6) + { + //LogInventoryRaw(tag, buffer); + return; + } + + byte byPackage = buffer[index++]; + byte ivtrSize = buffer[index++]; + uint contentLength = BitConverter.ToUInt32(buffer, index); index += 4; + + int remaining = buffer.Length - index; + int contentBytes = remaining; + if (contentLength < (uint)remaining) + { + contentBytes = (int)contentLength; + } + + if (contentBytes > 0) + { + byte[] content = new byte[contentBytes]; + Buffer.BlockCopy(buffer, index, content, 0, contentBytes); + } + + int trailing = buffer.Length - (index + contentBytes); + if (trailing > 0) + { + byte[] tail = new byte[trailing]; + Buffer.BlockCopy(buffer, index + contentBytes, tail, 0, trailing); + } + } + + public static void LogInventoryRaw(string tag, byte[] buffer) + { + Debug.Log($"[Inventory] {tag}: RAW HEX (len={buffer?.Length ?? 0})=\n{(buffer == null ? "" : BitConverter.ToString(buffer))}"); + } + } +} + diff --git a/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs.meta b/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs.meta new file mode 100644 index 0000000000..8f8e305b75 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0632a4edec7d56543a1bfea4c263ba81 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs b/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs deleted file mode 100644 index ce5b1312cf..0000000000 --- a/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace BrewMonster.Scripts.Managers -{ - - public static class EC_Inventory - { - private static readonly Dictionary _packSizeByPackage = new Dictionary(); - private static readonly Dictionary> _itemsByPackage = new Dictionary>(); - private const int MaxContentHexToLog = 64; - - // Package constants to mirror legacy C++ names - public const byte IVTRTYPE_PACK = 0; - public const byte IVTRTYPE_EQUIPPACK = 1; - public const byte IVTRTYPE_TASKPACK = 2; - - private static string GetPackageName(byte pkg) - { - switch (pkg) - { - case 0: return "IVTRTYPE_PACK"; - case 1: return "IVTRTYPE_EQUIPPACK"; - case 2: return "IVTRTYPE_TASKPACK"; - default: return "PACK_UNKNOWN"; - } - } - - private static string BytesToHex(byte[] bytes, int max) - { - if (bytes == null || bytes.Length == 0) return ""; - int len = Math.Min(bytes.Length, max); - string hex = BitConverter.ToString(bytes, 0, len); - if (bytes.Length > len) hex += "-..."; - return hex; - } - - public static Dictionary GetPack(byte byPackage) - { - return _itemsByPackage[byPackage]; - } - - public static EC_IvtrItem GetItem(int iSlot, bool bRemove) - { - // Backward-compatible overload defaulting to inventory package - return GetItem(IVTRTYPE_PACK, iSlot, bRemove); - } - - public static EC_IvtrItem GetItem(byte byPackage, int slot, bool remove) - { - if (!_itemsByPackage.TryGetValue(byPackage, out var slots) || slot < 0) - return null; - if (!slots.TryGetValue(slot, out var item)) - return null; - if (remove) - slots.Remove(slot); - return item; - } - - private static Dictionary EnsureSlots(byte byPackage) - { - if (!_itemsByPackage.TryGetValue(byPackage, out var slots) || slots == null) - { - slots = new Dictionary(); - _itemsByPackage[byPackage] = slots; - } - return slots; - } - - public static void Resize(byte byPackage, int newSize) - { - _packSizeByPackage[byPackage] = Math.Max(0, newSize); - EnsureSlots(byPackage); - } - - public static EC_IvtrItem PutItem(byte byPackage, int slot, EC_IvtrItem item) - { - var slots = EnsureSlots(byPackage); - if (slot < 0) return null; - slots.TryGetValue(slot, out var oldItem); - slots[slot] = item; - return oldItem; - } - - public static void SetItem(byte byPackage, int slot, EC_IvtrItem item) - { - var slots = EnsureSlots(byPackage); - if (slot < 0) return; - slots[slot] = item; - } - - public static void ExchangeItem(byte byPackage, int slot1, int slot2) - { - if (slot1 == slot2) return; - var slots = EnsureSlots(byPackage); - slots.TryGetValue(slot1, out var i1); - slots.TryGetValue(slot2, out var i2); - if (i1 != null) slots[slot2] = i1; else slots.Remove(slot2); - if (i2 != null) slots[slot1] = i2; else slots.Remove(slot1); - } - - public static bool RemoveItem(byte byPackage, int slot, int amount) - { - var slots = EnsureSlots(byPackage); - if (!slots.TryGetValue(slot, out var item) || item == null) - return true; - int newCount = Math.Max(0, item.m_iCount - Math.Max(0, amount)); - item.m_iCount = newCount; - if (newCount <= 0) - slots.Remove(slot); - else - slots[slot] = item; - return true; - } - - public static void RemoveAllItems(byte byPackage) - { - var slots = EnsureSlots(byPackage); - slots.Clear(); - } - - public static int SearchEmpty(byte byPackage) - { - int size = _packSizeByPackage.TryGetValue(byPackage, out var s) ? s : 0; - var slots = EnsureSlots(byPackage); - for (int i = 0; i < size; i++) - if (!slots.ContainsKey(i)) return i; - return -1; - } - - public static int GetEmptySlotNum(byte byPackage) - { - int size = _packSizeByPackage.TryGetValue(byPackage, out var s) ? s : 0; - int occupied = EnsureSlots(byPackage).Count; - int empty = Math.Max(0, size - occupied); - return empty; - } - - public static int FindItem(byte byPackage, int templateId, int baseIdx = 0) - { - var slots = EnsureSlots(byPackage); - int size = _packSizeByPackage.TryGetValue(byPackage, out var s) ? s : 0; - if (baseIdx < 0) baseIdx = 0; - for (int i = baseIdx; i < size; i++) - { - if (slots.TryGetValue(i, out var it) && it != null && it.m_tid == templateId) - return i; - } - return -1; - } - - public static int GetItemTotalNum(byte byPackage, int templateId) - { - int total = 0; - foreach (var kv in EnsureSlots(byPackage)) - { - var it = kv.Value; - if (it != null && it.m_tid == templateId) - total += Math.Max(0, it.m_iCount); - } - return total; - } - - public static int GetItemCanPileCount(byte byPackage, int templateId) - { - int ret = 0; - foreach (var kv in EnsureSlots(byPackage)) - { - var it = kv.Value; - if (it != null && it.m_tid == templateId) - { - int pileLimit = Math.Max(1, EC_IvtrItemUtils.GetPileLimit(templateId)); - ret += Math.Max(0, pileLimit - Math.Max(0, it.m_iCount)); - } - } - return ret; - } - - public static int CanAddItem(byte byPackage, int templateId, int amount, bool tryPile) - { - int firstEmpty = -1; - int pileLimit = Math.Max(1, EC_IvtrItemUtils.GetPileLimit(templateId)); - int size = _packSizeByPackage.TryGetValue(byPackage, out var s) ? s : 0; - var slots = EnsureSlots(byPackage); - for (int i = 0; i < size; i++) - { - if (!slots.TryGetValue(i, out var it) || it == null) - { - if (!tryPile) return i; - if (firstEmpty < 0) firstEmpty = i; - } - else if (it.m_tid == templateId && it.m_iCount + amount <= pileLimit) - { - return i; - } - } - return firstEmpty; - } - - public static bool MergeItem(byte byPackage, int templateId, int expireDate, int amount, out int lastSlot, out int slotAmount) - { - lastSlot = -1; - slotAmount = 0; - var slots = EnsureSlots(byPackage); - int pileLimit = Math.Max(1, EC_IvtrItemUtils.GetPileLimit(templateId)); - int firstEmpty = -1; - int size = _packSizeByPackage.TryGetValue(byPackage, out var s) ? s : 0; - for (int i = 0; i < size && amount > 0; i++) - { - slots.TryGetValue(i, out var slotItem); - if (slotItem == null) - { - if (firstEmpty < 0) firstEmpty = i; - continue; - } - if (slotItem.m_tid != templateId) continue; - int canAdd = Math.Max(0, pileLimit - Math.Max(0, slotItem.m_iCount)); - if (canAdd <= 0) continue; - int add = Math.Min(canAdd, amount); - slotItem.m_iCount += add; - amount -= add; - lastSlot = i; - slotAmount = slotItem.m_iCount; - } - if (amount <= 0) return true; - if (firstEmpty < 0) return false; - var newItem = new EC_IvtrItem - { - Package = byPackage, - Slot = firstEmpty, - m_tid = templateId, - m_expire_date = expireDate, - State = 0, - m_iCount = amount, - Crc = 0, - Content = null - }; - slots[firstEmpty] = newItem; - lastSlot = firstEmpty; - slotAmount = amount; - return true; - } - - public static bool MoveItem(byte byPackage, int src, int dest, int amount) - { - if (src == dest) return false; - if (amount <= 0) return false; - int size = _packSizeByPackage.TryGetValue(byPackage, out var s) ? s : 0; - if (src < 0 || src >= size || dest < 0 || dest >= size) return false; - var slots = EnsureSlots(byPackage); - if (!slots.TryGetValue(src, out var srcItem) || srcItem == null) return false; - slots.TryGetValue(dest, out var dstItem); - if (dstItem == null) - { - var clone = new EC_IvtrItem - { - Package = byPackage, - Slot = dest, - m_tid = srcItem.m_tid, - m_expire_date = srcItem.m_expire_date, - State = srcItem.State, - m_iCount = amount, - Crc = srcItem.Crc, - Content = srcItem.Content != null ? (byte[])srcItem.Content.Clone() : null - }; - slots[dest] = clone; - } - else - { - if (dstItem.m_tid != srcItem.m_tid) return false; - int pileLimit = Math.Max(1, EC_IvtrItemUtils.GetPileLimit(dstItem.m_tid)); - int canAdd = Math.Max(0, pileLimit - Math.Max(0, dstItem.m_iCount)); - int add = Math.Min(canAdd, amount); - if (add <= 0) return false; - dstItem.m_iCount += add; - amount = add; - } - RemoveItem(byPackage, src, amount); - return true; - } - public static void UpdatePack(byte byPackage, int ivtrSize, IEnumerable items) - { - _packSizeByPackage[byPackage] = ivtrSize; - if (!_itemsByPackage.TryGetValue(byPackage, out var slots)) - { - slots = new Dictionary(); - _itemsByPackage[byPackage] = slots; - } - slots.Clear(); - if (items != null) - { - foreach (var it in items) - { - if (it != null && it.Slot >= 0) - { - slots[it.Slot] = it; - } - } - } - // Log this pack's items - LogPackInternal(byPackage, ivtrSize, slots); - } - - public static bool ResetWithDetailData(byte byPackage, int ivtrSize, byte[] data) - { - // Uses EC_IvtrItem.TryParseInventoryDetail format - if (data == null) - { - Resize(byPackage, ivtrSize); - RemoveAllItems(byPackage); - return true; - } - if (!EC_IvtrItemUtils.TryParseInventoryDetail(data, out var pkg, out var size, out var items)) - return false; - // Prefer header values when valid - byte finalPkg = byPackage; - if (pkg == byPackage) finalPkg = pkg; - int finalSize = ivtrSize > 0 ? ivtrSize : size; - UpdatePack(finalPkg, finalSize, items); - return true; - } - - - private static void LogPackInternal(byte byPackage, int ivtrSize, IReadOnlyDictionary slots) - { - //Debug.Log($"[Inventory] === Pack {GetPackageName(byPackage)}({byPackage}) size={ivtrSize}, items={(slots?.Count ?? 0)} ==="); - if (slots == null || slots.Count == 0) - { - //Debug.Log("[Inventory] (empty)"); - return; - } - foreach (var kv in slots) - { - var it = kv.Value; - string itemName = EC_IvtrItemUtils.ResolveItemName(it.m_tid); - string extraHex = it.Content != null && it.Content.Length > 0 ? EC_IvtrItemUtils.BytesToHex(it.Content, MaxContentHexToLog) : ""; - //int extraLen = it.Content?.Length ?? 0; - //Debug.Log( - // $"[Inventory] pkg={GetPackageName(it.Package)}({it.Package}) slot={it.Slot} tid={it.TemplateId}{(string.IsNullOrEmpty(itemName) ? "" : " \"" + itemName + "\"")} count={it.Count} state={it.State} expire={it.ExpireDate} crc={it.Crc} content_len={extraLen}{(extraLen > 0 ? ", content_hex=" + extraHex : "")}" - //); - } - } - - public static void LogInventoryPacket(string tag, byte[] buffer, int hostId) - { - if (buffer == null) - { - return; - } - - int index = 0; - if (buffer.Length < 6) - { - //LogInventoryRaw(tag, buffer); - return; - } - - byte byPackage = buffer[index++]; - byte ivtrSize = buffer[index++]; - uint contentLength = BitConverter.ToUInt32(buffer, index); index += 4; - - int remaining = buffer.Length - index; - int contentBytes = remaining; - if (contentLength < (uint)remaining) - { - contentBytes = (int)contentLength; - } - - - if (contentBytes > 0) - { - byte[] content = new byte[contentBytes]; - Buffer.BlockCopy(buffer, index, content, 0, contentBytes); - } - - int trailing = buffer.Length - (index + contentBytes); - if (trailing > 0) - { - byte[] tail = new byte[trailing]; - Buffer.BlockCopy(buffer, index + contentBytes, tail, 0, trailing); - } - } - - public static void LogInventoryRaw(string tag, byte[] buffer) - { - Debug.Log($"[Inventory] {tag}: RAW HEX (len={buffer?.Length ?? 0})=\n{(buffer == null ? "" : BitConverter.ToString(buffer))}"); - } - - } -} - - diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs.meta b/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs.meta deleted file mode 100644 index 2a2e71aaea..0000000000 --- a/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b060723330d7f49409ca241f4e460bed \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs b/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs index 07549d66ee..29f0904155 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs @@ -384,7 +384,7 @@ namespace BrewMonster.Scripts.Managers { return string.Empty; } - string itemName = EC_IvtrItemUtils.ResolveItemName(itemData.m_tid); + string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(itemData.m_tid); string displayText = string.IsNullOrEmpty(itemName) ? $"Item {itemData.m_tid}" : itemName; if (itemData.m_iCount > 1) { @@ -723,7 +723,7 @@ namespace BrewMonster.Scripts.Managers // Set icon sprite based on item TemplateId if (hasItem && itemData != null && itemData.m_iCount > 0) { - var sprite = EC_IvtrItemUtils.ResolveItemIconSprite(itemData.m_tid); + var sprite = EC_IvtrItemUtils.Instance.ResolveItemIconSprite(itemData.m_tid); image.sprite = sprite; image.enabled = true; } @@ -906,7 +906,7 @@ namespace BrewMonster.Scripts.Managers } // Get user-friendly text from string tables - string itemName = EC_IvtrItemUtils.ResolveItemName(item.m_tid); + string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(item.m_tid); string itemDescription = GetItemDescription(item.m_tid); string itemExtendedDesc = GetItemExtendedDescription(item.m_tid); @@ -1075,7 +1075,7 @@ namespace BrewMonster.Scripts.Managers foreach (int holeTid in currentSelectedEquipment.Holes) { if (holeTid == 0) continue; - string stoneName = EC_IvtrItemUtils.ResolveItemName(holeTid); + string stoneName = EC_IvtrItemUtils.Instance.ResolveItemName(holeTid); // Try to fetch a description from string tables; fallback to name if unavailable string stoneDesc = GetItemDescription(holeTid) ?? stoneName; if (!string.IsNullOrEmpty(stoneName)) diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs index 7a0bd66425..311bb7e50a 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrEquip.cs @@ -735,7 +735,7 @@ namespace PerfectWorld.Scripts.Managers /// public string GetName() { - return EC_IvtrItemUtils.ResolveItemName(TemplateId); + return EC_IvtrItemUtils.Instance.ResolveItemName(TemplateId); } /// @@ -2386,7 +2386,7 @@ namespace PerfectWorld.Scripts.Managers continue; // Get item name - string itemName = EC_IvtrItemUtils.ResolveItemName(hole); + string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(hole); string descText = "null"; // This would normally check if it's a stone and get its description diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs index b1230d716b..bac2feae4a 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs @@ -10,30 +10,27 @@ using UnityEngine; namespace BrewMonster.Scripts.Managers { - public class EC_IvtrItem + // NOTE: The original lightweight EC_IvtrItem packet struct has been merged into the + // EC_IvtrItem class below (which mirrors C++ CECIvtrItem). Network-only fields such as + // Package / Slot / State / Crc / Content are now stored on that class. + /// + /// Non-static UI/data helper for inventory items. + /// This holds caches and helpers for names, icons, pile limits, etc. + /// + public class EC_IvtrItemUtils { - public byte Package; - public int Slot; + // Simple singleton-style access so existing systems can use it globally. + public static readonly EC_IvtrItemUtils Instance = new EC_IvtrItemUtils(); - public int m_tid; - public int m_expire_date; - public int State; - public int m_iCount; - public ushort Crc; - public byte[] Content; // variable-length item-specific payload (can be null) - } - - public static class EC_IvtrItemUtils - { - private static readonly Dictionary _tidNameCache = new Dictionary(); - private static readonly Dictionary _pileLimitCache = new Dictionary(); - private static readonly Dictionary _tidIconKeyCache = new Dictionary(); - private static readonly Dictionary _iconSpriteCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static Sprite[] _multiSpriteAtlas = null; - private static readonly Dictionary _spriteNameToIndexCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _tidNameCache = new Dictionary(); + private readonly Dictionary _pileLimitCache = new Dictionary(); + private readonly Dictionary _tidIconKeyCache = new Dictionary(); + private readonly Dictionary _iconSpriteCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private Sprite[] _multiSpriteAtlas = null; + private readonly Dictionary _spriteNameToIndexCache = new Dictionary(StringComparer.OrdinalIgnoreCase); private const int MaxContentHexToLog = 64; - public static string ResolveItemName(int templateId) + public string ResolveItemName(int templateId) { if (templateId <= 0) return ""; //if (_tidNameCache.TryGetValue(templateId, out var cached)) return cached; @@ -61,7 +58,7 @@ namespace BrewMonster.Scripts.Managers /// /// Resolve and load the item's icon Sprite from Resources/UI/IconSprites based on its element data file_icon (DDS) name. /// - public static Sprite ResolveItemIconSprite(int templateId) + public Sprite ResolveItemIconSprite(int templateId) { if (templateId <= 0) return null; if (_tidIconKeyCache.TryGetValue(templateId, out var cachedKey)) @@ -105,7 +102,7 @@ namespace BrewMonster.Scripts.Managers return spriteLoaded; } - private static Sprite LoadIconSpriteByKey(string key) + private Sprite LoadIconSpriteByKey(string key) { if (string.IsNullOrEmpty(key)) return null; @@ -148,7 +145,7 @@ namespace BrewMonster.Scripts.Managers return null; } - private static void LoadMultiSpriteAtlas() + private void LoadMultiSpriteAtlas() { try { @@ -183,7 +180,7 @@ namespace BrewMonster.Scripts.Managers } } - private static object TryFindElementByScanningArrays(object edm, uint id) + private object TryFindElementByScanningArrays(object edm, uint id) { if (edm == null) return null; try @@ -213,7 +210,7 @@ namespace BrewMonster.Scripts.Managers return null; } - private static string ExtractIconPathFromElement(object data) + private string ExtractIconPathFromElement(object data) { if (data == null) return string.Empty; var t = data.GetType(); @@ -255,7 +252,7 @@ namespace BrewMonster.Scripts.Managers return string.Empty; } - private static string NormalizeIconResourceKeyFromPath(string iconPath) + private string NormalizeIconResourceKeyFromPath(string iconPath) { if (string.IsNullOrEmpty(iconPath)) return string.Empty; try @@ -274,13 +271,13 @@ namespace BrewMonster.Scripts.Managers return string.Empty; } - private static string CacheAndReturn(int tid, string name) + private string CacheAndReturn(int tid, string name) { _tidNameCache[tid] = name ?? ""; return name ?? ""; } - private static string ExtractNameFromElement(object data) + private string ExtractNameFromElement(object data) { if (data == null) return ""; var t = data.GetType(); @@ -344,7 +341,7 @@ namespace BrewMonster.Scripts.Managers return ""; } - private static string TryFindNameByScanningArrays(object edm, uint id) + private string TryFindNameByScanningArrays(object edm, uint id) { try { @@ -376,7 +373,7 @@ namespace BrewMonster.Scripts.Managers return ""; } - public static bool TryParseInventoryDetail(byte[] buffer, out byte byPackage, out byte ivtrSize, out List items) + public bool TryParseInventoryDetail(byte[] buffer, out byte byPackage, out byte ivtrSize, out List items) { byPackage = 0; ivtrSize = 0; @@ -460,24 +457,22 @@ namespace BrewMonster.Scripts.Managers ci += extraLen; } - var item = new EC_IvtrItem + var item = new EC_IvtrItem(tid, expireDate) { Package = byPackage, Slot = slotIndex, - m_tid = tid, - m_expire_date = expireDate, State = state, - m_iCount = amount, Crc = crc, Content = extra }; + item.SetCount(amount); items.Add(item); } return true; } - public static string BytesToHex(byte[] bytes, int max) + public string BytesToHex(byte[] bytes, int max) { if (bytes == null || bytes.Length == 0) return ""; int len = Math.Min(bytes.Length, max); @@ -486,7 +481,7 @@ namespace BrewMonster.Scripts.Managers return hex; } - public static int GetPileLimit(int templateId) + public int GetPileLimit(int templateId) { if (templateId <= 0) return 1; if (_pileLimitCache.TryGetValue(templateId, out var cached)) return cached; @@ -508,7 +503,7 @@ namespace BrewMonster.Scripts.Managers return limit; } - private static int ExtractPileLimitFromElement(object data) + private int ExtractPileLimitFromElement(object data) { if (data == null) return 1; var t = data.GetType(); @@ -542,7 +537,783 @@ namespace BrewMonster.Scripts.Managers catch { } } } - return 1; - } + return 1; + } + } + + /// + /// C# mirror of C++ CECIvtrItem (defined in EC_IvtrItem.h / EC_IvtrItem.cpp). + /// This class intentionally keeps C++-style naming and layout so other C++ systems + /// can be ported over with minimal friction. + /// + public class EC_IvtrItem + { + // NOTE: The nested enums and fields mirror the original C++ names and values. + + // Inventory item class ID + public enum InventoryClassId + { + ICID_ITEM = -100, + ICID_EQUIP = -101, + ICID_ARMOR = 0, + ICID_ARMORRUNE, + ICID_ARROW, + ICID_DECORATION, + ICID_DMGRUNE, + ICID_ELEMENT, + ICID_FASHION, + ICID_FLYSWORD, + ICID_MATERIAL, + ICID_MEDICINE, + ICID_REVSCROLL, + ICID_SKILLTOME, + ICID_TOSSMAT, + ICID_TOWNSCROLL, + ICID_UNIONSCROLL, + ICID_WEAPON, + ICID_TASKITEM, + ICID_STONE, + ICID_WING, + ICID_TASKDICE, + ICID_TASKNMMATTER, + ICID_ERRORITEM, + ICID_FACETICKET, + ICID_FACEPILL, + ICID_GM_GENERATOR, + ICID_RECIPE, + ICID_PETEGG, + ICID_PETFOOD, + ICID_PETFACETICKET, + ICID_FIREWORK, + ICID_TANKCALLIN, + ICID_SKILLMATTER, + ICID_REFINETICKET, + ICID_DESTROYINGESSENCE, + ICID_BIBLE, + ICID_SPEAKER, + ICID_AUTOHP, + ICID_AUTOMP, + ICID_DOUBLEEXP, + ICID_TRANSMITSCROLL, + ICID_DYETICKET, + ICID_GOBLIN, + ICID_GOBLIN_EQUIP, + ICID_GOBLIN_EXPPILL, + ICID_CERTIFICATE, + ICID_TARGETITEM, + ICID_LOOKINFOITEM, + ICID_INCSKILLABILITY, + ICID_WEDDINGBOOKCARD, + ICID_WEDDINGINVITECARD, + ICID_SHARPENER, + ICID_FACTIONMATERIAL, + ICID_CONGREGATE, + ICID_FORCETOKEN, + ICID_DYNSKILLEQUIP, + ICID_MONEYCONVERTIBLE, + ICID_MONSTERSPIRIT, + ICID_GENERALCARD, + ICID_GENERALCARD_DICE, + ICID_SHOPTOKEN, + ICID_UNIVERSAL_TOKEN, + } + + // Item price scale type + public enum ScaleType + { + SCALE_BUY = 1, // Buy from NPC + SCALE_SELL, // Sell to NPC + SCALE_BOOTH, // Booth item + SCALE_MAKE, // Make item + SCALE_OFFLINESHOP, // Offline shop + } + + // Description type + public enum DescType + { + DESC_NORMAL = 0, + DESC_BOOTHBUY, + DESC_REPAIR, + DESC_REWARD, + DESC_PRODUCE, + } + + // Item use conditions (bit flags) + [Flags] + public enum UseCondition + { + USE_ATKTARGET = 0x0001, // Attack target + USE_PERSIST = 0x0002, // Persist some time + USE_TARGET = 0x0004, // Normal target + } + + // Proc-type (bit flags) + [Flags] + public enum ProcType + { + PROC_DROPWHENDIE = 0x0001, + PROC_DROPPABLE = 0x0002, + PROC_SELLABLE = 0x0004, + PROC_LOG = 0x0008, + + PROC_TRADEABLE = 0x0010, + PROC_TASK = 0x0020, + PROC_BIND = 0x0040, + PROC_UNBINDABLE = 0x0080, + + PROC_DISAPEAR = 0x0100, + PROC_USE = 0x0200, + PROC_DEADDROP = 0x0400, + PROC_OFFLINE = 0x0800, + PROC_UNREPAIRABLE = 0x1000, + PROC_DESTROYING = 0x2000, + PROC_NO_USER_TRASH = 0x4000, + PROC_BINDING = 0x8000, + PROC_CAN_WEBTRADE = 0x10000, + } + + // Network / UI metadata (from S2C::cmd_own_ivtr_detail_info etc.) + // These did not exist in the original C++ class but are useful on the client. + public byte Package; + public int Slot; + public int State; + public ushort Crc; + public byte[] Content; // variable-length item-specific payload (can be null) + + // Fields mirror the original C++ layout. + // NOTE: These are public so that legacy ported code can access them directly, + // matching the original C-style struct usage. Prefer using accessors where possible. + public int m_iCID; // Class ID + public int m_tid; // Template id + public int m_expire_date; // Expiration date + public int m_iCount; // Item count + public int m_iPileLimit; // Pile limit number + public int m_iPrice; // Item unit price + public int m_iShopPrice; // Shop price + public long m_i64EquipMask; // Equip mask + public bool m_bEmbeddable; // true, embeddable item + public bool m_bUseable; // true, item can be used + public bool m_bFrozen; // Frozen flag set by local reason + public bool m_bNetFrozen; // Frozen flag set by net reason + public uint m_dwUseFlags; // Use condition flags + public int m_iProcType; // proc-type flag + + public int m_iScaleType; // Item price scale type + public float m_fPriceScale; // Price scale + + public bool m_bNeedUpdate; // true, detail data needs to be updated + public bool m_bUpdating; // true, being updating detail data + public uint m_dwUptTime; // Time when updating request was sent (ms) + public string m_strDesc; // Item description + public bool m_bIsInNPCPack; // true, this item is in NPC package + public bool m_bLocalDetailData; // true, data from GetDetailDataFromLocal + + public CECInventory m_pDescIvtr; // Inventory only used to get item description + + #region Constructors + + public EC_IvtrItem() + { + // Default constructor for object-initializer scenarios; + // mirrors the main constructor but with tid/expire_date = 0. + m_iCID = (int)InventoryClassId.ICID_ITEM; + m_tid = 0; + m_expire_date = 0; + m_iCount = 0; + m_iPileLimit = 1; + m_iPrice = 1; + m_iShopPrice = 1; + m_bNeedUpdate = true; + m_bUpdating = false; + m_dwUptTime = 0; + m_i64EquipMask = 0; + m_bEmbeddable = false; + m_bUseable = false; + m_bFrozen = false; + m_bNetFrozen = false; + m_dwUseFlags = 0; + m_iProcType = 0; + m_iScaleType = (int)ScaleType.SCALE_SELL; + m_fPriceScale = 1.0f; // PLAYER_PRICE_SCALE equivalent + m_strDesc = string.Empty; + m_bIsInNPCPack = false; + m_bLocalDetailData = false; + m_pDescIvtr = null; + } + + public EC_IvtrItem(int tid, int expire_date) + { + m_iCID = (int)InventoryClassId.ICID_ITEM; + m_tid = tid; + m_expire_date = expire_date; + m_iCount = 0; + m_iPileLimit = 1; + m_iPrice = 1; + m_iShopPrice = 1; + m_bNeedUpdate = true; + m_bUpdating = false; + m_dwUptTime = 0; + m_i64EquipMask = 0; + m_bEmbeddable = false; + m_bUseable = false; + m_bFrozen = false; + m_bNetFrozen = false; + m_dwUseFlags = 0; + m_iProcType = 0; + m_iScaleType = (int)ScaleType.SCALE_SELL; + m_fPriceScale = 1.0f; // PLAYER_PRICE_SCALE equivalent + m_strDesc = string.Empty; + m_bIsInNPCPack = false; + m_bLocalDetailData = false; + m_pDescIvtr = null; + } + + public EC_IvtrItem(EC_IvtrItem s) + { + if (s == null) throw new ArgumentNullException(nameof(s)); + + m_iCID = s.m_iCID; + m_tid = s.m_tid; + m_expire_date = s.m_expire_date; + m_iCount = s.m_iCount; + m_iPileLimit = s.m_iPileLimit; + m_iPrice = s.m_iPrice; + m_iShopPrice = s.m_iShopPrice; + m_i64EquipMask = s.m_i64EquipMask; + m_bEmbeddable = s.m_bEmbeddable; + m_bUseable = s.m_bUseable; + m_bFrozen = false; + m_bNetFrozen = false; + m_dwUseFlags = s.m_dwUseFlags; + m_iProcType = s.m_iProcType; + m_iScaleType = s.m_iScaleType; + m_fPriceScale = s.m_fPriceScale; + m_bNeedUpdate = s.m_bNeedUpdate; + m_bUpdating = false; + m_dwUptTime = 0; + m_strDesc = s.m_strDesc; + m_bIsInNPCPack = s.m_bIsInNPCPack; + m_bLocalDetailData = s.m_bLocalDetailData; + m_pDescIvtr = s.m_pDescIvtr; + } + + #endregion + + #region Static helpers (mirror C++) + + /// + /// Create an inventory item. For now this returns the base type. + /// Later this can be expanded to instantiate specific subclasses (weapon, armor, etc.) + /// based on element data type, mirroring the C++ switch in CreateItem. + /// + public static EC_IvtrItem CreateItem(int tid, int expire_date, int iCount, int idSpace = 0) + { + var pItem = new EC_IvtrItem(tid, expire_date); + pItem.SetCount(iCount); + return pItem; + } + + /// + /// Get pile limit for a given template id. + /// C++ implementation creates a temporary item and calls its GetPileLimit(). + /// Here we read directly from element data, mirroring EC_IvtrItemUtils logic. + /// + public static int GetPileLimit(int tid) + { + if (tid <= 0) return 1; + + // Reuse the same cache as EC_IvtrItemUtils to avoid duplicate lookups. + // We keep this small helper inside the item class so gameplay code + // does not depend on UI helpers. + var utils = EC_IvtrItemUtils.Instance; + return utils.GetPileLimit(tid); + } + + // Check whether item2 is item1's candidate (only medicines have candidates). + // Porting the full logic requires medicine essence structures; for now keep the + // signature and return false so code can be wired up later. + public static bool IsCandidate(int tid1, int tid2) + { + return false; + } + + public static bool IsCandidate(int tid1, EC_IvtrItem pItem2) + { + return false; + } + + /// + /// Get scaled price of specified count of items. + /// Exact port of the C++ price scaling logic. + /// + public static int GetScaledPrice(int iUnitPrice, int iCount, int iScaleType, float fScale) + { + if (iCount == 0) + return 0; + + int iPrice = 0; + + switch ((ScaleType)iScaleType) + { + case ScaleType.SCALE_BUY: + iPrice = (int)(iUnitPrice * fScale + 0.5f); + if (iPrice >= 1000) + iPrice = ((iPrice + 99) / 100) * 100; + else if (iPrice >= 100) + iPrice = ((iPrice + 9) / 10) * 10; + + iPrice *= iCount; + break; + + case ScaleType.SCALE_SELL: + iPrice = (int)(iUnitPrice * iCount * fScale + 0.5f); + break; + + case ScaleType.SCALE_BOOTH: + case ScaleType.SCALE_MAKE: + iPrice = iUnitPrice * iCount; + break; + + default: + iPrice = iUnitPrice * iCount; + break; + } + + return iPrice; + } + + public static bool IsSharpenerProperty(byte propertyType) + { + return propertyType >= 100 && propertyType <= 115; + } + + #endregion + + #region Virtual operations (instance side) + + /// + /// Set item detail information. + /// C++ default just clears update flags; detail parsing happens in derived types. + /// + public virtual bool SetItemInfo(byte[] pInfoData, int iDataLen) + { + m_bNeedUpdate = false; + m_bUpdating = false; + m_strDesc = string.Empty; + return true; + } + + /// Get item default information from database (no-op base, like C++). + public virtual void DefaultInfo() + { + } + + /// Get item icon file name (C++ default returns "Unknown.dds"). + public virtual string GetIconFile() + { + return "Unknown.dds"; + } + + /// + /// Get item name. In C++ this uses string tables; here we go through + /// which already mirrors that logic. + /// + public virtual string GetName() + { + return EC_IvtrItemUtils.Instance.ResolveItemName(m_tid); + } + + /// + /// Get item name color. The original returns an A3DCOLOR; + /// here we return ARGB packed into a ; default is white. + /// + public virtual uint GetNameColor() + { + return 0xFFFFFFFFu; + } + + /// Use item. Base class just returns true. + public virtual bool Use() + { + return true; + } + + /// Get scaled price of this item instance. + public virtual int GetScaledPrice() + { + int iPrice = m_iScaleType == (int)ScaleType.SCALE_BUY ? m_iShopPrice : m_iPrice; + return GetScaledPrice(iPrice, m_iCount, m_iScaleType, m_fPriceScale); + } + + /// Clone item (shallow copy, same as C++ default). + public virtual EC_IvtrItem Clone() + { + return new EC_IvtrItem(this); + } + + /// Get item cool time in milliseconds (0 by default). + public virtual int GetCoolTime(out int? piMax) + { + piMax = null; + return 0; + } + + public virtual bool CheckUseCondition() + { + return IsUseable(); + } + + /// Get drop model for showing in world. + public virtual string GetDropModel() + { + return "Models\\Error\\Error.ecm"; + } + + /// Get item quality level. Base returns -1 (unknown). + public virtual int GetItemLevel() + { + return -1; + } + + /// + /// Get item description text (normal / booth-buy / reward / repair). + /// Mirrors the inline C++ GetDesc. + /// + public string GetDesc(DescType iDescType = DescType.DESC_NORMAL, CECInventory pInventory = null) + { + m_pDescIvtr = pInventory; + + switch (iDescType) + { + case DescType.DESC_BOOTHBUY: + return GetBoothBuyDesc(); + case DescType.DESC_REWARD: + return GetRewardDesc(); + default: + return GetNormalDesc(iDescType == DescType.DESC_REPAIR); + } + } + + /// + /// Merge item amount with another same kind item. + /// Returns the number of items actually merged. + /// + public int MergeItem(int tid, int iAmount) + { + if (tid != m_tid || m_iCount >= m_iPileLimit) + return 0; + + int iNumAdd = iAmount; + if (m_iCount + iNumAdd > m_iPileLimit) + iNumAdd = m_iPileLimit - m_iCount; + + m_iCount += iNumAdd; + return iNumAdd; + } + + /// Add item amount. Returns new amount of item. + public int AddAmount(int iAmount) + { + m_iCount += iAmount; + if (m_iCount < 0) m_iCount = 0; + if (m_iCount > m_iPileLimit) m_iCount = m_iPileLimit; + return m_iCount; + } + + public void SetAmount(int iAmount) + { + m_iCount = iAmount; + } + + public void SetExpireDate(int iExpireDate) + { + m_expire_date = iExpireDate; + } + + /// + /// Can this item be equipped to specified position? + /// Uses the same bitmask test as C++. + /// + public bool CanEquippedTo(int iSlot) + { + return (m_i64EquipMask & (1L << iSlot)) != 0; + } + + /// + /// Get first slot index starting from where this item can be equipped. + /// + public int GetEquippedSlot(int iStartSlot = 0) + { + if (!IsEquipment()) + return -1; + + // C++ uses SIZE_EQUIPIVTR constant; here caller should limit range if needed. + for (int i = iStartSlot; i < 64; ++i) + { + if (CanEquippedTo(i)) + return i; + } + return -1; + } + + /// + /// Can this item be put into account box? + /// Full C++ logic depends on config data; here we mirror the simple local flag check + /// and leave server-side blacklist for future porting. + /// + public bool CanPutIntoAccBox() + { + if ((m_iProcType & (int)ProcType.PROC_NO_USER_TRASH) != 0) + return false; + return true; + } + + #endregion + + #region Simple property-style accessors (1:1 with C++) + + public int GetClassID() => m_iCID; + public int GetTemplateID() => m_tid; + public int GetExpireDate() => m_expire_date; + public int GetCount() => m_iCount; + public void SetCount(int iCount) => m_iCount = iCount; + public int GetPileLimitInstance() => m_iPileLimit; + public int GetUnitPrice() => m_iPrice; + public void SetUnitPrice(int iPrice) => m_iPrice = iPrice; + public int GetShopPrice() => m_iShopPrice; + public long GetEquipMask() => m_i64EquipMask; + public void SetPriceScale(int iType, float fScale) + { + m_iScaleType = iType; + m_fPriceScale = fScale; + } + + public bool IsInNPCPack() => m_bIsInNPCPack; + public void SetInNPCPack(bool bInNPCPack) => m_bIsInNPCPack = bInNPCPack; + + public int GetProcType() => m_iProcType; + public void SetProcType(int iType) => m_iProcType = iType; + public bool IsEmbeddable() => m_bEmbeddable; + public bool IsUseable() => m_bUseable; + public bool IsEquipment() => m_i64EquipMask != 0; + public bool IsFrozen() => m_bFrozen || m_bNetFrozen; + + public virtual bool IsTradeable() + { + bool tradeableFlag = (m_iProcType & (int)ProcType.PROC_TRADEABLE) != 0; + bool bindingFlag = (m_iProcType & (int)ProcType.PROC_BINDING) != 0; + return !(tradeableFlag || bindingFlag); + } + + public virtual bool IsWebTradeable() + { + return IsTradeable() || (m_iProcType & (int)ProcType.PROC_CAN_WEBTRADE) != 0; + } + + public bool IsBinding() + { + bool binding = (m_iProcType & (int)ProcType.PROC_BINDING) != 0; + bool canWebTrade = (m_iProcType & (int)ProcType.PROC_CAN_WEBTRADE) != 0; + return binding && !canWebTrade; + } + + public bool IsSellable() + { + return (m_iProcType & (int)ProcType.PROC_SELLABLE) == 0; + } + + public bool IsRepairable() + { + return (m_iProcType & (int)ProcType.PROC_UNREPAIRABLE) == 0; + } + + public virtual bool IsRare() + { + return GetUnitPrice() >= 10000 || m_iCID == (int)InventoryClassId.ICID_MONEYCONVERTIBLE; + } + + public bool NeedUpdate() => m_bNeedUpdate; + + public void GetDetailDataFromSev(int iPack, int iSlot) + { + // Full network request logic will be wired when the session layer is ported. + // We keep the state flags to mirror C++ behavior. + if (!m_bNeedUpdate) + return; + + if (m_bUpdating) + { + // In C++ this checks a timer and can resend; here we just early-out. + return; + } + + m_bUpdating = true; + // m_dwUptTime could be set from a game time provider when available. + } + + public void GetDetailDataFromLocal() + { + // Placeholder: when itemdataman is ported, this will read default item content. + SetItemInfo(null, 0); + m_bLocalDetailData = true; + } + + public bool IsDataFromLocal() => m_bLocalDetailData; + + public void Freeze(bool bFreeze) => m_bFrozen = bFreeze; + public void NetFreeze(bool bFreeze) => m_bNetFrozen = bFreeze; + + public uint GetUseFlags() => m_dwUseFlags; + public bool Use_AtkTarget() => (m_dwUseFlags & (uint)UseCondition.USE_ATKTARGET) != 0; + public bool Use_Persist() => (m_dwUseFlags & (uint)UseCondition.USE_PERSIST) != 0; + public bool Use_Target() => (m_dwUseFlags & (uint)UseCondition.USE_TARGET) != 0; + + #endregion + + #region Protected description helpers (simplified stubs) + + /// + /// Base implementation: just returns current description string. + /// Derived item types can override and build rich description like in C++. + /// + protected virtual string GetNormalDesc(bool bRepair) + { + return string.IsNullOrEmpty(m_strDesc) ? null : m_strDesc; + } + + protected virtual string GetBoothBuyDesc() + { + return GetNormalDesc(false); + } + + protected virtual string GetRewardDesc() + { + return GetNormalDesc(false); + } + + protected virtual void AddPriceDesc(int col, bool bRepair) + { + // Full text/color building uses string tables; keep a minimal stub for now. + } + + protected virtual void AddProfReqDesc(int iProfReq) + { + } + + protected virtual int DecideNameCol() + { + return -1; + } + + protected virtual void SetLocalProps() + { + } + + protected void AddDescText(int iCol, bool bRet, string szText, params object[] args) + { + string line = (args != null && args.Length > 0) ? string.Format(szText, args) : szText; + m_strDesc += line; + if (bRet) + m_strDesc += "\n"; + } + + protected void AddExtDescText() + { + // Extension description comes from game configs; keep stubbed for now. + } + + protected void AddExpireTimeDesc() + { + } + + protected void AddExpireTimeDesc(int expire_date) + { + } + + protected void AddIDDescText() + { + } + + protected void AddBindDescText() + { + } + + protected void AddActionTypeDescText(int action_type) + { + } + + protected void TrimLastReturn() + { + if (string.IsNullOrEmpty(m_strDesc)) + return; + + if (m_strDesc.EndsWith("\n", StringComparison.Ordinal)) + m_strDesc = m_strDesc.Substring(0, m_strDesc.Length - 1); + } + + protected void BuildPriceNumberStr(int iPrice, out string str) + { + str = iPrice.ToString(); + } + + protected void BuildPriceNumberStr(uint iPrice, out string str) + { + str = iPrice.ToString(); + } + + protected int GetColorStrID(int tid) + { + return -1; + } + + protected int VisualizeFloatPercent(int p) + { + var bytes = BitConverter.GetBytes(p); + float f = BitConverter.ToSingle(bytes, 0); + return (int)(f * 100.0f + 0.5f); + } + + #endregion + } + + /// + /// C# mirror of C++ CECIvtrUnknown (fallback item type). + /// + public class CECIvtrUnknown : EC_IvtrItem + { + public CECIvtrUnknown(int tid) : base(tid, 0) + { + m_iCID = (int)InventoryClassId.ICID_ERRORITEM; + m_bNeedUpdate = false; + m_bUpdating = false; + } + + public CECIvtrUnknown(CECIvtrUnknown s) : base(s) + { + } + + public override string GetIconFile() + { + return "Unknown.dds"; + } + + public override string GetName() + { + // In C++ this pulls from ITEMDESC_ERRORITEM string table. + return "Unknown Item"; + } + + public override EC_IvtrItem Clone() + { + return new CECIvtrUnknown(this); + } + + protected override string GetNormalDesc(bool bRepair) + { + // Minimal mirror of C++: show an error item description with id. + m_strDesc = string.Empty; + AddDescText(0, false, "Error Item ({0})", m_tid); + return m_strDesc; + } } } diff --git a/Assets/PerfectWorld/Scripts/Task/UI/TaskWindow.cs b/Assets/PerfectWorld/Scripts/Task/UI/TaskWindow.cs index 9264486616..fd7db7c9a9 100644 --- a/Assets/PerfectWorld/Scripts/Task/UI/TaskWindow.cs +++ b/Assets/PerfectWorld/Scripts/Task/UI/TaskWindow.cs @@ -815,7 +815,7 @@ namespace BrewMonster.PerfectWorld.Scripts.Task.UI { var img = m_pImg_Item[i]; if (img == null) continue; - var sprite = EC_IvtrItemUtils.ResolveItemIconSprite((int)award.m_ItemsId[i]); + var sprite = EC_IvtrItemUtils.Instance.ResolveItemIconSprite((int)award.m_ItemsId[i]); if (sprite != null) img.sprite = sprite; img.color = Color.white; img.gameObject.SetActive(true); @@ -1198,7 +1198,7 @@ namespace BrewMonster.PerfectWorld.Scripts.Task.UI // Resolve item name // 解析物品名称 int itemTid = unchecked((int)tsi.m_ItemsWanted[i].m_ulItemId); - string itemName = EC_IvtrItemUtils.ResolveItemName(itemTid); + string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(itemTid); if (string.IsNullOrEmpty(itemName)) itemName = $"Item {itemTid}"; // Compose line: name and progress (gained/toGet) diff --git a/Assets/PerfectWorld/Scripts/UI/ShopItemPanel.cs b/Assets/PerfectWorld/Scripts/UI/ShopItemPanel.cs index 6de64a1a5c..6e4b377cca 100644 --- a/Assets/PerfectWorld/Scripts/UI/ShopItemPanel.cs +++ b/Assets/PerfectWorld/Scripts/UI/ShopItemPanel.cs @@ -76,7 +76,7 @@ public class ShopItemPanel : MonoBehaviour } // Use the existing icon loading system from EC_IvtrItem - Sprite iconSprite = EC_IvtrItemUtils.ResolveItemIconSprite(itemId); + Sprite iconSprite = EC_IvtrItemUtils.Instance.ResolveItemIconSprite(itemId); if (iconSprite != null) { diff --git a/Assets/PerfectWorld/Scripts/UI/pickupItem.cs b/Assets/PerfectWorld/Scripts/UI/pickupItem.cs index 287ee86686..aa1465a646 100644 --- a/Assets/PerfectWorld/Scripts/UI/pickupItem.cs +++ b/Assets/PerfectWorld/Scripts/UI/pickupItem.cs @@ -215,7 +215,7 @@ public class pickupItem : MonoBehaviour TextMeshPro textMesh = textObject.AddComponent(); // Get the item name - string itemName = EC_IvtrItemUtils.ResolveItemName(tid); + string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(tid); if (string.IsNullOrEmpty(itemName)) { itemName = $"Item {tid}"; diff --git a/Assets/Scripts/CECHostPlayer.cs b/Assets/Scripts/CECHostPlayer.cs index 61e5a2fcaa..f9cb9061a6 100644 --- a/Assets/Scripts/CECHostPlayer.cs +++ b/Assets/Scripts/CECHostPlayer.cs @@ -961,7 +961,7 @@ public partial class CECHostPlayer : CECPlayer { byte byPackage = data[0]; byte ivtrSize = data[1]; - if (EC_IvtrItemUtils.TryParseInventoryDetail(data, out var pkg, + if (EC_IvtrItemUtils.Instance.TryParseInventoryDetail(data, out var pkg, out var size, out var items)) { EC_Inventory.UpdatePack(pkg, size, items); From a35cd6d0e7a9dac6e56bf26cfb7622702a6e96cc Mon Sep 17 00:00:00 2001 From: HungDK <> Date: Thu, 20 Nov 2025 13:47:10 +0700 Subject: [PATCH 3/3] Update EC_Invtr --- .../Scripts/Inventory/EC_IvtrType.cs | 5 + .../Scripts/Managers/CECInventory.cs | 798 ------------------ .../Scripts/Managers/EC_Inventory.cs | 305 +++++++ ...Inventory.cs.meta => EC_Inventory.cs.meta} | 0 .../Scripts/Managers/EC_InventoryUI.cs | 38 +- .../Scripts/Managers/EC_IvtrItem.cs | 4 +- .../Scripts/Managers/EC_IvtrType.cs | 7 +- .../Scripts/Task/CECTaskInterface.cs | 14 +- Assets/Prefabs/UI/InventoryUI.prefab | 9 - Assets/Scripts/CECHostPlayer.cs | 117 ++- Assets/Scripts/CECPlayer_Inventory.cs | 11 +- 11 files changed, 445 insertions(+), 863 deletions(-) delete mode 100644 Assets/PerfectWorld/Scripts/Managers/CECInventory.cs create mode 100644 Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs rename Assets/PerfectWorld/Scripts/Managers/{CECInventory.cs.meta => EC_Inventory.cs.meta} (100%) diff --git a/Assets/PerfectWorld/Scripts/Inventory/EC_IvtrType.cs b/Assets/PerfectWorld/Scripts/Inventory/EC_IvtrType.cs index c42244873d..a1d0ea8fe1 100644 --- a/Assets/PerfectWorld/Scripts/Inventory/EC_IvtrType.cs +++ b/Assets/PerfectWorld/Scripts/Inventory/EC_IvtrType.cs @@ -61,5 +61,10 @@ namespace BrewMonster.Scripts public const int IVTRSIZE_CLIENTCARDPACK = 32; // Client pack for general card collection public const int NUM_NPCIVTR = 8; // NPC inventory number + + // Inventory package types (match legacy C++ IVTRTYPE_*) + public const byte IVTRTYPE_PACK = 0; + public const byte IVTRTYPE_EQUIPPACK = 1; + public const byte IVTRTYPE_TASKPACK = 2; } } \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs b/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs deleted file mode 100644 index 107493f88f..0000000000 --- a/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs +++ /dev/null @@ -1,798 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace BrewMonster.Scripts.Managers -{ - /// - /// C# mirror of C++ CECInventory (EC_Inventory.h / EC_Inventory.cpp). - /// This is an instance-based inventory using a fixed-size item array. - /// - public class CECInventory - { - // Item array: index is slot, null means empty. - private EC_IvtrItem[] m_aItems = Array.Empty(); - - public CECInventory() - { - } - - public bool Init(int iSize) - { - Resize(iSize); - return true; - } - - public void Release() - { - // In C++ this deletes all heap-allocated items. - // Here we simply clear references so GC can collect them. - RemoveAllItems(); - m_aItems = Array.Empty(); - } - - public void RemoveAllItems() - { - for (int i = 0; i < m_aItems.Length; i++) - { - m_aItems[i] = null; - } - } - - public void Resize(int iNewSize) - { - int oldSize = m_aItems.Length; - if (iNewSize < 0) iNewSize = 0; - - if (iNewSize == oldSize) - return; - - var newArray = iNewSize > 0 ? new EC_IvtrItem[iNewSize] : Array.Empty(); - - if (oldSize > 0 && iNewSize > 0) - { - Array.Copy(m_aItems, newArray, Math.Min(oldSize, iNewSize)); - } - - m_aItems = newArray; - } - - public EC_IvtrItem PutItem(int iSlot, EC_IvtrItem pItem) - { - if (iSlot < 0 || iSlot >= m_aItems.Length) - { - return null; - } - - var old = m_aItems[iSlot]; - m_aItems[iSlot] = pItem; - return old; - } - - public void SetItem(int iSlot, EC_IvtrItem pItem) - { - if (iSlot < 0 || iSlot >= m_aItems.Length) - { - return; - } - - m_aItems[iSlot] = pItem; - } - - public EC_IvtrItem GetItem(int iSlot, bool bRemove = false) - { - if (iSlot < 0 || iSlot >= m_aItems.Length) - { - return null; - } - - var pItem = m_aItems[iSlot]; - if (bRemove) - m_aItems[iSlot] = null; - return pItem; - } - - public void ExchangeItem(int iSlot1, int iSlot2) - { - if (iSlot1 < 0 || iSlot1 >= m_aItems.Length || - iSlot2 < 0 || iSlot2 >= m_aItems.Length) - { - return; - } - - if (iSlot1 == iSlot2) - return; - - var tmp = m_aItems[iSlot1]; - m_aItems[iSlot1] = m_aItems[iSlot2]; - m_aItems[iSlot2] = tmp; - } - - public bool MergeItem(int tid, int iExpireDate, int iAmount, out int piLastSlot, out int piLastAmount) - { - piLastSlot = -1; - piLastAmount = 0; - - int firstEmpty = -1; - - for (int i = 0; i < m_aItems.Length; i++) - { - var slotItem = m_aItems[i]; - if (slotItem != null) - { - if (slotItem.GetTemplateID() != tid) - continue; - - int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(tid)); - int canAdd = Math.Max(0, pileLimit - Math.Max(0, slotItem.GetCount())); - if (canAdd <= 0) continue; - - int add = Math.Min(canAdd, iAmount); - slotItem.AddAmount(add); - iAmount -= add; - - if (iAmount == 0) - { - piLastSlot = i; - piLastAmount = slotItem.GetCount(); - return true; - } - } - else if (firstEmpty < 0) - { - firstEmpty = i; - } - } - - if (firstEmpty < 0 || iAmount <= 0) - { - return false; - } - - var newItem = new EC_IvtrItem(tid, iExpireDate) - { - Slot = firstEmpty, - State = 0, - Crc = 0, - Content = null - }; - newItem.SetCount(iAmount); - - m_aItems[firstEmpty] = newItem; - piLastSlot = firstEmpty; - piLastAmount = iAmount; - return true; - } - - public bool MoveItem(int iSrc, int iDest, int iAmount) - { - if (iSrc < 0 || iSrc >= m_aItems.Length || - iDest < 0 || iDest >= m_aItems.Length) - { - return false; - } - - var pSrc = m_aItems[iSrc]; - var pDst = m_aItems[iDest]; - - if (pSrc == null) - return false; - - if (iAmount == 0) - return false; - - if (pDst == null) - { - var clone = new EC_IvtrItem(pSrc.GetTemplateID(), pSrc.GetExpireDate()) - { - Slot = iDest, - Package = pSrc.Package, - State = pSrc.State, - Crc = pSrc.Crc, - Content = pSrc.Content != null ? (byte[])pSrc.Content.Clone() : null - }; - clone.SetCount(iAmount); - m_aItems[iDest] = clone; - } - else - { - if (pSrc.GetTemplateID() != pDst.GetTemplateID()) - return false; - - int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(pDst.GetTemplateID())); - int canAdd = Math.Max(0, pileLimit - Math.Max(0, pDst.GetCount())); - int add = Math.Min(canAdd, iAmount); - if (add <= 0) return false; - pDst.AddAmount(add); - iAmount = add; - } - - RemoveItem(iSrc, iAmount); - return true; - } - - public bool RemoveItem(int iSlot, int iAmount) - { - if (iSlot < 0 || iSlot >= m_aItems.Length) - { - return false; - } - - var pItem = m_aItems[iSlot]; - if (pItem == null) - return true; - - int newCount = pItem.AddAmount(-Math.Max(0, iAmount)); - if (newCount <= 0) - m_aItems[iSlot] = null; - - return true; - } - - public int FindItem(int idItem, int baseIdx = 0) - { - if (baseIdx < 0) baseIdx = 0; - for (int i = baseIdx; i < m_aItems.Length; i++) - { - var pItem = m_aItems[i]; - if (pItem != null && pItem.GetTemplateID() == idItem) - return i; - } - return -1; - } - - public int GetItemTotalNum(int idItem) - { - int count = 0; - for (int i = 0; i < m_aItems.Length; i++) - { - var pItem = m_aItems[i]; - if (pItem != null && pItem.GetTemplateID() == idItem) - count += Math.Max(0, pItem.GetCount()); - } - return count; - } - - public int SearchEmpty() - { - for (int i = 0; i < m_aItems.Length; i++) - { - if (m_aItems[i] == null) - return i; - } - return -1; - } - - public int GetEmptySlotNum() - { - int count = 0; - for (int i = 0; i < m_aItems.Length; i++) - { - if (m_aItems[i] == null) - count++; - } - return count; - } - - public int CanAddItem(int idItem, int iAmount, bool tryPile) - { - int foundEmpty = -1; - for (int i = 0; i < m_aItems.Length; i++) - { - var pItem = m_aItems[i]; - if (pItem == null) - { - if (!tryPile) return i; - if (foundEmpty < 0) foundEmpty = i; - } - else if (pItem.GetTemplateID() == idItem) - { - int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(idItem)); - if (pItem.GetCount() + iAmount <= pileLimit) - return i; - } - } - return foundEmpty; - } - - public int GetSize() - { - return m_aItems.Length; - } - } - - /// - /// Static inventory facade used by the current client code. - /// This implementation was originally in EC_Inventory.cs and is now colocated - /// with CECInventory so both static-style and instance-style code continue to work. - /// - public static class EC_Inventory - { - // We currently support exactly three inventory packs, matching legacy C++ semantics: - // 0 = normal pack, 1 = equip pack, 2 = task pack. - private const int PackageCount = 3; - - // Fixed-size arrays per package, mimicking C++ CECInventory::m_aItems (APtrArray). - // Index is the slot index; a null entry means the slot is empty. - private static readonly int[] _packSizeByPackage = new int[PackageCount]; - private static readonly EC_IvtrItem[][] _itemsByPackage = new EC_IvtrItem[PackageCount][]; - private const int MaxContentHexToLog = 64; - - // Package constants to mirror legacy C++ names - public const byte IVTRTYPE_PACK = 0; - public const byte IVTRTYPE_EQUIPPACK = 1; - public const byte IVTRTYPE_TASKPACK = 2; - - private static int GetPackageIndex(byte pkg) - { - switch (pkg) - { - case IVTRTYPE_PACK: - case IVTRTYPE_EQUIPPACK: - case IVTRTYPE_TASKPACK: - return pkg; - default: - // Only three legacy packages are currently supported; ignore others safely. - Debug.LogWarning($"[Inventory] Unsupported package id={pkg}, expected 0..2."); - return -1; - } - } - - private static int GetPackSize(byte byPackage) - { - int idx = GetPackageIndex(byPackage); - if (idx < 0) return 0; - return _packSizeByPackage[idx]; - } - - private static EC_IvtrItem[] EnsureSlots(byte byPackage) - { - int idx = GetPackageIndex(byPackage); - if (idx < 0) - return Array.Empty(); - - int size = Math.Max(0, _packSizeByPackage[idx]); - var slots = _itemsByPackage[idx]; - - if (slots == null || slots.Length != size) - { - var newSlots = size > 0 ? new EC_IvtrItem[size] : Array.Empty(); - - // Preserve items that still fit into the resized pack, similar to C++ Resize. - if (slots != null && slots.Length > 0 && newSlots.Length > 0) - { - Array.Copy(slots, newSlots, Math.Min(slots.Length, newSlots.Length)); - } - - _itemsByPackage[idx] = newSlots; - slots = newSlots; - } - - return slots; - } - - private static string GetPackageName(byte pkg) - { - switch (pkg) - { - case 0: return "IVTRTYPE_PACK"; - case 1: return "IVTRTYPE_EQUIPPACK"; - case 2: return "IVTRTYPE_TASKPACK"; - default: return "PACK_UNKNOWN"; - } - } - - private static string BytesToHex(byte[] bytes, int max) - { - if (bytes == null || bytes.Length == 0) return ""; - int len = Math.Min(bytes.Length, max); - string hex = BitConverter.ToString(bytes, 0, len); - if (bytes.Length > len) hex += "-..."; - return hex; - } - - // C++ has CECInventory per pack; this helper returns a snapshot Dictionary view - // of the current slots for convenience where a map-like API is easier to use. - public static Dictionary GetPack(byte byPackage) - { - var slots = EnsureSlots(byPackage); - var result = new Dictionary(slots.Length); - for (int i = 0; i < slots.Length; i++) - { - var it = slots[i]; - if (it != null) - result[i] = it; - } - return result; - } - - public static EC_IvtrItem GetItem(int iSlot, bool bRemove) - { - // Backward-compatible overload defaulting to inventory package - return GetItem(IVTRTYPE_PACK, iSlot, bRemove); - } - - public static EC_IvtrItem GetItem(byte byPackage, int slot, bool remove) - { - var slots = EnsureSlots(byPackage); - if (slot < 0 || slot >= slots.Length) - return null; - - var item = slots[slot]; - if (remove && slot >= 0 && slot < slots.Length) - slots[slot] = null; - return item; - } - - public static void Resize(byte byPackage, int newSize) - { - int idx = GetPackageIndex(byPackage); - if (idx < 0) - return; - - newSize = Math.Max(0, newSize); - int oldSize = _packSizeByPackage[idx]; - _packSizeByPackage[idx] = newSize; - - var oldSlots = _itemsByPackage[idx]; - - if (oldSlots == null) - { - _itemsByPackage[idx] = newSize > 0 ? new EC_IvtrItem[newSize] : Array.Empty(); - return; - } - - if (oldSize == newSize && oldSlots.Length == newSize) - return; - - var newSlots = newSize > 0 ? new EC_IvtrItem[newSize] : Array.Empty(); - if (oldSlots.Length > 0 && newSlots.Length > 0) - { - Array.Copy(oldSlots, newSlots, Math.Min(oldSlots.Length, newSlots.Length)); - } - _itemsByPackage[idx] = newSlots; - } - - public static EC_IvtrItem PutItem(byte byPackage, int slot, EC_IvtrItem item) - { - var slots = EnsureSlots(byPackage); - if (slot < 0 || slot >= slots.Length) - return null; - - var oldItem = slots[slot]; - slots[slot] = item; - return oldItem; - } - - public static void SetItem(byte byPackage, int slot, EC_IvtrItem item) - { - var slots = EnsureSlots(byPackage); - if (slot < 0 || slot >= slots.Length) - return; - slots[slot] = item; - } - - public static void ExchangeItem(byte byPackage, int slot1, int slot2) - { - if (slot1 == slot2) return; - var slots = EnsureSlots(byPackage); - if (slot1 < 0 || slot1 >= slots.Length || slot2 < 0 || slot2 >= slots.Length) - return; - - var i1 = slots[slot1]; - var i2 = slots[slot2]; - slots[slot1] = i2; - slots[slot2] = i1; - } - - public static bool RemoveItem(byte byPackage, int slot, int amount) - { - var slots = EnsureSlots(byPackage); - if (slot < 0 || slot >= slots.Length) - return false; - - var item = slots[slot]; - if (item == null) - return true; - - int newCount = Math.Max(0, item.m_iCount - Math.Max(0, amount)); - item.m_iCount = newCount; - if (newCount <= 0) - slots[slot] = null; - - return true; - } - - public static void RemoveAllItems(byte byPackage) - { - var slots = EnsureSlots(byPackage); - for (int i = 0; i < slots.Length; i++) - { - slots[i] = null; - } - } - - public static int SearchEmpty(byte byPackage) - { - var slots = EnsureSlots(byPackage); - for (int i = 0; i < slots.Length; i++) - { - if (slots[i] == null) - return i; - } - return -1; - } - - public static int GetEmptySlotNum(byte byPackage) - { - var slots = EnsureSlots(byPackage); - int empty = 0; - for (int i = 0; i < slots.Length; i++) - { - if (slots[i] == null) - empty++; - } - return empty; - } - - public static int FindItem(byte byPackage, int templateId, int baseIdx = 0) - { - var slots = EnsureSlots(byPackage); - if (baseIdx < 0) baseIdx = 0; - for (int i = baseIdx; i < slots.Length; i++) - { - var it = slots[i]; - if (it != null && it.m_tid == templateId) - return i; - } - return -1; - } - - public static int GetItemTotalNum(byte byPackage, int templateId) - { - int total = 0; - var slots = EnsureSlots(byPackage); - for (int i = 0; i < slots.Length; i++) - { - var it = slots[i]; - if (it != null && it.m_tid == templateId) - total += Math.Max(0, it.m_iCount); - } - return total; - } - - public static int GetItemCanPileCount(byte byPackage, int templateId) - { - int ret = 0; - var slots = EnsureSlots(byPackage); - for (int i = 0; i < slots.Length; i++) - { - var it = slots[i]; - if (it != null && it.m_tid == templateId) - { - int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(templateId)); - ret += Math.Max(0, pileLimit - Math.Max(0, it.m_iCount)); - } - } - return ret; - } - - public static int CanAddItem(byte byPackage, int templateId, int amount, bool tryPile) - { - int firstEmpty = -1; - int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(templateId)); - var slots = EnsureSlots(byPackage); - for (int i = 0; i < slots.Length; i++) - { - var it = slots[i]; - if (it == null) - { - // return first empty slot if not trying to pile item - if (!tryPile) return i; - if (firstEmpty < 0) firstEmpty = i; - } - else if (it.m_tid == templateId && it.m_iCount + amount <= pileLimit) - { - return i; - } - } - return firstEmpty; - } - - public static bool MergeItem(byte byPackage, int templateId, int expireDate, int amount, out int lastSlot, out int slotAmount) - { - lastSlot = -1; - slotAmount = 0; - var slots = EnsureSlots(byPackage); - int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(templateId)); - int firstEmpty = -1; - - for (int i = 0; i < slots.Length && amount > 0; i++) - { - var slotItem = slots[i]; - if (slotItem == null) - { - if (firstEmpty < 0) firstEmpty = i; - continue; - } - if (slotItem.m_tid != templateId) continue; - int canAdd = Math.Max(0, pileLimit - Math.Max(0, slotItem.m_iCount)); - if (canAdd <= 0) continue; - int add = Math.Min(canAdd, amount); - slotItem.m_iCount += add; - amount -= add; - lastSlot = i; - slotAmount = slotItem.m_iCount; - } - if (amount <= 0) return true; - if (firstEmpty < 0) return false; - - var newItem = new EC_IvtrItem - { - Package = byPackage, - Slot = firstEmpty, - m_tid = templateId, - m_expire_date = expireDate, - State = 0, - m_iCount = amount, - Crc = 0, - Content = null - }; - slots[firstEmpty] = newItem; - lastSlot = firstEmpty; - slotAmount = amount; - return true; - } - - public static bool MoveItem(byte byPackage, int src, int dest, int amount) - { - if (src == dest) return false; - if (amount <= 0) return false; - - var slots = EnsureSlots(byPackage); - if (src < 0 || src >= slots.Length || dest < 0 || dest >= slots.Length) return false; - - var srcItem = slots[src]; - var dstItem = slots[dest]; - if (srcItem == null) return false; - - if (dstItem == null) - { - var clone = new EC_IvtrItem - { - Package = byPackage, - Slot = dest, - m_tid = srcItem.m_tid, - m_expire_date = srcItem.m_expire_date, - State = srcItem.State, - m_iCount = amount, - Crc = srcItem.Crc, - Content = srcItem.Content != null ? (byte[])srcItem.Content.Clone() : null - }; - slots[dest] = clone; - } - else - { - if (dstItem.m_tid != srcItem.m_tid) return false; - int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(dstItem.m_tid)); - int canAdd = Math.Max(0, pileLimit - Math.Max(0, dstItem.m_iCount)); - int add = Math.Min(canAdd, amount); - if (add <= 0) return false; - dstItem.m_iCount += add; - amount = add; - } - RemoveItem(byPackage, src, amount); - return true; - } - - public static void UpdatePack(byte byPackage, int ivtrSize, IEnumerable items) - { - Resize(byPackage, ivtrSize); - var slots = EnsureSlots(byPackage); - - // Clear existing entries; keep size. - for (int i = 0; i < slots.Length; i++) - slots[i] = null; - - if (items != null) - { - foreach (var it in items) - { - if (it != null && it.Slot >= 0 && it.Slot < slots.Length) - { - slots[it.Slot] = it; - } - } - } - // Log this pack's items - LogPackInternal(byPackage, ivtrSize, slots); - } - - public static bool ResetWithDetailData(byte byPackage, int ivtrSize, byte[] data) - { - // Uses EC_IvtrItem.TryParseInventoryDetail format - if (data == null) - { - Resize(byPackage, ivtrSize); - RemoveAllItems(byPackage); - return true; - } - if (!EC_IvtrItemUtils.Instance.TryParseInventoryDetail(data, out var pkg, out var size, out var items)) - return false; - // Prefer header values when valid - byte finalPkg = byPackage; - if (pkg == byPackage) finalPkg = pkg; - int finalSize = ivtrSize > 0 ? ivtrSize : size; - UpdatePack(finalPkg, finalSize, items); - return true; - } - - private static void LogPackInternal(byte byPackage, int ivtrSize, EC_IvtrItem[] slots) - { - //Debug.Log($"[Inventory] === Pack {GetPackageName(byPackage)}({byPackage}) size={ivtrSize}, items={slots?.Length ?? 0} ==="); - if (slots == null || slots.Length == 0) - { - //Debug.Log("[Inventory] (empty)"); - return; - } - for (int i = 0; i < slots.Length; i++) - { - var it = slots[i]; - if (it == null) - continue; - - string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(it.m_tid); - string extraHex = it.Content != null && it.Content.Length > 0 ? EC_IvtrItemUtils.Instance.BytesToHex(it.Content, MaxContentHexToLog) : ""; - //int extraLen = it.Content?.Length ?? 0; - //Debug.Log( - // $"[Inventory] pkg={GetPackageName(it.Package)}({it.Package}) slot={it.Slot} tid={it.TemplateId}{(string.IsNullOrEmpty(itemName) ? "" : " \"" + itemName + "\"")} count={it.Count} state={it.State} expire={it.ExpireDate} crc={it.Crc} content_len={extraLen}{(extraLen > 0 ? ", content_hex=" + extraHex : "")}" - //); - } - } - - public static void LogInventoryPacket(string tag, byte[] buffer, int hostId) - { - if (buffer == null) - { - return; - } - - int index = 0; - if (buffer.Length < 6) - { - //LogInventoryRaw(tag, buffer); - return; - } - - byte byPackage = buffer[index++]; - byte ivtrSize = buffer[index++]; - uint contentLength = BitConverter.ToUInt32(buffer, index); index += 4; - - int remaining = buffer.Length - index; - int contentBytes = remaining; - if (contentLength < (uint)remaining) - { - contentBytes = (int)contentLength; - } - - if (contentBytes > 0) - { - byte[] content = new byte[contentBytes]; - Buffer.BlockCopy(buffer, index, content, 0, contentBytes); - } - - int trailing = buffer.Length - (index + contentBytes); - if (trailing > 0) - { - byte[] tail = new byte[trailing]; - Buffer.BlockCopy(buffer, index + contentBytes, tail, 0, trailing); - } - } - - public static void LogInventoryRaw(string tag, byte[] buffer) - { - Debug.Log($"[Inventory] {tag}: RAW HEX (len={buffer?.Length ?? 0})=\n{(buffer == null ? "" : BitConverter.ToString(buffer))}"); - } - } -} - diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs b/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs new file mode 100644 index 0000000000..2109586757 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace BrewMonster.Scripts.Managers +{ + /// + /// C# mirror of C++ CECInventory (EC_Inventory.h / EC_Inventory.cpp). + /// Instance-based inventory core plus static helpers used by existing client code. + /// + public class EC_Inventory + { + // ===== Instance-based inventory (per-pack) ===== + + // Item array: index is slot, null means empty. + private EC_IvtrItem[] m_aItems = Array.Empty(); + + public EC_Inventory() + { + } + + public bool Init(int iSize) + { + Resize(iSize); + return true; + } + + public void Release() + { + // In C++ this deletes all heap-allocated items. + // Here we simply clear references so GC can collect them. + RemoveAllItems(); + m_aItems = Array.Empty(); + } + + public void RemoveAllItems() + { + for (int i = 0; i < m_aItems.Length; i++) + { + m_aItems[i] = null; + } + } + + public void Resize(int iNewSize) + { + int oldSize = m_aItems.Length; + if (iNewSize < 0) iNewSize = 0; + + if (iNewSize == oldSize) + return; + + var newArray = iNewSize > 0 ? new EC_IvtrItem[iNewSize] : Array.Empty(); + + if (oldSize > 0 && iNewSize > 0) + { + Array.Copy(m_aItems, newArray, Math.Min(oldSize, iNewSize)); + } + + m_aItems = newArray; + } + + public EC_IvtrItem PutItem(int iSlot, EC_IvtrItem pItem) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return null; + } + + var old = m_aItems[iSlot]; + m_aItems[iSlot] = pItem; + return old; + } + + public void SetItem(int iSlot, EC_IvtrItem pItem) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return; + } + + m_aItems[iSlot] = pItem; + } + + public EC_IvtrItem GetItem(int iSlot, bool bRemove = false) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return null; + } + + var pItem = m_aItems[iSlot]; + if (bRemove) + m_aItems[iSlot] = null; + return pItem; + } + + public void ExchangeItem(int iSlot1, int iSlot2) + { + if (iSlot1 < 0 || iSlot1 >= m_aItems.Length || + iSlot2 < 0 || iSlot2 >= m_aItems.Length) + { + return; + } + + if (iSlot1 == iSlot2) + return; + + var tmp = m_aItems[iSlot1]; + m_aItems[iSlot1] = m_aItems[iSlot2]; + m_aItems[iSlot2] = tmp; + } + + public bool MergeItem(int tid, int iExpireDate, int iAmount, out int piLastSlot, out int piLastAmount) + { + piLastSlot = -1; + piLastAmount = 0; + + int firstEmpty = -1; + + for (int i = 0; i < m_aItems.Length; i++) + { + var slotItem = m_aItems[i]; + if (slotItem != null) + { + if (slotItem.GetTemplateID() != tid) + continue; + + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(tid)); + int canAdd = Math.Max(0, pileLimit - Math.Max(0, slotItem.GetCount())); + if (canAdd <= 0) continue; + + int add = Math.Min(canAdd, iAmount); + slotItem.AddAmount(add); + iAmount -= add; + + if (iAmount == 0) + { + piLastSlot = i; + piLastAmount = slotItem.GetCount(); + return true; + } + } + else if (firstEmpty < 0) + { + firstEmpty = i; + } + } + + if (firstEmpty < 0 || iAmount <= 0) + { + return false; + } + + var newItem = new EC_IvtrItem(tid, iExpireDate) + { + Slot = firstEmpty, + State = 0, + Crc = 0, + Content = null + }; + newItem.SetCount(iAmount); + + m_aItems[firstEmpty] = newItem; + piLastSlot = firstEmpty; + piLastAmount = iAmount; + return true; + } + + public bool MoveItem(int iSrc, int iDest, int iAmount) + { + if (iSrc < 0 || iSrc >= m_aItems.Length || + iDest < 0 || iDest >= m_aItems.Length) + { + return false; + } + + var pSrc = m_aItems[iSrc]; + var pDst = m_aItems[iDest]; + + if (pSrc == null) + return false; + + if (iAmount == 0) + return false; + + if (pDst == null) + { + var clone = new EC_IvtrItem(pSrc.GetTemplateID(), pSrc.GetExpireDate()) + { + Slot = iDest, + Package = pSrc.Package, + State = pSrc.State, + Crc = pSrc.Crc, + Content = pSrc.Content != null ? (byte[])pSrc.Content.Clone() : null + }; + clone.SetCount(iAmount); + m_aItems[iDest] = clone; + } + else + { + if (pSrc.GetTemplateID() != pDst.GetTemplateID()) + return false; + + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(pDst.GetTemplateID())); + int canAdd = Math.Max(0, pileLimit - Math.Max(0, pDst.GetCount())); + int add = Math.Min(canAdd, iAmount); + if (add <= 0) return false; + pDst.AddAmount(add); + iAmount = add; + } + + RemoveItem(iSrc, iAmount); + return true; + } + + public bool RemoveItem(int iSlot, int iAmount) + { + if (iSlot < 0 || iSlot >= m_aItems.Length) + { + return false; + } + + var pItem = m_aItems[iSlot]; + if (pItem == null) + return true; + + int newCount = pItem.AddAmount(-Math.Max(0, iAmount)); + if (newCount <= 0) + m_aItems[iSlot] = null; + + return true; + } + + public int FindItem(int idItem, int baseIdx = 0) + { + if (baseIdx < 0) baseIdx = 0; + for (int i = baseIdx; i < m_aItems.Length; i++) + { + var pItem = m_aItems[i]; + if (pItem != null && pItem.GetTemplateID() == idItem) + return i; + } + return -1; + } + + public int GetItemTotalNum(int idItem) + { + int count = 0; + for (int i = 0; i < m_aItems.Length; i++) + { + var pItem = m_aItems[i]; + if (pItem != null && pItem.GetTemplateID() == idItem) + count += Math.Max(0, pItem.GetCount()); + } + return count; + } + + public int SearchEmpty() + { + for (int i = 0; i < m_aItems.Length; i++) + { + if (m_aItems[i] == null) + return i; + } + return -1; + } + + public int GetEmptySlotNum() + { + int count = 0; + for (int i = 0; i < m_aItems.Length; i++) + { + if (m_aItems[i] == null) + count++; + } + return count; + } + + public int CanAddItem(int idItem, int iAmount, bool tryPile) + { + int foundEmpty = -1; + for (int i = 0; i < m_aItems.Length; i++) + { + var pItem = m_aItems[i]; + if (pItem == null) + { + if (!tryPile) return i; + if (foundEmpty < 0) foundEmpty = i; + } + else if (pItem.GetTemplateID() == idItem) + { + int pileLimit = Math.Max(1, EC_IvtrItem.GetPileLimit(idItem)); + if (pItem.GetCount() + iAmount <= pileLimit) + return i; + } + } + return foundEmpty; + } + + public int GetSize() + { + return m_aItems.Length; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Managers/CECInventory.cs.meta b/Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs.meta similarity index 100% rename from Assets/PerfectWorld/Scripts/Managers/CECInventory.cs.meta rename to Assets/PerfectWorld/Scripts/Managers/EC_Inventory.cs.meta diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs b/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs index 29f0904155..181114ab42 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_InventoryUI.cs @@ -10,6 +10,7 @@ using BrewMonster; using BrewMonster.Common; using ModelRenderer.Scripts.GameData; using PerfectWorld.Scripts.Managers; +using BrewMonster.Scripts; namespace BrewMonster.Scripts.Managers { @@ -654,34 +655,23 @@ namespace BrewMonster.Scripts.Managers // === MVC: Model === private class InventoryModel { - private readonly FieldInfo itemsByPackageField; - - public InventoryModel() - { - var inventoryType = typeof(EC_Inventory); - itemsByPackageField = inventoryType.GetField("_itemsByPackage", BindingFlags.NonPublic | BindingFlags.Static); - if (itemsByPackageField == null) - { - Debug.LogError("[InventoryUI] Could not access _itemsByPackage field from EC_Inventory"); - } - } - public Dictionary GetInventoryData(byte package) { - if (itemsByPackageField == null) + // Read from host player's per-package CECInventory instance + var host = CECGameRun.Instance?.GetHostPlayer(); + var inv = host?.GetInventory(package); + var result = new Dictionary(); + if (inv == null) + return result; + + int size = inv.GetSize(); + for (int i = 0; i < size; i++) { - return new Dictionary(); + var item = inv.GetItem(i, false); + if (item != null) + result[i] = item; } - var itemsByPackage = itemsByPackageField.GetValue(null) as Dictionary>; - if (itemsByPackage == null) - { - return new Dictionary(); - } - if (itemsByPackage.TryGetValue(package, out var packageItems)) - { - return packageItems; - } - return new Dictionary(); + return result; } } diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs index bac2feae4a..dcf525698d 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrItem.cs @@ -708,7 +708,7 @@ namespace BrewMonster.Scripts.Managers public bool m_bIsInNPCPack; // true, this item is in NPC package public bool m_bLocalDetailData; // true, data from GetDetailDataFromLocal - public CECInventory m_pDescIvtr; // Inventory only used to get item description + public EC_Inventory m_pDescIvtr; // Inventory only used to get item description #region Constructors @@ -979,7 +979,7 @@ namespace BrewMonster.Scripts.Managers /// Get item description text (normal / booth-buy / reward / repair). /// Mirrors the inline C++ GetDesc. /// - public string GetDesc(DescType iDescType = DescType.DESC_NORMAL, CECInventory pInventory = null) + public string GetDesc(DescType iDescType = DescType.DESC_NORMAL, EC_Inventory pInventory = null) { m_pDescIvtr = pInventory; diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrType.cs b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrType.cs index bde0c2132f..b42a4e4aba 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_IvtrType.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_IvtrType.cs @@ -442,15 +442,18 @@ namespace BrewMonster.Scripts.Managers /// private static IndexOfIteminEquipmentInventory GetAvailableFingerSlot() { + var host = CECGameRun.Instance?.GetHostPlayer(); + var equipInv = host?.GetInventory(InventoryConst.IVTRTYPE_EQUIPPACK); + // Check if FINGER1 slot is empty - var finger1Item = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, (int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER1, false); + var finger1Item = equipInv?.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER1, false); if (finger1Item == null) { return IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER1; } // Check if FINGER2 slot is empty - var finger2Item = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, (int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER2, false); + var finger2Item = equipInv?.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER2, false); if (finger2Item == null) { return IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER2; diff --git a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs index f3c3990cd2..3d3b764b3e 100644 --- a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs +++ b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs @@ -454,12 +454,10 @@ namespace BrewMonster.Scripts.Task public int GetCommonItemCount(uint ulCommonItem) { // CECInventory pPack = m_pHost.GetPack(); - //EC_Inventory pPack = m_pHost.GetPack(); - - // return pPack != null ? EC_Inventory.GetItemTotalNum( 0, (int)ulCommonItem) : 0; + // return pPack != null ? pPack.GetItemTotalNum((int)ulCommonItem) : 0; - // paramater 1 to get type inventory (0:inventoty, 1:equip,...) - return EC_Inventory.GetItemTotalNum(EC_Inventory.IVTRTYPE_PACK, (int)ulCommonItem); + var inv = m_pHost?.GetInventory(InventoryConst.IVTRTYPE_PACK); + return inv != null ? inv.GetItemTotalNum((int)ulCommonItem) : 0; } public int GetTaskItemCount(uint ulTaskItem) @@ -467,7 +465,8 @@ namespace BrewMonster.Scripts.Task // CECInventory* pPack = m_pHost->GetTaskPack(); // return pPack ? pPack->GetItemTotalNum((int)ulTaskItem) : 0; - return EC_Inventory.GetItemTotalNum(EC_Inventory.IVTRTYPE_TASKPACK, (int)ulTaskItem); + var inv = m_pHost?.GetInventory(InventoryConst.IVTRTYPE_TASKPACK); + return inv != null ? inv.GetItemTotalNum((int)ulTaskItem) : 0; } private ATaskTemplMan GetTaskTemplMan() @@ -725,7 +724,8 @@ namespace BrewMonster.Scripts.Task public uint GetInvEmptySlot() { - return (uint)EC_Inventory.GetEmptySlotNum(EC_Inventory.IVTRTYPE_PACK); + var inv = m_pHost?.GetInventory(InventoryConst.IVTRTYPE_PACK); + return inv != null ? (uint)inv.GetEmptySlotNum() : 0u; } public int GetFactionContrib() diff --git a/Assets/Prefabs/UI/InventoryUI.prefab b/Assets/Prefabs/UI/InventoryUI.prefab index 8fead78cbd..4693a4da1b 100644 --- a/Assets/Prefabs/UI/InventoryUI.prefab +++ b/Assets/Prefabs/UI/InventoryUI.prefab @@ -718,7 +718,6 @@ GameObject: m_Component: - component: {fileID: 5275130098495308601} - component: {fileID: 7874153745862569389} - - component: {fileID: 112648634020396885} - component: {fileID: 3332719603249310962} m_Layer: 5 m_Name: PreviewCamera @@ -797,14 +796,6 @@ Camera: m_OcclusionCulling: 1 m_StereoConvergence: 10 m_StereoSeparation: 0.022 ---- !u!81 &112648634020396885 -AudioListener: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 633515748786992396} - m_Enabled: 1 --- !u!114 &3332719603249310962 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/CECHostPlayer.cs b/Assets/Scripts/CECHostPlayer.cs index f9cb9061a6..e532a65bd2 100644 --- a/Assets/Scripts/CECHostPlayer.cs +++ b/Assets/Scripts/CECHostPlayer.cs @@ -116,6 +116,31 @@ public partial class CECHostPlayer : CECPlayer public bool IsChangingFace() { return m_bChangingFace; } + // ===== Inventory packs (instance-based) ===== + // 0 = normal pack, 1 = equip pack, 2 = task pack (see InventoryConst.IVTRTYPE_*) + private readonly EC_Inventory m_packInventory = new EC_Inventory(); + private readonly EC_Inventory m_equipInventory = new EC_Inventory(); + private readonly EC_Inventory m_taskInventory = new EC_Inventory(); + + public EC_Inventory PackInventory => m_packInventory; + public EC_Inventory EquipInventory => m_equipInventory; + public EC_Inventory TaskInventory => m_taskInventory; + + public EC_Inventory GetInventory(byte byPackage) + { + switch (byPackage) + { + case InventoryConst.IVTRTYPE_PACK: + return m_packInventory; + case InventoryConst.IVTRTYPE_EQUIPPACK: + return m_equipInventory; + case InventoryConst.IVTRTYPE_TASKPACK: + return m_taskInventory; + default: + return null; + } + } + private void Awake() { base.Awake(); @@ -773,8 +798,9 @@ public partial class CECHostPlayer : CECPlayer Content = null }; - // Add item to inventory - EC_Inventory.SetItem(where, index, newItem); + // Add item to inventory (instance-based) + var inv = GetInventory(where); + inv?.SetItem(index, newItem); Debug.Log( $"[HOST_OBTAIN_ITEM] Successfully added item {type} to package {where}, slot {index} with count {amount}"); @@ -824,8 +850,9 @@ public partial class CECHostPlayer : CECPlayer Content = null }; - // Add item to inventory - EC_Inventory.SetItem(byPackage, bySlot, newItem); + // Add item to inventory (instance-based) + var inv = GetInventory(byPackage); + inv?.SetItem(bySlot, newItem); //Debug.Log($"[Inventory] Successfully added item {tid} to package {byPackage}, slot {bySlot} with count {iAmount}"); @@ -865,7 +892,8 @@ public partial class CECHostPlayer : CECPlayer $"[Inventory] PLAYER_DROP_ITEM: package={byPackage}, slot={bySlot}, count={count}, tid={tid}, reason={reason}"); // Update the inventory by removing the item - bool success = EC_Inventory.RemoveItem(byPackage, bySlot, count); + var inv = GetInventory(byPackage); + bool success = inv != null && inv.RemoveItem(bySlot, count); if (success) { @@ -896,20 +924,22 @@ public partial class CECHostPlayer : CECPlayer byte index_inv = data[0]; byte index_equip = data[1]; // Update client-side data: move item between PACK_INVENTORY and PACK_EQUIPMENT - var invItem = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_PACK, index_inv, true); - var equipItem = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, index_equip, true); + var packInv = GetInventory(InventoryConst.IVTRTYPE_PACK); + var equipInv = GetInventory(InventoryConst.IVTRTYPE_EQUIPPACK); + var invItem = packInv?.GetItem(index_inv, true); + var equipItem = equipInv?.GetItem(index_equip, true); if (invItem != null) { - invItem.Package = EC_Inventory.IVTRTYPE_EQUIPPACK; + invItem.Package = InventoryConst.IVTRTYPE_EQUIPPACK; invItem.Slot = index_equip; - EC_Inventory.SetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, index_equip, invItem); + equipInv?.SetItem(index_equip, invItem); } if (equipItem != null) { - equipItem.Package = EC_Inventory.IVTRTYPE_PACK; + equipItem.Package = InventoryConst.IVTRTYPE_PACK; equipItem.Slot = index_inv; - EC_Inventory.SetItem(EC_Inventory.IVTRTYPE_PACK, index_inv, equipItem); + packInv?.SetItem(index_inv, equipItem); } // Trigger UI refresh if an EC_InventoryUI is present in scene @@ -934,7 +964,7 @@ public partial class CECHostPlayer : CECPlayer Debug.Log("[Inventory] OWN_ITEM_INFO received"); var data = Msg.dwParam1 as byte[]; int hostId = Convert.ToInt32(Msg.dwParam3); - EC_Inventory.LogInventoryPacket("OWN_ITEM_INFO", data, hostId); + LogInventoryPacket("OWN_ITEM_INFO", data, hostId); break; } } @@ -950,7 +980,7 @@ public partial class CECHostPlayer : CECPlayer { case CommandID.OWN_IVTR_DATA: { - EC_Inventory.LogInventoryPacket("OWN_IVTR_DATA", data, hostId); + LogInventoryPacket("OWN_IVTR_DATA", data, hostId); break; } case CommandID.OWN_IVTR_DETAIL_DATA: @@ -964,11 +994,25 @@ public partial class CECHostPlayer : CECPlayer if (EC_IvtrItemUtils.Instance.TryParseInventoryDetail(data, out var pkg, out var size, out var items)) { - EC_Inventory.UpdatePack(pkg, size, items); + var inv = GetInventory(pkg); + if (inv != null) + { + inv.Resize(size); + inv.RemoveAllItems(); + + if (items != null) + { + foreach (var it in items) + { + if (it != null && it.Slot >= 0 && it.Slot < size) + inv.SetItem(it.Slot, it); + } + } + } } // check if we got the item from the Equipment Pack. If so, we have to load the equipment items - if (byPackage == EC_Inventory.IVTRTYPE_EQUIPPACK) + if (byPackage == InventoryConst.IVTRTYPE_EQUIPPACK) { UpdateEquipSkins(); } @@ -1063,6 +1107,49 @@ public partial class CECHostPlayer : CECPlayer } } + // ===== Inventory packet logging helpers (was static on EC_Inventory/CECInventory) ===== + private void LogInventoryPacket(string tag, byte[] buffer, int hostId) + { + if (buffer == null) + return; + + int index = 0; + if (buffer.Length < 6) + { + //LogInventoryRaw(tag, buffer); + return; + } + + byte byPackage = buffer[index++]; + byte ivtrSize = buffer[index++]; + uint contentLength = BitConverter.ToUInt32(buffer, index); index += 4; + + int remaining = buffer.Length - index; + int contentBytes = remaining; + if (contentLength < (uint)remaining) + { + contentBytes = (int)contentLength; + } + + if (contentBytes > 0) + { + byte[] content = new byte[contentBytes]; + Buffer.BlockCopy(buffer, index, content, 0, contentBytes); + } + + int trailing = buffer.Length - (index + contentBytes); + if (trailing > 0) + { + byte[] tail = new byte[trailing]; + Buffer.BlockCopy(buffer, index + contentBytes, tail, 0, trailing); + } + } + + private void LogInventoryRaw(string tag, byte[] buffer) + { + Debug.Log($"[Inventory] {tag}: RAW HEX (len={buffer?.Length ?? 0})=\n{(buffer == null ? "" : BitConverter.ToString(buffer))}"); + } + public void OnMsgHstCorrectPos(in ECMSG Msg) { Debug.LogWarning("HoangDev : OnMsgHstCorrectPos"); diff --git a/Assets/Scripts/CECPlayer_Inventory.cs b/Assets/Scripts/CECPlayer_Inventory.cs index fb27db140a..516a114e86 100644 --- a/Assets/Scripts/CECPlayer_Inventory.cs +++ b/Assets/Scripts/CECPlayer_Inventory.cs @@ -129,17 +129,16 @@ public partial class CECPlayer public bool UpdateEquipSkins() { - var equipPack = EC_Inventory.GetPack(EC_Inventory.IVTRTYPE_EQUIPPACK); - int[] aNewEquips = new int[InventoryConst.IVTRSIZE_EQUIPPACK]; EC_IvtrItem pItem = null; for (int i = 0; i < InventoryConst.IVTRSIZE_EQUIPPACK; i++) { - - if (equipPack.TryGetValue(i, out pItem)) - { + // Use host player's equipment inventory (per-instance CECInventory) + var host = CECGameRun.Instance?.GetHostPlayer(); + var equipInv = host?.EquipInventory; + pItem = equipInv?.GetItem(i, false); + if (pItem != null) aNewEquips[i] = pItem.m_tid; - } } ShowEquipments(aNewEquips, true, true);