using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.IO; using BrewMonster; using ModelRenderer.Scripts.Common; using ModelRenderer.Scripts.GameData; using UnityEngine; namespace PerfectWorld.Scripts.Managers { public class InventoryItemData { public byte Package; public int Slot; public int TemplateId; public int ExpireDate; public int State; public int Count; public ushort Crc; public byte[] Content; // variable-length item-specific payload (can be null) } public static class EC_IvtrItem { private static readonly Dictionary _tidNameCache = new Dictionary(); private static readonly Dictionary _pileLimitCache = new Dictionary(); private static readonly Dictionary _tidIconKeyCache = new Dictionary(); private static readonly Dictionary _iconSpriteCache = new Dictionary(StringComparer.OrdinalIgnoreCase); private static Sprite[] _multiSpriteAtlas = null; private static readonly Dictionary _spriteNameToIndexCache = new Dictionary(StringComparer.OrdinalIgnoreCase); private const int MaxContentHexToLog = 64; public static 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 static 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 static 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 static void LoadMultiSpriteAtlas() { try { // Load the multi-sprite atlas from Resources var atlasSprites = 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; } } Debug.Log($"[Inventory] Loaded multi-sprite atlas with {atlasSprites.Length} sprites"); } else { Debug.LogWarning("[Inventory] Failed to load multi-sprite atlas: iconlist_ivtrm_multisprite"); _multiSpriteAtlas = new Sprite[0]; // Prevent repeated loading attempts } } catch (Exception ex) { Debug.LogError($"[Inventory] Error loading multi-sprite atlas: {ex.Message}"); _multiSpriteAtlas = new Sprite[0]; // Prevent repeated loading attempts } } private static 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 static 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 static 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 static string CacheAndReturn(int tid, string name) { _tidNameCache[tid] = name ?? ""; return name ?? ""; } private static 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))); Debug.Log($"[Inventory] Raw ushort array data (first 10): [{rawData}]"); } // Vietnamese names are stored as wide chars; decode as Unicode first. var s = ByteToStringUtils.UshortArrayToUnicodeString(arr); // Debug log to see what we're getting Debug.Log($"[Inventory] Unicode decode result: '{s}' (length: {s?.Length ?? 0})"); if (!string.IsNullOrEmpty(s) && !string.IsNullOrWhiteSpace(s)) return s; // Fallback to legacy CP936 if Unicode was empty s = ByteToStringUtils.UshortArrayToCP936String(arr); Debug.Log($"[Inventory] CP936 fallback result: '{s}' (length: {s?.Length ?? 0})"); 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; Debug.Log($"[Inventory] GetName method result: '{val}' (length: {val?.Length ?? 0})"); if (!string.IsNullOrEmpty(val) && !string.IsNullOrWhiteSpace(val)) return val; } catch (Exception ex) { Debug.LogWarning($"[Inventory] Error calling GetName method: {ex.Message}"); } } return ""; } private static 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 static 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 = new InventoryItemData { Package = byPackage, Slot = slotIndex, TemplateId = tid, ExpireDate = expireDate, State = state, Count = amount, Crc = crc, Content = extra }; items.Add(item); } return true; } public static string BytesToHex(byte[] bytes, int max) { if (bytes == null || bytes.Length == 0) return ""; int len = Math.Min(bytes.Length, max); string hex = BitConverter.ToString(bytes, 0, len); if (bytes.Length > len) hex += "-..."; return hex; } public static 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 static int ExtractPileLimitFromElement(object data) { if (data == null) return 1; var t = data.GetType(); // Common field/property names across item essences string[] names = new[] { "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; } } }