Merge remote-tracking branch 'origin/develop' into implement_task_UI

This commit is contained in:
MinhHai
2025-11-21 11:31:09 +07:00
16 changed files with 1241 additions and 466 deletions
@@ -61,5 +61,10 @@ namespace BrewMonster.Scripts
public const int IVTRSIZE_CLIENTCARDPACK = 32; // Client pack for general card collection
public const int NUM_NPCIVTR = 8; // NPC inventory number
// Inventory package types (match legacy C++ IVTRTYPE_*)
public const byte IVTRTYPE_PACK = 0;
public const byte IVTRTYPE_EQUIPPACK = 1;
public const byte IVTRTYPE_TASKPACK = 2;
}
}
@@ -4,390 +4,302 @@ using UnityEngine;
namespace BrewMonster.Scripts.Managers
{
/// <summary>
/// C# mirror of C++ CECInventory (EC_Inventory.h / EC_Inventory.cpp).
/// Instance-based inventory core plus static helpers used by existing client code.
/// </summary>
public class EC_Inventory
{
// ===== Instance-based inventory (per-pack) =====
public static class EC_Inventory
{
private static readonly Dictionary<byte, int> _packSizeByPackage = new Dictionary<byte, int>();
private static readonly Dictionary<byte, Dictionary<int, EC_IvtrItem>> _itemsByPackage = new Dictionary<byte, Dictionary<int, EC_IvtrItem>>();
private const int MaxContentHexToLog = 64;
// Item array: index is slot, null means empty.
private EC_IvtrItem[] m_aItems = Array.Empty<EC_IvtrItem>();
// 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;
public EC_Inventory()
{
}
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";
}
}
public bool Init(int iSize)
{
Resize(iSize);
return true;
}
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 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<EC_IvtrItem>();
}
public static Dictionary<int, EC_IvtrItem> GetPack(byte byPackage)
{
return _itemsByPackage[byPackage];
}
public void RemoveAllItems()
{
for (int i = 0; i < m_aItems.Length; i++)
{
m_aItems[i] = null;
}
}
public static EC_IvtrItem GetItem(int iSlot, bool bRemove)
{
// Backward-compatible overload defaulting to inventory package
return GetItem(IVTRTYPE_PACK, iSlot, bRemove);
}
public void Resize(int iNewSize)
{
int oldSize = m_aItems.Length;
if (iNewSize < 0) iNewSize = 0;
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;
}
if (iNewSize == oldSize)
return;
private static Dictionary<int, EC_IvtrItem> EnsureSlots(byte byPackage)
{
if (!_itemsByPackage.TryGetValue(byPackage, out var slots) || slots == null)
{
slots = new Dictionary<int, EC_IvtrItem>();
_itemsByPackage[byPackage] = slots;
}
return slots;
}
var newArray = iNewSize > 0 ? new EC_IvtrItem[iNewSize] : Array.Empty<EC_IvtrItem>();
public static void Resize(byte byPackage, int newSize)
{
_packSizeByPackage[byPackage] = Math.Max(0, newSize);
EnsureSlots(byPackage);
}
if (oldSize > 0 && iNewSize > 0)
{
Array.Copy(m_aItems, newArray, Math.Min(oldSize, iNewSize));
}
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;
}
m_aItems = newArray;
}
public static void SetItem(byte byPackage, int slot, EC_IvtrItem item)
{
var slots = EnsureSlots(byPackage);
if (slot < 0) return;
slots[slot] = item;
}
public EC_IvtrItem PutItem(int iSlot, EC_IvtrItem pItem)
{
if (iSlot < 0 || iSlot >= m_aItems.Length)
{
return null;
}
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);
}
var old = m_aItems[iSlot];
m_aItems[iSlot] = pItem;
return old;
}
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 void SetItem(int iSlot, EC_IvtrItem pItem)
{
if (iSlot < 0 || iSlot >= m_aItems.Length)
{
return;
}
public static void RemoveAllItems(byte byPackage)
{
var slots = EnsureSlots(byPackage);
slots.Clear();
}
m_aItems[iSlot] = pItem;
}
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 EC_IvtrItem GetItem(int iSlot, bool bRemove = false)
{
if (iSlot < 0 || iSlot >= m_aItems.Length)
{
return null;
}
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;
}
var pItem = m_aItems[iSlot];
if (bRemove)
m_aItems[iSlot] = null;
return pItem;
}
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 void ExchangeItem(int iSlot1, int iSlot2)
{
if (iSlot1 < 0 || iSlot1 >= m_aItems.Length ||
iSlot2 < 0 || iSlot2 >= m_aItems.Length)
{
return;
}
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;
}
if (iSlot1 == iSlot2)
return;
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;
}
var tmp = m_aItems[iSlot1];
m_aItems[iSlot1] = m_aItems[iSlot2];
m_aItems[iSlot2] = tmp;
}
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 bool MergeItem(int tid, int iExpireDate, int iAmount, out int piLastSlot, out int piLastAmount)
{
piLastSlot = -1;
piLastAmount = 0;
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;
}
int firstEmpty = -1;
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<EC_IvtrItem> items)
{
_packSizeByPackage[byPackage] = ivtrSize;
if (!_itemsByPackage.TryGetValue(byPackage, out var slots))
{
slots = new Dictionary<int, EC_IvtrItem>();
_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);
}
for (int i = 0; i < m_aItems.Length; i++)
{
var slotItem = m_aItems[i];
if (slotItem != null)
{
if (slotItem.GetTemplateID() != tid)
continue;
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;
}
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;
private static void LogPackInternal(byte byPackage, int ivtrSize, IReadOnlyDictionary<int, EC_IvtrItem> 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 : "")}"
//);
}
}
if (iAmount == 0)
{
piLastSlot = i;
piLastAmount = slotItem.GetCount();
return true;
}
}
else if (firstEmpty < 0)
{
firstEmpty = i;
}
}
public static void LogInventoryPacket(string tag, byte[] buffer, int hostId)
{
if (buffer == null)
{
return;
}
if (firstEmpty < 0 || iAmount <= 0)
{
return false;
}
int index = 0;
if (buffer.Length < 6)
{
//LogInventoryRaw(tag, buffer);
return;
}
var newItem = new EC_IvtrItem(tid, iExpireDate)
{
Slot = firstEmpty,
State = 0,
Crc = 0,
Content = null
};
newItem.SetCount(iAmount);
byte byPackage = buffer[index++];
byte ivtrSize = buffer[index++];
uint contentLength = BitConverter.ToUInt32(buffer, index); index += 4;
m_aItems[firstEmpty] = newItem;
piLastSlot = firstEmpty;
piLastAmount = iAmount;
return true;
}
int remaining = buffer.Length - index;
int contentBytes = remaining;
if (contentLength < (uint)remaining)
{
contentBytes = (int)contentLength;
}
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 (contentBytes > 0)
{
byte[] content = new byte[contentBytes];
Buffer.BlockCopy(buffer, index, content, 0, contentBytes);
}
if (pSrc == null)
return false;
int trailing = buffer.Length - (index + contentBytes);
if (trailing > 0)
{
byte[] tail = new byte[trailing];
Buffer.BlockCopy(buffer, index + contentBytes, tail, 0, trailing);
}
}
if (iAmount == 0)
return false;
public static void LogInventoryRaw(string tag, byte[] buffer)
{
Debug.Log($"[Inventory] {tag}: RAW HEX (len={buffer?.Length ?? 0})=\n{(buffer == null ? "<null>" : BitConverter.ToString(buffer))}");
}
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;
}
}
}
@@ -1,2 +1,2 @@
fileFormatVersion: 2
guid: b060723330d7f49409ca241f4e460bed
guid: 0632a4edec7d56543a1bfea4c263ba81
@@ -10,6 +10,7 @@ using BrewMonster;
using BrewMonster.Common;
using ModelRenderer.Scripts.GameData;
using PerfectWorld.Scripts.Managers;
using BrewMonster.Scripts;
namespace BrewMonster.Scripts.Managers
{
@@ -296,7 +297,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)
{
@@ -566,34 +567,23 @@ namespace BrewMonster.Scripts.Managers
// === MVC: Model ===
private class InventoryModel
{
private readonly FieldInfo itemsByPackageField;
public InventoryModel()
{
var inventoryType = typeof(EC_Inventory);
itemsByPackageField = inventoryType.GetField("_itemsByPackage", BindingFlags.NonPublic | BindingFlags.Static);
if (itemsByPackageField == null)
{
Debug.LogError("[InventoryUI] Could not access _itemsByPackage field from EC_Inventory");
}
}
public Dictionary<int, EC_IvtrItem> GetInventoryData(byte package)
{
if (itemsByPackageField == null)
// Read from host player's per-package CECInventory instance
var host = CECGameRun.Instance?.GetHostPlayer();
var inv = host?.GetInventory(package);
var result = new Dictionary<int, EC_IvtrItem>();
if (inv == null)
return result;
int size = inv.GetSize();
for (int i = 0; i < size; i++)
{
return new Dictionary<int, EC_IvtrItem>();
var item = inv.GetItem(i, false);
if (item != null)
result[i] = item;
}
var itemsByPackage = itemsByPackageField.GetValue(null) as Dictionary<byte, Dictionary<int, EC_IvtrItem>>;
if (itemsByPackage == null)
{
return new Dictionary<int, EC_IvtrItem>();
}
if (itemsByPackage.TryGetValue(package, out var packageItems))
{
return packageItems;
}
return new Dictionary<int, EC_IvtrItem>();
return result;
}
}
@@ -635,7 +625,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;
}
@@ -818,7 +808,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);
@@ -987,7 +977,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))
@@ -735,7 +735,7 @@ namespace PerfectWorld.Scripts.Managers
/// </summary>
public string GetName()
{
return EC_IvtrItemUtils.ResolveItemName(TemplateId);
return EC_IvtrItemUtils.Instance.ResolveItemName(TemplateId);
}
/// <summary>
@@ -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
@@ -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.
/// <summary>
/// Non-static UI/data helper for inventory items.
/// This holds caches and helpers for names, icons, pile limits, etc.
/// </summary>
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<int, string> _tidNameCache = new Dictionary<int, string>();
private static readonly Dictionary<int, int> _pileLimitCache = new Dictionary<int, int>();
private static readonly Dictionary<int, string> _tidIconKeyCache = new Dictionary<int, string>();
private static readonly Dictionary<string, Sprite> _iconSpriteCache = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
private static Sprite[] _multiSpriteAtlas = null;
private static readonly Dictionary<string, int> _spriteNameToIndexCache = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<int, string> _tidNameCache = new Dictionary<int, string>();
private readonly Dictionary<int, int> _pileLimitCache = new Dictionary<int, int>();
private readonly Dictionary<int, string> _tidIconKeyCache = new Dictionary<int, string>();
private readonly Dictionary<string, Sprite> _iconSpriteCache = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
private Sprite[] _multiSpriteAtlas = null;
private readonly Dictionary<string, int> _spriteNameToIndexCache = new Dictionary<string, int>(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
/// <summary>
/// Resolve and load the item's icon Sprite from Resources/UI/IconSprites based on its element data file_icon (DDS) name.
/// </summary>
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<EC_IvtrItem> items)
public bool TryParseInventoryDetail(byte[] buffer, out byte byPackage, out byte ivtrSize, out List<EC_IvtrItem> 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;
}
}
/// <summary>
/// C# mirror of C++ <c>CECIvtrItem</c> (defined in <c>EC_IvtrItem.h / EC_IvtrItem.cpp</c>).
/// This class intentionally keeps C++-style naming and layout so other C++ systems
/// can be ported over with minimal friction.
/// </summary>
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 EC_Inventory 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++)
/// <summary>
/// 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 <c>CreateItem</c>.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Get scaled price of specified count of items.
/// Exact port of the C++ price scaling logic.
/// </summary>
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)
/// <summary>
/// Set item detail information.
/// C++ default just clears update flags; detail parsing happens in derived types.
/// </summary>
public virtual bool SetItemInfo(byte[] pInfoData, int iDataLen)
{
m_bNeedUpdate = false;
m_bUpdating = false;
m_strDesc = string.Empty;
return true;
}
/// <summary>Get item default information from database (no-op base, like C++).</summary>
public virtual void DefaultInfo()
{
}
/// <summary>Get item icon file name (C++ default returns "Unknown.dds").</summary>
public virtual string GetIconFile()
{
return "Unknown.dds";
}
/// <summary>
/// Get item name. In C++ this uses string tables; here we go through
/// <see cref="EC_IvtrItemUtils.ResolveItemName(int)"/> which already mirrors that logic.
/// </summary>
public virtual string GetName()
{
return EC_IvtrItemUtils.Instance.ResolveItemName(m_tid);
}
/// <summary>
/// Get item name color. The original returns an <c>A3DCOLOR</c>;
/// here we return ARGB packed into a <see cref="uint"/>; default is white.
/// </summary>
public virtual uint GetNameColor()
{
return 0xFFFFFFFFu;
}
/// <summary>Use item. Base class just returns true.</summary>
public virtual bool Use()
{
return true;
}
/// <summary>Get scaled price of this item instance.</summary>
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);
}
/// <summary>Clone item (shallow copy, same as C++ default).</summary>
public virtual EC_IvtrItem Clone()
{
return new EC_IvtrItem(this);
}
/// <summary>Get item cool time in milliseconds (0 by default).</summary>
public virtual int GetCoolTime(out int? piMax)
{
piMax = null;
return 0;
}
public virtual bool CheckUseCondition()
{
return IsUseable();
}
/// <summary>Get drop model for showing in world.</summary>
public virtual string GetDropModel()
{
return "Models\\Error\\Error.ecm";
}
/// <summary>Get item quality level. Base returns -1 (unknown).</summary>
public virtual int GetItemLevel()
{
return -1;
}
/// <summary>
/// Get item description text (normal / booth-buy / reward / repair).
/// Mirrors the inline C++ <c>GetDesc</c>.
/// </summary>
public string GetDesc(DescType iDescType = DescType.DESC_NORMAL, EC_Inventory 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);
}
}
/// <summary>
/// Merge item amount with another same kind item.
/// Returns the number of items actually merged.
/// </summary>
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;
}
/// <summary>Add item amount. Returns new amount of item.</summary>
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;
}
/// <summary>
/// Can this item be equipped to specified position?
/// Uses the same bitmask test as C++.
/// </summary>
public bool CanEquippedTo(int iSlot)
{
return (m_i64EquipMask & (1L << iSlot)) != 0;
}
/// <summary>
/// Get first slot index starting from <paramref name="iStartSlot"/> where this item can be equipped.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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)
/// <summary>
/// Base implementation: just returns current description string.
/// Derived item types can override and build rich description like in C++.
/// </summary>
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
}
/// <summary>
/// C# mirror of C++ <c>CECIvtrUnknown</c> (fallback item type).
/// </summary>
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;
}
}
}
@@ -442,15 +442,18 @@ namespace BrewMonster.Scripts.Managers
/// </summary>
private static IndexOfIteminEquipmentInventory GetAvailableFingerSlot()
{
var host = CECGameRun.Instance?.GetHostPlayer();
var equipInv = host?.GetInventory(InventoryConst.IVTRTYPE_EQUIPPACK);
// Check if FINGER1 slot is empty
var finger1Item = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, (int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER1, false);
var finger1Item = equipInv?.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER1, false);
if (finger1Item == null)
{
return IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER1;
}
// Check if FINGER2 slot is empty
var finger2Item = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, (int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER2, false);
var finger2Item = equipInv?.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER2, false);
if (finger2Item == null)
{
return IndexOfIteminEquipmentInventory.EQUIPIVTR_FINGER2;
@@ -454,12 +454,10 @@ namespace BrewMonster.Scripts.Task
public int GetCommonItemCount(uint ulCommonItem)
{
// CECInventory pPack = m_pHost.GetPack();
//EC_Inventory pPack = m_pHost.GetPack();
// return pPack != null ? EC_Inventory.GetItemTotalNum( 0, (int)ulCommonItem) : 0;
// return pPack != null ? pPack.GetItemTotalNum((int)ulCommonItem) : 0;
// paramater 1 to get type inventory (0:inventoty, 1:equip,...)
return EC_Inventory.GetItemTotalNum(EC_Inventory.IVTRTYPE_PACK, (int)ulCommonItem);
var inv = m_pHost?.GetInventory(InventoryConst.IVTRTYPE_PACK);
return inv != null ? inv.GetItemTotalNum((int)ulCommonItem) : 0;
}
public int GetTaskItemCount(uint ulTaskItem)
@@ -467,7 +465,8 @@ namespace BrewMonster.Scripts.Task
// CECInventory* pPack = m_pHost->GetTaskPack();
// return pPack ? pPack->GetItemTotalNum((int)ulTaskItem) : 0;
return EC_Inventory.GetItemTotalNum(EC_Inventory.IVTRTYPE_TASKPACK, (int)ulTaskItem);
var inv = m_pHost?.GetInventory(InventoryConst.IVTRTYPE_TASKPACK);
return inv != null ? inv.GetItemTotalNum((int)ulTaskItem) : 0;
}
private ATaskTemplMan GetTaskTemplMan()
@@ -725,7 +724,8 @@ namespace BrewMonster.Scripts.Task
public uint GetInvEmptySlot()
{
return (uint)EC_Inventory.GetEmptySlotNum(EC_Inventory.IVTRTYPE_PACK);
var inv = m_pHost?.GetInventory(InventoryConst.IVTRTYPE_PACK);
return inv != null ? (uint)inv.GetEmptySlotNum() : 0u;
}
public int GetFactionContrib()
@@ -832,7 +832,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);
@@ -1215,7 +1215,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)
@@ -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)
{
+1 -1
View File
@@ -215,7 +215,7 @@ public class pickupItem : MonoBehaviour
TextMeshPro textMesh = textObject.AddComponent<TextMeshPro>();
// Get the item name
string itemName = EC_IvtrItemUtils.ResolveItemName(tid);
string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(tid);
if (string.IsNullOrEmpty(itemName))
{
itemName = $"Item {tid}";
-9
View File
@@ -718,7 +718,6 @@ GameObject:
m_Component:
- component: {fileID: 5275130098495308601}
- component: {fileID: 7874153745862569389}
- component: {fileID: 112648634020396885}
- component: {fileID: 3332719603249310962}
m_Layer: 5
m_Name: PreviewCamera
@@ -797,14 +796,6 @@ Camera:
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!81 &112648634020396885
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 633515748786992396}
m_Enabled: 1
--- !u!114 &3332719603249310962
MonoBehaviour:
m_ObjectHideFlags: 0
+103 -16
View File
@@ -116,6 +116,31 @@ public partial class CECHostPlayer : CECPlayer
public bool IsChangingFace() { return m_bChangingFace; }
// ===== Inventory packs (instance-based) =====
// 0 = normal pack, 1 = equip pack, 2 = task pack (see InventoryConst.IVTRTYPE_*)
private readonly EC_Inventory m_packInventory = new EC_Inventory();
private readonly EC_Inventory m_equipInventory = new EC_Inventory();
private readonly EC_Inventory m_taskInventory = new EC_Inventory();
public EC_Inventory PackInventory => m_packInventory;
public EC_Inventory EquipInventory => m_equipInventory;
public EC_Inventory TaskInventory => m_taskInventory;
public EC_Inventory GetInventory(byte byPackage)
{
switch (byPackage)
{
case InventoryConst.IVTRTYPE_PACK:
return m_packInventory;
case InventoryConst.IVTRTYPE_EQUIPPACK:
return m_equipInventory;
case InventoryConst.IVTRTYPE_TASKPACK:
return m_taskInventory;
default:
return null;
}
}
private void Awake()
{
base.Awake();
@@ -773,8 +798,9 @@ public partial class CECHostPlayer : CECPlayer
Content = null
};
// Add item to inventory
EC_Inventory.SetItem(where, index, newItem);
// Add item to inventory (instance-based)
var inv = GetInventory(where);
inv?.SetItem(index, newItem);
Debug.Log(
$"[HOST_OBTAIN_ITEM] Successfully added item {type} to package {where}, slot {index} with count {amount}");
@@ -824,8 +850,9 @@ public partial class CECHostPlayer : CECPlayer
Content = null
};
// Add item to inventory
EC_Inventory.SetItem(byPackage, bySlot, newItem);
// Add item to inventory (instance-based)
var inv = GetInventory(byPackage);
inv?.SetItem(bySlot, newItem);
//Debug.Log($"[Inventory] Successfully added item {tid} to package {byPackage}, slot {bySlot} with count {iAmount}");
@@ -865,7 +892,8 @@ public partial class CECHostPlayer : CECPlayer
$"[Inventory] PLAYER_DROP_ITEM: package={byPackage}, slot={bySlot}, count={count}, tid={tid}, reason={reason}");
// Update the inventory by removing the item
bool success = EC_Inventory.RemoveItem(byPackage, bySlot, count);
var inv = GetInventory(byPackage);
bool success = inv != null && inv.RemoveItem(bySlot, count);
if (success)
{
@@ -896,20 +924,22 @@ public partial class CECHostPlayer : CECPlayer
byte index_inv = data[0];
byte index_equip = data[1];
// Update client-side data: move item between PACK_INVENTORY and PACK_EQUIPMENT
var invItem = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_PACK, index_inv, true);
var equipItem = EC_Inventory.GetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, index_equip, true);
var packInv = GetInventory(InventoryConst.IVTRTYPE_PACK);
var equipInv = GetInventory(InventoryConst.IVTRTYPE_EQUIPPACK);
var invItem = packInv?.GetItem(index_inv, true);
var equipItem = equipInv?.GetItem(index_equip, true);
if (invItem != null)
{
invItem.Package = EC_Inventory.IVTRTYPE_EQUIPPACK;
invItem.Package = InventoryConst.IVTRTYPE_EQUIPPACK;
invItem.Slot = index_equip;
EC_Inventory.SetItem(EC_Inventory.IVTRTYPE_EQUIPPACK, index_equip, invItem);
equipInv?.SetItem(index_equip, invItem);
}
if (equipItem != null)
{
equipItem.Package = EC_Inventory.IVTRTYPE_PACK;
equipItem.Package = InventoryConst.IVTRTYPE_PACK;
equipItem.Slot = index_inv;
EC_Inventory.SetItem(EC_Inventory.IVTRTYPE_PACK, index_inv, equipItem);
packInv?.SetItem(index_inv, equipItem);
}
// Trigger UI refresh if an EC_InventoryUI is present in scene
@@ -934,7 +964,7 @@ public partial class CECHostPlayer : CECPlayer
Debug.Log("[Inventory] OWN_ITEM_INFO received");
var data = Msg.dwParam1 as byte[];
int hostId = Convert.ToInt32(Msg.dwParam3);
EC_Inventory.LogInventoryPacket("OWN_ITEM_INFO", data, hostId);
LogInventoryPacket("OWN_ITEM_INFO", data, hostId);
break;
}
}
@@ -950,7 +980,7 @@ public partial class CECHostPlayer : CECPlayer
{
case CommandID.OWN_IVTR_DATA:
{
EC_Inventory.LogInventoryPacket("OWN_IVTR_DATA", data, hostId);
LogInventoryPacket("OWN_IVTR_DATA", data, hostId);
break;
}
case CommandID.OWN_IVTR_DETAIL_DATA:
@@ -961,14 +991,28 @@ 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);
var inv = GetInventory(pkg);
if (inv != null)
{
inv.Resize(size);
inv.RemoveAllItems();
if (items != null)
{
foreach (var it in items)
{
if (it != null && it.Slot >= 0 && it.Slot < size)
inv.SetItem(it.Slot, it);
}
}
}
}
// check if we got the item from the Equipment Pack. If so, we have to load the equipment items
if (byPackage == EC_Inventory.IVTRTYPE_EQUIPPACK)
if (byPackage == InventoryConst.IVTRTYPE_EQUIPPACK)
{
UpdateEquipSkins();
}
@@ -1063,6 +1107,49 @@ public partial class CECHostPlayer : CECPlayer
}
}
// ===== Inventory packet logging helpers (was static on EC_Inventory/CECInventory) =====
private void LogInventoryPacket(string tag, byte[] buffer, int hostId)
{
if (buffer == null)
return;
int index = 0;
if (buffer.Length < 6)
{
//LogInventoryRaw(tag, buffer);
return;
}
byte byPackage = buffer[index++];
byte ivtrSize = buffer[index++];
uint contentLength = BitConverter.ToUInt32(buffer, index); index += 4;
int remaining = buffer.Length - index;
int contentBytes = remaining;
if (contentLength < (uint)remaining)
{
contentBytes = (int)contentLength;
}
if (contentBytes > 0)
{
byte[] content = new byte[contentBytes];
Buffer.BlockCopy(buffer, index, content, 0, contentBytes);
}
int trailing = buffer.Length - (index + contentBytes);
if (trailing > 0)
{
byte[] tail = new byte[trailing];
Buffer.BlockCopy(buffer, index + contentBytes, tail, 0, trailing);
}
}
private void LogInventoryRaw(string tag, byte[] buffer)
{
Debug.Log($"[Inventory] {tag}: RAW HEX (len={buffer?.Length ?? 0})=\n{(buffer == null ? "<null>" : BitConverter.ToString(buffer))}");
}
public void OnMsgHstCorrectPos(in ECMSG Msg)
{
Debug.LogWarning("HoangDev : OnMsgHstCorrectPos");
+5 -6
View File
@@ -129,17 +129,16 @@ public partial class CECPlayer
public bool UpdateEquipSkins()
{
var equipPack = EC_Inventory.GetPack(EC_Inventory.IVTRTYPE_EQUIPPACK);
int[] aNewEquips = new int[InventoryConst.IVTRSIZE_EQUIPPACK];
EC_IvtrItem pItem = null;
for (int i = 0; i < InventoryConst.IVTRSIZE_EQUIPPACK; i++)
{
if (equipPack.TryGetValue(i, out pItem))
{
// Use host player's equipment inventory (per-instance CECInventory)
var host = CECGameRun.Instance?.GetHostPlayer();
var equipInv = host?.EquipInventory;
pItem = equipInv?.GetItem(i, false);
if (pItem != null)
aNewEquips[i] = pItem.m_tid;
}
}
ShowEquipments(aNewEquips, true, true);
+1
View File
@@ -11,6 +11,7 @@
"com.unity.ide.visualstudio": "2.0.23",
"com.unity.inputsystem": "1.14.2",
"com.unity.multiplayer.center": "1.0.0",
"com.unity.multiplayer.playmode": "1.6.1",
"com.unity.render-pipelines.universal": "17.0.4",
"com.unity.test-framework": "1.5.1",
"com.unity.timeline": "1.8.9",
+16
View File
@@ -134,6 +134,15 @@
"com.unity.modules.uielements": "1.0.0"
}
},
"com.unity.multiplayer.playmode": {
"version": "1.6.1",
"depth": 0,
"source": "registry",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "2.0.2"
},
"url": "https://packages.unity.com"
},
"com.unity.nuget.mono-cecil": {
"version": "1.11.4",
"depth": 3,
@@ -141,6 +150,13 @@
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.nuget.newtonsoft-json": {
"version": "3.2.1",
"depth": 1,
"source": "registry",
"dependencies": {},
"url": "https://packages.unity.com"
},
"com.unity.profiling.core": {
"version": "1.0.2",
"depth": 1,