From e90ae48c402777555e0a4e69d5e00e51cda874cd Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Mon, 23 Feb 2026 13:24:21 -0800 Subject: [PATCH] Fix item upgrade collisions (Closes #1461) (#1464) --- Daybreak.API/EntryPoint.cs | 4 +- Daybreak.API/Interop/GWCA.cs | 12 +-- .../Services/Interop/UIContextService.cs | 9 ++- Daybreak.API/Services/InventoryService.cs | 16 ++-- Daybreak.API/Services/UIService.cs | 60 +-------------- Daybreak.Generators/MsvcDemangler.cs | 16 +++- .../Models/Guildwars/ItemModifier.cs | 10 ++- .../Models/Guildwars/ItemProperty.cs | 10 ++- .../Models/Guildwars/ItemUpgrade.cs | 73 +++++++++++++++---- .../Models/Guildwars/ItemUpgradeFlag.cs | 12 +++ 10 files changed, 122 insertions(+), 100 deletions(-) create mode 100644 Daybreak.Shared/Models/Guildwars/ItemUpgradeFlag.cs diff --git a/Daybreak.API/EntryPoint.cs b/Daybreak.API/EntryPoint.cs index 92cdd8d5..b686ea6d 100644 --- a/Daybreak.API/EntryPoint.cs +++ b/Daybreak.API/EntryPoint.cs @@ -34,9 +34,9 @@ public class EntryPoint { Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", null, EnvironmentVariableTarget.Process); Environment.SetEnvironmentVariable("ASPNETCORE_PREVENTHOSTINGSTARTUP", "true", EnvironmentVariableTarget.Process); - +#if DEBUG ConsoleExtensions.AllocateAnsiConsole(); - +#endif var port = FindAvailablePort(StartPort); var app = CreateApplication(port); var runTask = Task.Run(() => StartServer(app), CancellationTokenSource.Token); diff --git a/Daybreak.API/Interop/GWCA.cs b/Daybreak.API/Interop/GWCA.cs index 10bd4368..3afafc6d 100644 --- a/Daybreak.API/Interop/GWCA.cs +++ b/Daybreak.API/Interop/GWCA.cs @@ -128,11 +128,11 @@ internal static unsafe partial class GWCA // GW::RegisterLogHandler [LibraryImport(DllName, EntryPoint = "?RegisterLogHandler@GW@@YAXP6AXPAXW4LogLevel@1@PBD2I2@Z0@Z")] - internal static partial void RegisterLogHandler(nint callback); + internal static partial void RegisterLogHandler(nint callback1, nint ptr2); // GW::RegisterPanicHandler [LibraryImport(DllName, EntryPoint = "?RegisterPanicHandler@GW@@YAXP6AXPAXPBD1I1@Z0@Z")] - internal static partial void RegisterPanicHandler(nint callback); + internal static partial void RegisterPanicHandler(nint callback1, nint ptr2); // GW::Terminate [LibraryImport(DllName, EntryPoint = "?Terminate@GW@@YAXXZ")] @@ -2041,7 +2041,7 @@ internal static unsafe partial class GWCA // GW::TabsFrame::AddTab [LibraryImport(DllName, EntryPoint = "?AddTab@TabsFrame@GW@@QAEPAUFrame@UI@2@PB_WIIP6AXPAUInteractionMessage@42@PAX2@Z2@Z")] [UnmanagedCallConv(CallConvs = [typeof(CallConvThiscall)])] - internal static partial global::Daybreak.API.Interop.GuildWars.Frame* AddTab(nint self, ushort* ptr2, uint value3, uint value4, nint callback5); + internal static partial global::Daybreak.API.Interop.GuildWars.Frame* AddTab(nint self, ushort* ptr2, uint value3, uint value4, nint callback5, nint ptr6); // GW::TabsFrame::ChooseTab [LibraryImport(DllName, EntryPoint = "?ChooseTab@TabsFrame@GW@@QAE_NI@Z")] @@ -2171,11 +2171,11 @@ internal static unsafe partial class GWCA // GW::UI::AddFrameUIInteractionCallback [LibraryImport(DllName, EntryPoint = "?AddFrameUIInteractionCallback@UI@GW@@YA_NPAUFrame@12@P6AXPAUInteractionMessage@12@PAX2@Z2@Z")] [return: MarshalAs(UnmanagedType.U1)] - internal static partial bool AddFrameUIInteractionCallback(global::Daybreak.API.Interop.GuildWars.Frame* frame1, nint callback2); + internal static partial bool AddFrameUIInteractionCallback(global::Daybreak.API.Interop.GuildWars.Frame* frame1, nint callback2, nint ptr3); // GW::UI::AsyncDecodeStr [LibraryImport(DllName, EntryPoint = "?AsyncDecodeStr@UI@GW@@YAXPB_WP6AXPAX0@Z1W4Language@Constants@2@@Z")] - internal static partial void AsyncDecodeStr(ushort* ptr1, nint callback2); + internal static partial void AsyncDecodeStr(ushort* ptr1, nint callback2, nint ptr3, global::Daybreak.API.Interop.GuildWars.Language language4); // GW::UI::AsyncDecodeStr [LibraryImport(DllName, EntryPoint = "?AsyncDecodeStr@UI@GW@@YAXPB_WPA_WI@Z")] @@ -2188,7 +2188,7 @@ internal static unsafe partial class GWCA // GW::UI::CreateUIComponent [LibraryImport(DllName, EntryPoint = "?CreateUIComponent@UI@GW@@YAIIIIP6AXPAUInteractionMessage@12@PAX1@Z1PB_W@Z")] - internal static partial uint CreateUIComponent(uint value1, uint value2, uint value3, nint callback4); + internal static partial uint CreateUIComponent(uint value1, uint value2, uint value3, nint callback4, nint ptr5, ushort* ptr6); // GW::UI::Default_UICallback | interactionMessage1: TODO: map struct GW::UI::InteractionMessage // [LibraryImport(DllName, EntryPoint = "?Default_UICallback@UI@GW@@YA_NPAUInteractionMessage@12@PAX1@Z")] diff --git a/Daybreak.API/Services/Interop/UIContextService.cs b/Daybreak.API/Services/Interop/UIContextService.cs index 728b16fc..1a98e61d 100644 --- a/Daybreak.API/Services/Interop/UIContextService.cs +++ b/Daybreak.API/Services/Interop/UIContextService.cs @@ -109,7 +109,7 @@ public sealed class UIContextService(ILogger logger) public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam) => GWCA.GW.UI.SendUIMessage(message, (void*)wParam, (nint)lParam); - public unsafe Task AsyncDecodeStringAsync(ushort* encodedString, CancellationToken cancellationToken = default) + public unsafe Task AsyncDecodeStringAsync(ushort* encodedString, Language language = Language.Unknown, CancellationToken cancellationToken = default) { var taskCompletionSource = new TaskCompletionSource(); cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(cancellationToken)); @@ -144,17 +144,18 @@ public sealed class UIContextService(ILogger logger) this.prevent_GC_callbacks[callback] = true; var funcPtr = Marshal.GetFunctionPointerForDelegate(callback); - GWCA.GW.UI.AsyncDecodeStr(encodedString, funcPtr); + // GWCA's AsyncDecodeStr handles language switching internally (sets language, decodes, restores) + GWCA.GW.UI.AsyncDecodeStr(encodedString, funcPtr, 0, language); return taskCompletionSource.Task; } - public unsafe Task AsyncDecodeStringAsync(string encodedString, CancellationToken cancellationToken = default) + public unsafe Task AsyncDecodeStringAsync(string encodedString, Language language = Language.Unknown, CancellationToken cancellationToken = default) { // Pin the string and convert to ushort* fixed (char* encodedPtr = encodedString) { - return this.AsyncDecodeStringAsync((ushort*)encodedPtr, cancellationToken); + return this.AsyncDecodeStringAsync((ushort*)encodedPtr, language, cancellationToken); } } } diff --git a/Daybreak.API/Services/InventoryService.cs b/Daybreak.API/Services/InventoryService.cs index b7bfbe24..f283a2ce 100644 --- a/Daybreak.API/Services/InventoryService.cs +++ b/Daybreak.API/Services/InventoryService.cs @@ -83,17 +83,21 @@ public sealed class InventoryService( return default; } - var decodedItemTuples = await Task.WhenAll(itemTuples.Select(async tuple => + // 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<(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)> Items)>(); + foreach (var tuple in itemTuples) { - var decodedItems = await Task.WhenAll(tuple.Item2.Select(async item => + 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)>(); + foreach (var item in tuple.Item2) { var decodedName = await this.uIService.DecodeString(item.EncodedName, Language.English, cancellationToken); var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, Language.English, cancellationToken); var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, Language.English, cancellationToken); - return (item.ModelId, item.EncodedName, DecodedName: decodedName, item.EncodedSingleName, DecodedSingleName: decodedSingleName, item.EncodedCompleteName, DecodedCompleteName: decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType); - })); - return (tuple.Type, Items: decodedItems.ToList()); - })); + decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType)); + } + decodedItemTuples.Add((tuple.Type, decodedItems)); + } return new InventoryInformation( Bags: [.. decodedItemTuples.Select(tuple => diff --git a/Daybreak.API/Services/UIService.cs b/Daybreak.API/Services/UIService.cs index 8789c2fa..b5d5f815 100644 --- a/Daybreak.API/Services/UIService.cs +++ b/Daybreak.API/Services/UIService.cs @@ -8,12 +8,10 @@ namespace Daybreak.API.Services; public sealed class UIService( GameThreadService gameThreadService, - GameContextService gameContextService, UIContextService uIContextService, ILogger logger) { private readonly GameThreadService gameThreadService = gameThreadService; - private readonly GameContextService gameContextService = gameContextService; private readonly UIContextService uIContextService = uIContextService; private readonly ILogger logger = logger; @@ -90,62 +88,8 @@ public sealed class UIService( public async Task DecodeString(string encoded, Language language, CancellationToken cancellationToken) { - var scopedLogger = this.logger.CreateScopedLogger(); - var prevLanguage = await this.gameThreadService.QueueOnGameThread(() => - { - unsafe - { - var gameContext = this.gameContextService.GetGameContext(); - if (gameContext.IsNull) - { - scopedLogger.LogError("Game context is null"); - return null; - } - - var textParser = gameContext.Pointer->TextParserContext; - if (textParser is null) - { - scopedLogger.LogError("Text parser context is null"); - return null; - } - - var prevLanguage = textParser->Language; - if (language is not Language.Unknown) - { - textParser->Language = language; - } - - return prevLanguage; - } - }, cancellationToken); - - var decoded = await this.uIContextService.AsyncDecodeStringAsync(encoded, cancellationToken); - await this.gameThreadService.QueueOnGameThread(() => - { - unsafe - { - var gameContext = this.gameContextService.GetGameContext(); - if (gameContext.IsNull) - { - scopedLogger.LogError("Game context is null"); - return; - } - - var textParser = gameContext.Pointer->TextParserContext; - if (textParser is null) - { - scopedLogger.LogError("Text parser context is null"); - return; - } - - if (prevLanguage.HasValue) - { - textParser->Language = prevLanguage.Value; - } - } - }, cancellationToken); - - return decoded; + // GWCA's AsyncDecodeStr handles language switching internally (sets language, decodes, restores) + return await this.uIContextService.AsyncDecodeStringAsync(encoded, language, cancellationToken); } private static ControlAction GetUIActionFromFrameLabel(string frameLabel) diff --git a/Daybreak.Generators/MsvcDemangler.cs b/Daybreak.Generators/MsvcDemangler.cs index 41ab9e68..46b17891 100644 --- a/Daybreak.Generators/MsvcDemangler.cs +++ b/Daybreak.Generators/MsvcDemangler.cs @@ -450,10 +450,22 @@ internal sealed class MsvcDemangler if (pos < s.Length && s[pos] >= 'A' && s[pos] <= 'Z') pos++; // Return type ReadType(s, ref pos, nameTable); - // Params until @ or Z + // Params until @Z (function pointer terminator) while (pos < s.Length) { - if (s[pos] is '@' or 'Z') { pos++; break; } + // Function pointers are terminated by @Z + if (s[pos] == '@') + { + pos++; // skip @ + if (pos < s.Length && s[pos] == 'Z') + { + pos++; // skip Z - this terminates the function pointer, not the outer param list + } + break; + } + + // Single Z also terminates (void params case) + if (s[pos] == 'Z') { pos++; break; } ReadType(s, ref pos, nameTable); } diff --git a/Daybreak.Shared/Models/Guildwars/ItemModifier.cs b/Daybreak.Shared/Models/Guildwars/ItemModifier.cs index 44612ef5..adcff934 100644 --- a/Daybreak.Shared/Models/Guildwars/ItemModifier.cs +++ b/Daybreak.Shared/Models/Guildwars/ItemModifier.cs @@ -23,8 +23,10 @@ public enum ItemModifierIdentifier : uint Armor1 = 0x27B, // Armor value (arg1) Armor2 = 0x23C, // Armor value (arg1) Energy = 0x27C, // Energy value (arg1) - Upgrade1 = 0x21E, // Suffix/Prefix/Inscription upgrade (upgrade id) - Upgrade2 = 0x240, // Suffix/Prefix/Inscription upgrade (upgrade id) + UpgradeSuffix = 0x21E, // Suffix upgrade (upgrade id) + UpgradePrefix = 0x240, // Prefix upgrade (upgrade id) + UpgradeSuffix2 = 0x27E, // Suffix upgrade. Param: 8=Minor, 9=Major, A=Superior (upgrade id) + UpgradeSuffix3 = 0x253, // Suffix rule. This is normally not correct but for now we'll use this to map runes that alter stats such as hp or energy OfTheProfession = 0x28A, // +5 to primary of profession (arg2) (arg1) DamageType = 0x24B, // Damage type (arg1) @@ -104,5 +106,7 @@ public enum ItemModifierIdentifier : uint public enum ItemModifierParam : uint { LabelInName = 0x0, - Description = 0x8 + Description = 0x8, + Superior = 0x9, + Major = 0xA, } diff --git a/Daybreak.Shared/Models/Guildwars/ItemProperty.cs b/Daybreak.Shared/Models/Guildwars/ItemProperty.cs index 8a88a03f..832bed32 100644 --- a/Daybreak.Shared/Models/Guildwars/ItemProperty.cs +++ b/Daybreak.Shared/Models/Guildwars/ItemProperty.cs @@ -85,8 +85,10 @@ public abstract class ItemProperty ItemModifierIdentifier.Armor1 => new ArmorProperty { Armor =(int)m.Argument1 }, ItemModifierIdentifier.Armor2 => new ArmorProperty { Armor = (int)m.Argument1 }, ItemModifierIdentifier.Energy => new EnergyProperty { Energy = (int)m.Argument1 }, - ItemModifierIdentifier.Upgrade1 => ParseUpgradeProperty(m), - ItemModifierIdentifier.Upgrade2 => ParseUpgradeProperty(m), + ItemModifierIdentifier.UpgradeSuffix => ParseUpgradeProperty(m, type: ItemUpgradeType.Suffix), + ItemModifierIdentifier.UpgradePrefix => ParseUpgradeProperty(m, type: ItemUpgradeType.Prefix), + ItemModifierIdentifier.UpgradeSuffix2 => ParseUpgradeProperty(m, type: ItemUpgradeType.Suffix, flag: (ItemUpgradeFlag)m.Param), + ItemModifierIdentifier.UpgradeSuffix3 => ParseUpgradeProperty(m, ItemUpgradeType.Suffix, flag: ItemUpgradeFlag.Stat), ItemModifierIdentifier.OfTheProfession => ParseOfTheProfessionProperty(m), ItemModifierIdentifier.DamageType => new DamageTypeProperty { DamageType = (DamageType)m.Argument1 }, @@ -176,9 +178,9 @@ public abstract class ItemProperty return new OfTheProfessionProperty { Profession = (attribute.Profession ?? Profession.None).Name ?? string.Empty, Attribute = attribute.Name ?? string.Empty, Value = (int)m.Argument2 }; } - private static ItemProperty ParseUpgradeProperty(ItemModifier m) + private static ItemProperty ParseUpgradeProperty(ItemModifier m, ItemUpgradeType type = ItemUpgradeType.Unknown, ItemUpgradeFlag flag = ItemUpgradeFlag.Default) { - if (!ItemUpgrade.TryParse((int)m.UpgradeId, out var upgrade) || + if (!ItemUpgrade.TryParse((int)m.UpgradeId, type, flag, out var upgrade) || upgrade == ItemUpgrade.Unknown) { return new UnknownUpgradeProperty { RawModifier = m }; diff --git a/Daybreak.Shared/Models/Guildwars/ItemUpgrade.cs b/Daybreak.Shared/Models/Guildwars/ItemUpgrade.cs index d83e9b48..9f029ee6 100644 --- a/Daybreak.Shared/Models/Guildwars/ItemUpgrade.cs +++ b/Daybreak.Shared/Models/Guildwars/ItemUpgrade.cs @@ -274,29 +274,28 @@ public sealed class ItemUpgrade public static readonly ItemUpgrade Windwalker = new(0x0202, "Windwalker", ItemUpgradeType.Prefix); public static readonly ItemUpgrade Forsaken = new(0x0203, "Forsaken", ItemUpgradeType.Prefix); public static readonly ItemUpgrade Centurions = new(0x0207, "Centurion's", ItemUpgradeType.Prefix); - public static readonly ItemUpgrade OfAttunement = new(0x0211, "of Attunement", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfRecovery = new(0x0213, "of Recovery", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfRestoration = new(0x0214, "of Restoration", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfClarity = new(0x0215, "of Clarity", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfPurity = new(0x0216, "of Purity", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfMinorVigor = new(0x00FF, "of Minor Vigor", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfMinorVigor2 = new(0x00C2, "of Minor Vigor", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfSuperiorVigor = new(0x0101, "of Superior Vigor", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfMajorVigor = new(0x0100, "of Major Vigor", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfVitae = new(0x0212, "of Vitae", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfMinorAbsorption = new(0x00FC, "of Minor Absorption", ItemUpgradeType.Suffix); + public static readonly ItemUpgrade OfAttunement = new(0x0423, "of Attunement", ItemUpgradeType.Suffix, ItemUpgradeFlag.Stat); + public static readonly ItemUpgrade OfRecovery = new(0x0427, "of Recovery", ItemUpgradeType.Suffix, ItemUpgradeFlag.Stat); + public static readonly ItemUpgrade OfRestoration = new(0x0429, "of Restoration", ItemUpgradeType.Suffix, ItemUpgradeFlag.Stat); + public static readonly ItemUpgrade OfClarity = new(0x042B, "of Clarity", ItemUpgradeType.Suffix, ItemUpgradeFlag.Stat); + public static readonly ItemUpgrade OfPurity = new(0x042D, "of Purity", ItemUpgradeType.Suffix, ItemUpgradeFlag.Stat); + public static readonly ItemUpgrade OfMinorVigor = new(0x02C2, "of Minor Vigor", ItemUpgradeType.Suffix, ItemUpgradeFlag.Base); + public static readonly ItemUpgrade OfSuperiorVigor = new(0x02C2, "of Superior Vigor", ItemUpgradeType.Suffix, ItemUpgradeFlag.Superior); + public static readonly ItemUpgrade OfMajorVigor = new(0x02C2, "of Major Vigor", ItemUpgradeType.Suffix, ItemUpgradeFlag.Major); + public static readonly ItemUpgrade OfVitae = new(0x0425, "of Vitae", ItemUpgradeType.Suffix, ItemUpgradeFlag.Stat); + public static readonly ItemUpgrade OfMinorAbsorption = new(0x02EA, "of Minor Absorption", ItemUpgradeType.Suffix, ItemUpgradeFlag.Base); public static readonly ItemUpgrade OfMinorTactics = new(0x1501, "of Minor Tactics", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMinorStrength = new(0x1101, "of Minor Strength", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMinorAxeMastery = new(0x1201, "of Minor Axe Mastery", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMinorHammerMastery = new(0x1301, "of Minor Hammer Mastery", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMinorSwordsmanship = new(0x1401, "of Minor Swordsmanship", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfMajorAbsorption = new(0x00FD, "of Major Absorption", ItemUpgradeType.Suffix); + public static readonly ItemUpgrade OfMajorAbsorption = new(0x02EA, "of Major Absorption", ItemUpgradeType.Suffix, ItemUpgradeFlag.Major); public static readonly ItemUpgrade OfMajorTactics = new(0x1502, "of Major Tactics", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMajorStrength = new(0x1102, "of Major Strength", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMajorAxeMastery = new(0x1202, "of Major Axe Mastery", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMajorHammerMastery = new(0x1302, "of Major Hammer Mastery", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfMajorSwordsmanship = new(0x1402, "of Major Swordsmanship", ItemUpgradeType.Suffix); - public static readonly ItemUpgrade OfSuperiorAbsorption = new(0x00FE, "of Superior Absorption", ItemUpgradeType.Suffix); + public static readonly ItemUpgrade OfSuperiorAbsorption = new(0x02EA, "of Superior Absorption", ItemUpgradeType.Suffix, ItemUpgradeFlag.Superior); public static readonly ItemUpgrade OfSuperiorTactics = new(0x1503, "of Superior Tactics", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfSuperiorStrength = new(0x1103, "of Superior Strength", ItemUpgradeType.Suffix); public static readonly ItemUpgrade OfSuperiorAxeMastery = new(0x1203, "of Superior Axe Mastery", ItemUpgradeType.Suffix); @@ -758,7 +757,6 @@ public sealed class ItemUpgrade OfClarity, OfPurity, OfMinorVigor, - OfMinorVigor2, OfSuperiorVigor, OfMajorVigor, OfVitae, @@ -969,6 +967,47 @@ public sealed class ItemUpgrade return upgrade is not null; } + public static bool TryParse(int id, ItemUpgradeType type, [NotNullWhen(true)] out ItemUpgrade? upgrade) + { + if (type is ItemUpgradeType.Unknown) + { + return TryParse(id, out upgrade); + } + + upgrade = ItemUpgrades.FirstOrDefault(u => u.Id == id && u.Type == type); + return upgrade is not null; + } + + public static bool TryParse(int id, ItemUpgradeFlag flag, [NotNullWhen(true)] out ItemUpgrade? upgrade) + { + if (flag is ItemUpgradeFlag.Default) + { + return TryParse(id, out upgrade); + } + + upgrade = ItemUpgrades.FirstOrDefault(u => u.Id == id && u.Flag == flag); + return upgrade is not null; + } + + public static bool TryParse(int id, ItemUpgradeType type, ItemUpgradeFlag flag, [NotNullWhen(true)] out ItemUpgrade? upgrade) + { + if (flag is ItemUpgradeFlag.Default && type is ItemUpgradeType.Unknown) + { + return TryParse(id, out upgrade); + } + else if (flag is ItemUpgradeFlag.Default) + { + return TryParse(id, type, out upgrade); + } + else if (type is ItemUpgradeType.Unknown) + { + return TryParse(id, flag, out upgrade); + } + + upgrade = ItemUpgrades.FirstOrDefault(u => u.Id == id && u.Type == type && u.Flag == flag); + return upgrade is not null; + } + [JsonPropertyName("id")] [JsonConverter(typeof(HexIntConverter))] public int Id { get; } @@ -979,10 +1018,14 @@ public sealed class ItemUpgrade [JsonPropertyName("type")] public ItemUpgradeType Type { get; } - private ItemUpgrade(int id, string name, ItemUpgradeType type) + [JsonPropertyName("flag")] + public ItemUpgradeFlag Flag { get; } + + private ItemUpgrade(int id, string name, ItemUpgradeType type, ItemUpgradeFlag flag = ItemUpgradeFlag.Default) { this.Id = id; this.Name = name; this.Type = type; + this.Flag = flag; } } diff --git a/Daybreak.Shared/Models/Guildwars/ItemUpgradeFlag.cs b/Daybreak.Shared/Models/Guildwars/ItemUpgradeFlag.cs new file mode 100644 index 00000000..860c9ada --- /dev/null +++ b/Daybreak.Shared/Models/Guildwars/ItemUpgradeFlag.cs @@ -0,0 +1,12 @@ +namespace Daybreak.Shared.Models.Guildwars; + +public enum ItemUpgradeFlag : uint +{ + Default = 0x00, + Base = 0x08, + Major = 0x09, + Superior = 0x0A, + + // Not a real flag, just used for the stat runes + Stat = 0x02 +} \ No newline at end of file