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);