340 lines
10 KiB
C#
340 lines
10 KiB
C#
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<byte, int> _packSizeByPackage = new Dictionary<byte, int>();
|
|
private static readonly Dictionary<byte, Dictionary<int, InventoryItemData>> _itemsByPackage = new Dictionary<byte, Dictionary<int, InventoryItemData>>();
|
|
private static readonly Dictionary<int, string> _tidNameCache = new Dictionary<int, string>();
|
|
|
|
public static event Action<byte, IReadOnlyDictionary<int, InventoryItemData>> 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<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 void UpdatePack(byte byPackage, int ivtrSize, IEnumerable<InventoryItemData> items)
|
|
{
|
|
_packSizeByPackage[byPackage] = ivtrSize;
|
|
if (!_itemsByPackage.TryGetValue(byPackage, out var slots))
|
|
{
|
|
slots = new Dictionary<int, InventoryItemData>();
|
|
_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<int, InventoryItemData> 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<int, InventoryItemData> 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<InventoryItemData> GetAllItems()
|
|
{
|
|
foreach (var pack in _itemsByPackage.Values)
|
|
{
|
|
foreach (var kv in pack)
|
|
{
|
|
yield return kv.Value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|