using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using BrewMonster; using BrewMonster.Network; using ModelRenderer.Scripts.Common; using ModelRenderer.Scripts.GameData; using UnityEngine; namespace BrewMonster.Scripts { // 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 { // Simple singleton-style access so existing systems can use it globally. public static readonly EC_IvtrItemUtils Instance = new EC_IvtrItemUtils(); 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 string ResolveItemName(int templateId) { if (templateId <= 0) return ""; //if (_tidNameCache.TryGetValue(templateId, out var cached)) return cached; try { var edm = ElementDataManProvider.GetElementDataMan(); if (edm == null) return CacheAndReturn(templateId, ""); uint id = unchecked((uint)templateId); DATA_TYPE dATA_TYPE = default; object data = edm.get_data_ptr(id, ID_SPACE.ID_SPACE_ESSENCE, ref dATA_TYPE); string name = ExtractNameFromElement(data); if (string.IsNullOrEmpty(name)) { name = TryFindNameByScanningArrays(edm, id); } return CacheAndReturn(templateId, name ?? ""); } catch (Exception ex) { Debug.LogWarning($"[Inventory] ResolveItemName error for tid={templateId}: {ex.Message}"); return CacheAndReturn(templateId, ""); } } /// /// Resolve and load the item's icon Sprite from Resources/UI/IconSprites based on its element data file_icon (DDS) name. /// public Sprite ResolveItemIconSprite(int templateId) { if (templateId <= 0) return null; if (_tidIconKeyCache.TryGetValue(templateId, out var cachedKey)) { if (string.IsNullOrEmpty(cachedKey)) return null; if (_iconSpriteCache.TryGetValue(cachedKey, out var cachedSprite) && cachedSprite != null) return cachedSprite; var sprite = LoadIconSpriteByKey(cachedKey); _iconSpriteCache[cachedKey] = sprite; return sprite; } string key = string.Empty; try { var edm = ElementDataManProvider.GetElementDataMan(); if (edm != null) { uint id = unchecked((uint)templateId); DATA_TYPE dt = DATA_TYPE.DT_INVALID; object data = edm.get_data_ptr(id, ID_SPACE.ID_SPACE_ESSENCE, ref dt); if (data == null) { data = TryFindElementByScanningArrays(edm, id); } string iconPath = ExtractIconPathFromElement(data); key = NormalizeIconResourceKeyFromPath(iconPath); } } catch { } _tidIconKeyCache[templateId] = key ?? string.Empty; if (string.IsNullOrEmpty(key)) return null; if (_iconSpriteCache.TryGetValue(key, out var spriteCached) && spriteCached != null) return spriteCached; var spriteLoaded = LoadIconSpriteByKey(key); _iconSpriteCache[key] = spriteLoaded; return spriteLoaded; } private Sprite LoadIconSpriteByKey(string key) { if (string.IsNullOrEmpty(key)) return null; // Load multi-sprite atlas if not already loaded if (_multiSpriteAtlas == null) { LoadMultiSpriteAtlas(); } if (_multiSpriteAtlas == null) return null; // Try to find sprite by name in the atlas if (_spriteNameToIndexCache.TryGetValue(key, out var index)) { if (index >= 0 && index < _multiSpriteAtlas.Length) { return _multiSpriteAtlas[index]; } } // Fallback: try to find by name directly in the atlas foreach (var sprite in _multiSpriteAtlas) { if (sprite != null && string.Equals(sprite.name, key, StringComparison.OrdinalIgnoreCase)) { return sprite; } } // Try lowercase/uppercase variants as fallback foreach (var sprite in _multiSpriteAtlas) { if (sprite != null && (string.Equals(sprite.name, key.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase) || string.Equals(sprite.name, key.ToUpperInvariant(), StringComparison.OrdinalIgnoreCase))) { return sprite; } } return null; } private void LoadMultiSpriteAtlas() { try { // Load the multi-sprite atlas from Resources var atlasSprites = EC_Game.GetGameRun().GetUIManager().IconlistIvtr; //Resources.LoadAll("UI/IconSprites/iconlist_ivtrm_multisprite"); if (atlasSprites != null && atlasSprites.Length > 0) { _multiSpriteAtlas = atlasSprites; // Build name-to-index cache for faster lookups _spriteNameToIndexCache.Clear(); for (int i = 0; i < atlasSprites.Length; i++) { if (atlasSprites[i] != null && !string.IsNullOrEmpty(atlasSprites[i].name)) { _spriteNameToIndexCache[atlasSprites[i].name] = i; } } } else { _multiSpriteAtlas = new Sprite[0]; // Prevent repeated loading attempts } } catch (Exception ex) { _multiSpriteAtlas = new Sprite[0]; // Prevent repeated loading attempts } } private object TryFindElementByScanningArrays(object edm, uint id) { if (edm == null) return null; try { var fields = edm.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); foreach (var f in fields) { if (!f.FieldType.IsArray) continue; var arr = f.GetValue(edm) as Array; if (arr == null || arr.Length == 0) continue; var elemType = f.FieldType.GetElementType(); var idField = elemType.GetField("id", BindingFlags.Public | BindingFlags.Instance); if (idField == null || idField.FieldType != typeof(uint)) continue; for (int i = 0; i < arr.Length; i++) { var element = arr.GetValue(i); if (element == null) continue; var value = (uint)idField.GetValue(element); if (value == id) { return element; } } } } catch { } return null; } private string ExtractIconPathFromElement(object data) { if (data == null) return string.Empty; var t = data.GetType(); // Common field/property name for icon path in element data is file_icon (byte[128]) var field = t.GetField("file_icon", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (field != null && typeof(byte[]).IsAssignableFrom(field.FieldType)) { try { var bytes = field.GetValue(data) as byte[]; string s = ByteToStringUtils.ByteArrayToCP936String(bytes); if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUTF8String(bytes); if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUnicodeString(bytes); return s ?? string.Empty; } catch { } } var prop = t.GetProperty("file_icon", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (prop != null) { try { if (prop.PropertyType == typeof(string)) { var s = prop.GetValue(data, null) as string; return s ?? string.Empty; } if (prop.PropertyType == typeof(byte[])) { var bytes = prop.GetValue(data, null) as byte[]; string s = ByteToStringUtils.ByteArrayToCP936String(bytes); if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUTF8String(bytes); if (string.IsNullOrEmpty(s)) s = ByteToStringUtils.ByteArrayToUnicodeString(bytes); return s ?? string.Empty; } } catch { } } return string.Empty; } private string NormalizeIconResourceKeyFromPath(string iconPath) { if (string.IsNullOrEmpty(iconPath)) return string.Empty; try { string p = iconPath.Replace('\\', '/'); // Some data might contain leading directories; take file name string fileName = Path.GetFileName(p); if (string.IsNullOrEmpty(fileName)) fileName = p; // Remove extension (.dds/.tga/.png) string name = Path.GetFileNameWithoutExtension(fileName); if (string.IsNullOrEmpty(name)) name = fileName; // Many PW icons are numeric (e.g., 8800). Use as-is return name.Trim(); } catch { } return string.Empty; } private string CacheAndReturn(int tid, string name) { _tidNameCache[tid] = name ?? ""; return name ?? ""; } private string ExtractNameFromElement(object data) { if (data == null) return ""; var t = data.GetType(); // Debug: Log all available fields and properties // Debug.Log($"[Inventory] Data type: {t.Name}"); var fields = t.GetFields(BindingFlags.Public | BindingFlags.Instance); // foreach (var f in fields) // { // Debug.Log($"[Inventory] Field: {f.Name} ({f.FieldType.Name})"); // } var props = t.GetProperties(BindingFlags.Public | BindingFlags.Instance); // foreach (var p in props) // { // Debug.Log($"[Inventory] Property: {p.Name} ({p.PropertyType.Name})"); // } var methods = t.GetMethods(BindingFlags.Public | BindingFlags.Instance); // foreach (var m in methods) // { // if (m.Name.ToLower().Contains("name") || m.Name.ToLower().Contains("getname")) // { // Debug.Log($"[Inventory] Method: {m.Name} ({m.ReturnType.Name})"); // } // } // Prefer decoding the raw fields first to control encoding (Unicode for Vietnamese), // then fall back to any string properties if needed. var fieldName = t.GetField("name", BindingFlags.Public | BindingFlags.Instance); if (fieldName != null && fieldName.FieldType == typeof(ushort[])) { var arr = fieldName.GetValue(data) as ushort[]; // Debug: Log the raw ushort array data if (arr != null && arr.Length > 0) { var rawData = string.Join(",", arr.Take(Math.Min(10, arr.Length))); } // Vietnamese names are stored as wide chars; decode as Unicode first. var s = ByteToStringUtils.UshortArrayToUnicodeString(arr); // Debug log to see what we're getting if (!string.IsNullOrEmpty(s) && !string.IsNullOrWhiteSpace(s)) return s; // Fallback to legacy CP936 if Unicode was empty s = ByteToStringUtils.UshortArrayToCP936String(arr); if (!string.IsNullOrEmpty(s)) return s; } // Try calling GetName method if it exists (similar to C++ pIt->GetName()) var getNameMethod = t.GetMethod("GetName", BindingFlags.Public | BindingFlags.Instance); if (getNameMethod != null && getNameMethod.ReturnType == typeof(string)) { try { var val = getNameMethod.Invoke(data, null) as string; if (!string.IsNullOrEmpty(val) && !string.IsNullOrWhiteSpace(val)) return val; } catch (Exception ex) { Debug.LogWarning($"[Inventory] Error calling GetName method: {ex.Message}"); } } return ""; } private string TryFindNameByScanningArrays(object edm, uint id) { try { var fields = edm.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance); foreach (var f in fields) { if (!f.FieldType.IsArray) continue; var arr = f.GetValue(edm) as Array; if (arr == null || arr.Length == 0) continue; var elemType = f.FieldType.GetElementType(); var idField = elemType.GetField("id", BindingFlags.Public | BindingFlags.Instance); if (idField == null || idField.FieldType != typeof(uint)) continue; for (int i = 0; i < arr.Length; i++) { var element = arr.GetValue(i); if (element == null) continue; var value = (uint)idField.GetValue(element); if (value == id) { var name = ExtractNameFromElement(element); if (!string.IsNullOrEmpty(name)) return name; var toStr = element.ToString(); if (!string.IsNullOrEmpty(toStr)) return toStr; } } } } catch { } return ""; } public bool TryParseInventoryDetail(byte[] buffer, out byte byPackage, out byte ivtrSize, out List items) { byPackage = 0; ivtrSize = 0; items = null; if (buffer == null || buffer.Length < 6) { return false; } int index = 0; byPackage = buffer[index++]; 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) { items = new List(); return true; } byte[] content = new byte[contentBytes]; Buffer.BlockCopy(buffer, index, content, 0, contentBytes); // Parse S2C::cmd_own_ivtr_detail_info.content int ci = 0; if (contentBytes < 4) { return false; } int numItems = BitConverter.ToInt32(content, ci); ci += 4; items = new List(numItems > 0 ? numItems : 0); for (int i = 0; i < numItems; i++) { // Ensure enough bytes for fixed part: index, tid, expire, state, amount, crc(2), len(2) => 4*5 + 2 + 2 = 24 bytes if (ci + 24 > contentBytes) { return false; } int slotIndex = BitConverter.ToInt32(content, ci); ci += 4; if (slotIndex < 0) { // Skip invalid slot but continue parsing to keep stream in sync // Still need to consume fields even if slot is negative int skipTid = BitConverter.ToInt32(content, ci); ci += 4; int skipExpire = BitConverter.ToInt32(content, ci); ci += 4; int skipState = BitConverter.ToInt32(content, ci); ci += 4; int skipAmt = BitConverter.ToInt32(content, ci); ci += 4; ushort skipCrc = BitConverter.ToUInt16(content, ci); ci += 2; ushort skipLen = BitConverter.ToUInt16(content, ci); ci += 2; if (skipLen > 0) { if (ci + skipLen > contentBytes) return false; ci += skipLen; } continue; } int tid = BitConverter.ToInt32(content, ci); ci += 4; int expireDate = BitConverter.ToInt32(content, ci); ci += 4; int state = BitConverter.ToInt32(content, ci); ci += 4; int amount = BitConverter.ToInt32(content, ci); ci += 4; ushort crc = BitConverter.ToUInt16(content, ci); ci += 2; ushort extraLen = BitConverter.ToUInt16(content, ci); ci += 2; byte[] extra = null; if (extraLen > 0) { if (ci + extraLen > contentBytes) { return false; } extra = new byte[extraLen]; Buffer.BlockCopy(content, ci, extra, 0, extraLen); ci += extraLen; } var item = EC_IvtrItem.CreateItem(tid, expireDate, amount); item.Package = byPackage; item.Slot = slotIndex; item.State = state; item.Crc = crc; item.Content = extra; items.Add(item); } return true; } public 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 int GetPileLimit(int templateId) { if (templateId <= 0) return 1; if (_pileLimitCache.TryGetValue(templateId, out var cached)) return cached; int limit = 1; try { var edm = ElementDataManProvider.GetElementDataMan(); if (edm != null) { uint id = unchecked((uint)templateId); DATA_TYPE dt = DATA_TYPE.DT_INVALID; object data = edm.get_data_ptr(id, ID_SPACE.ID_SPACE_ESSENCE, ref dt); limit = ExtractPileLimitFromElement(data); } } catch { } if (limit <= 0) limit = 1; _pileLimitCache[templateId] = limit; return limit; } private int ExtractPileLimitFromElement(object data) { if (data == null) return 1; var t = data.GetType(); // Common field/property names across item essences string[] names = new[] { "pile_num_max", "pilelimit", "pile_limit", "pileLimit", "stack", "stack_max", "stackMax", "max_stack", "maxStack" }; foreach (var name in names) { var f = t.GetField(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (f != null && (f.FieldType == typeof(int) || f.FieldType == typeof(uint) || f.FieldType == typeof(short) || f.FieldType == typeof(ushort) || f.FieldType == typeof(byte))) { try { var val = f.GetValue(data); int limit = Convert.ToInt32(val); if (limit > 0) return limit; } catch { } } var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if (p != null && (p.PropertyType == typeof(int) || p.PropertyType == typeof(uint) || p.PropertyType == typeof(short) || p.PropertyType == typeof(ushort) || p.PropertyType == typeof(byte))) { try { var val = p.GetValue(data, null); int limit = Convert.ToInt32(val); if (limit > 0) return limit; } catch { } } } 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. /// /// TODO: have to rename to CECIvtrItem to match C++ naming 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 public SKILLMATTER_ESSENCE m_pDBEssence; private ushort m_dwData; private string m_strDataName; #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) { EC_IvtrItem pItem; DATA_TYPE DataType = DATA_TYPE.DT_INVALID; object data = ElementDataManProvider.GetElementDataMan().get_data_ptr((uint)tid, ID_SPACE.ID_SPACE_ESSENCE, ref DataType); //Debug.Log("Create item data: DataType: " + DataType); switch (DataType) { case DATA_TYPE.DT_WEAPON_ESSENCE: pItem = new CECIvtrWeapon(tid, expire_date); break; case DATA_TYPE.DT_PROJECTILE_ESSENCE: pItem = new CECIvtrArrow(tid, expire_date); break; case DATA_TYPE.DT_ARMOR_ESSENCE: pItem = new EC_IvtrArmor(tid, expire_date); break; case DATA_TYPE.DT_DECORATION_ESSENCE: pItem = new EC_IvtrDecoration(tid, expire_date); break; case DATA_TYPE.DT_FASHION_ESSENCE: pItem = new EC_IvtrFashion(tid, expire_date); break; case DATA_TYPE.DT_MEDICINE_ESSENCE: pItem = new EC_IvtrMedicine(tid, expire_date); break; case DATA_TYPE.DT_MATERIAL_ESSENCE: pItem = new EC_IvtrMaterial(tid, expire_date); break; case DATA_TYPE.DT_DAMAGERUNE_ESSENCE: pItem = new EC_IvtrDamagerune(tid, expire_date); break; case DATA_TYPE.DT_ARMORRUNE_ESSENCE: pItem = new EC_IvtrArmorrune(tid, expire_date); break; case DATA_TYPE.DT_SKILLTOME_ESSENCE: pItem = new EC_IvtrSkilltome(tid, expire_date); break; case DATA_TYPE.DT_FLYSWORD_ESSENCE: pItem = new EC_IvtrFlysword(tid, expire_date); break; case DATA_TYPE.DT_TOWNSCROLL_ESSENCE: pItem = new EC_IvtrTownScroll(tid, expire_date); break; case DATA_TYPE.DT_UNIONSCROLL_ESSENCE: pItem = new EC_IvtrUnionscroll(tid, expire_date); break; case DATA_TYPE.DT_REVIVESCROLL_ESSENCE: pItem = new EC_IvtrRevScroll(tid, expire_date); break; case DATA_TYPE.DT_ELEMENT_ESSENCE: pItem = new EC_IvtrElement(tid, expire_date); break; case DATA_TYPE.DT_TOSSMATTER_ESSENCE: pItem = new EC_IvtrTossMat(tid, expire_date); break; case DATA_TYPE.DT_TASKMATTER_ESSENCE: pItem = new EC_IvtrTaskItem(tid, expire_date); break; case DATA_TYPE.DT_STONE_ESSENCE: pItem = new EC_IvtrStone(tid, expire_date); break; case DATA_TYPE.DT_WINGMANWING_ESSENCE: pItem = new EC_IvtrWing(tid, expire_date); break; case DATA_TYPE.DT_TASKDICE_ESSENCE: pItem = new EC_IvtrTaskDice(tid, expire_date); break; case DATA_TYPE.DT_TASKNORMALMATTER_ESSENCE: pItem = new EC_IvtrTaskNmMatter(tid, expire_date); break; case DATA_TYPE.DT_FACETICKET_ESSENCE: pItem = new EC_IvtrFaceTicket(tid, expire_date); break; case DATA_TYPE.DT_FACEPILL_ESSENCE: pItem = new EC_IvtrFacePill(tid, expire_date); break; case DATA_TYPE.DT_GM_GENERATOR_ESSENCE: pItem = new EC_IvtrGmGenerator(tid, expire_date); break; case DATA_TYPE.DT_RECIPE_ESSENCE: pItem = new EC_IvtrRecipe(tid, expire_date); break; case DATA_TYPE.DT_PET_EGG_ESSENCE: pItem = new EC_IvtrPetEgg(tid, expire_date); break; case DATA_TYPE.DT_PET_FOOD_ESSENCE: pItem = new EC_IvtrPetFood(tid, expire_date); break; case DATA_TYPE.DT_PET_FACETICKET_ESSENCE: pItem = new EC_IvtrPetFaceTicket(tid, expire_date); break; case DATA_TYPE.DT_FIREWORKS_ESSENCE: pItem = new EC_IvtrFirework(tid, expire_date); break; case DATA_TYPE.DT_WAR_TANKCALLIN_ESSENCE: pItem = new EC_IvtrWarTankCallin(tid, expire_date); break; case DATA_TYPE.DT_SKILLMATTER_ESSENCE: pItem = new EC_IvtrSkillMat(tid, expire_date); break; case DATA_TYPE.DT_INC_SKILL_ABILITY_ESSENCE: pItem = new EC_IvtrIncSkillAbility(tid, expire_date); break; case DATA_TYPE.DT_REFINE_TICKET_ESSENCE: pItem = new EC_IvtrRefineTicket(tid, expire_date); break; case DATA_TYPE.DT_DESTROYING_ESSENCE: pItem = new EC_IvtrDestroyingEssence(tid, expire_date); break; case DATA_TYPE.DT_BIBLE_ESSENCE: pItem = new EC_IvtrBible(tid, expire_date); break; case DATA_TYPE.DT_SPEAKER_ESSENCE: pItem = new EC_IvtrSpeaker(tid, expire_date); break; case DATA_TYPE.DT_AUTOHP_ESSENCE: pItem = new EC_IvtrAutoHp(tid, expire_date); break; case DATA_TYPE.DT_AUTOMP_ESSENCE: pItem = new EC_IvtrAutoMp(tid, expire_date); break; case DATA_TYPE.DT_DOUBLE_EXP_ESSENCE: pItem = new EC_IvtrDoubleExp(tid, expire_date); break; case DATA_TYPE.DT_DYE_TICKET_ESSENCE: pItem = new EC_IvtrDyeTicket(tid, expire_date); break; case DATA_TYPE.DT_TRANSMITSCROLL_ESSENCE: pItem = new EC_IvtrTransmitScroll(tid, expire_date); break; case DATA_TYPE.DT_GOBLIN_ESSENCE: pItem = new EC_IvtrGoblin(tid, expire_date); break; case DATA_TYPE.DT_GOBLIN_EQUIP_ESSENCE: pItem = new EC_IvtrGoblinEquip(tid, expire_date); break; case DATA_TYPE.DT_GOBLIN_EXPPILL_ESSENCE: pItem = new EC_IvtrGoblinExpPill(tid, expire_date); break; case DATA_TYPE.DT_SELL_CERTIFICATE_ESSENCE: pItem = new EC_IvtrCertificate(tid, expire_date); break; case DATA_TYPE.DT_TARGET_ITEM_ESSENCE: pItem = new EC_IvtrTargetItem(tid, expire_date); break; case DATA_TYPE.DT_LOOK_INFO_ESSENCE: pItem = new EC_IvtrLookInfoItem(tid, expire_date); break; case DATA_TYPE.DT_WEDDING_BOOKCARD_ESSENCE: pItem = new EC_IvtrWeddingBookCard(tid, expire_date); break; case DATA_TYPE.DT_WEDDING_INVITECARD_ESSENCE: pItem = new EC_IvtrWeddingInviteCard(tid, expire_date); break; case DATA_TYPE.DT_SHARPENER_ESSENCE: pItem = new EC_IvtrSharpener(tid, expire_date); break; case DATA_TYPE.DT_FACTION_MATERIAL_ESSENCE: pItem = new EC_IvtrFactionMaterial(tid, expire_date); break; case DATA_TYPE.DT_CONGREGATE_ESSENCE: pItem = new EC_IvtrCongregate(tid, expire_date); break; case DATA_TYPE.DT_FORCE_TOKEN_ESSENCE: pItem = new EC_IvtrForceToken(tid, expire_date); break; case DATA_TYPE.DT_DYNSKILLEQUIP_ESSENCE: pItem = new EC_IvtrDynSkillEquip(tid, expire_date); break; case DATA_TYPE.DT_MONEY_CONVERTIBLE_ESSENCE: pItem = new EC_IvtrMoneyConvertible(tid, expire_date); break; case DATA_TYPE.DT_MONSTER_SPIRIT_ESSENCE: pItem = new EC_IvtrMonsterSpirit(tid, expire_date); break; case DATA_TYPE.DT_POKER_ESSENCE: pItem = new EC_IvtrGeneralCard(tid, expire_date); break; case DATA_TYPE.DT_POKER_DICE_ESSENCE: pItem = new EC_IvtrGeneralCardDice(tid, expire_date); break; case DATA_TYPE.DT_SHOP_TOKEN_ESSENCE: pItem = new EC_IvtrShopToken(tid, expire_date); break; case DATA_TYPE.DT_UNIVERSAL_TOKEN_ESSENCE: pItem = new EC_IvtrUniversalToken(tid, expire_date); break; default: pItem = new EC_IvtrUnknown(tid, expire_date); break; } 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 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 = (int)((iPrice + 99) / 100) * 100; else if (iPrice >= 100) iPrice = (int)((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) { if (pInfoData != null && iDataLen > 0) { this.Content = new byte[iDataLen]; Buffer.BlockCopy(pInfoData, 0, this.Content, 0, iDataLen); } else { this.Content = null; } 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 CreateItem(m_tid, m_expire_date, m_iCount, m_iCID); } /// Get item cool time in milliseconds (0 by default). public virtual int GetCoolTime(out int piMax) { piMax = -1; 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, 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); } } /// /// 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 count (C++ a_Clamp to m_iPileLimit). 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 virtual void GetDetailDataFromLocal() { //itemdataman* pItemDataMan = g_pGame->GetItemDataMan(); object pData_temp = itemdataman.get_item_for_sell((uint)m_tid); if (pData_temp == null) { SetItemInfo(null, 0); SetLocalProps(); m_bLocalDetailData = true; return; } item_data pData = (item_data)pData_temp; SetItemInfo(pData.item_content, pData.content_length); SetLocalProps(); 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) { // Build a simple but centralized description: // - Name // - String-table description (if any) // - Extended description (if any) m_strDesc = string.Empty; // Item name line CECStringTab pDescTab = EC_Game.GetItemDesc(); AddDescText((int)DescriptipionMsg.ITEMDESC_COL_WHITE, false, pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_ERRORITEM)); AddDescText((int)DescriptipionMsg.ITEMDESC_COL_WHITE, false, "({0})", m_tid); m_strDesc += "\\r"; AddDescText((int)DescriptipionMsg.ITEMDESC_COL_RED, false, "This Item Type is not implemented yet. Type of {0}", this.GetType().Name); TrimLastReturn(); return m_strDesc; } protected virtual string GetBoothBuyDesc() { return GetNormalDesc(false); } protected virtual string GetRewardDesc() { return GetNormalDesc(false); } protected virtual void AddPriceDesc(int col, bool bRepair) { // Basic price string using scaled price; color is ignored at this level. if ((!IsEquipment() && bRepair) || m_iPrice == 0 || m_fPriceScale == 0.0f) { TrimLastReturn(); return; } // use specific color for the item price if ((int)DescriptipionMsg.ITEMDESC_COL_WHITE == col) { if (m_iPrice >= 100000000) // 100 million col = (int)DescriptipionMsg.ITEMDESC_COL_GREEN; else if (m_iPrice >= 10000000) // 10 million col = (int)DescriptipionMsg.ITEMDESC_COL_DARKGOLD; else if (m_iPrice >= 1000000) // 1 million col = (int)DescriptipionMsg.ITEMDESC_COL_YELLOW; } CECStringTab pDescTab = EC_Game.GetItemDesc(); if (m_iScaleType == (int)ScaleType.SCALE_OFFLINESHOP) { AddDescText(col, false, pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_PRICE)); string s1, s2; BuildPriceNumberStr(m_iPrice, out s1); if (GetCount() > 1) { s2 = (m_iPrice * (long)GetCount()).ToString(); AddDescText(-1, false, " %s (%s)", s1, s2); } else AddDescText(-1, false, " %s", s1); } else if (m_iScaleType == (int)ScaleType.SCALE_BOOTH || m_tid == 21652) // 21651: yinpiao { string s1; BuildPriceNumberStr(m_iPrice, out s1); AddDescText(col, false, pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_UNITPRICE)); AddDescText(-1, false, " %s", s1); } else if (m_iScaleType == (int)ScaleType.SCALE_SELL && m_iCount > 1 && m_tid != 21652) { string s1, s2; BuildPriceNumberStr(m_iPrice, out s1); BuildPriceNumberStr(GetScaledPrice(), out s2); AddDescText(col, false, pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_PRICE)); AddDescText(-1, false, " %s (%s)", s1, s2); } else { string s1; BuildPriceNumberStr(GetScaledPrice(), out s1); AddDescText(col, false, pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_PRICE)); AddDescText(-1, false, " %s", s1); } } protected virtual void AddProfReqDesc(uint iProfReq) { if (EC_ProfConfigs.ContainsAllProfession(iProfReq)) { return;// All profession permit equirement } CECStringTab pDescTab = EC_Game.GetItemDesc(); CECGameRun pGameRun = EC_Game.GetGameRun(); CECHostPlayer pHost = pGameRun.GetHostPlayer(); int col = (iProfReq & (1 << pHost.GetProfession())) != 0 ? (int)DescriptipionMsg.ITEMDESC_COL_WHITE : (int)DescriptipionMsg.ITEMDESC_COL_RED; AddDescText(col, false, pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_PROFESSIONREQ)); for (int i = 0; i < (int)Profession.NUM_PROFESSION; i++) { if ((iProfReq & (1 << i)) != 0) { m_strDesc += " "; string profName = pGameRun.GetProfName(i); // Remove newline and carriage return characters that cause UI display issues if (!string.IsNullOrEmpty(profName)) { profName = profName.Replace("\r", "").Replace("\n", "").Trim(); } AddDescText(col, false, profName); } } AddDescText(col, true, " "); } protected virtual int DecideNameCol() { return -1; } public virtual void SetLocalProps() { } protected virtual void AddDescText(int iCol, bool bRet, string szText, params object[] args) { // Add color prefix if color is specified if (iCol >= 0) { string colorStr = GetColorString((DescriptipionMsg)iCol); m_strDesc += colorStr; } string line; if (args != null && args.Length > 0) { // Accept both C#-style ("{0}") and printf-style ("%d", "%+d", "%.2f", "%%") formats if (szText.IndexOf('%') >= 0 && szText.IndexOf('{') < 0) { line = FormatPrintfLike(szText, args); } else { line = string.Format(szText, args); } } else { line = szText; } m_strDesc += line; if (bRet) m_strDesc += "\n"; } /// /// Get color string for color ID /// Returns color codes in ^RRGGBB format (6 hex digits) for text formatting /// protected virtual string GetColorString(DescriptipionMsg colorId) { switch (colorId) { case DescriptipionMsg.ITEMDESC_COL_WHITE: return "^FFFFFF"; // White case DescriptipionMsg.ITEMDESC_COL_GREEN: return "^00FF00"; // Green case DescriptipionMsg.ITEMDESC_COL_YELLOW: return "^FFFF00"; // Yellow case DescriptipionMsg.ITEMDESC_COL_DARKGOLD: return "^FF8C00"; // Dark Gold / Orange case DescriptipionMsg.ITEMDESC_COL_LIGHTBLUE: return "^5998FF"; // Light Blue case DescriptipionMsg.ITEMDESC_COL_CYANINE: return "^00FFFF"; // Cyan case DescriptipionMsg.ITEMDESC_COL_RED: return "^FF0000"; // Red case DescriptipionMsg.ITEMDESC_COL_GRAY: return "^808080"; // Gray default: return "^FFFFFF"; // Default to white } } /// /// Format string using printf-style format specifiers (%d, %+d, %.2f, %s, etc.) /// Converts printf-style formats to C# string formatting /// private static string FormatPrintfLike(string fmt, object[] args) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); int argIndex = 0; for (int i = 0; i < fmt.Length; i++) { char c = fmt[i]; if (c != '%') { sb.Append(c); continue; } if (i + 1 < fmt.Length && fmt[i + 1] == '%') { sb.Append('%'); i += 1; continue; } bool plus = false; int precision = -1; int j = i + 1; if (j < fmt.Length && fmt[j] == '+') { plus = true; j++; } if (j < fmt.Length && fmt[j] == '.') { // precision like %.2f j++; int start = j; while (j < fmt.Length && char.IsDigit(fmt[j])) j++; int.TryParse(fmt.Substring(start, j - start), out precision); } if (j < fmt.Length) { char spec = fmt[j]; if (spec == 'd') { object val = argIndex < args.Length ? args[argIndex++] : 0; long iv = Convert.ToInt64(val); string s = iv.ToString(); if (plus && iv >= 0) s = "+" + s; sb.Append(s); i = j; continue; } else if (spec == 'f') { object val = argIndex < args.Length ? args[argIndex++] : 0.0; double dv = Convert.ToDouble(val); string s = precision >= 0 ? dv.ToString("F" + precision) : dv.ToString("F"); if (plus && dv >= 0) s = "+" + s; sb.Append(s); i = j; continue; } else if (spec == 's') { object val = argIndex < args.Length ? args[argIndex++] : ""; string s = val != null ? val.ToString() : ""; sb.Append(s); i = j; continue; } } // Fallback: treat '%' as literal if format not recognized sb.Append('%'); } return sb.ToString(); } // Add extend description to description string / 添加扩展描述到描述字符串 protected void AddExtDescText() { // Get extended description from item_ext_desc.txt using tid / 使用tid从item_ext_desc.txt获取扩展描述 string szExtDesc = TryGetItemExtDesc(); // Note: Original C++ had early return commented out / 注意:原始C++代码的早期返回被注释掉了 // if (!szExtDesc || !szExtDesc[0]) // return; m_strDesc += "\\r"; bool bAddLine = true; // Add special properties description / 添加特殊属性描述 var pDescTab = EC_Game.GetItemDesc(); // Note: ITEMDESC_COL2_BRIGHTBLUE constant - adjust based on actual string table / 注意:ITEMDESC_COL2_BRIGHTBLUE常量 - 根据实际字符串表调整 int green = (int)DescriptipionMsg.ITEMDESC_COL2_BRIGHTBLUE; // ITEMDESC_COL2_BRIGHTBLUE placeholder - adjust this value if (m_iCID != (int)InventoryClassId.ICID_GOBLIN) // goblin does not need to display these special properties / 地精不需要显示这些特殊属性 { // Exact C++ logic: (PROC_NO_USER_TRASH) || (!PROC_BINDING && (PROC_DROPWHENDIE || ...)) // 精确的C++逻辑:(PROC_NO_USER_TRASH) || (!PROC_BINDING && (PROC_DROPWHENDIE || ...)) if ((m_iProcType & (int)ProcType.PROC_NO_USER_TRASH) != 0 || (!((m_iProcType & (int)ProcType.PROC_BINDING) != 0) && ((m_iProcType & (int)ProcType.PROC_DROPWHENDIE) != 0 || (m_iProcType & (int)ProcType.PROC_DROPPABLE) != 0 || (m_iProcType & (int)ProcType.PROC_SELLABLE) != 0 || (m_iProcType & (int)ProcType.PROC_TRADEABLE) != 0 || (m_iProcType & (int)ProcType.PROC_DISAPEAR) != 0 || (m_iProcType & (int)ProcType.PROC_USE) != 0 || (m_iProcType & (int)ProcType.PROC_DEADDROP) != 0 || (m_iProcType & (int)ProcType.PROC_OFFLINE) != 0 || (m_iProcType & (int)ProcType.PROC_UNREPAIRABLE) != 0))) { bAddLine = false; if (pDescTab != null && pDescTab.IsInitialized()) { string szCol = pDescTab.GetWideString(green); if (!string.IsNullOrEmpty(szCol)) { m_strDesc += szCol; } } if ((m_iProcType & (int)ProcType.PROC_DROPWHENDIE) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_DEAD_PROTECT); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_DROPPABLE) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_NO_DROP); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_SELLABLE) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_NO_TRADE); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_TRADEABLE) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_NO_PLAYER_TRADE); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_DISAPEAR) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_LEAVE_SCENE_DISAPEAR); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_USE) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_USE_AFTER_PICK_UP); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_DEADDROP) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_DROP_WHEN_DEAD); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_OFFLINE) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_DROP_WHEN_OFFLINE); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_UNREPAIRABLE) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_UNREPAIRABLE); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } if ((m_iProcType & (int)ProcType.PROC_NO_USER_TRASH) != 0) { m_strDesc += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { string desc = pDescTab.GetWideString((int)DescriptipionMsg.ITEMDESC_NO_USER_TRASH); if (!string.IsNullOrEmpty(desc)) m_strDesc += desc; } } } else { TrimLastReturn(); } } else { TrimLastReturn(); } if (string.IsNullOrEmpty(szExtDesc)) return; // Extend description is below special properties / 扩展描述在特殊属性下方 if (bAddLine) m_strDesc += "\\r\\r"; else m_strDesc += "\\r"; m_strDesc += szExtDesc; } protected void AddExpireTimeDesc() { // Placeholder: expiry description can be added here when time system is fully wired. } protected void AddExpireTimeDesc(int expire_date) { // Overwrite, call standard one; we don't change m_expire_date in this simplified port. AddExpireTimeDesc(); } protected void AddIDDescText() { // Optional: show internal id for debugging #if UNITY_EDITOR AddDescText(0, true, "ID: {0}", m_tid); #endif } protected void AddBindDescText() { // Simple binding flags display based on ProcType if ((m_iProcType & (int)ProcType.PROC_BINDING) != 0) { AddDescText(0, true, "[Bound]"); } else if ((m_iProcType & (int)ProcType.PROC_BIND) != 0) { AddDescText(0, true, "[Bind on Use]"); } } protected void AddActionTypeDescText(int action_type) { // Action/weapon type formatting can be added later as needed. } protected void TrimLastReturn() { if (string.IsNullOrEmpty(m_strDesc)) return; // Remove trailing "\r" or "\n" for consistency with C++ style if (m_strDesc.EndsWith("\\r", StringComparison.Ordinal)) m_strDesc = m_strDesc.Substring(0, m_strDesc.Length - 2); else 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(); } public virtual int GetColorStrID(int templateId) { // Placeholder: color index lookup; return -1 (white) by default. 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); } /// /// Try to get the main description line for this item from item_desc.txt via EC_Game. /// This mirrors the behaviour used by Inventory UI and NPC shop, but centralised here. /// private string TryGetItemMainDesc() { try { if (EC_Game.TryGetItemMsg(m_tid, out int messageId, out int displayMode)) { var tab = EC_Game.GetItemDesc(); if (tab != null && tab.IsInitialized()) { var s = tab.GetWideString(messageId); if (!string.IsNullOrEmpty(s)) return s; } } // Fallback: direct template id lookup { var tab = EC_Game.GetItemDesc(); if (tab != null && tab.IsInitialized()) { var s = tab.GetWideString(m_tid); if (!string.IsNullOrEmpty(s)) return s; } } } catch (Exception ex) { Debug.LogWarning($"[EC_IvtrItem] Error getting main description for tid={m_tid}: {ex.Message}"); } return string.Empty; } /// /// Try to get the extended description line from item_ext_desc.txt. /// private string TryGetItemExtDesc() { try { if (EC_Game.TryGetItemMsg(m_tid, out int messageId, out int displayMode)) { var tab = EC_Game.GetItemExtDesc(); if (tab != null && tab.IsInitialized()) { var s = tab.GetWideString(messageId); if (!string.IsNullOrEmpty(s)) return s; } } // Fallback: direct id { var tab = EC_Game.GetItemExtDesc(); if (tab != null && tab.IsInitialized()) { var s = tab.GetWideString(m_tid); if (!string.IsNullOrEmpty(s)) return s; } } } catch (Exception ex) { Debug.LogWarning($"[EC_IvtrItem] Error getting extended description for tid={m_tid}: {ex.Message}"); } return string.Empty; } /// /// Get extended description text for UI display. /// This method mirrors AddExtDescText() logic but returns a string instead of modifying m_strDesc. /// 此方法镜像AddExtDescText()逻辑,但返回字符串而不是修改m_strDesc /// public string GetExtendedDescText() { string result = string.Empty; // Get extended description from item_ext_desc.txt using tid / 使用tid从item_ext_desc.txt获取扩展描述 string szExtDesc = TryGetItemExtDesc(); // Note: Original C++ had early return commented out / 注意:原始C++代码的早期返回被注释掉了 // if (!szExtDesc || !szExtDesc[0]) // return; result += "\\r"; bool bAddLine = true; // Add special properties description / 添加特殊属性描述 var pDescTab = EC_Game.GetItemDesc(); // Note: ITEMDESC_COL2_BRIGHTBLUE constant - adjust based on actual string table / 注意:ITEMDESC_COL2_BRIGHTBLUE常量 - 根据实际字符串表调整 int green = 1000; // ITEMDESC_COL2_BRIGHTBLUE placeholder - adjust this value if (m_iCID != (int)InventoryClassId.ICID_GOBLIN) // goblin does not need to display these special properties / 地精不需要显示这些特殊属性 { // Exact C++ logic: (PROC_NO_USER_TRASH) || (!PROC_BINDING && (PROC_DROPWHENDIE || ...)) // 精确的C++逻辑:(PROC_NO_USER_TRASH) || (!PROC_BINDING && (PROC_DROPWHENDIE || ...)) if ((m_iProcType & (int)ProcType.PROC_NO_USER_TRASH) != 0 || (!((m_iProcType & (int)ProcType.PROC_BINDING) != 0) && ((m_iProcType & (int)ProcType.PROC_DROPWHENDIE) != 0 || (m_iProcType & (int)ProcType.PROC_DROPPABLE) != 0 || (m_iProcType & (int)ProcType.PROC_SELLABLE) != 0 || (m_iProcType & (int)ProcType.PROC_TRADEABLE) != 0 || (m_iProcType & (int)ProcType.PROC_DISAPEAR) != 0 || (m_iProcType & (int)ProcType.PROC_USE) != 0 || (m_iProcType & (int)ProcType.PROC_DEADDROP) != 0 || (m_iProcType & (int)ProcType.PROC_OFFLINE) != 0 || (m_iProcType & (int)ProcType.PROC_UNREPAIRABLE) != 0))) { bAddLine = false; if (pDescTab != null && pDescTab.IsInitialized()) { string szCol = pDescTab.GetWideString(green); if (!string.IsNullOrEmpty(szCol)) { result += szCol; } } // Note: These message IDs are placeholders - adjust based on actual string table / 注意:这些消息ID是占位符 - 根据实际字符串表调整 if ((m_iProcType & (int)ProcType.PROC_DROPWHENDIE) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_DEAD_PROTECT placeholder - adjust this value string desc = pDescTab.GetWideString(2000); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_DROPPABLE) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_NO_DROP placeholder - adjust this value string desc = pDescTab.GetWideString(2001); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_SELLABLE) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_NO_TRADE placeholder - adjust this value string desc = pDescTab.GetWideString(2002); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_TRADEABLE) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_NO_PLAYER_TRADE placeholder - adjust this value string desc = pDescTab.GetWideString(2003); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_DISAPEAR) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_LEAVE_SCENE_DISAPEAR placeholder - adjust this value string desc = pDescTab.GetWideString(2004); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_USE) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_USE_AFTER_PICK_UP placeholder - adjust this value string desc = pDescTab.GetWideString(2005); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_DEADDROP) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_DROP_WHEN_DEAD placeholder - adjust this value string desc = pDescTab.GetWideString(2006); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_OFFLINE) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_DROP_WHEN_OFFLINE placeholder - adjust this value string desc = pDescTab.GetWideString(2007); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_UNREPAIRABLE) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_UNREPAIRABLE placeholder - adjust this value string desc = pDescTab.GetWideString(2008); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } if ((m_iProcType & (int)ProcType.PROC_NO_USER_TRASH) != 0) { result += "\\r"; if (pDescTab != null && pDescTab.IsInitialized()) { // ITEMDESC_NO_USER_TRASH placeholder - adjust this value string desc = pDescTab.GetWideString(2009); // Placeholder ID if (!string.IsNullOrEmpty(desc)) result += desc; } } } } if (string.IsNullOrEmpty(szExtDesc)) return result; // Extend description is below special properties / 扩展描述在特殊属性下方 if (bAddLine) result += "\\r\\r"; else result += "\\r"; result += szExtDesc; return result; } #endregion // public ushort GetData(string strName = "") // { // // if (0 != m_dwData && strName != m_strDataName) // // AUI_ReportError(__LINE__, 1, "AUIObject::GetData(), data name not match"); // return m_dwData; // } // public void SetData(ushort dwData, string strName) // { // m_strDataName = strName; // m_dwData = dwData; // } } /// /// 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; } } }