Fix item upgrade collisions (Closes #1461) (#1464)

This commit is contained in:
2026-02-23 13:24:21 -08:00
parent ef513e1230
commit e90ae48c40
10 changed files with 122 additions and 100 deletions
+2 -2
View File
@@ -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);
+6 -6
View File
@@ -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")]
@@ -109,7 +109,7 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam) => GWCA.GW.UI.SendUIMessage(message, (void*)wParam, (nint)lParam);
public unsafe Task<string> AsyncDecodeStringAsync(ushort* encodedString, CancellationToken cancellationToken = default)
public unsafe Task<string> AsyncDecodeStringAsync(ushort* encodedString, Language language = Language.Unknown, CancellationToken cancellationToken = default)
{
var taskCompletionSource = new TaskCompletionSource<string>();
cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(cancellationToken));
@@ -144,17 +144,18 @@ public sealed class UIContextService(ILogger<UIContextService> 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<string> AsyncDecodeStringAsync(string encodedString, CancellationToken cancellationToken = default)
public unsafe Task<string> 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);
}
}
}
+10 -6
View File
@@ -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 =>
+2 -58
View File
@@ -8,12 +8,10 @@ namespace Daybreak.API.Services;
public sealed class UIService(
GameThreadService gameThreadService,
GameContextService gameContextService,
UIContextService uIContextService,
ILogger<UIService> logger)
{
private readonly GameThreadService gameThreadService = gameThreadService;
private readonly GameContextService gameContextService = gameContextService;
private readonly UIContextService uIContextService = uIContextService;
private readonly ILogger<UIService> logger = logger;
@@ -90,62 +88,8 @@ public sealed class UIService(
public async Task<string?> DecodeString(string encoded, Language language, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var prevLanguage = await this.gameThreadService.QueueOnGameThread<Language?>(() =>
{
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)
+14 -2
View File
@@ -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);
}
@@ -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,
}
@@ -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 };
+58 -15
View File
@@ -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;
}
}
@@ -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
}