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