From 072f00638cc37c8effbb689113d703b1bcb8c75a Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Sat, 17 Jan 2026 12:23:32 +0100 Subject: [PATCH] Fix title info API (Closes #1290) (#1303) --- Daybreak.API/Interop/GuildWars/GuildWarsArray.cs | 10 ++++++++++ Daybreak.API/Interop/GuildWars/PlayerContext.cs | 7 +++++-- Daybreak.API/Interop/GuildWars/Title.cs | 10 +++++++++- Daybreak.API/Services/Interop/GameThreadService.cs | 6 +++--- Daybreak.API/Services/MainPlayerService.cs | 14 +++++--------- Daybreak.Shared/Models/Guildwars/Map.cs | 2 ++ 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Daybreak.API/Interop/GuildWars/GuildWarsArray.cs b/Daybreak.API/Interop/GuildWars/GuildWarsArray.cs index 2f42bf80..a1af610a 100644 --- a/Daybreak.API/Interop/GuildWars/GuildWarsArray.cs +++ b/Daybreak.API/Interop/GuildWars/GuildWarsArray.cs @@ -12,6 +12,16 @@ public readonly unsafe struct GuildWarsArray : IEnumerable public readonly uint Size; public readonly uint Param; + public T this[int index] + { + get + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Size); + return this.Buffer[index]; + } + } + public Enumerator GetEnumerator() => new(this.Buffer, this.Size); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); diff --git a/Daybreak.API/Interop/GuildWars/PlayerContext.cs b/Daybreak.API/Interop/GuildWars/PlayerContext.cs index fc751766..3232c5d4 100644 --- a/Daybreak.API/Interop/GuildWars/PlayerContext.cs +++ b/Daybreak.API/Interop/GuildWars/PlayerContext.cs @@ -9,7 +9,7 @@ public enum PlayerContextFlags : uint PvP = 0x800 } -[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x4C)] +[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x50)] public readonly unsafe struct PlayerContext { [FieldOffset(0x0000)] @@ -37,8 +37,11 @@ public readonly unsafe struct PlayerContext public readonly uint ActiveTitleTier; [FieldOffset(0x0034)] - public readonly uint PlayerNumber; + public readonly uint ReforgedOrDhuumsFlags; [FieldOffset(0x0038)] + public readonly uint PlayerNumber; + + [FieldOffset(0x003C)] public readonly uint PartySize; } diff --git a/Daybreak.API/Interop/GuildWars/Title.cs b/Daybreak.API/Interop/GuildWars/Title.cs index 31707de1..be64dbe3 100644 --- a/Daybreak.API/Interop/GuildWars/Title.cs +++ b/Daybreak.API/Interop/GuildWars/Title.cs @@ -11,16 +11,24 @@ public enum TitleProps : uint // Note: The HasTiers check uses (props & 3) == 2, which means bit 1 is set but bit 0 is not } -[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 40)] +[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x2C)] public readonly struct TitleContext { + [FieldOffset(0x0000)] public readonly TitleProps Props; + [FieldOffset(0x0004)] public readonly uint CurrentPoints; + [FieldOffset(0x0008)] public readonly uint CurrentTitleTierIndex; + [FieldOffset(0x000C)] public readonly uint PointsNeededForCurrentRank; + [FieldOffset(0x0014)] public readonly uint NextTitleTierIndex; + [FieldOffset(0x0018)] public readonly uint PointsNeededForNextRank; + [FieldOffset(0x001C)] public readonly uint MaxTitleRank; + [FieldOffset(0x0020)] public readonly uint MaxTitleTierIndex; } diff --git a/Daybreak.API/Services/Interop/GameThreadService.cs b/Daybreak.API/Services/Interop/GameThreadService.cs index 6e23f238..6ef4b146 100644 --- a/Daybreak.API/Services/Interop/GameThreadService.cs +++ b/Daybreak.API/Services/Interop/GameThreadService.cs @@ -16,7 +16,7 @@ public sealed class GameThreadService [SuppressUnmanagedCodeSecurity] [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void LeaveGameThread(nint ctx); + private delegate void LeaveGameThread(nint arg1, nint arg2); private readonly MemoryScanningService memoryScanningService; private readonly GWHook leaveGameThreadHook; @@ -103,7 +103,7 @@ public sealed class GameThreadService scopedLogger.LogInformation("Hooks initialized"); } - private void OnLeaveGameThread(nint ctx) + private void OnLeaveGameThread(nint arg1, nint arg2) { var scopedLogger = this.logger.CreateScopedLogger(); while(this.queuedItems.TryDequeue(out var item)) @@ -137,6 +137,6 @@ public sealed class GameThreadService } } - this.leaveGameThreadHook.Continue(ctx); + this.leaveGameThreadHook.Continue(arg1, arg2); } } diff --git a/Daybreak.API/Services/MainPlayerService.cs b/Daybreak.API/Services/MainPlayerService.cs index 3148b553..7512dd3c 100644 --- a/Daybreak.API/Services/MainPlayerService.cs +++ b/Daybreak.API/Services/MainPlayerService.cs @@ -392,19 +392,15 @@ public sealed class MainPlayerService : IDisposable return default; } - if (!gameContext.TryGetPlayerId(out var playerId)) + var playerIndex = gameContext.Pointer->CharContext->PlayerNumber; + var players = gameContext.Pointer->WorldContext->Players; + if (playerIndex < 0 || playerIndex >= players.Size) { - scopedLogger.LogError("Failed to get player id"); - return default; - } - - var player = gameContext.Pointer->WorldContext->Players.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerId); - if (player.AgentId != playerId) - { - scopedLogger.LogError("Failed to find player with id {playerId}", playerId); + scopedLogger.LogError("Failed to get player id. Got {playerNumber}", playerIndex); return default; } + var player = gameContext.Pointer->WorldContext->Players[(int)playerIndex]; var currentTier = player.ActiveTitleTier; TitleContext? currentTitle = default; int? id = -1; diff --git a/Daybreak.Shared/Models/Guildwars/Map.cs b/Daybreak.Shared/Models/Guildwars/Map.cs index a6256956..8f32e9b2 100644 --- a/Daybreak.Shared/Models/Guildwars/Map.cs +++ b/Daybreak.Shared/Models/Guildwars/Map.cs @@ -653,6 +653,7 @@ public sealed class Map : IWikiEntity public static readonly Map TheElusiveGolemancerMission = new() { Id = 769, Name = "The Elusive Golemancer Mission", WikiUrl = "https://wiki.guildwars.com/wiki/The_Elusive_Golemancer" }; public static readonly Map SecretLairOftheSnowmen2 = new() { Id = 770, Name = "Secret Lair of the Snowmen2", WikiUrl = "https://wiki.guildwars.com/wiki/Secret_Lair_of_the_Snowmen" }; public static readonly Map SecretLairOftheSnowmen3 = new() { Id = 782, Name = "Secret Lair of the Snowmen3", WikiUrl = "https://wiki.guildwars.com/wiki/Secret_Lair_of_the_Snowmen" }; + public static readonly Map PikenSquarePreSearingOutpost = new() { Id = 779, Name = "Piken Square", WikiUrl = "https://wiki.guildwars.com/wiki/Piken_Square_(pre-Searing)" }; public static readonly Map DroknarsForgeCinematic = new() { Id = 783, Name = "Droknars Forge (cinematic)", WikiUrl = "https://wiki.guildwars.com/wiki/Droknar's_Forge" }; public static readonly Map IsleOfTheNamelessPvP = new() { Id = 784, Name = "Isle of the Nameless PvP", WikiUrl = "https://wiki.guildwars.com/wiki/Isle_of_the_Nameless_(PvP)" }; public static readonly Map TempleOfTheAgesROX = new() { Id = 785, Name = "Temple of the Ages ROX", WikiUrl = "https://wiki.guildwars.com/wiki/Deactivating_R.O.X." }; @@ -1357,6 +1358,7 @@ public sealed class Map : IWikiEntity TheElusiveGolemancerMission, SecretLairOftheSnowmen2, SecretLairOftheSnowmen3, + PikenSquarePreSearingOutpost, DroknarsForgeCinematic, IsleOfTheNamelessPvP, TempleOfTheAgesROX,