552 lines
22 KiB
C#
552 lines
22 KiB
C#
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<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 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, "");
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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<Sprite>("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<InventoryItemData> 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<InventoryItemData>();
|
|
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<InventoryItemData>(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;
|
|
}
|
|
}
|
|
}
|