using Daybreak.API.Interop; using Daybreak.API.Interop.GuildWars; using Daybreak.API.Services.Interop; using Daybreak.Shared.Models.Api; using Daybreak.Shared.Models.Guildwars; using System.Extensions.Core; using System.Runtime.CompilerServices; using System.Text; namespace Daybreak.API.Services; public sealed class InventoryService( GameContextService gameContextService, GameThreadService gameThreadService, UIService uIService, ILogger logger) { private readonly GameContextService gameContextService = gameContextService; private readonly GameThreadService gameThreadService = gameThreadService; private readonly UIService uIService = uIService; private readonly ILogger logger = logger; public async Task GetInventoryInformation(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(); var itemTuples = await this.gameThreadService.QueueOnGameThread(() => { unsafe { var gameContext = this.gameContextService.GetGameContext(); if (gameContext.IsNull) { scopedLogger.LogError("Game context is null"); return default; } var inventory = gameContext.Pointer->Items->Inventory; if (inventory is null) { scopedLogger.LogError("Inventory is null"); return default; } var itemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint, ushort Value)>)>(); for (var i = 0; i < 23; i++) { // Use GWCA helper to fetch bag pointers by index — safer than reading an inline array directly. var bag = GWCA.GW.Items.GetBagByIndex((uint)i); if (bag is null) { continue; } var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint, ushort Value)>(); itemTuples.Add((bag->BagType, retBag)); if (bag->ItemsCount is 0) { continue; } foreach (var itemPtr in bag->Items.Value) { if (itemPtr is 0) { continue; } var item = (ItemStruct*)itemPtr; var singleItemName = new string((char*)item->SingleItemName); var nameEncoded = new string((char*)item->NameEnc); var completeNameEncoded = new string((char*)item->CompleteNameEnc); var modifiers = new uint[item->ModStructSize]; for (var j = 0; j < item->ModStructSize; j++) { modifiers[j] = item->ModStruct[j].Mod; } retBag.Add((item->ModelId, completeNameEncoded, singleItemName, nameEncoded, (item->Interaction & 0x08000000) != 0, item->Quantity, modifiers, (ItemType)item->Type, item->Interaction, item->ModelFileId, item->Dye.DyeTint, item->Value)); } } return itemTuples; } }, cancellationToken); if (itemTuples is null) { return default; } // Decode strings sequentially to avoid race conditions with the game's TextParser language field. // Parallel decoding causes crashes because multiple operations race to set/restore the language. var decodedItemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, byte DyeTint, ushort Value)> Items)>(); foreach (var tuple in itemTuples) { var decodedItems = new List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, byte DyeTint, ushort Value)>(); foreach (var item in tuple.Item2) { var decodedName = await this.uIService.DecodeString(item.EncodedName, (GWCA.GW.Constants.Language)Language.English, cancellationToken); var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, (GWCA.GW.Constants.Language)Language.English, cancellationToken); var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, (GWCA.GW.Constants.Language)Language.English, cancellationToken); decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType, item.Interaction, item.ModelFileId, item.DyeTint, item.Value)); } decodedItemTuples.Add((tuple.Type, decodedItems)); } return new InventoryInformation( Bags: [.. decodedItemTuples.Select(tuple => new BagEntry( BagType: tuple.Type.ToString(), Items: [.. tuple.Items.Select(item => new ItemEntry( ItemModelId: item.ModelId, EncodedName: ToBase64(item.EncodedName), DecodedName: item.DecodedName ?? string.Empty, EncodedSingleName: ToBase64(item.EncodedSingleName), DecodedSingleName: item.DecodedSingleName ?? string.Empty, EncodedCompleteName: ToBase64(item.EncodedCompleteName), DecodedCompleteName: item.DecodedCompleteName ?? string.Empty, Inscribable: item.Inscribable, Quantity: item.Quantity, Modifiers: item.Modifiers, Properties: [.. ItemProperty.ParseItemModifiers([.. item.Modifiers.Select(m => (Shared.Models.Guildwars.ItemModifier)m)])], ItemType: item.ItemType.ToString(), Interaction: item.Interaction, ModelFileId: item.ModelFileId, DyeTint: item.DyeTint, Value: item.Value))]))]); } private static string ToBase64(string encoded) { return Convert.ToBase64String(Encoding.Unicode.GetBytes(encoded)); } }