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)