using Daybreak.API.Interop; using Daybreak.API.Interop.GuildWars; using Daybreak.API.Models; using System.Collections.Concurrent; using System.Core.Extensions; using System.Extensions.Core; using System.Runtime.InteropServices; using System.Security; namespace Daybreak.API.Services.Interop; public sealed class UIContextService(ILogger logger) { [SuppressUnmanagedCodeSecurity] [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private unsafe delegate void AsyncDecodeStrCallback(void* param, ushort* decodedString); // Prevent GC of delegates passed to native code private readonly ConcurrentDictionary prevent_GC_callbacks = []; private readonly ILogger logger = logger.ThrowIfNull(); public unsafe WrappedPointer GetFrameContext(WrappedPointer frame) where T : unmanaged { if (frame.IsNull || frame.Pointer->FrameCallbacks.Size == 0) { return null; } for (uint i = 0; i < frame.Pointer->FrameCallbacks.Size; i++) { var callback = frame.Pointer->FrameCallbacks.Buffer[i]; if (callback.UiCtl_Context != null) { return new WrappedPointer((T*)callback.UiCtl_Context); } } return null; } public unsafe WrappedPointer GetChildFrame(WrappedPointer parent, uint childOffset) { var scopedLogger = this.logger.CreateScopedLogger(); if (parent.IsNull) { scopedLogger.LogError("Parent frame is null"); return null; } return GWCA.GW.UI.GetChildFrame(parent, childOffset); } public WrappedPointer GetButtonActionFrame() { return this.GetChildFrame(this.GetFrameByLabel("Game"), 6); } public unsafe bool SendFrameUIMessage(WrappedPointer frame, UIMessage messageId, void* arg1, void* arg2 = null) { var scopedLogger = this.logger.CreateScopedLogger(); if (frame.IsNull) { scopedLogger.LogError("Frame is null"); return false; } return GWCA.GW.UI.SendFrameUIMessage(frame, messageId, arg1, (nint)arg2); } public unsafe bool SetFrameDisabled(WrappedPointer frame, bool disabled) => GWCA.GW.UI.SetFrameDisabled(frame, disabled); public unsafe bool SetFrameVisible(WrappedPointer frame, bool visible) => GWCA.GW.UI.SetFrameVisible(frame, visible); public unsafe bool KeyDown(ControlAction action, WrappedPointer frame) => GWCA.GW.UI.Keydown(action, frame); public unsafe bool KeyUp(ControlAction action, WrappedPointer frame) => GWCA.GW.UI.Keyup(action, frame); public unsafe WrappedPointer GetFrameByLabel(string label) { fixed (char* labelPtr = label) { return GWCA.GW.UI.GetFrameByLabel((ushort*)labelPtr); } } public unsafe WrappedPointer GetFrameById(uint frameId) => GWCA.GW.UI.GetFrameById(frameId); public unsafe WrappedPointer GetParentFrame(WrappedPointer frame) { if (frame.IsNull) { return null; } return GWCA.GW.UI.GetParentFrame(frame); } public unsafe bool ButtonClick(WrappedPointer frame) { if (frame.IsNull) { return false; } return GWCA.GW.UI.ButtonClick(frame); } 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, Language language = Language.Unknown, CancellationToken cancellationToken = default) { var taskCompletionSource = new TaskCompletionSource(); cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(cancellationToken)); void callback(void* param, ushort* decodedString) { try { if (cancellationToken.IsCancellationRequested) { taskCompletionSource.TrySetCanceled(cancellationToken); return; } // Convert the decoded wide string to a managed string var result = decodedString != null ? new string((char*)decodedString) : string.Empty; taskCompletionSource.TrySetResult(result); } catch (Exception ex) { taskCompletionSource.TrySetException(ex); } finally { this.prevent_GC_callbacks.TryRemove(callback, out _); } } // CRITICAL: Keep delegate alive until callback executes this.prevent_GC_callbacks[callback] = true; var funcPtr = Marshal.GetFunctionPointerForDelegate(callback); // 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, Language language = Language.Unknown, CancellationToken cancellationToken = default) { // Pin the string and convert to ushort* fixed (char* encodedPtr = encodedString) { return this.AsyncDecodeStringAsync((ushort*)encodedPtr, language, cancellationToken); } } }