diff --git a/Assets/PerfectWorld/Scripts/Managers/InventoryManager.cs b/Assets/PerfectWorld/Scripts/Managers/InventoryManager.cs new file mode 100644 index 0000000000..77d544018b --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/InventoryManager.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +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 InventoryManager + { + private static readonly Dictionary _packSizeByPackage = new Dictionary(); + private static readonly Dictionary> _itemsByPackage = new Dictionary>(); + private static readonly Dictionary _tidNameCache = new Dictionary(); + + public static event Action> OnPackUpdated; + + private const int MaxContentHexToLog = 64; + + private static string GetPackageName(byte pkg) + { + switch (pkg) + { + case 0: return "PACK_INVENTORY"; + case 1: return "PACK_EQUIPMENT"; + case 2: return "PACK_TASKINVENTORY"; + default: return "PACK_UNKNOWN"; + } + } + + private static string BytesToHex(byte[] bytes, int max) + { + if (bytes == null || bytes.Length == 0) return ""; + int len = Math.Min(bytes.Length, max); + string hex = BitConverter.ToString(bytes, 0, len); + if (bytes.Length > len) hex += "-..."; + return hex; + } + + private 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); + object data = edm.get_data_ptr(id, ID_SPACE.ID_SPACE_ESSENCE); + 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, ""); + } + } + + 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(); + var prop = t.GetProperty("Name", BindingFlags.Public | BindingFlags.Instance); + if (prop != null && prop.PropertyType == typeof(string)) + { + var val = prop.GetValue(data) as string; + if (!string.IsNullOrEmpty(val)) return val; + } + var propReal = t.GetProperty("RealName", BindingFlags.Public | BindingFlags.Instance); + if (propReal != null && propReal.PropertyType == typeof(string)) + { + var val = propReal.GetValue(data) as string; + if (!string.IsNullOrEmpty(val)) return val; + } + var fieldName = t.GetField("name", BindingFlags.Public | BindingFlags.Instance); + if (fieldName != null && fieldName.FieldType == typeof(ushort[])) + { + var arr = fieldName.GetValue(data) as ushort[]; + var s = ByteToStringUtils.UshortArrayToCP936String(arr); + if (!string.IsNullOrEmpty(s)) return s; + } + var fieldReal = t.GetField("realname", BindingFlags.Public | BindingFlags.Instance); + if (fieldReal != null && fieldReal.FieldType == typeof(byte[])) + { + var arr = fieldReal.GetValue(data) as byte[]; + var s = ByteToStringUtils.ByteArrayToCP936String(arr); + if (!string.IsNullOrEmpty(s)) return s; + } + 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 void UpdatePack(byte byPackage, int ivtrSize, IEnumerable items) + { + _packSizeByPackage[byPackage] = ivtrSize; + if (!_itemsByPackage.TryGetValue(byPackage, out var slots)) + { + slots = new Dictionary(); + _itemsByPackage[byPackage] = slots; + } + slots.Clear(); + if (items != null) + { + foreach (var it in items) + { + if (it != null && it.Slot >= 0) + { + slots[it.Slot] = it; + } + } + } + OnPackUpdated?.Invoke(byPackage, slots); + + // Log this pack's items + LogPackInternal(byPackage, ivtrSize, slots); + } + + public static void LogPack(byte byPackage) + { + if (_itemsByPackage.TryGetValue(byPackage, out var slots)) + { + int size = _packSizeByPackage.TryGetValue(byPackage, out var s) ? s : 0; + LogPackInternal(byPackage, size, slots); + } + else + { + Debug.Log($"[Inventory] Pack {GetPackageName(byPackage)}({byPackage}) has no data yet."); + } + } + + public static void LogAllItems() + { + foreach (var kv in _itemsByPackage) + { + int size = _packSizeByPackage.TryGetValue(kv.Key, out var s) ? s : 0; + LogPackInternal(kv.Key, size, kv.Value); + } + } + + private static void LogPackInternal(byte byPackage, int ivtrSize, IReadOnlyDictionary slots) + { + Debug.Log($"[Inventory] === Pack {GetPackageName(byPackage)}({byPackage}) size={ivtrSize}, items={(slots?.Count ?? 0)} ==="); + if (slots == null || slots.Count == 0) + { + Debug.Log("[Inventory] (empty)"); + return; + } + foreach (var kv in slots) + { + var it = kv.Value; + string itemName = ResolveItemName(it.TemplateId); + string extraHex = it.Content != null && it.Content.Length > 0 ? BytesToHex(it.Content, MaxContentHexToLog) : ""; + int extraLen = it.Content?.Length ?? 0; + Debug.Log( + $"[Inventory] pkg={GetPackageName(it.Package)}({it.Package}) slot={it.Slot} tid={it.TemplateId}{(string.IsNullOrEmpty(itemName) ? "" : " \"" + itemName + "\"")} count={it.Count} state={it.State} expire={it.ExpireDate} crc={it.Crc} content_len={extraLen}{(extraLen > 0 ? ", content_hex=" + extraHex : "")}" + ); + } + } + + public static IReadOnlyDictionary GetPack(byte byPackage) + { + return _itemsByPackage.TryGetValue(byPackage, out var dict) ? dict : null; + } + + public static int GetPackSize(byte byPackage) + { + return _packSizeByPackage.TryGetValue(byPackage, out var size) ? size : 0; + } + + public static IEnumerable GetAllItems() + { + foreach (var pack in _itemsByPackage.Values) + { + foreach (var kv in pack) + { + yield return kv.Value; + } + } + } + } +} + diff --git a/Assets/PerfectWorld/Scripts/Managers/InventoryManager.cs.meta b/Assets/PerfectWorld/Scripts/Managers/InventoryManager.cs.meta new file mode 100644 index 0000000000..d2ca04007c --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/InventoryManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 135967a674c33d64fb368288f9e719ef \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs index aca8c53adb..296baacafd 100644 --- a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs @@ -1,6 +1,7 @@ using BrewMonster; using CSNetwork; using CSNetwork.Protocols; +using CSNetwork.C2SCommand; using CSNetwork.Protocols.RPCData; using CSNetwork.Security; using System; @@ -108,6 +109,36 @@ namespace BrewMonster.Network Instance._gameSession.RequestInventoryAsync(callback); } + public static void RequestInventoryByPackageAsync(byte byPackage, Action callback = null) + { + var req = new gamedatasend(); + req.Data = C2SCommandFactory.CreateGetInventoryDetail(byPackage); + SendProtocol(req, callback); + } + + public static void RequestAllInventoriesAsync(Action callback = null, params byte[] packages) + { + if (packages == null || packages.Length == 0) + { + packages = new byte[] { 0, 1, 2 }; + } + + int remaining = packages.Length; + Action onOneDone = () => + { + remaining--; + if (remaining <= 0) + { + callback?.Invoke(); + } + }; + + foreach (var p in packages) + { + RequestInventoryByPackageAsync(p, onOneDone); + } + } + public void LoadScene(string sceneName, LoadSceneMode mode, Action actDone) { StartCoroutine(LoadSceneCoroutine(sceneName, mode, actDone)); diff --git a/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs b/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs index f9b40066d9..c92732df81 100644 --- a/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs +++ b/Assets/PerfectWorld/Scripts/UI/Login/LoginScreenUI.cs @@ -106,7 +106,8 @@ namespace BrewMonster.UI { await Task.Delay(2000); Logger.Log("Entered world successfully."); - UnityGameSession.RequestInventoryAsync(() => { Logger.Log("Sent Inventory Detail Request"); }); + // Request all known packages: 0=Inventory,1=Equipment,2=Task + UnityGameSession.RequestAllInventoriesAsync(() => { Logger.Log("Sent Inventory Detail Requests (all packs)"); }, 0, 1, 2); } //private void OnInventoryReceived(List inventoryData) diff --git a/Assets/Scripts/CECHostPlayer.cs b/Assets/Scripts/CECHostPlayer.cs index 7e9df64e70..2c97cd5971 100644 --- a/Assets/Scripts/CECHostPlayer.cs +++ b/Assets/Scripts/CECHostPlayer.cs @@ -233,7 +233,16 @@ public class CECHostPlayer : MonoBehaviour { Debug.Log("[Inventory] OWN_IVTR_DETAIL_DATA received"); LogInventoryPacket("OWN_IVTR_DETAIL_DATA", data, hostId); - //ElementDataManProvider.GetElementDataMan().armor_essence_array. + // Parse and store + if (data != null && data.Length >= 6) + { + byte byPackage = data[0]; + byte ivtrSize = data[1]; + if (PerfectWorld.Scripts.Managers.InventoryManager.TryParseInventoryDetail(data, out var pkg, out var size, out var items)) + { + PerfectWorld.Scripts.Managers.InventoryManager.UpdatePack(pkg, size, items); + } + } break; }