2183 lines
87 KiB
C#
2183 lines
87 KiB
C#
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.
|
|
/// <summary>
|
|
/// Non-static UI/data helper for inventory items.
|
|
/// This holds caches and helpers for names, icons, pile limits, etc.
|
|
/// </summary>
|
|
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<int, string> _tidNameCache = new Dictionary<int, string>();
|
|
private readonly Dictionary<int, int> _pileLimitCache = new Dictionary<int, int>();
|
|
private readonly Dictionary<int, string> _tidIconKeyCache = new Dictionary<int, string>();
|
|
private readonly Dictionary<string, Sprite> _iconSpriteCache = new Dictionary<string, Sprite>(StringComparer.OrdinalIgnoreCase);
|
|
private Sprite[] _multiSpriteAtlas = null;
|
|
private readonly Dictionary<string, int> _spriteNameToIndexCache = new Dictionary<string, int>(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, "");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve and load the item's icon Sprite from Resources/UI/IconSprites based on its element data file_icon (DDS) name.
|
|
/// </summary>
|
|
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<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;
|
|
}
|
|
}
|
|
|
|
}
|
|
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<EC_IvtrItem> 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<EC_IvtrItem>();
|
|
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<EC_IvtrItem>(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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// C# mirror of C++ <c>CECIvtrItem</c> (defined in <c>EC_IvtrItem.h / EC_IvtrItem.cpp</c>).
|
|
/// This class intentionally keeps C++-style naming and layout so other C++ systems
|
|
/// can be ported over with minimal friction.
|
|
/// </summary>
|
|
/// 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++)
|
|
|
|
/// <summary>
|
|
/// 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 <c>CreateItem</c>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get scaled price of specified count of items.
|
|
/// Exact port of the C++ price scaling logic.
|
|
/// </summary>
|
|
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)
|
|
|
|
/// <summary>
|
|
/// Set item detail information.
|
|
/// C++ default just clears update flags; detail parsing happens in derived types.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Get item default information from database (no-op base, like C++).</summary>
|
|
public virtual void DefaultInfo()
|
|
{
|
|
}
|
|
|
|
/// <summary>Get item icon file name (C++ default returns "Unknown.dds").</summary>
|
|
public virtual string GetIconFile()
|
|
{
|
|
return "Unknown.dds";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get item name. In C++ this uses string tables; here we go through
|
|
/// <see cref="EC_IvtrItemUtils.ResolveItemName(int)"/> which already mirrors that logic.
|
|
/// </summary>
|
|
public virtual string GetName()
|
|
{
|
|
return EC_IvtrItemUtils.Instance.ResolveItemName(m_tid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get item name color. The original returns an <c>A3DCOLOR</c>;
|
|
/// here we return ARGB packed into a <see cref="uint"/>; default is white.
|
|
/// </summary>
|
|
public virtual uint GetNameColor()
|
|
{
|
|
return 0xFFFFFFFFu;
|
|
}
|
|
|
|
/// <summary>Use item. Base class just returns true.</summary>
|
|
public virtual bool Use()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Get scaled price of this item instance.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Clone item (shallow copy, same as C++ default).</summary>
|
|
public virtual EC_IvtrItem Clone()
|
|
{
|
|
return CreateItem(m_tid, m_expire_date, m_iCount, m_iCID);
|
|
}
|
|
|
|
/// <summary>Get item cool time in milliseconds (0 by default).</summary>
|
|
public virtual int GetCoolTime(out int piMax)
|
|
{
|
|
piMax = -1;
|
|
return 0;
|
|
}
|
|
|
|
public virtual bool CheckUseCondition()
|
|
{
|
|
return IsUseable();
|
|
}
|
|
|
|
/// <summary>Get drop model for showing in world.</summary>
|
|
public virtual string GetDropModel()
|
|
{
|
|
return "Models\\Error\\Error.ecm";
|
|
}
|
|
|
|
/// <summary>Get item quality level. Base returns -1 (unknown).</summary>
|
|
public virtual int GetItemLevel()
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get item description text (normal / booth-buy / reward / repair).
|
|
/// Mirrors the inline C++ <c>GetDesc</c>.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merge item amount with another same kind item.
|
|
/// Returns the number of items actually merged.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Add item amount. Returns new count (C++ <c>a_Clamp</c> to <c>m_iPileLimit</c>).</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can this item be equipped to specified position?
|
|
/// Uses the same bitmask test as C++.
|
|
/// </summary>
|
|
public bool CanEquippedTo(int iSlot)
|
|
{
|
|
return (m_i64EquipMask & (1L << iSlot)) != 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get first slot index starting from <paramref name="iStartSlot"/> where this item can be equipped.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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)
|
|
|
|
/// <summary>
|
|
/// Base implementation: just returns current description string.
|
|
/// Derived item types can override and build rich description like in C++.
|
|
/// </summary>
|
|
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";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get color string for color ID
|
|
/// Returns color codes in ^RRGGBB format (6 hex digits) for text formatting
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format string using printf-style format specifiers (%d, %+d, %.2f, %s, etc.)
|
|
/// Converts printf-style formats to C# string formatting
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to get the extended description line from item_ext_desc.txt.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get extended description text for UI display.
|
|
/// This method mirrors AddExtDescText() logic but returns a string instead of modifying m_strDesc.
|
|
/// 此方法镜像AddExtDescText()逻辑,但返回字符串而不是修改m_strDesc
|
|
/// </summary>
|
|
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;
|
|
// }
|
|
}
|
|
|
|
/// <summary>
|
|
/// C# mirror of C++ <c>CECIvtrUnknown</c> (fallback item type).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|