Compare commits

...
Author SHA1 Message Date
Alexandru Macocian 310dbd59b8 Move to fully managed frame 2026-03-06 15:43:20 +01:00
Alexandru Macocian eae44fb576 Adjust layout pass 2026-03-06 15:43:20 +01:00
Alexandru Macocian 8d847159f3 Try to manually place frame 2026-03-06 15:43:20 +01:00
Alexandru Macocian 82514d1707 Fix kMeasureContent 2026-03-06 15:43:20 +01:00
Alexandru Macocian 84ee5a9faf GWCA 2026-03-06 15:43:19 +01:00
Alexandru Macocian 2634c32b22 GWCA 2026-03-06 15:43:19 +01:00
Alexandru Macocian 538e56d0a9 Fix 2026-03-06 15:43:19 +01:00
Alexandru Macocian 59b1acf9df fixes to the labels 2026-03-06 15:43:19 +01:00
Alexandru Macocian 9c3f644869 diag info 2026-03-06 15:43:19 +01:00
Alexandru Macocian 0240acb37a Compute layout independently 2026-03-06 15:43:19 +01:00
Alexandru Macocian 02c24e6e86 GWCA fixes 2026-03-06 15:43:19 +01:00
Alexandru Macocian 53da02d440 GWCA fixes 2026-03-06 15:43:19 +01:00
Alexandru Macocian 06c27da672 Fix for UI instability 2026-03-06 15:43:19 +01:00
Alexandru Macocian 54bf57e6c1 Reentrancy guard 2026-03-06 15:43:18 +01:00
Alexandru Macocian 733d6fc9f6 Updates to GWCA 2026-03-06 15:43:18 +01:00
amacocianandAlexandru Macocian 46943e940b First working iteration 2026-03-06 15:43:18 +01:00
amacocianandAlexandru Macocian 13985d703f Adjut populate skill data 2026-03-06 15:43:18 +01:00
amacocianandAlexandru Macocian e01aa28e01 Fix templates summary callback 2026-03-06 15:43:18 +01:00
amacocianandAlexandru Macocian f5eb67fd1c Update GWCA 2026-03-06 15:43:18 +01:00
amacocianandAlexandru Macocian d380949b45 Remove debug logging 2026-03-06 15:43:11 +01:00
amacocianandAlexandru Macocian b23811604d Fix hooking 2026-03-06 15:43:11 +01:00
amacocianandAlexandru Macocian 7a665491f1 Adjustments 2026-03-06 15:43:11 +01:00
amacocianandAlexandru Macocian a966906d6e Reinit load skill template hook 2026-03-06 15:43:11 +01:00
amacocianandAlexandru Macocian 5b554707bf Implement hooking in PartyService 2026-03-06 15:43:11 +01:00
amacocianandAlexandru Macocian aeb2c9cc99 Switch to base roslyn server 2026-03-06 15:42:56 +01:00
amacocianandAlexandru Macocian c1aaf2d1e7 Fix party loading by template 2026-03-06 15:42:55 +01:00
amacocianandAlexandru Macocian 98fa8ffa3f Rebase onto release 2026-03-06 15:42:55 +01:00
19c7b6a852 Fix template loading from API (#1495)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-03-06 15:41:25 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0905bf05cb Bump actions/download-artifact from 7 to 8 (#1492)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 11:58:38 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
132d032546 Bump actions/upload-artifact from 6 to 7 (#1493)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 11:58:17 +01:00
Brendan McCoyandGitHub 78646fd1b5 Add dye tint support to inventory item model (#1490)
Switched to using GWCA's GetBagByIndex for safer bag pointer access.
2026-02-28 17:17:24 +01:00
github-actions[bot] d8f92b4b91 Bump version to 0.9.10.5 2026-02-26 22:21:39 +00:00
49 changed files with 3865 additions and 1529 deletions
+9 -9
View File
@@ -49,7 +49,7 @@ jobs:
shell: pwsh
- name: Upload x86 artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: x86-components
path: |
@@ -89,7 +89,7 @@ jobs:
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Download x86 artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: x86-components
path: Publish/
@@ -99,7 +99,7 @@ jobs:
shell: pwsh
- name: Upload Windows build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: windows-build
path: Publish/
@@ -133,7 +133,7 @@ jobs:
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Download x86 artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: x86-components
path: Publish/
@@ -149,7 +149,7 @@ jobs:
chmod +x Publish/Installer/Daybreak.Installer
- name: Upload Linux build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: linux-build
path: Publish/
@@ -169,7 +169,7 @@ jobs:
steps:
# ── Windows package ──
- name: Download Windows build
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: windows-build
path: windows-publish/
@@ -187,7 +187,7 @@ jobs:
# ── Linux package ──
- name: Download Linux build
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: linux-build
path: linux-publish/
@@ -206,7 +206,7 @@ jobs:
cd linux-publish && zip -r ../daybreakv${{ env.Version }}-linux.zip . && cd ..
- name: Upload release artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: release-zips
path: |
@@ -234,7 +234,7 @@ jobs:
submodules: recursive
- name: Download release artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: release-zips
path: release-assets/
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"ms-dotnettools.csharp"
],
"unwantedRecommendations": [
"ms-dotnettools.csdevkit"
]
}
+6 -2
View File
@@ -3,9 +3,13 @@
"configurations": [
{
"name": "Debug Daybreak.Linux",
"type": "dotnet",
"type": "coreclr",
"request": "launch",
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj"
"program": "${workspaceFolder}/Daybreak.Linux/bin/x64/Debug/net10.0/linux-x64/Daybreak.dll",
"cwd": "${workspaceFolder}/Daybreak.Linux/bin/x64/Debug/net10.0/linux-x64",
"preLaunchTask": "build-linux",
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": "Debug Daybreak.Windows",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"dotnet.defaultSolution": "Daybreak.slnx",
"omnisharp.projectLoadTimeout": 120,
"dotnet.preferCSharpExtension": true,
"csharp.semanticHighlighting.enabled": true,
"search.followSymlinks": false,
"search.exclude": {
+36
View File
@@ -24,6 +24,42 @@
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Daybreak.slnx",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/Daybreak.slnx",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/Daybreak.slnx"
],
"problemMatcher": "$msCompile"
}
]
}
@@ -1,2 +1,10 @@
{
"Logging": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning"
}
}
}
}
@@ -1,10 +0,0 @@
{
"Logging": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning"
}
}
}
}
@@ -1,2 +1,10 @@
{
"Logging": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning"
}
}
}
}
@@ -1,5 +1,4 @@
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
@@ -14,8 +13,8 @@ public sealed class PartyController(PartyService partyService)
[GenerateGet("loadout")]
[EndpointName("GetPartyLoadout")]
[EndpointSummary("Get the current party loadout")]
[EndpointDescription("Get the current party loadout. Returns a json serialized PartyLoadout object")]
[ProducesResponseType<PartyLoadout>(StatusCodes.Status200OK)]
[EndpointDescription("Get the current party loadout. Returns the encoded party loadout template code")]
[ProducesResponseType<string>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetPartyLoadout(
CancellationToken cancellationToken)
@@ -27,12 +26,12 @@ public sealed class PartyController(PartyService partyService)
[GeneratePost("loadout")]
[EndpointName("SetPartyLoadout")]
[EndpointSummary("Set the current party loadout")]
[EndpointDescription("Set the current party loadout")]
[EndpointDescription("Set the current party loadout from an encoded template code")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> SetPartyLoadout([FromBody] PartyLoadout partyLoadout, CancellationToken cancellationToken)
public async Task<IResult> SetPartyLoadout([FromQuery(Name = "code")] string? code, CancellationToken cancellationToken)
{
var result = await this.partyService.SetPartyLoadout(partyLoadout, cancellationToken);
var result = await this.partyService.SetPartyLoadout(code ?? string.Empty, cancellationToken);
return result ? Results.Ok() : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
}
+7 -1
View File
@@ -6,7 +6,6 @@
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x86</RuntimeIdentifier>
<StripSymbols>true</StripSymbols>
<InteropExports>true</InteropExports>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
@@ -27,7 +26,14 @@
<ResolveStaticWebAssetsInputsDependsOn>RemoveDuplicateStaticWebAssets;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<!-- Faster ILC compilation for iterative development -->
<IlcOptimizationPreference>None</IlcOptimizationPreference>
<IlcTrimMetadata>false</IlcTrimMetadata>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<StripSymbols>true</StripSymbols>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
+67 -15
View File
@@ -19,12 +19,25 @@ using Net.Sdk.Web;
namespace Daybreak.API;
public class EntryPoint
public static class EntryPoint
{
private const bool IsDebug =
#if DEBUG
true;
#else
false;
#endif
private const int StartPort = 5080;
private static readonly TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5);
private static readonly CancellationTokenSource CancellationTokenSource = new();
// GWCA log handler - must be static to prevent GC
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void GWCALogHandler(nint context, int level, nint msg, nint file, uint line, nint function);
private static GWCALogHandler? gwcaLogHandlerDelegate;
private static ILogger? gwcaLogger;
[UnmanagedCallersOnly(EntryPoint = "ThreadInit"), STAThread]
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
[RequiresDynamicCode("The handler uses a static method, so there's no dynamic code to worry about")]
@@ -34,20 +47,27 @@ public class EntryPoint
{
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", null, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("ASPNETCORE_PREVENTHOSTINGSTARTUP", "true", EnvironmentVariableTarget.Process);
#if DEBUG
ConsoleExtensions.AllocateAnsiConsole();
#endif
// Enable console if GW was launched with --debug-api or if we're running a debug build
var args = Environment.GetCommandLineArgs();
if (IsDebug ||
args.Any(arg => arg.Equals("--debug-api", StringComparison.OrdinalIgnoreCase) ||
arg.Equals("-debug-api", StringComparison.OrdinalIgnoreCase)))
{
NativeMethods.AllocConsole();
}
var port = FindAvailablePort(StartPort);
var app = CreateApplication(port);
var runTask = Task.Run(() => StartServer(app), CancellationTokenSource.Token);
var scopedLogger = app.Services.GetRequiredService<ILogger<EntryPoint>>().CreateScopedLogger();
var scopedLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(EntryPoint));
var healthCheck = app.Services.GetRequiredService<HealthCheckService>();
var sw = Stopwatch.StartNew();
var healthy = false;
while (sw.Elapsed < InitializationTimeout)
{
var status = Task.Run(() => healthCheck.CheckHealthAsync()).Result;
scopedLogger.LogWarning("HealthCheck status: {status} in {duration}. Report: {report}", status.Status, status.TotalDuration, string.Join("\n", status.Entries.Select(e => $"{e.Key}: {e.Value.Status}")));
scopedLogger.LogWarning("{methodName}: HealthCheck status: {status} in {duration}. Report: {report}", nameof(ThreadInit), status.Status, status.TotalDuration, string.Join("\n", status.Entries.Select(e => $"{e.Key}: {e.Value.Status}")));
if (status.Status is HealthStatus.Healthy)
{
healthy = true;
@@ -61,12 +81,12 @@ public class EntryPoint
if (healthy)
{
scopedLogger.LogInformation("Daybreak API is healthy on port {port}. Initialization succeeded in {duration}", port, sw.Elapsed);
scopedLogger.LogInformation("{methodName}: Daybreak API is healthy on port {port}. Initialization succeeded in {duration}", nameof(ThreadInit), port, sw.Elapsed);
return port;
}
else
{
scopedLogger.LogError("Daybreak API failed to initialize in {duration}", sw.Elapsed);
scopedLogger.LogError("{methodName}: Daybreak API failed to initialize in {duration}", nameof(ThreadInit), sw.Elapsed);
CancellationTokenSource.Cancel();
return -1;
}
@@ -133,22 +153,54 @@ public class EntryPoint
private static async Task StartServer(WebApplication app)
{
var scopedLogger = app.Services.GetRequiredService<ILogger<EntryPoint>>().CreateScopedLogger();
var scopedLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(EntryPoint));
try
{
scopedLogger.LogDebug("Initializing GWCA");
scopedLogger.LogDebug("{methodName}: Initializing GWCA", nameof(StartServer));
PreloadNativeDependencies(scopedLogger);
SetupGWCALogging(app);
scopedLogger.LogDebug("{methodName}: GWCA log handler registered", nameof(StartServer));
var result = GWCA.GW.Initialize();
scopedLogger.LogDebug($"GWCA.GW.Initialize() returned: {result}");
scopedLogger.LogDebug("{methodName}: GWCA.GW.Initialize() returned: {result}", nameof(StartServer), result);
await app.RunAsync();
}
catch (Exception ex)
{
scopedLogger.LogCritical(ex, $"Encountered fatal error while starting API server {ex}");
scopedLogger.LogCritical(ex, "{methodName}: Encountered fatal error while starting API server {exception}", nameof(StartServer), ex);
throw;
}
}
private static void SetupGWCALogging(WebApplication app)
{
gwcaLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("GWCA");
gwcaLogHandlerDelegate = OnGWCALog;
var handlerPtr = Marshal.GetFunctionPointerForDelegate(gwcaLogHandlerDelegate);
GWCA.GW.RegisterLogHandler(handlerPtr, nint.Zero);
}
private static void OnGWCALog(nint context, int level, nint msg, nint file, uint line, nint function)
{
var msgStr = msg != nint.Zero ? Marshal.PtrToStringAnsi(msg) : "<null>";
var fileStr = file != nint.Zero ? Marshal.PtrToStringAnsi(file) : "<null>";
var funcStr = function != nint.Zero ? Marshal.PtrToStringAnsi(function) : "<null>";
var logLevel = level switch
{
0 => LogLevel.Trace, // LEVEL_TRACE
1 => LogLevel.Debug, // LEVEL_DEBUG
2 => LogLevel.Information, // LEVEL_INFO
3 => LogLevel.Warning, // LEVEL_WARN
4 => LogLevel.Error, // LEVEL_ERR
5 => LogLevel.Critical, // LEVEL_CRITICAL
_ => LogLevel.Debug
};
gwcaLogger?.Log(logLevel, "{gwcaSource}: {file}:{line} ({function}) {message}", nameof(GWCA), fileStr, line, funcStr, msgStr);
}
private static int FindAvailablePort(int startPort)
{
var port = startPort;
@@ -182,7 +234,7 @@ public class EntryPoint
return true;
}
private static void PreloadNativeDependencies(ScopedLogger<EntryPoint> logger)
private static void PreloadNativeDependencies(ILogger logger)
{
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
@@ -193,11 +245,11 @@ public class EntryPoint
if (File.Exists(gwcaPath))
{
NativeLibrary.Load(gwcaPath);
logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}");
logger.LogDebug("{methodName}: Preloaded gwca.dll from {path}", nameof(PreloadNativeDependencies), gwcaPath);
}
else
{
logger.LogError($"gwca.dll not found at {gwcaPath}");
logger.LogError("{methodName}: gwca.dll not found at {path}", nameof(PreloadNativeDependencies), gwcaPath);
}
break;
+10 -27
View File
@@ -1,7 +1,6 @@
using Daybreak.API.Models;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Daybreak.API.Services.Interop;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using ZLinq;
namespace Daybreak.API.Health;
@@ -12,37 +11,21 @@ public static class WebApplicationExtensions
[RequiresDynamicCode("The handler uses a static method, so there's no dynamic code to worry about")]
public static WebApplication UseHealthChecks(this WebApplication app)
{
app.MapGet("/api/v1/rest/health", static async (HealthCheckService healthCheckService, CancellationToken cancellationToken) =>
app.MapGet("/api/v1/rest/health", static (IEnumerable<IInteropHealthService> services) =>
{
var report = await healthCheckService.CheckHealthAsync(cancellationToken);
var response = new HealthCheckResponse
{
ProcessId = Environment.ProcessId,
Status = report.Status,
TotalDuration = report.TotalDuration,
Entries = report.Entries.ToDictionary(
e => e.Key,
e => new HealthCheckEntryResponse
{
Status = e.Value.Status,
Description = e.Value.Description ?? string.Empty,
Tags = [.. e.Value.Tags],
Data = e.Value.Data
.Where(kvp => kvp.Value is JsonElement je)
.ToDictionary(kvp => kvp.Key,
kvp => (JsonElement)kvp.Value)
})
};
var entries = services
.SelectMany(s => s.GetAddressHealth())
.ToList();
var response = new HealthCheckResponse(Environment.ProcessId, entries);
return Results.Json(response);
})
.WithName("Health")
.WithSummary("Service health")
.WithDescription($"Current health status of the service. Returns a serialized object of type {nameof(HealthCheckResponse)}")
.WithTags("Service")
.WithSummary("Interop hook health")
.WithDescription("Returns the status of all low-level game hooks")
.WithTags("Health")
.Produces<HealthCheckResponse>(StatusCodes.Status200OK)
.WithOpenApi();
return app;
}
}
+7 -3
View File
@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using Daybreak.API.Interop.GuildWars;
namespace Daybreak.API.Interop;
@@ -18,17 +19,20 @@ public readonly struct Frame
[FieldOffset(0x0024)]
public readonly uint TemplateType;
[FieldOffset(0x00b8)]
[FieldOffset(0x00B8)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00bc)]
[FieldOffset(0x00BC)]
public readonly uint FrameId;
[FieldOffset(0x00D8)]
public readonly FramePositionData Position;
[FieldOffset(0x018C)]
public readonly uint FrameState;
public bool IsCreated => (this.FrameState & 0x4) != 0;
public bool IsHidden => (this.FrameState & 0x200) != 0;
public bool IsVisible => !this.IsHidden;
public bool IsDIsabled => (this.FrameState & 0x10) != 0;
public bool IsDisabled => (this.FrameState & 0x10) != 0;
}
+26
View File
@@ -0,0 +1,26 @@
using System.Core.Extensions;
namespace Daybreak.API.Interop;
public sealed class GWAddressCache(Func<nuint> provider)
{
private readonly Func<nuint> provider = provider.ThrowIfNull();
private nuint? cachedAddress;
public nuint? GetAddress()
{
if (this.cachedAddress.HasValue)
{
return this.cachedAddress.Value;
}
var addr = this.provider();
if (addr is 0x0)
{
return null;
}
this.cachedAddress = addr;
return addr;
}
}
File diff suppressed because it is too large Load Diff
+312
View File
@@ -0,0 +1,312 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
public abstract class GWFastCall(GWAddressCache target)
{
public readonly struct Void { }
public GWAddressCache Cache { get; } = target;
public abstract void Initialize();
/// <summary>
/// Builds a thunk for __fastcall with 1 parameter (ECX only).
/// </summary>
protected unsafe static nint BuildFastCallThunk1(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[10]
{
0x58, // pop eax (ret)
0x59, // pop ecx (arg1)
0x50, // push eax (ret)
0xB8, 0, 0, 0, 0, // mov eax, TARGET
0xFF, 0xE0 // jmp eax
}; // 10 bytes total
const int TARGET_OFFSET = 4;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 10, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
if (mem is 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
}
Buffer.MemoryCopy(code, (void*)mem, 10, 10);
return mem;
}
/// <summary>
/// Builds a thunk for __fastcall with 2+ parameters (ECX and EDX).
/// </summary>
protected unsafe static nint BuildFastCallThunk2(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[11]
{
0x58, // pop eax (ret)
0x59, // pop ecx (ctx)
0x5A, // pop edx (edxVal)
0x50, // push eax (ret)
0xB8,0,0,0,0, // mov eax, TARGET
0xFF,0xE0 // jmp eax
}; // 11 bytes total
const int TARGET_OFFSET = 5;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 11, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
if (mem is 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
}
Buffer.MemoryCopy(code, (void*)mem, 11, 11);
return mem;
}
}
public unsafe sealed class GWFastCall<T1, T2>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T2? Invoke(T1 t1)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T2) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, void>)this.func)(t1);
return default;
}
return this.func(t1);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk1(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T3? Invoke(T1 t1, T2 t2)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T3) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, void>)this.func)(t1, t2);
return default;
}
return this.func(t1, t2);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T4? Invoke(T1 t1, T2 t2, T3 t3)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T4) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, void>)this.func)(t1, t2, t3);
return default;
}
return this.func(t1, t2, t3);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T5? Invoke(T1 t1, T2 t2, T3 t3, T4 t4)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T5) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, void>)this.func)(t1, t2, t3, t4);
return default;
}
return this.func(t1, t2, t3, t4);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T6? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T6) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, void>)this.func)(t1, t2, t3, t4, t5);
return default;
}
return this.func(t1, t2, t3, t4, t5);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6, T7>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T7? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T7) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, void>)this.func)(t1, t2, t3, t4, t5, t6);
return default;
}
return this.func(t1, t2, t3, t4, t5, t6);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7>)call;
}
}
+243
View File
@@ -0,0 +1,243 @@
using System.Runtime.InteropServices;
using MinHook;
namespace Daybreak.API.Interop;
/// <summary>
/// A thin convenience wrapper around MinHook that
/// resolves the target address lazily through <see cref="GWAddressCache"/>
/// unwraps an existing "CALL/JMP rel32" stub so we patch the real code
/// exposes <see cref="Original"/> so your detour can chain
/// </summary>
public sealed class GWHook<T>(
GWAddressCache funcAddress,
T detour,
bool bypassPreviousHooks = false)
where T : Delegate
{
private const uint PAGE_EXECUTE_READWRITE = 0x40;
private readonly bool bypassPreviousHooks = bypassPreviousHooks;
private readonly GWAddressCache addressCache = funcAddress ?? throw new ArgumentNullException(nameof(funcAddress));
private readonly T detour = detour ?? throw new ArgumentNullException(nameof(detour));
private readonly SemaphoreSlim semaphore = new(1, 1);
private T? cont; // trampoline returned by MinHook, or previous hook's detour
private HookEngine? engine;
private bool usedJmpPatch; // true if we patched an existing JMP instead of using MinHook
private int originalRel32; // saved rel32 for restoration when usedJmpPatch
/// <summary>Delegate that calls the next hook / real function.</summary>
public T Continue => this.cont ??
throw new InvalidOperationException("Hook not initialised call EnsureInitialized() first.");
public bool Hooked { get; private set; } = false;
public nuint TargetAddress { get; private set; }
public nuint ContinueAddress { get; private set; }
public nuint DetourAddress { get; private set; }
/// <summary>Installs the hook exactly once (thread-safe).</summary>
public bool EnsureInitialized()
{
if (this.Hooked)
{
return true;
}
this.semaphore.Wait();
try
{
if (this.Hooked)
{
return true;
}
var addr = this.addressCache.GetAddress();
if (addr.HasValue is false ||
addr.Value is 0)
{
return false;
}
var target = this.bypassPreviousHooks
? UnwrapNearBranch(addr.Value)
: addr.Value;
if (target is 0)
{
return false;
}
// If not bypassing previous hooks and target starts with JMP, use our JMP patching
// This handles the case where GWCA already hooked the function
if (!this.bypassPreviousHooks && StartsWithJmp(target))
{
if (this.TryPatchExistingJmp(target))
{
return true;
}
}
// Try MinHook for normal (unhooked) functions
if (this.TryMinHook(target))
{
return true;
}
// Fallback: try JMP patching even if we didn't detect JMP initially
// (in case StartsWithJmp was wrong or MinHook failed for other reasons)
if (this.TryPatchExistingJmp(target))
{
return true;
}
return false;
}
finally
{
this.semaphore.Release();
}
}
public void Dispose()
{
this.semaphore.Wait();
try
{
if (!this.Hooked)
{
return;
}
if (this.usedJmpPatch)
{
// Restore the original JMP target
RestoreJmpRel32(this.TargetAddress, this.originalRel32);
}
else if (this.engine is not null)
{
this.engine.DisableHooks();
this.engine.Dispose();
}
this.cont = null;
this.Hooked = false;
}
finally
{
this.semaphore.Release();
}
}
private bool TryMinHook(nuint target)
{
try
{
this.engine = new HookEngine();
this.cont = this.engine.CreateHook((nint)target, this.detour);
this.engine.EnableHooks();
this.TargetAddress = target;
this.ContinueAddress = (nuint)this.cont.Method.MethodHandle.GetFunctionPointer();
this.DetourAddress = (nuint)this.detour.Method.MethodHandle.GetFunctionPointer();
this.Hooked = true;
this.usedJmpPatch = false;
return true;
}
catch
{
this.engine?.Dispose();
this.engine = null;
return false;
}
}
/// <summary>
/// If the target starts with JMP rel32 (0xE9), another hook is already installed.
/// We patch that JMP to point to our detour, and call the previous hook's detour as Continue.
/// </summary>
private unsafe bool TryPatchExistingJmp(nuint target)
{
var p = (byte*)target;
if (*p != 0xE9)
{
return false;
}
// Read the existing rel32 offset (points to previous hook's detour)
var existingRel32 = *(int*)(p + 1);
var previousDetourAddr = (nuint)(p + 5 + existingRel32);
// Create a delegate to call the previous hook
this.cont = Marshal.GetDelegateForFunctionPointer<T>((nint)previousDetourAddr);
// Calculate new rel32 to point to our detour
var ourDetourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var newRel32 = (int)((nint)ourDetourPtr - (nint)(p + 5));
// Patch the JMP
if (!PatchJmpRel32(target, newRel32))
{
this.cont = null;
return false;
}
this.originalRel32 = existingRel32;
this.TargetAddress = target;
this.ContinueAddress = previousDetourAddr;
this.DetourAddress = (nuint)ourDetourPtr;
this.Hooked = true;
this.usedJmpPatch = true;
return true;
}
private static unsafe bool PatchJmpRel32(nuint jmpAddr, int newRel32)
{
var rel32Ptr = (byte*)(jmpAddr + 1);
if (!NativeMethods.VirtualProtect(rel32Ptr, 4, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
try
{
*(int*)rel32Ptr = newRel32;
return true;
}
finally
{
NativeMethods.VirtualProtect(rel32Ptr, 4, oldProtect, out _);
}
}
/// <summary>
/// Checks if the target address starts with a JMP rel32 instruction (0xE9).
/// </summary>
private static unsafe bool StartsWithJmp(nuint addr)
{
var p = (byte*)addr;
return *p == 0xE9;
}
private static unsafe void RestoreJmpRel32(nuint jmpAddr, int originalRel32)
{
var rel32Ptr = (byte*)(jmpAddr + 1);
if (NativeMethods.VirtualProtect(rel32Ptr, 4, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
*(int*)rel32Ptr = originalRel32;
NativeMethods.VirtualProtect(rel32Ptr, 4, oldProtect, out _);
}
}
/// <summary>
/// Follows a <c>CALL rel32</c> (<c>E8</c>) **or** <c>JMP rel32</c> (<c>E9</c>)
/// trampoline that may have been planted by a previous hook.
/// </summary>
private static unsafe nuint UnwrapNearBranch(nuint addr)
{
var p = (byte*)addr;
if (*p != 0xE8 && *p != 0xE9)
return addr;
var rel = *(int*)(p + 1);
return (nuint)(p + 5 + rel);
}
}
@@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
public readonly struct BitReader
{
[FieldOffset(0x0000)]
public readonly int HasError;
[FieldOffset(0x0004)]
public readonly int RackBitCount;
[FieldOffset(0x0008)]
public readonly long RackData;
[FieldOffset(0x0010)]
public readonly nint CurrentPtr;
[FieldOffset(0x0014)]
public readonly nint StartPtr;
[FieldOffset(0x0018)]
public readonly nint EndPtr;
}
@@ -1,11 +1,11 @@
namespace Daybreak.API.Interop.GuildWars;
public readonly unsafe struct WrappedPointer<T>(T* pointer)
public unsafe readonly struct WrappedPointer<T>(T* pointer)
where T : unmanaged
{
public readonly T* Pointer = pointer;
public bool IsNull => this.Pointer is null;
public readonly bool IsNull => this.Pointer is null;
public static implicit operator WrappedPointer<T>(T* pointer) => new(pointer);
@@ -0,0 +1,16 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x94)]
public struct TemplateSummaryFrameData
{
[FieldOffset(0x00)]
public uint FrameId;
[FieldOffset(0x04)]
public GuildWars.SkillTemplate SkillTemplate;
[FieldOffset(0x90)]
public uint AgentId;
}
+3
View File
@@ -0,0 +1,3 @@
namespace Daybreak.API.Models;
public sealed record AddressHealth(string Name, string Address, bool Hooked);
+4 -2
View File
@@ -2,9 +2,11 @@
namespace Daybreak.API.Models;
public sealed class CallbackRegistration(Guid uid, Action onDispose) : IDisposable
public sealed class CallbackRegistration<T>(Guid uid, T callback, Action onDispose) : IDisposable
where T : Delegate
{
private readonly Guid uid = uid;
public Guid Uid { get; } = uid;
public T Callback { get; } = callback.ThrowIfNull();
private readonly Action onDispose = onDispose.ThrowIfNull();
public void Dispose()
@@ -1,14 +0,0 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.API.Models;
public sealed class HealthCheckEntryResponse
{
[JsonConverter(typeof(JsonStringEnumConverter<HealthStatus>))]
public required HealthStatus Status { get; set; }
public required string Description { get; set; }
public required Dictionary<string, JsonElement> Data { get; set; }
public required List<string> Tags { get; set; }
}
+1 -11
View File
@@ -1,13 +1,3 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json.Serialization;
namespace Daybreak.API.Models;
public sealed class HealthCheckResponse
{
public required int ProcessId { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter<HealthStatus>))]
public required HealthStatus Status { get; set; }
public required TimeSpan TotalDuration { get; set; }
public required Dictionary<string, HealthCheckEntryResponse> Entries { get; set; }
}
public sealed record HealthCheckResponse(int ProcessId, IEnumerable<AddressHealth> AddressHealth);
@@ -15,8 +15,9 @@ namespace Daybreak.API.Serialization;
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(AddressHealth))]
[JsonSerializable(typeof(List<AddressHealth>))]
[JsonSerializable(typeof(HealthCheckResponse))]
[JsonSerializable(typeof(HealthCheckEntryResponse))]
[JsonSerializable(typeof(MainPlayerState))]
[JsonSerializable(typeof(QuestLogInformation))]
[JsonSerializable(typeof(QuestInformation))]
@@ -25,8 +26,6 @@ namespace Daybreak.API.Serialization;
[JsonSerializable(typeof(CharacterSelectEntry))]
[JsonSerializable(typeof(BuildEntry))]
[JsonSerializable(typeof(List<BuildEntry>))]
[JsonSerializable(typeof(PartyLoadoutEntry))]
[JsonSerializable(typeof(List<PartyLoadout>))]
[JsonSerializable(typeof(InstanceInfo))]
[JsonSerializable(typeof(TitleInfo))]
[JsonSerializable(typeof(LoginInfo))]
@@ -15,7 +15,7 @@ public sealed class GameThreadService
// Prevent GC of delegates passed to native code
private readonly ConcurrentDictionary<GameThreadCallback, bool> prevent_GC_callbacks = [];
public Task QueueOnGameThread(Action action, CancellationToken cancellationToken)
public Task QueueOnGameThread(Action action, CancellationToken cancellationToken, bool forceEnqueue = false)
{
var taskCompletionSource = new TaskCompletionSource();
var item = new WorkItem(action, taskCompletionSource, cancellationToken);
@@ -47,12 +47,12 @@ public sealed class GameThreadService
this.prevent_GC_callbacks[callback] = true;
var funcPtr = Marshal.GetFunctionPointerForDelegate(callback);
GWCA.GW.GameThread.Enqueue(funcPtr, false);
GWCA.GW.GameThread.Enqueue(funcPtr, forceEnqueue);
return taskCompletionSource.Task;
}
public Task<T> QueueOnGameThread<T>(Func<T> action, CancellationToken cancellationToken)
public Task<T> QueueOnGameThread<T>(Func<T> action, CancellationToken cancellationToken, bool forceEnqueue = false)
{
var taskCompletionSource = new TaskCompletionSource<T>();
var item = new WorkItem<T>(action, taskCompletionSource, cancellationToken);
@@ -85,7 +85,7 @@ public sealed class GameThreadService
this.prevent_GC_callbacks[callback] = true;
var funcPtr = Marshal.GetFunctionPointerForDelegate(callback);
GWCA.GW.GameThread.Enqueue(funcPtr, false);
GWCA.GW.GameThread.Enqueue(funcPtr, forceEnqueue);
return taskCompletionSource.Task;
}
@@ -0,0 +1,8 @@
using Daybreak.API.Models;
namespace Daybreak.API.Services.Interop;
public interface IInteropHealthService
{
IEnumerable<AddressHealth> GetAddressHealth();
}
@@ -224,6 +224,35 @@ public sealed unsafe class MemoryScanningService(
return FindInRange(prologue, mask, 0, start, end);
}
/// <summary>
/// Finds the start of a function by scanning backwards from <paramref name="addressInFunction"/>
/// for <c>CC CC</c> padding bytes (INT 3) that MSVC emits between functions.
/// Requires at least 2 consecutive <c>CC</c> bytes to avoid false positives from
/// single <c>CC</c> bytes that appear in instruction encodings.
/// Unlike <see cref="ToFunctionStart"/>, this works even when a previous hook
/// (e.g. GWCA) has overwritten the prologue with a JMP.
/// </summary>
public nuint ToFunctionStartFromPadding(nuint addressInFunction, uint scanRange = 0x500)
{
if (addressInFunction is 0)
{
return 0;
}
var p = (byte*)addressInFunction;
var limit = p - scanRange;
for (var scan = p - 1; scan > limit; scan--)
{
// Need at least 2 consecutive CC bytes (real inter-function padding)
if (*scan == 0xCC && *(scan - 1) == 0xCC && *(scan + 1) != 0xCC)
{
return (nuint)(scan + 1);
}
}
return 0;
}
public nuint FindNthUseOfString(string str, uint nth, int offset = 0, bool useTextSection = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
@@ -1,9 +1,286 @@
using Daybreak.API.Interop;
using System.Extensions;
using System.Extensions.Core;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.Shared.Models.Builds;
using Daybreak.Shared.Services.BuildTemplates;
namespace Daybreak.API.Services.Interop;
public sealed class SkillbarContextService
public sealed class SkillbarContextService : IHostedService, IInteropHealthService
{
private const string Base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static readonly byte[] DecodeTemplateHeaderSignature =
[
0x55, 0x8B, 0xEC, 0x56,
0x8B, 0x75, 0x0C, 0x8B,
0xCE, 0x6A, 0x04, 0xE8,
0x00, 0x00, 0x00, 0x00,
0x8B, 0xC8, 0x83, 0xE1,
0x0E, 0x80, 0xF9, 0x0E,
0x75, 0x00, 0xA9, 0xF1,
0xFF, 0xFF, 0xFF, 0x74
];
private const string DecodeTemplateHeaderMask = "xxxxxxxxxxxx????xxxxxxxxx?xxxxxx";
private const int DecodeTemplateHeaderOffset = 0x0;
private const string LoadSkillTemplateAssertionFile = "TemplatesHelpers.cpp";
private const string LoadSkillTemplateAssertionMessage = "targetPrimaryProf == templateData.profPrimary";
private static readonly byte[] PopulateSkillDataSignature =
[
0x55, 0x8B, 0xEC, 0x81,
0xEC, 0x50, 0x01, 0x00,
0x00, 0xA1, 0x00, 0x00,
0x00, 0x00, 0x33, 0xC5,
0x89, 0x45, 0xFC, 0x53,
0x56, 0x8B, 0x75, 0x08
];
private const string PopulateSkillDataMask = "xxxxxxxxxx????xxxxxxxxxx";
private const int PopulateSkillDataOffset = 0x0;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate byte DecodeTemplateHeaderDelegate(SkillTemplate* outTemplate, BitReader* bitReader);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate void LoadSkillTemplateDelegate(int targetAgentId, SkillTemplate* templateData);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private unsafe delegate void PopulateSkillDataDelegate(TemplateSummaryFrameData* thisPtr, SkillTemplate* skillTemplateData);
private readonly ChatService chatService;
private readonly MemoryScanningService memoryScanningService;
private readonly IBuildTemplateManager templateManager;
private readonly GWAddressCache decodeTemplateHeaderAddressCache;
private readonly GWHook<DecodeTemplateHeaderDelegate> decodeTemplateHeaderHook;
private readonly GWAddressCache loadSkillTemplateAddressCache;
private readonly GWHook<LoadSkillTemplateDelegate> loadSkillTemplateHook;
private readonly GWAddressCache populateSkillDataAddressCache;
private readonly GWHook<PopulateSkillDataDelegate> populateSkillDataHook;
private readonly ILogger<SkillbarContextService> logger;
private readonly List<CallbackRegistration<Func<string, WrappedPointer<SkillTemplate>, bool>>> decodeTemplateHeaderCallbacks = [];
private readonly List<CallbackRegistration<Func<SkillTemplate, bool>>> loadSkillTemplateCallbacks = [];
private readonly List<CallbackRegistration<Func<WrappedPointer<TemplateSummaryFrameData>, WrappedPointer<SkillTemplate>, bool>>> populateSkillDataCallbacks = [];
public unsafe SkillbarContextService(
ChatService chatService,
IBuildTemplateManager buildTemplateManager,
MemoryScanningService memoryScanningService,
ILogger<SkillbarContextService> logger)
{
this.chatService = chatService;
this.templateManager = buildTemplateManager;
this.memoryScanningService = memoryScanningService;
this.decodeTemplateHeaderAddressCache = new GWAddressCache(() =>
this.memoryScanningService.FindAddress(DecodeTemplateHeaderSignature, DecodeTemplateHeaderMask, DecodeTemplateHeaderOffset));
this.decodeTemplateHeaderHook = new GWHook<DecodeTemplateHeaderDelegate>(
this.decodeTemplateHeaderAddressCache,
this.DecodeTemplateHeaderDetour);
this.loadSkillTemplateAddressCache = new GWAddressCache(() =>
{
var assertion = this.memoryScanningService.FindAssertion(
LoadSkillTemplateAssertionFile,
LoadSkillTemplateAssertionMessage);
if (assertion is 0) return 0;
return this.memoryScanningService.ToFunctionStartFromPadding(assertion);
});
this.loadSkillTemplateHook = new GWHook<LoadSkillTemplateDelegate>(
this.loadSkillTemplateAddressCache,
this.LoadSkillTemplateDetour);
this.populateSkillDataAddressCache = new GWAddressCache(() =>
this.memoryScanningService.FindAddress(PopulateSkillDataSignature, PopulateSkillDataMask, PopulateSkillDataOffset));
this.populateSkillDataHook = new GWHook<PopulateSkillDataDelegate>(
this.populateSkillDataAddressCache,
this.PopulateSkillDataDetour);
this.logger = logger;
}
public unsafe void LoadBuild(uint agentId, SkillTemplate* template) => GWCA.GW.SkillbarMgr.LoadSkillTemplate(agentId, template);
public unsafe void CallOriginalPopulateSkillData(TemplateSummaryFrameData* frameData, SkillTemplate* skillTemplateData)
{
this.populateSkillDataHook.Continue(frameData, skillTemplateData);
}
public Task StartAsync(CancellationToken cancellationToken)
{
Task.Run(async () =>
{
while (!this.decodeTemplateHeaderHook.EnsureInitialized())
{
await Task.Delay(500, cancellationToken);
}
this.logger.LogInformation(
"DecodeTemplateHeader hook installed at 0x{target:X8}",
this.decodeTemplateHeaderHook.TargetAddress);
while (!this.loadSkillTemplateHook.EnsureInitialized())
{
await Task.Delay(500, cancellationToken);
}
this.logger.LogInformation(
"LoadSkillTemplate hook installed at 0x{target:X8}",
this.loadSkillTemplateHook.TargetAddress);
while (!this.populateSkillDataHook.EnsureInitialized())
{
await Task.Delay(500, cancellationToken);
}
this.logger.LogInformation(
"PopulateSkillData hook installed at 0x{target:X8}",
this.populateSkillDataHook.TargetAddress);
}, cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.decodeTemplateHeaderHook.Dispose();
this.loadSkillTemplateHook.Dispose();
this.populateSkillDataHook.Dispose();
return Task.CompletedTask;
}
public IEnumerable<AddressHealth> GetAddressHealth()
{
yield return new AddressHealth(
"DecodeTemplateHeader",
$"0x{this.decodeTemplateHeaderHook.TargetAddress:X8}",
this.decodeTemplateHeaderHook.Hooked);
yield return new AddressHealth(
"LoadSkillTemplate",
$"0x{this.loadSkillTemplateHook.TargetAddress:X8}",
this.loadSkillTemplateHook.Hooked);
yield return new AddressHealth(
"PopulateSkillData",
$"0x{this.populateSkillDataHook.TargetAddress:X8}",
this.populateSkillDataHook.Hooked);
}
public CallbackRegistration<Func<string, WrappedPointer<SkillTemplate>, bool>> RegisterDecodeTemplateHeaderHandler(Func<string, WrappedPointer<SkillTemplate>, bool> callback)
{
var uid = Guid.NewGuid();
var registration =
new CallbackRegistration<Func<string, WrappedPointer<SkillTemplate>, bool>>(uid, callback, () => this.decodeTemplateHeaderCallbacks.RemoveAll(r => r.Uid == uid));
this.decodeTemplateHeaderCallbacks.Add(registration);
return registration;
}
public CallbackRegistration<Func<SkillTemplate, bool>> RegisterLoadSkillTemplateHandler(Func<SkillTemplate, bool> callback)
{
var uid = Guid.NewGuid();
var registration =
new CallbackRegistration<Func<SkillTemplate, bool>>(uid, callback, () => this.loadSkillTemplateCallbacks.RemoveAll(r => r.Uid == uid));
this.loadSkillTemplateCallbacks.Add(registration);
return registration;
}
public CallbackRegistration<Func<WrappedPointer<TemplateSummaryFrameData>, WrappedPointer<SkillTemplate>, bool>> RegisterPopulateSkillDataHandler(Func<WrappedPointer<TemplateSummaryFrameData>, WrappedPointer<SkillTemplate>, bool> callback)
{
var uid = Guid.NewGuid();
var registration =
new CallbackRegistration<Func<WrappedPointer<TemplateSummaryFrameData>, WrappedPointer<SkillTemplate>, bool>>(uid, callback, () => this.populateSkillDataCallbacks.RemoveAll(r => r.Uid == uid));
this.populateSkillDataCallbacks.Add(registration);
return registration;
}
private unsafe byte DecodeTemplateHeaderDetour(SkillTemplate* outTemplate, BitReader* bitReader)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.decodeTemplateHeaderCallbacks.Count > 0)
{
// Read the full raw buffer before the game consumes it
var rawBytes = ReadBitReaderBuffer(bitReader);
var templateString = ReencodeToBase64(rawBytes);
if (this.decodeTemplateHeaderCallbacks.Any(r => r.Callback(templateString, outTemplate)))
{
scopedLogger.LogDebug("DecodeTemplateHeader callback handled template: {template}", templateString);
return 1;
}
}
return this.decodeTemplateHeaderHook.Continue(outTemplate, bitReader);
}
private unsafe void LoadSkillTemplateDetour(int targetAgentId, SkillTemplate* templateData)
{
if (this.loadSkillTemplateCallbacks.Any(r => r.Callback(*templateData)))
{
return;
}
this.loadSkillTemplateHook.Continue(targetAgentId, templateData);
}
private unsafe void PopulateSkillDataDetour(TemplateSummaryFrameData* thisPtr, SkillTemplate* skillTemplateData)
{
if (this.populateSkillDataCallbacks.Any(r => r.Callback(thisPtr, skillTemplateData)))
{
return;
}
this.populateSkillDataHook.Continue(thisPtr, skillTemplateData);
}
/// <summary>
/// Copies the entire byte buffer backing a BitReader (from StartPtr to EndPtr) into a managed byte array.
/// </summary>
private static unsafe byte[] ReadBitReaderBuffer(BitReader* reader)
{
var start = reader->StartPtr;
var end = reader->EndPtr;
if (start == 0 || end == 0 || end <= start)
{
return [];
}
var length = (int)(end - start);
var buffer = new byte[length];
fixed (byte* dst = buffer)
{
Buffer.MemoryCopy((void*)start, dst, length, length);
}
return buffer;
}
/// <summary>
/// Re-encodes the raw BitReader buffer bytes back into the original base64 template string.
/// The buffer contains packed bits (8 per byte) written by Base64DecodeTemplate's BitWriter,
/// which wrote 6 bits per base64 character. We extract 6 bits at a time, LSB-first.
/// </summary>
private static string ReencodeToBase64(byte[] rawBytes)
{
var totalBits = rawBytes.Length * 8;
var charCount = totalBits / 6;
var chars = new char[charCount];
var bitPosition = 0;
for (var i = 0; i < charCount; i++)
{
var value = 0;
for (var b = 0; b < 6; b++)
{
var byteIndex = bitPosition / 8;
var bitIndex = bitPosition % 8;
if (byteIndex < rawBytes.Length && ((rawBytes[byteIndex] >> bitIndex) & 1) != 0)
{
value |= 1 << b;
}
bitPosition++;
}
chars[i] = value < Base64Alphabet.Length ? Base64Alphabet[value] : '?';
}
return new string(chars);
}
}
+11 -9
View File
@@ -4,6 +4,7 @@ using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Guildwars;
using System.Extensions.Core;
using System.Logging;
using System.Runtime.CompilerServices;
using System.Text;
@@ -41,17 +42,17 @@ public sealed class InventoryService(
return default;
}
var itemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId)>)>();
var bags = (BagStruct**)Unsafe.AsPointer(ref inventory->Bags);
var itemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint)>)>();
for (var i = 0; i < 23; i++)
{
var bag = bags[i];
// Use GWCA helper to fetch bag pointers by index — safer than reading an inline array directly.
var bag = GWCA.GW.Items.GetBagByIndex((uint)i);
if (bag is null)
{
continue;
}
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId)>();
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint)>();
itemTuples.Add((bag->BagType, retBag));
if (bag->ItemsCount is 0)
{
@@ -75,7 +76,7 @@ public sealed class InventoryService(
modifiers[j] = item->ModStruct[j].Mod;
}
retBag.Add((item->ModelId, completeNameEncoded, singleItemName, nameEncoded, (item->Interaction & 0x08000000) != 0, item->Quantity, modifiers, (ItemType)item->Type, item->Interaction, item->ModelFileId));
retBag.Add((item->ModelId, completeNameEncoded, singleItemName, nameEncoded, (item->Interaction & 0x08000000) != 0, item->Quantity, modifiers, (ItemType)item->Type, item->Interaction, item->ModelFileId, item->Dye.DyeTint));
}
}
@@ -90,16 +91,16 @@ public sealed class InventoryService(
// 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<(GWCA.GW.Constants.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, uint Interaction, uint ModelFileId)> Items)>();
var decodedItemTuples = new List<(GWCA.GW.Constants.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, uint Interaction, uint ModelFileId, byte DyeTint)> Items)>();
foreach (var tuple in itemTuples)
{
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, uint Interaction, uint ModelFileId)>();
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, uint Interaction, uint ModelFileId, byte DyeTint)>();
foreach (var item in tuple.Item2)
{
var decodedName = await this.uIService.DecodeString(item.EncodedName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType, item.Interaction, item.ModelFileId));
decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType, item.Interaction, item.ModelFileId, item.DyeTint));
}
decodedItemTuples.Add((tuple.Type, decodedItems));
@@ -124,7 +125,8 @@ public sealed class InventoryService(
Properties: [.. ItemProperty.ParseItemModifiers([.. item.Modifiers.Select(m => (Shared.Models.Guildwars.ItemModifier)m)])],
ItemType: item.ItemType.ToString(),
Interaction: item.Interaction,
ModelFileId: item.ModelFileId))]))]);
ModelFileId: item.ModelFileId,
DyeTint: item.DyeTint))]))]);
}
private static string ToBase64(string encoded)
+409 -132
View File
@@ -5,6 +5,7 @@ using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Builds;
using Daybreak.Shared.Models.Guildwars;
using Daybreak.Shared.Services.BuildTemplates;
using System.Core.Extensions;
@@ -16,35 +17,75 @@ using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
namespace Daybreak.API.Services;
public sealed class PartyService(
ChatService chatService,
IBuildTemplateManager buildTemplateManager,
UIService uiService,
UIContextService uIContextService,
SkillbarContextService skillbarContextService,
InstanceContextService instanceContextService,
PartyContextService partyContextService,
GameThreadService gameThreadService,
GameContextService gameContextService,
ILogger<PartyService> logger)
public sealed class PartyService : IHostedService
{
private static readonly TimeSpan HeroSpawnDelay = TimeSpan.FromSeconds(1);
private readonly ChatService chatService = chatService.ThrowIfNull();
private readonly IBuildTemplateManager buildTemplateManager = buildTemplateManager.ThrowIfNull();
private readonly UIService uiService = uiService.ThrowIfNull();
private readonly UIContextService uIContextService = uIContextService.ThrowIfNull();
private readonly SkillbarContextService skillbarContextService = skillbarContextService.ThrowIfNull();
private readonly InstanceContextService instanceContextService = instanceContextService.ThrowIfNull();
private readonly PartyContextService partyContextService = partyContextService.ThrowIfNull();
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
private readonly GameContextService gameContextService = gameContextService.ThrowIfNull();
private readonly ILogger<PartyService> logger = logger.ThrowIfNull();
private readonly ChatService chatService;
private readonly IBuildTemplateManager buildTemplateManager;
private readonly UIService uiService;
private readonly UIContextService uIContextService;
private readonly SkillbarContextService skillbarContextService;
private readonly InstanceContextService instanceContextService;
private readonly PartyContextService partyContextService;
private readonly GameThreadService gameThreadService;
private readonly GameContextService gameContextService;
private readonly ILogger<PartyService> logger;
private readonly CallbackRegistration<Func<string, WrappedPointer<SkillTemplate>, bool>> decodeTemplateHeaderCallbackRegistration;
private readonly CallbackRegistration<Func<SkillTemplate, bool>> loadSkillTemplateCallbackRegistration;
private readonly CallbackRegistration<Func<WrappedPointer<TemplateSummaryFrameData>, WrappedPointer<SkillTemplate>, bool>> populateSkillDataCallbackRegistration;
public async Task<bool> SetPartyLoadout(PartyLoadout partyLoadout, CancellationToken cancellationToken)
private string? cachedTemplateCode;
private nint floatingPreviewFrame;
private bool isPopulatingExtraBuilds;
public PartyService(
ChatService chatService,
IBuildTemplateManager buildTemplateManager,
UIService uiService,
UIContextService uIContextService,
SkillbarContextService skillbarContextService,
InstanceContextService instanceContextService,
PartyContextService partyContextService,
GameThreadService gameThreadService,
GameContextService gameContextService,
ILogger<PartyService> logger)
{
this.chatService = chatService.ThrowIfNull();
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
this.uiService = uiService.ThrowIfNull();
this.uIContextService = uIContextService.ThrowIfNull();
this.skillbarContextService = skillbarContextService.ThrowIfNull();
this.instanceContextService = instanceContextService.ThrowIfNull();
this.partyContextService = partyContextService.ThrowIfNull();
this.gameThreadService = gameThreadService.ThrowIfNull();
this.gameContextService = gameContextService.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.decodeTemplateHeaderCallbackRegistration = this.skillbarContextService.RegisterDecodeTemplateHeaderHandler(this.DecodeTemplateHeader);
this.loadSkillTemplateCallbackRegistration = this.skillbarContextService.RegisterLoadSkillTemplateHandler(this.LoadSkillTemplate);
this.populateSkillDataCallbackRegistration = this.skillbarContextService.RegisterPopulateSkillDataHandler(this.PopulateSkillData);
}
public Task StartAsync(CancellationToken cancellationToken)
{
// Needed to be a hosted service so that we can register the callbacks in the constructor.
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.decodeTemplateHeaderCallbackRegistration.Dispose();
this.loadSkillTemplateCallbackRegistration.Dispose();
this.populateSkillDataCallbackRegistration.Dispose();
// Floating preview will be cleaned up by the game on shutdown.
this.floatingPreviewFrame = 0;
return Task.CompletedTask;
}
public async Task<bool> SetPartyLoadout(string partyLoadoutTemplate, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (!await this.IsInValidOutpost(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Not in a valid outpost");
@@ -52,6 +93,14 @@ public sealed class PartyService(
return false;
}
if (!this.buildTemplateManager.TryDecodeTemplate(partyLoadoutTemplate, out var build) ||
build is not TeamBuildEntry teamBuild)
{
scopedLogger.LogError("Could not set party loadout. Not a valid party loadout");
await this.chatService.AddMessageAsync("Cannot set party loadout. Not a valid party loadout.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
return false;
}
if (!await this.KickAllHeroes(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not leave party");
@@ -59,7 +108,7 @@ public sealed class PartyService(
return false;
}
if (!await this.gameThreadService.QueueOnGameThread(() => this.SpawnHeroes(partyLoadout), cancellationToken))
if (!await this.gameThreadService.QueueOnGameThread(() => this.SpawnHeroes(teamBuild), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not spawn heroes");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not spawn heroes.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
@@ -68,14 +117,14 @@ public sealed class PartyService(
await Task.Delay(HeroSpawnDelay, cancellationToken);
if (!await this.gameThreadService.QueueOnGameThread(() => this.ApplyBuilds(partyLoadout), cancellationToken))
if (!await this.gameThreadService.QueueOnGameThread(() => this.ApplyBuilds(teamBuild), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not apply builds");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not apply builds.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
return false;
}
var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(partyLoadout), cancellationToken);
var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(teamBuild), cancellationToken);
foreach (var (AgentId, Behavior) in heroBehaviorSetup ?? [])
{
if (!await this.SetHeroBehavior(AgentId, Behavior, cancellationToken))
@@ -89,10 +138,10 @@ public sealed class PartyService(
return true;
}
public Task<PartyLoadout?> GetPartyLoadout(CancellationToken cancellationToken)
public async Task<string?> GetPartyLoadout(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
return this.gameThreadService.QueueOnGameThread(() =>
var teamBuild = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
@@ -133,25 +182,47 @@ public sealed class PartyService(
.OfType<(uint AgentId, BuildEntry BuildEntry, API.Interop.GWCA.GW.HeroBehavior Behavior)>()
.ToList();
return new PartyLoadout(
[.. buildTuples.Select(t =>
{
if (t.AgentId == playerId)
{
return new PartyLoadoutEntry(0, Shared.Models.Api.HeroBehavior.Undefined, t.BuildEntry);
}
else if (heroes.Value.FirstOrDefault(h => h.AgentId == t.AgentId) is HeroPartyMember hero &&
hero.AgentId == t.AgentId)
{
return new PartyLoadoutEntry((int)hero.HeroId, (Shared.Models.Api.HeroBehavior)t.Behavior, t.BuildEntry);
}
var teamBuildEntry = this.buildTemplateManager.CreateTeamBuild();
teamBuildEntry.Builds.Clear();
var partyComposition = new List<PartyCompositionMetadataEntry>();
return default;
})
.Where(t => t is not null)
.OfType<PartyLoadoutEntry>()]);
foreach (var t in buildTuples.OrderBy(t => t.AgentId == playerId ? 0 : 1))
{
var singleBuild = this.buildTemplateManager.CreateSingleBuild(t.BuildEntry);
teamBuildEntry.Builds.Add(singleBuild);
if (t.AgentId == playerId)
{
partyComposition.Add(new PartyCompositionMetadataEntry
{
Type = PartyCompositionMemberType.MainPlayer,
Index = teamBuildEntry.Builds.Count - 1
});
}
else if (heroes.Value.FirstOrDefault(h => h.AgentId == t.AgentId) is HeroPartyMember hero &&
hero.AgentId == t.AgentId)
{
partyComposition.Add(new PartyCompositionMetadataEntry
{
Type = PartyCompositionMemberType.Hero,
Index = teamBuildEntry.Builds.Count - 1,
HeroId = (int)hero.HeroId,
Behavior = (Shared.Models.Api.HeroBehavior)t.Behavior
});
}
}
teamBuildEntry.PartyComposition = partyComposition;
return teamBuildEntry;
}
}, cancellationToken);
if (teamBuild is null)
{
return default;
}
return this.buildTemplateManager.EncodeTemplate(teamBuild);
}
public async Task<bool> KickAllHeroes(CancellationToken cancellationToken)
@@ -232,6 +303,210 @@ public sealed class PartyService(
return result;
}
private unsafe bool DecodeTemplateHeader(string templateCode, WrappedPointer<SkillTemplate> skillTemplate)
{
var scopedLogger = this.logger.CreateScopedLogger();
this.cachedTemplateCode = default;
// Clean up any previous floating preview when a new template is being decoded
this.CleanupFloatingPreview();
if (!this.buildTemplateManager.TryDecodeTemplate(templateCode, out var build) ||
build is not TeamBuildEntry teamBuildEntry ||
teamBuildEntry.Builds.Count is 0)
{
return false;
}
this.cachedTemplateCode = templateCode;
PopulateSkillTemplate(teamBuildEntry.Builds[0], skillTemplate);
scopedLogger.LogDebug("Decoded skill template from template code: {templateCode}", templateCode);
return true;
}
private bool LoadSkillTemplate(SkillTemplate template)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.cachedTemplateCode is null)
{
return false;
}
var pendingLoadout = this.cachedTemplateCode;
this.cachedTemplateCode = default;
scopedLogger.LogDebug("Loading skill template from cached template code: {templateCode}", pendingLoadout);
Task.Run(async () => await this.SetPartyLoadout(pendingLoadout, CancellationToken.None));
// Return false to allow the normal template loading to continue.
// Returning true would block the original LoadSkillTemplate call chain,
// leaving the game with uninitialized template data and causing
// TemplatesSkillsCanApply assertion failures.
return false;
}
/// <summary>
/// Intercepts TemplatesSummary_PopulateSkillData when a team build is cached.
/// Blocks the game's own rendering and creates our own floating frame
/// containing ALL builds (0 through N-1), positioned near the template dialog.
/// Returns true to suppress the game's PopulateSkillData entirely.
/// </summary>
private unsafe bool PopulateSkillData(WrappedPointer<TemplateSummaryFrameData> frameData, WrappedPointer<SkillTemplate> skillTemplateData)
{
// Reentrancy guard — PopulateSkillTemplatePreview internally calls the game's
// PopulateSkillData which re-enters this hook for each child frame we create.
if (this.isPopulatingExtraBuilds)
{
return false;
}
var scopedLogger = this.logger.CreateScopedLogger();
if (this.cachedTemplateCode is null)
{
this.CleanupFloatingPreview();
return false;
}
if (!this.buildTemplateManager.TryDecodeTemplate(this.cachedTemplateCode, out var build) ||
build is not TeamBuildEntry teamBuild ||
teamBuild.Builds.Count <= 1)
{
this.CleanupFloatingPreview();
return false;
}
// If we already have a floating preview for this template, suppress the game's draw
if (this.floatingPreviewFrame != 0)
{
return true;
}
scopedLogger.LogDebug(
"Creating floating preview for all {count} builds",
teamBuild.Builds.Count);
// Create a container frame for ALL builds (including build 0).
fixed (char* label = "DaybreakTeamPreview")
{
var floatingFrame = GWCA.GW.FrameMgr.CreateContainerFrame(
(ushort*)label, 50f, 2f, 4f);
if (floatingFrame is null)
{
scopedLogger.LogWarning("Failed to create floating preview frame");
return false;
}
this.floatingPreviewFrame = (nint)floatingFrame;
this.isPopulatingExtraBuilds = true;
try
{
// Create and populate a child frame for EVERY build in the team
for (var i = 0; i < teamBuild.Builds.Count; i++)
{
var template = new SkillTemplate();
PopulateSkillTemplate(teamBuild.Builds[i], &template);
if (!GWCA.GW.FrameMgr.PopulateSkillTemplatePreview(floatingFrame, &template, (uint)i))
{
scopedLogger.LogWarning("Failed to create and populate preview for build {index}", i);
continue;
}
}
}
finally
{
this.isPopulatingExtraBuilds = false;
}
// Compute desired height for all builds
var desiredH = GWCA.GW.FrameMgr.GetContainerDesiredHeight(floatingFrame);
scopedLogger.LogDebug("Container created with {count} builds, desiredHeight={h}",
teamBuild.Builds.Count, desiredH);
// Defer positioning until the game's layout pass has computed
// screen coordinates for the original summary frame. We use that
// frame's position as an anchor even though we're replacing its content.
var origFrameId = frameData.Pointer->FrameId;
var floatingFramePtr = floatingFrame;
var height = desiredH;
_ = this.gameThreadService.QueueOnGameThread(() =>
{
this.PositionFloatingPreview(origFrameId, floatingFramePtr, height, retriesRemaining: 10);
}, CancellationToken.None, forceEnqueue: true);
}
// Return true to suppress the game's own PopulateSkillData for build 0.
// We're drawing everything ourselves.
return true;
}
/// <summary>
/// Deferred positioning callback. Waits for the original summary frame
/// to have valid screen coordinates after the layout pass, then positions
/// the floating preview at the same location (replacing it visually).
/// </summary>
private unsafe void PositionFloatingPreview(uint origFrameId, Frame* floatingFrame, float desiredH, int retriesRemaining)
{
var scopedLogger = this.logger.CreateScopedLogger();
var origFrame = GWCA.GW.UI.GetFrameById(origFrameId);
if (origFrame is null)
{
scopedLogger.LogWarning("Original frame {fid} no longer valid", origFrameId);
return;
}
var origPos = (FramePositionData*)((byte*)origFrame + 0xD8);
var origHeight = origPos->ScreenTop - origPos->ScreenBottom;
var origWidth = origPos->ScreenRight - origPos->ScreenLeft;
if (origHeight <= 0f || origWidth <= 0f)
{
if (retriesRemaining <= 0)
{
scopedLogger.LogWarning("Original frame still has zero dimensions after all retries");
return;
}
_ = this.gameThreadService.QueueOnGameThread(() =>
{
this.PositionFloatingPreview(origFrameId, floatingFrame, desiredH, retriesRemaining - 1);
}, CancellationToken.None, forceEnqueue: true);
return;
}
// Position our container at the same location as the original summary frame,
// extending downward to fit all builds.
var fpos = (FramePositionData*)((byte*)floatingFrame + 0xD8);
fpos->Flags = 0;
fpos->Left = origPos->ScreenLeft;
fpos->Top = origPos->ScreenTop;
fpos->Right = origPos->ScreenRight;
fpos->Bottom = origPos->ScreenTop - desiredH;
fpos->ScreenLeft = origPos->ScreenLeft;
fpos->ScreenTop = origPos->ScreenTop;
fpos->ScreenRight = origPos->ScreenRight;
fpos->ScreenBottom = origPos->ScreenTop - desiredH;
GWCA.GW.UI.TriggerFrameRedraw(floatingFrame);
// Hide the original frame since we're replacing it
GWCA.GW.UI.SetFrameVisible(origFrame, false);
// Layout children within the now-sized container
GWCA.GW.FrameMgr.LayoutContainer(floatingFrame);
scopedLogger.LogDebug(
"Positioned at Screen=[L={sl}, B={sb}, R={sr}, T={st}], origFrame hidden",
fpos->ScreenLeft, fpos->ScreenBottom, fpos->ScreenRight, fpos->ScreenTop);
}
private unsafe void CleanupFloatingPreview()
{
if (this.floatingPreviewFrame != 0)
{
var frame = (Frame*)this.floatingPreviewFrame;
GWCA.GW.FrameMgr.DestroyFloatingFrame(frame);
this.floatingPreviewFrame = 0;
}
}
private async Task<bool> IsInValidOutpost(CancellationToken cancellationToken)
{
return await this.gameThreadService.QueueOnGameThread(() =>
@@ -240,21 +515,25 @@ public sealed class PartyService(
}, cancellationToken);
}
private unsafe bool SpawnHeroes(PartyLoadout partyLoadout)
private bool SpawnHeroes(TeamBuildEntry partyLoadout)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug("Spawning {heroCount} heroes for party loadout", partyLoadout.Entries.AsValueEnumerable().Count(c => c.HeroId != 0));
foreach (var entry in partyLoadout.Entries.AsValueEnumerable().OrderBy(e => e.Build.Primary))
scopedLogger.LogDebug("Spawning {heroCount} heroes for party loadout", partyLoadout.PartyComposition?.AsValueEnumerable().Count(c => c.HeroId != 0) ?? 0);
foreach ((var index, var entry) in partyLoadout.Builds.AsValueEnumerable().OrderBy(e => e.Primary.Id)
.Select((e, i) => (e, partyLoadout.PartyComposition?.First(e => e.Index == i)))
.OfType<(SingleBuildEntry, PartyCompositionMetadataEntry)>()
.Where(t => t.Item2.Type is PartyCompositionMemberType.Hero))
{
if (entry.HeroId != 0 &&
Hero.TryParse(entry.HeroId, out var hero))
if (entry.HeroId is not null &&
entry.HeroId != 0 &&
Hero.TryParse(entry.HeroId.Value, out var hero))
{
scopedLogger.LogInformation("Adding hero [{heroId}] [{heroName}] with behavior {behavior}", entry.HeroId, hero.Name, entry.HeroBehavior);
scopedLogger.LogInformation("Adding hero [{heroId}] [{heroName}] with behavior {behavior}", entry.HeroId, hero.Name, entry.Behavior ?? Shared.Models.Api.HeroBehavior.Guard);
this.partyContextService.AddHero((uint)entry.HeroId);
}
else
{
scopedLogger.LogWarning("Invalid hero entry in party loadout: {heroId}", entry.HeroId);
scopedLogger.LogWarning("Invalid hero entry in party loadout: {heroId}", entry.HeroId ?? -1);
continue;
}
}
@@ -262,7 +541,7 @@ public sealed class PartyService(
return true;
}
private unsafe bool ApplyBuilds(PartyLoadout partyLoadout)
private unsafe bool ApplyBuilds(TeamBuildEntry partyLoadout)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug("Applying builds");
@@ -299,103 +578,71 @@ public sealed class PartyService(
return false;
}
var parsedEntries = partyLoadout.Entries.AsValueEnumerable()
.Select(entry =>
{
if (entry.HeroId is 0)
{
return (entry, playerId);
}
else
{
var hero = heroes.Value.AsValueEnumerable().FirstOrDefault(h => (int)h.HeroId == entry.HeroId);
return (entry, hero.AgentId);
}
})
.Select(t => (t.entry, t.playerId, professions.Value.AsValueEnumerable().FirstOrDefault(p => p.AgentId == t.playerId)))
.Select(t =>
{
var validSkills = t.playerId == playerId
? unlockedSkills
: accountContext.Pointer->UnlockedAccountSkills;
var unlockedProfessionsFlags = t.playerId == playerId
? playerProfessionContext.UnlockedProfessions
: uint.MaxValue;
if (!this.buildTemplateManager.CanTemplateApply(
new BuildTemplateValidationRequest(
(uint)t.entry.Build.Primary,
(uint)t.entry.Build.Secondary,
[.. t.entry.Build.Skills],
(uint)t.Item3.Primary,
unlockedProfessionsFlags,
[.. validSkills])))
{
return (t.entry, t.playerId, t.Item3, false);
}
return (t.entry, t.playerId, t.Item3, true);
})
.Where(t =>
{
if (!t.Item4)
{
if (t.entry.HeroId is 0)
{
scopedLogger.LogError("Cannot apply build for player");
}
else
{
scopedLogger.LogError("Cannot apply build for hero {heroId}", t.entry.HeroId);
}
}
return t.Item4;
});
foreach (var (entry, maybeAgentId, _, _) in parsedEntries)
for (var buildIndex = 0; buildIndex < partyLoadout.Builds.Count; buildIndex++)
{
if (maybeAgentId is not uint agentId)
var build = partyLoadout.Builds[buildIndex];
var composition = partyLoadout.PartyComposition?.FirstOrDefault(c => c.Index == buildIndex);
if (composition is null)
{
continue;
}
var attributeIds = new AttributeArray12();
var attributeValues = new Array12Uint();
for (var i = 0; i < entry.Build.Attributes.Count && i < 12; i++)
uint agentId;
if (composition.HeroId is 0 or null)
{
attributeIds[i] = (GWCA.GW.Constants.Attribute)entry.Build.Attributes[i].Id;
attributeValues[i] = (uint)entry.Build.Attributes[i].BasePoints;
agentId = playerId.Value;
}
else
{
var hero = heroes.Value.AsValueEnumerable().FirstOrDefault(h => (int)h.HeroId == composition.HeroId.GetValueOrDefault());
agentId = hero.AgentId;
}
var skills = new SkillIDArray8();
for (var i = 0; i < entry.Build.Skills.Count && i < 8; i++)
var professionContext = professions.Value.AsValueEnumerable().FirstOrDefault(p => p.AgentId == agentId);
var validSkills = agentId == playerId
? unlockedSkills
: accountContext.Pointer->UnlockedAccountSkills;
var unlockedProfessionsFlags = agentId == playerId
? playerProfessionContext.UnlockedProfessions
: uint.MaxValue;
if (!this.buildTemplateManager.CanTemplateApply(
new BuildTemplateValidationRequest(
(uint)build.Primary.Id,
(uint)build.Secondary.Id,
[.. build.Skills.Select(s => (uint)s.Id)],
(uint)(int)professionContext.Primary,
unlockedProfessionsFlags,
[.. validSkills])))
{
skills[i] = (GWCA.GW.Constants.SkillID)entry.Build.Skills[i];
if (composition.HeroId is 0 or null)
{
scopedLogger.LogError("Cannot apply build for player");
}
else
{
scopedLogger.LogError("Cannot apply build for hero {heroId}", composition.HeroId);
}
continue;
}
var skillTemplate = new SkillTemplate
{
Primary = (GWCA.GW.Constants.Profession)entry.Build.Primary,
Secondary = (GWCA.GW.Constants.Profession)entry.Build.Secondary,
AttributesCount = (uint)entry.Build.Attributes.Count,
AttributeIds = attributeIds,
Skills = skills
};
Unsafe.CopyBlock(skillTemplate.AttributeValues, Unsafe.AsPointer(ref attributeValues), (uint)(12 * sizeof(uint)));
scopedLogger.LogInformation("Applying build for agent {agentId} with primary {primary} and secondary {secondary}", agentId, entry.Build.Primary, entry.Build.Secondary);
var skillTemplate = new SkillTemplate();
PopulateSkillTemplate(build, &skillTemplate);
scopedLogger.LogInformation("Applying build for agent {agentId} with primary {primary} and secondary {secondary}", agentId, build.Primary, build.Secondary);
this.skillbarContextService.LoadBuild(agentId, &skillTemplate);
}
return true;
}
private unsafe List<(uint AgentId, Shared.Models.Api.HeroBehavior Behavior)>? GetHeroBehaviorSetup(PartyLoadout partyLoadout)
private unsafe List<(uint AgentId, Shared.Models.Api.HeroBehavior Behavior)>? GetHeroBehaviorSetup(TeamBuildEntry teamBuild)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug("Getting hero behavior setup");
if (!this.gameContextService.GetGameContext().TryGetPlayerParty(out _, out _, out var heroes, out _) ||
!heroes.HasValue)
{
@@ -403,6 +650,12 @@ public sealed class PartyService(
return default;
}
if (teamBuild.PartyComposition is null)
{
scopedLogger.LogWarning("No party composition metadata in team build");
return default;
}
return heroes.Value.AsValueEnumerable()
.Select(h =>
{
@@ -411,14 +664,14 @@ public sealed class PartyService(
return default;
}
var entry = partyLoadout.Entries.AsValueEnumerable().FirstOrDefault(e => e.HeroId == (int)h.HeroId);
var entry = teamBuild.PartyComposition.AsValueEnumerable().FirstOrDefault(e => e.HeroId == (int)h.HeroId);
if (entry is null)
{
scopedLogger.LogWarning("No entry found for hero {heroId}", h.HeroId);
return default;
}
return (h.AgentId, entry.HeroBehavior);
return (h.AgentId, entry.Behavior ?? Shared.Models.Api.HeroBehavior.Guard);
})
.Where(t => t.AgentId is not 0)
.ToList();
@@ -469,7 +722,7 @@ public sealed class PartyService(
var playerParty = GWCA.GW.PartyMgr.GetPartyInfo(0);
var playerId = charContext->PlayerNumber;
var offset = 0;
foreach(var hero in playerParty->Heroes)
foreach (var hero in playerParty->Heroes)
{
if (hero.OwnerPlayerId == playerId)
{
@@ -507,4 +760,28 @@ public sealed class PartyService(
(uint)skillbar.Skills[7].SkillId
]);
}
private static unsafe void PopulateSkillTemplate(SingleBuildEntry build, SkillTemplate* skillTemplate)
{
var attributeIds = new AttributeArray12();
var attributeValues = new Array12Uint();
for (var i = 0; i < build.Attributes.Count && i < 12; i++)
{
attributeIds[i] = (GWCA.GW.Constants.Attribute)(build.Attributes[i].Attribute?.Id ?? 0);
attributeValues[i] = (uint)build.Attributes[i].Points;
}
var skills = new SkillIDArray8();
for (var i = 0; i < build.Skills.Count && i < 8; i++)
{
skills[i] = (GWCA.GW.Constants.SkillID)build.Skills[i].Id;
}
skillTemplate->Primary = (GWCA.GW.Constants.Profession)build.Primary.Id;
skillTemplate->Secondary = (GWCA.GW.Constants.Profession)build.Secondary.Id;
skillTemplate->AttributesCount = (uint)Math.Min(build.Attributes.Count, 12);
skillTemplate->AttributeIds = attributeIds;
skillTemplate->Skills = skills;
Unsafe.CopyBlock(skillTemplate->AttributeValues, Unsafe.AsPointer(ref attributeValues), (uint)(12 * sizeof(uint)));
}
}
@@ -1,4 +1,5 @@
using Daybreak.API.Services.Interop;
using System.Diagnostics.CodeAnalysis;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Services.BuildTemplates;
using Daybreak.Shared.Services.BuildTemplates.Parsers;
@@ -16,12 +17,10 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddSingleton<ChatService>();
builder.Services.AddSingleton<MainPlayerService>();
builder.Services.AddSingleton<CharacterSelectService>();
builder.Services.AddSingleton<PartyService>();
builder.Services.AddSingleton<UIService>();
builder.Services.AddSingleton<LoginService>();
builder.Services.AddSingleton<InventoryService>();
builder.Services.AddSingleton<GameThreadService>();
builder.Services.AddSingleton<SkillbarContextService>();
builder.Services.AddSingleton<GameContextService>();
builder.Services.AddSingleton<InstanceContextService>();
builder.Services.AddSingleton<AgentContextService>();
@@ -29,6 +28,18 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddSingleton<PartyContextService>();
builder.Services.AddSingleton<PreferencesService>();
builder.Services.AddSingleton<UIContextService>();
builder.Services.AddHostedSingleton<PartyService>();
builder.Services.AddHostedSingleton<SkillbarContextService>();
builder.Services.AddSingleton<IInteropHealthService>(sp => sp.GetRequiredService<SkillbarContextService>());
return builder;
}
public static IServiceCollection AddHostedSingleton<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services)
where TService : class, IHostedService
{
services.AddSingleton<TService>();
services.AddHostedService(sp => sp.GetRequiredService<TService>());
return services;
}
}
+8 -3
View File
@@ -235,8 +235,8 @@ public sealed class FocusViewModel(
}
else if (buildEntry is TeamBuildEntry teamBuild)
{
var partyLoadout = this.buildTemplateManager.ConvertToPartyLoadout(teamBuild);
await this.apiContext.PostPartyLoadout(partyLoadout, this.cancellationSource?.Token ?? CancellationToken.None);
var code = this.buildTemplateManager.EncodeTemplate(teamBuild);
await this.apiContext.PostPartyLoadout(code, this.cancellationSource?.Token ?? CancellationToken.None);
}
}
@@ -270,7 +270,12 @@ public sealed class FocusViewModel(
return;
}
var teamBuildEntry = this.buildTemplateManager.CreateTeamBuild(loadout);
if (!this.buildTemplateManager.TryDecodeTemplate(loadout, out var build) ||
build is not TeamBuildEntry teamBuildEntry)
{
return;
}
this.viewManager.ShowView<BuildRoutingView>((nameof(BuildRoutingView.BuildName), Uri.EscapeDataString(teamBuildEntry.Name ?? throw new InvalidOperationException())));
}
+3 -1
View File
@@ -2,6 +2,7 @@ using Daybreak.Shared.Converters;
using Daybreak.Shared.Models.Guildwars;
using System.Text.Json.Serialization;
namespace Daybreak.Shared.Models.Api;
public sealed record ItemEntry(
@@ -18,6 +19,7 @@ public sealed record ItemEntry(
[property: JsonConverter(typeof(HexUIntArrayJsonConverter))] uint[] Modifiers,
ItemProperty[] Properties,
uint Interaction,
uint ModelFileId)
uint ModelFileId,
byte DyeTint)
{
}
@@ -1,4 +0,0 @@
namespace Daybreak.Shared.Models.Api;
public sealed record PartyLoadout(List<PartyLoadoutEntry> Entries)
{
}
@@ -1,4 +0,0 @@
namespace Daybreak.Shared.Models.Api;
public sealed record PartyLoadoutEntry(int HeroId, HeroBehavior HeroBehavior, BuildEntry Build)
{
}
@@ -26,6 +26,7 @@ public sealed class ScopedApiContext(
private const string GetMainPlayerBuildContextPath = "/api/v1/rest/main-player/build-context";
private const string PostMainPlayerBuildPath = $"/api/v1/rest/main-player/build?code={CodePlaceholder}";
private const string PartyLoadoutPath = "/api/v1/rest/party/loadout";
private const string PostPartyLoadoutPath = $"/api/v1/rest/party/loadout?code={CodePlaceholder}";
private const string GetTitleInfoPath = "/api/v1/rest/main-player/title";
private const string GetLoginInfoPath = "/api/v1/rest/login";
@@ -83,7 +84,7 @@ public sealed class ScopedApiContext(
return await this.Post(path, request => emptyContent, DefaultRequestTimeout, cancellationToken);
}
public Task<PartyLoadout?> GetPartyLoadout(CancellationToken cancellationToken) => this.GetPayload<PartyLoadout>(PartyLoadoutPath, DefaultRequestTimeout, cancellationToken);
public Task<string?> GetPartyLoadout(CancellationToken cancellationToken) => this.GetPayload<string>(PartyLoadoutPath, DefaultRequestTimeout, cancellationToken);
public Task<TitleInfo?> GetTitleInfo(CancellationToken cancellationToken) => this.GetPayload<TitleInfo>(GetTitleInfoPath, DefaultRequestTimeout, cancellationToken);
@@ -93,7 +94,13 @@ public sealed class ScopedApiContext(
public Task<MainPlayerBuildContext?> GetMainPlayerBuildContext(CancellationToken cancellationToken) => this.GetPayload<MainPlayerBuildContext>(GetMainPlayerBuildContextPath, DefaultRequestTimeout, cancellationToken);
public async Task<bool> PostPartyLoadout(PartyLoadout partyLoadout, CancellationToken cancellationToken) => await this.PostPayload(PartyLoadoutPath, partyLoadout, PostPartyLoadoutTimeout, cancellationToken);
public async Task<bool> PostPartyLoadout(string partyLoadoutTemplate, CancellationToken cancellationToken)
{
var encodedCode = UrlEncoder.Default.Encode(partyLoadoutTemplate);
var path = PostPartyLoadoutPath.Replace(CodePlaceholder, encodedCode);
using var emptyContent = new StringContent(string.Empty);
return await this.Post(path, request => emptyContent, PostPartyLoadoutTimeout, cancellationToken);
}
private async Task<bool> PostPayload<T>(string path, T payload, TimeSpan timeout, CancellationToken cancellationToken)
{
@@ -109,17 +109,6 @@ public sealed class BuildTemplateManager(
return entry;
}
public TeamBuildEntry CreateTeamBuild(PartyLoadout partyLoadout)
{
var entry = this.CreateTeamBuild();
return this.PopulateTeamBuild(entry, partyLoadout);
}
public TeamBuildEntry CreateTeamBuild(PartyLoadout partyLoadout, string name)
{
var entry = this.CreateTeamBuild(name);
return this.PopulateTeamBuild(entry, partyLoadout);
}
public SingleBuildEntry ConvertToSingleBuildEntry(TeamBuildEntry teamBuildEntry)
{
@@ -156,32 +145,6 @@ public sealed class BuildTemplateManager(
return teamBuildEntry;
}
public PartyLoadout ConvertToPartyLoadout(TeamBuildEntry teamBuildEntry)
{
if (teamBuildEntry.PartyComposition is not List<PartyCompositionMetadataEntry> partyComposition ||
partyComposition.Count != teamBuildEntry.Builds.Count)
{
throw new InvalidOperationException("Invalid team build entry. Team build entry contains invalid party composition metadata");
}
if (partyComposition.Any(c => c.Type is PartyCompositionMemberType.Hero && (c.HeroId is null || c.Behavior is null)))
{
throw new InvalidOperationException("Invalid team build entry. Hero entries without specified heroes and behaviors are not currently supported");
}
var loadoutEntryComposition = teamBuildEntry.Builds.Select((build, index) =>
{
var compositionEntry = partyComposition.First(c => c.Index == index);
return (build, compositionEntry, index);
});
return new PartyLoadout(
Entries: [.. loadoutEntryComposition.Select(entry => new PartyLoadoutEntry(
HeroId: entry.compositionEntry.Type is PartyCompositionMemberType.MainPlayer ? 0 : entry.compositionEntry.HeroId ?? throw new InvalidOperationException(),
HeroBehavior: entry.compositionEntry.Behavior ?? HeroBehavior.Guard,
Build: this.ConvertToBuildEntry(entry.build)))]);
}
public BuildEntry ConvertToBuildEntry(SingleBuildEntry singleBuildEntry)
{
return new BuildEntry(
@@ -398,6 +361,7 @@ public sealed class BuildTemplateManager(
teamBuild.Name = randomName;
teamBuild.PreviousName = randomName;
build = teamBuild;
this.BuildMemoryCache.Add(build);
return true;
}
@@ -413,6 +377,8 @@ public sealed class BuildTemplateManager(
Attributes = single.Attributes,
Skills = single.Skills
};
this.BuildMemoryCache.Add(build);
return true;
}
@@ -421,7 +387,7 @@ public sealed class BuildTemplateManager(
{
Name = randomName,
PreviousName = randomName,
Builds = maybeBuilds.OfType<SingleBuildEntry>().Select(b => new SingleBuildEntry
Builds = [.. maybeBuilds.OfType<SingleBuildEntry>().Select(b => new SingleBuildEntry
{
Name = randomName,
PreviousName = randomName,
@@ -429,9 +395,10 @@ public sealed class BuildTemplateManager(
Secondary = b.Secondary,
Attributes = b.Attributes,
Skills = b.Skills
}).ToList()
})]
};
this.BuildMemoryCache.Add(build);
return true;
}
}
@@ -472,40 +439,6 @@ public sealed class BuildTemplateManager(
throw new InvalidOperationException($"Unknown build entry of type {build.GetType().Name}");
}
private TeamBuildEntry PopulateTeamBuild(TeamBuildEntry teamBuildEntry, PartyLoadout partyLoadout)
{
teamBuildEntry.Builds.Clear();
var partyCompositionMetadata = new List<PartyCompositionMetadataEntry>();
foreach (var entry in partyLoadout.Entries.OrderBy(e => e.HeroId))
{
var build = entry.Build;
var buildEntry = this.CreateSingleBuild();
this.PopulateSingleBuild(buildEntry, build);
var partyCompositionEntry = new PartyCompositionMetadataEntry
{
Type = entry.HeroId is 0 ? PartyCompositionMemberType.MainPlayer : PartyCompositionMemberType.Hero,
Index = entry.HeroId is 0 ? 0 : teamBuildEntry.Builds.Count,
Behavior = entry.HeroId is 0 ? default : (HeroBehavior)entry.HeroBehavior,
HeroId = entry.HeroId is 0 ? default : entry.HeroId
};
if (partyCompositionEntry.Type is PartyCompositionMemberType.MainPlayer)
{
teamBuildEntry.Builds.Insert(0, buildEntry);
partyCompositionMetadata.Insert(0, partyCompositionEntry);
}
else
{
teamBuildEntry.Builds.Add(buildEntry);
partyCompositionMetadata.Add(partyCompositionEntry);
}
}
teamBuildEntry.PartyComposition = partyCompositionMetadata;
return teamBuildEntry;
}
private SingleBuildEntry PopulateSingleBuild(SingleBuildEntry singleBuildEntry, BuildEntry buildEntry)
{
var scopedLogger = this.logger.CreateScopedLogger();
@@ -17,9 +17,7 @@ public interface IBuildTemplateManager
SingleBuildEntry CreateSingleBuild(string name, BuildEntry buildEntry);
TeamBuildEntry CreateTeamBuild();
TeamBuildEntry CreateTeamBuild(string name);
TeamBuildEntry CreateTeamBuild(PartyLoadout partyLoadout);
TeamBuildEntry CreateTeamBuild(PartyLoadout partyLoadout, string name);
PartyLoadout ConvertToPartyLoadout(TeamBuildEntry teamBuildEntry);
BuildEntry ConvertToBuildEntry(SingleBuildEntry singleBuildEntry);
bool CanApply(MainPlayerBuildContext mainPlayerBuildContext, TeamBuildEntry teamBuildEntry);
bool CanApply(MainPlayerBuildContext mainPlayerBuildContext, SingleBuildEntry singleBuildEntry);
+1 -1
View File
@@ -892,7 +892,7 @@ namespace GW {
Lakeside_County_1070_AE,
Ashford_Catacombs_1070_AE,
Count = 0x370,
Count = 0x373,
};
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,975 @@
namespace GW {
enum class CallTargetType : uint32_t;
enum class WorldActionId : uint32_t;
struct GamePos;
struct Effect;
struct Vec2f;
typedef uint32_t AgentID;
namespace Merchant {
enum class TransactionType : uint32_t;
}
namespace Constants {
enum class Language;
enum class MapID : uint32_t;
enum class InstanceType;
enum class QuestID : uint32_t;
enum class SkillID : uint32_t;
}
namespace Chat {
enum Channel : int;
typedef uint32_t Color;
}
namespace Chat {
enum Channel : int;
}
namespace SkillbarMgr {
struct SkillTemplate;
}
namespace UI {
struct WindowPosition;
enum class FlagPreference : uint32_t;
enum class NumberPreference : uint32_t;
enum class StringPreference : uint32_t;
enum class EnumPreference : uint32_t;
struct CompassPoint;
enum class UIMessage : uint32_t {
kNone = 0x0, // 0x0
kFrameMessage_0x1, // 0x1
kFrameMessage_0x2, // 0x2
kFrameMessage_0x3, // 0x3
kFrameMessage_0x4, // 0x4
kFrameMessage_0x5, // 0x5
kFrameMessage_0x6, // 0x6
kFrameMessage_0x7, // 0x7
kResize, // 0x8
kInitFrame, // 0x9
kFrameMessage_0xa, // 0xa
kDestroyFrame, // 0xb
kFrameDisabledChange, // 0xc, wparam = is_enabled
kFrameMessage_0xd, // 0xd
kFrameMessage_0xe, // 0xe
kFrameMessage_0xf, // 0xf
kFrameMessage_0x10, // 0x10
kFrameMessage_0x11, // 0x11
kFrameMessage_0x12, // 0x12
kFrameMessage_0x13, // 0x13
kFrameMessage_0x14, // 0x14
kFrameMessage_0x15, // 0x15
kFrameMessage_0x16, // 0x16
kFrameMessage_0x17, // 0x17
kFrameMessage_0x18, // 0x18
kFrameMessage_0x19, // 0x19
kFrameMessage_0x1a, // 0x1a
kFrameMessage_0x1b, // 0x1b
kFrameMessage_0x1c, // 0x1c
kFrameMessage_0x1d, // 0x1d
kKeyDown = 0x20, // 0x20, wparam = UIPacket::kKeyAction* - Updated from 0x1e to 0x20
kSetFocus, // 0x21, wparam = 1 or 0
kKeyUp, // 0x22, wparam = UIPacket::kKeyAction*
kFrameMessage_0x21, // 0x23
kMouseClick, // 0x24, wparam = UIPacket::kMouseClick*
kFrameMessage_0x23, // 0x25
kMouseCoordsClick, // 0x26, wparam = UIPacket::kMouseCoordsClick*
kFrameMessage_0x25, // 0x27
kMouseUp, // 0x28, wparam = UIPacket::kMouseCoordsClick*
kFrameMessage_0x27, // 0x29
kFrameMessage_0x28, // 0x2a
kFrameMessage_0x29, // 0x2b
kFrameMessage_0x2a, // 0x2c
kFrameMessage_0x2b, // 0x2d
kToggleButtonDown, // 0x2e
kFrameMessage_0x2d, // 0x2f
kMouseClick2 = 0x31, // 0x31, wparam = UIPacket::kMouseAction*
kMouseAction, // 0x32, wparam = UIPacket::kMouseAction*
kRenderFrame_0x30, // 0x33
kRenderFrame_0x31 = 0x35, // 0x35
kFrameVisibilityChanged, // 0x36, wparam = is_visible
kSetLayout, // 0x37
kMeasureContent, // 0x38
kFrameMessage_0x35, // 0x39
kFrameMessage_0x36, // 0x3a
kRefreshContent, // 0x3b
kFrameMessage_0x38, // 0x3c
kFrameMessage_0x39, // 0x3d
kFrameMessage_0x3a, // 0x3e
kFrameMessage_0x3b, // 0x3f
kFrameMessage_0x3c = 0x44, // 0x44
kFrameMessage_0x3d, // 0x45
kFrameMessage_0x3e, // 0x46
kFrameMessage_0x3f, // 0x47
kFrameMessage_0x40, // 0x48
kFrameMessage_0x41, // 0x49
kFrameMessage_0x42, // 0x4a
kRenderFrame_0x43, // 0x4b
kFrameMessage_0x44 = 0x52, // 0x52
kFrameMessage_0x45, // 0x53
kFrameMessage_0x46 = 0x56, // 0x56
kFrameMessage_0x47, // 0x57, Multiple uses depending on frame
kFrameMessage_0x48, // 0x58, Multiple uses depending on frame
kFrameMessage_0x49, // 0x59, Multiple uses depending on frame
kFrameMessage_0x4a, // 0x5a, Multiple uses depending on frame
kFrameMessage_0x4b, // 0x5b, Multiple uses depending on frame
kFrameMessage_0x4c, // 0x5c, Multiple uses depending on frame
kFrameMessage_0x4d, // 0x5d, Multiple uses depending on frame
kFrameMessage_0x4e, // 0x5e, Multiple uses depending on frame
kFrameMessage_0x4f, // 0x5f, Multiple uses depending on frame
kFrameMessage_0x50, // 0x60, Multiple uses depending on frame
kFrameMessage_0x51, // 0x61, Multiple uses depending on frame
kFrameMessage_0x52, // 0x62, Multiple uses depending on frame
kFrameMessage_0x53, // 0x63, Multiple uses depending on frame
kFrameMessage_0x54, // 0x64, Multiple uses depending on frame
kFrameMessage_0x55, // 0x65, Multiple uses depending on frame
kFrameMessage_0x56, // 0x66, Multiple uses depending on frame
kFrameMessage_0x57, // 0x67, Multiple uses depending on frame
// High bit messages start at 0x10000000
kMessage_0x10000000 = 0x10000000, // 0x10000000
kMessage_0x10000001, // 0x10000001
kMessage_0x10000002, // 0x10000002
kMessage_0x10000003, // 0x10000003
kMessage_0x10000004, // 0x10000004
kMessage_0x10000005, // 0x10000005
kMessage_0x10000006, // 0x10000006
kAgentUpdate, // 0x10000007, wparam = uint32_t agent_id
kAgentDestroy, // 0x10000008, wparam = uint32_t agent_id
kUpdateAgentEffects, // 0x10000009
kMessage_0x1000000a, // 0x1000000a
kMessage_0x1000000b, // 0x1000000b
kDialogueMessage, // 0x1000000c
kMessage_0x1000000d, // 0x1000000d
kMessage_0x1000000e, // 0x1000000e
kMessage_0x1000000f, // 0x1000000f
kMessage_0x10000010, // 0x10000010
kMessage_0x10000011, // 0x10000011
kMessage_0x10000012, // 0x10000012
kMessage_0x10000013, // 0x10000013
kMessage_0x10000014, // 0x10000014
kMessage_0x10000015, // 0x10000015
kMessage_0x10000016, // 0x10000016
kAgentSpeechBubble, // 0x10000017
kMessage_0x10000018, // 0x10000018
kShowAgentNameTag, // 0x10000019, wparam = AgentNameTagInfo*
kHideAgentNameTag, // 0x1000001a
kSetAgentNameTagAttribs, // 0x1000001b, wparam = AgentNameTagInfo*
kMessage_0x1000001c, // 0x1000001c
kSetAgentProfession, // 0x1000001d, wparam = UIPacket::kSetAgentProfession*
kMessage_0x1000001e, // 0x1000001e
kMessage_0x1000001f, // 0x1000001f
kChangeTarget, // 0x10000020, wparam = UIPacket::kChangeTarget*
kMessage_0x10000021, // 0x10000021
kMessage_0x10000022, // 0x10000022
kMessage_0x10000023, // 0x10000023
kAgentSkillActivated, // 0x10000024, kAgentSkillPacket
kAgentSkillActivatedInstantly, // 0x10000025, kAgentSkillPacket
kAgentSkillCancelled, // 0x10000026, kAgentSkillPacket
kAgentSkillStartedCast, // 0x10000027, wparam = UIPacket::kAgentStartCasting*
kMessage_0x10000028, // 0x10000028
kShowMapEntryMessage, // 0x10000029, wparam = { wchar_t* title, wchar_t* subtitle }
kSetCurrentPlayerData, // 0x1000002a, fired after setting the worldcontext player name
kMessage_0x1000002b, // 0x1000002b
kMessage_0x1000002c, // 0x1000002c
kMessage_0x1000002d, // 0x1000002d
kMessage_0x1000002e, // 0x1000002e
kMessage_0x1000002f, // 0x1000002f
kMessage_0x10000030, // 0x10000030
kMessage_0x10000031, // 0x10000031
kMessage_0x10000032, // 0x10000032
kMessage_0x10000033, // 0x10000033
kPostProcessingEffect, // 0x10000034, Triggered when drunk. wparam = UIPacket::kPostProcessingEffect
kMessage_0x10000035, // 0x10000035
kMessage_0x10000036, // 0x10000036
kMessage_0x10000037, // 0x10000037
kHeroAgentAdded, // 0x10000038, hero assigned to agent/inventory/ai mode
kHeroDataAdded, // 0x10000039, hero info received from server (name, level etc)
kMessage_0x1000003a, // 0x1000003a
kMessage_0x1000003b, // 0x1000003b
kMessage_0x1000003c, // 0x1000003c
kMessage_0x1000003d, // 0x1000003d
kMessage_0x1000003e, // 0x1000003e
kMessage_0x1000003f, // 0x1000003f
kShowXunlaiChest, // 0x10000040
kMessage_0x10000041, // 0x10000041
kMessage_0x10000042, // 0x10000042
kMessage_0x10000043, // 0x10000043
kMessage_0x10000044, // 0x10000044
kMessage_0x10000045, // 0x10000045
kMinionCountUpdated, // 0x10000046
kMoraleChange, // 0x10000047, wparam = {agent id, morale percent }
kMessage_0x10000048, // 0x10000048
kMessage_0x10000049, // 0x10000049
kMessage_0x1000004a, // 0x1000004a
kMessage_0x1000004b, // 0x1000004b
kMessage_0x1000004c, // 0x1000004c
kMessage_0x1000004d, // 0x1000004d
kMessage_0x1000004e, // 0x1000004e
kMessage_0x1000004f, // 0x1000004f
kLoginStateChanged, // 0x10000050, wparam = {bool is_logged_in, bool unk }
kMessage_0x10000051, // 0x10000051
kMessage_0x10000052, // 0x10000052
kMessage_0x10000053, // 0x10000053
kMessage_0x10000054, // 0x10000054
kEffectAdd, // 0x10000055, wparam = {agent_id, GW::Effect*}
kEffectRenew, // 0x10000056, wparam = GW::Effect*
kEffectRemove, // 0x10000057, wparam = effect id
kMessage_0x10000058, // 0x10000058
kMessage_0x10000059, // 0x10000059
kMessage_0x1000005a, // 0x1000005a
kSkillActivated, // 0x1000005b, wparam ={ uint32_t agent_id , uint32_t skill_id }
kMessage_0x1000005c, // 0x1000005c
kMessage_0x1000005d, // 0x1000005d
kUpdateSkillbar, // 0x1000005e, wparam ={ uint32_t agent_id , ... }
kUpdateSkillsAvailable, // 0x1000005f, Triggered on a skill unlock, profession change or map load
kMessage_0x10000060, // 0x10000060
kMessage_0x10000061, // 0x10000061
kMessage_0x10000062, // 0x10000062
kMessage_0x10000063, // 0x10000063
kPlayerTitleChanged, // 0x10000064, wparam = { uint32_t player_id, uint32_t title_id }
kTitleProgressUpdated, // 0x10000065, wparam = title_id
kExperienceGained, // 0x10000066, wparam = experience amount
kMessage_0x10000067, // 0x10000067
kMessage_0x10000068, // 0x10000068
kMessage_0x10000069, // 0x10000069
kMessage_0x1000006a, // 0x1000006a
kMessage_0x1000006b, // 0x1000006b
kMessage_0x1000006c, // 0x1000006c
kMessage_0x1000006d, // 0x1000006d
kMessage_0x1000006e, // 0x1000006e
kMessage_0x1000006f, // 0x1000006f
kMessage_0x10000070, // 0x10000070
kMessage_0x10000071, // 0x10000071
kMessage_0x10000072, // 0x10000072
kMessage_0x10000073, // 0x10000073
kMessage_0x10000074, // 0x10000074
kMessage_0x10000075, // 0x10000075
kMessage_0x10000076, // 0x10000076
kMessage_0x10000077, // 0x10000077
kMessage_0x10000078, // 0x10000078
kMessage_0x10000079, // 0x10000079
kMessage_0x1000007a, // 0x1000007a
kMessage_0x1000007b, // 0x1000007b
kMessage_0x1000007c, // 0x1000007c
kMessage_0x1000007d, // 0x1000007d
kMessage_0x1000007e, // 0x1000007e
kWriteToChatLog, // 0x1000007f, wparam = UIPacket::kWriteToChatLog*
kWriteToChatLogWithSender, // 0x10000080, wparam = UIPacket::kWriteToChatLogWithSender*
kAllyOrGuildMessage, // 0x10000081, wparam = UIPacket::kAllyOrGuildMessage*
kPlayerChatMessage, // 0x10000082, wparam = UIPacket::kPlayerChatMessage*
kMessage_0x10000083, // 0x10000083
kFloatingWindowMoved, // 0x10000084, wparam = frame_id
kMessage_0x10000085, // 0x10000085
kMessage_0x10000086, // 0x10000086
kMessage_0x10000087, // 0x10000087
kMessage_0x10000088, // 0x10000088
kMessage_0x10000089, // 0x10000089
kMessage_0x1000008a, // 0x1000008a
kFriendUpdated, // 0x1000008b, wparam = { GW::Friend*, ... }
kMapLoaded, // 0x1000008c
kMessage_0x1000008d, // 0x1000008d
kMessage_0x1000008e, // 0x1000008e
kMessage_0x1000008f, // 0x1000008f
kMessage_0x10000090, // 0x10000090
kMessage_0x10000091, // 0x10000091
kOpenWhisper, // 0x10000092, wparam = wchar* name
kMessage_0x10000093, // 0x10000093
kMessage_0x10000094, // 0x10000094
kMessage_0x10000095, // 0x10000095
kMessage_0x10000096, // 0x10000096
kMessage_0x10000097, // 0x10000097
kLoadMapContext, // 0x10000098, wparam = UIPacket::kLoadMapContext
kMessage_0x10000099, // 0x10000099
kMessage_0x1000009a, // 0x1000009a
kMessage_0x1000009b, // 0x1000009b
kDialogueMessageUpdated, // 0x1000009c
kLogout, // 0x1000009d, wparam = { bool unknown, bool character_select }
kCompassDraw, // 0x1000009e, wparam = UIPacket::kCompassDraw*
kMessage_0x1000009f, // 0x1000009f
kMessage_0x100000a0, // 0x100000a0
kMessage_0x100000a1, // 0x100000a1
kOnScreenMessage, // 0x100000a2, wparam = wchar_** encoded_string
kDialogButton, // 0x100000a3, wparam = DialogButtonInfo*
kMessage_0x100000a4, // 0x100000a4
kMessage_0x100000a5, // 0x100000a5
kDialogBody, // 0x100000a6, wparam = DialogBodyInfo*
kMessage_0x100000a7, // 0x100000a7
kMessage_0x100000a8, // 0x100000a8
kMessage_0x100000a9, // 0x100000a9
kMessage_0x100000aa, // 0x100000aa
kMessage_0x100000ab, // 0x100000ab
kMessage_0x100000ac, // 0x100000ac
kMessage_0x100000ad, // 0x100000ad
kMessage_0x100000ae, // 0x100000ae
kMessage_0x100000af, // 0x100000af
kMessage_0x100000b0, // 0x100000b0
kMessage_0x100000b1, // 0x100000b1
kMessage_0x100000b2, // 0x100000b2
kTargetNPCPartyMember, // 0x100000b3, wparam = { uint32_t unk, uint32_t agent_id }
kTargetPlayerPartyMember, // 0x100000b4, wparam = { uint32_t unk, uint32_t player_number }
kVendorWindow, // 0x100000b5, wparam = UIPacket::kVendorWindow
kMessage_0x100000b6, // 0x100000b6
kMessage_0x100000b7, // 0x100000b7
kMessage_0x100000b8, // 0x100000b8
kVendorItems, // 0x100000b9, wparam = UIPacket::kVendorItems
kMessage_0x100000ba, // 0x100000ba
kVendorTransComplete, // 0x100000bb, wparam = *TransactionType
kMessage_0x100000bc, // 0x100000bc
kVendorQuote, // 0x100000bd, wparam = UIPacket::kVendorQuote
kMessage_0x100000be, // 0x100000be
kMessage_0x100000bf, // 0x100000bf
kMessage_0x100000c0, // 0x100000c0
kMessage_0x100000c1, // 0x100000c1
kStartMapLoad, // 0x100000c2, wparam = { uint32_t map_id, ...}
kMessage_0x100000c3, // 0x100000c3
kMessage_0x100000c4, // 0x100000c4
kMessage_0x100000c5, // 0x100000c5
kMessage_0x100000c6, // 0x100000c6
kWorldMapUpdated, // 0x100000c7, Triggered when an area in the world map has been discovered/updated
kMessage_0x100000c8, // 0x100000c8
kMessage_0x100000c9, // 0x100000c9
kMessage_0x100000ca, // 0x100000ca
kMessage_0x100000cb, // 0x100000cb
kMessage_0x100000cc, // 0x100000cc
kMessage_0x100000cd, // 0x100000cd
kMessage_0x100000ce, // 0x100000ce
kMessage_0x100000cf, // 0x100000cf
kMessage_0x100000d0, // 0x100000d0
kMessage_0x100000d1, // 0x100000d1
kMessage_0x100000d2, // 0x100000d2
kMessage_0x100000d3, // 0x100000d3
kMessage_0x100000d4, // 0x100000d4
kMessage_0x100000d5, // 0x100000d5
kMessage_0x100000d6, // 0x100000d6
kMessage_0x100000d7, // 0x100000d7
kMessage_0x100000d8, // 0x100000d8
kMessage_0x100000d9, // 0x100000d9
kGuildMemberUpdated, // 0x100000da, wparam = { GuildPlayer::name_ptr }
kMessage_0x100000db, // 0x100000db
kMessage_0x100000dc, // 0x100000dc
kMessage_0x100000dd, // 0x100000dd
kMessage_0x100000de, // 0x100000de
kMessage_0x100000df, // 0x100000df
kMessage_0x100000e0, // 0x100000e0
kShowHint, // 0x100000e1, wparam = { uint32_t icon_type, wchar_t* message_enc }
kMessage_0x100000e2, // 0x100000e2
kMessage_0x100000e3, // 0x100000e3
kMessage_0x100000e4, // 0x100000e4
kMessage_0x100000e5, // 0x100000e5
kMessage_0x100000e6, // 0x100000e6
kMessage_0x100000e7, // 0x100000e7
kMessage_0x100000e8, // 0x100000e8
kWeaponSetSwapComplete, // 0x100000e9, wparam = UIPacket::kWeaponSwap*
kWeaponSetSwapCancel, // 0x100000ea
kWeaponSetUpdated, // 0x100000eb
kUpdateGoldCharacter, // 0x100000ec, wparam = { uint32_t unk, uint32_t gold_character }
kUpdateGoldStorage, // 0x100000ed, wparam = { uint32_t unk, uint32_t gold_storage }
kInventorySlotUpdated, // 0x100000ee, Triggered when an item is moved into a slot
kEquipmentSlotUpdated, // 0x100000ef, Triggered when an item is moved into a slot
kMessage_0x100000f0, // 0x100000f0
kInventorySlotCleared, // 0x100000f1, Triggered when an item has been removed from a slot
kEquipmentSlotCleared, // 0x100000f2, Triggered when an item has been removed from a slot
kMessage_0x100000f3, // 0x100000f3
kMessage_0x100000f4, // 0x100000f4
kMessage_0x100000f5, // 0x100000f5
kMessage_0x100000f6, // 0x100000f6
kMessage_0x100000f7, // 0x100000f7
kMessage_0x100000f8, // 0x100000f8
kMessage_0x100000f9, // 0x100000f9
kPvPWindowContent, // 0x100000fa
kMessage_0x100000fb, // 0x100000fb
kMessage_0x100000fc, // 0x100000fc
kMessage_0x100000fd, // 0x100000fd
kMessage_0x100000fe, // 0x100000fe
kMessage_0x100000ff, // 0x100000ff
kMessage_0x10000100, // 0x10000100
kMessage_0x10000101, // 0x10000101
kPreStartSalvage, // 0x10000102, { uint32_t item_id, uint32_t kit_id }
kTomeSkillSelection, // 0x10000103, wparam = UIPacket::kTomeSkillSelection*
kMessage_0x10000104, // 0x10000104
kTradePlayerUpdated, // 0x10000105, wparam = GW::TraderPlayer*
kItemUpdated, // 0x10000106, wparam = UIPacket::kItemUpdated*
kMessage_0x10000107, // 0x10000107
kMessage_0x10000108, // 0x10000108
kMessage_0x10000109, // 0x10000109
kMessage_0x1000010a, // 0x1000010a
kMessage_0x1000010b, // 0x1000010b
kMessage_0x1000010c, // 0x1000010c
kMessage_0x1000010d, // 0x1000010d
kMessage_0x1000010e, // 0x1000010e
kMessage_0x1000010f, // 0x1000010f
kMessage_0x10000110, // 0x10000110
kMapChange, // 0x10000111, wparam = map id
kMessage_0x10000112, // 0x10000112
kMessage_0x10000113, // 0x10000113
kMessage_0x10000114, // 0x10000114
kCalledTargetChange, // 0x10000115, wparam = { player_number, target_id }
kMessage_0x10000116, // 0x10000116
kMessage_0x10000117, // 0x10000117
kMessage_0x10000118, // 0x10000118
kErrorMessage, // 0x10000119, wparam = { int error_index, wchar_t* error_encoded_string }
kPartyHardModeChanged, // 0x1000011a, wparam = { int is_hard_mode }
kPartyAddHenchman, // 0x1000011b
kPartyRemoveHenchman, // 0x1000011c
kMessage_0x1000011d, // 0x1000011d
kPartyAddHero, // 0x1000011e
kPartyRemoveHero, // 0x1000011f
kMessage_0x10000120, // 0x10000120
kMessage_0x10000121, // 0x10000121
kMessage_0x10000122, // 0x10000122
kMessage_0x10000123, // 0x10000123
kPartyAddPlayer, // 0x10000124
kMessage_0x10000125, // 0x10000125
kPartyRemovePlayer, // 0x10000126
kMessage_0x10000127, // 0x10000127
kMessage_0x10000128, // 0x10000128
kMessage_0x10000129, // 0x10000129
kDisableEnterMissionBtn, // 0x1000012a, wparam = boolean (1 = disabled, 0 = enabled)
kMessage_0x1000012b, // 0x1000012b
kMessage_0x1000012c, // 0x1000012c
kShowCancelEnterMissionBtn, // 0x1000012d
kMessage_0x1000012e, // 0x1000012e
kPartyDefeated, // 0x1000012f
kMessage_0x10000130, // 0x10000130
kMessage_0x10000131, // 0x10000131
kMessage_0x10000132, // 0x10000132
kPartySearchCreated, // 0x10000133, wparam = GW::PartySearch*
kPartySearchIdChanged, // 0x10000134, wparam = uint32_t* party_search_id
kPartySearchRemoved, // 0x10000135, wparam = uint32_t* party_search_id
kPartySearchUpdated, // 0x10000136, wparam = GW::PartySearch*
kPartySearchInviteReceived, // 0x10000137, wparam = UIPacket::kPartySearchInviteReceived*
kMessage_0x10000138, // 0x10000138
kPartySearchInviteSent, // 0x10000139
kPartyShowConfirmDialog, // 0x1000013a, wparam = UIPacket::kPartyShowConfirmDialog
kMessage_0x1000013b, // 0x1000013b
kMessage_0x1000013c, // 0x1000013c
kMessage_0x1000013d, // 0x1000013d
kMessage_0x1000013e, // 0x1000013e
kMessage_0x1000013f, // 0x1000013f
kPreferenceEnumChanged, // 0x10000140, wparam = UiPacket::kPreferenceEnumChanged
kPreferenceFlagChanged, // 0x10000141, wparam = UiPacket::kPreferenceFlagChanged
kPreferenceValueChanged, // 0x10000142, wparam = UiPacket::kPreferenceValueChanged
kUIPositionChanged, // 0x10000143, wparam = UIPacket::kUIPositionChanged
kPreBuildLoginScene, // 0x10000144, Called with no args right before login scene is drawn
kMessage_0x10000145, // 0x10000145
kMessage_0x10000146, // 0x10000146
kMessage_0x10000147, // 0x10000147
kMessage_0x10000148, // 0x10000148
kMessage_0x10000149, // 0x10000149
kMessage_0x1000014a, // 0x1000014a
kMessage_0x1000014b, // 0x1000014b
kMessage_0x1000014c, // 0x1000014c
kMessage_0x1000014d, // 0x1000014d
kQuestAdded, // 0x1000014e, wparam = { quest_id, ... }
kQuestDetailsChanged, // 0x1000014f, wparam = { quest_id, ... }
kQuestRemoved, // 0x10000150, wparam = { quest_id, ... }
kClientActiveQuestChanged, // 0x10000151, wparam = { quest_id, ... }. Triggered when the game requests the current quest to change
kMessage_0x10000152, // 0x10000152
kServerActiveQuestChanged, // 0x10000153, wparam = UIPacket::kServerActiveQuestChanged*. Triggered when the server requests the current quest to change
kUnknownQuestRelated, // 0x10000154
kMessage_0x10000155, // 0x10000155
kDungeonComplete, // 0x10000156
kMissionComplete, // 0x10000157
kMessage_0x10000158, // 0x10000158
kVanquishComplete, // 0x10000159
kObjectiveAdd, // 0x1000015a, wparam = UIPacket::kObjectiveAdd*
kObjectiveComplete, // 0x1000015b, wparam = UIPacket::kObjectiveComplete*
kObjectiveUpdated, // 0x1000015c, wparam = UIPacket::kObjectiveUpdated*
kMessage_0x1000015d, // 0x1000015d
kMessage_0x1000015e, // 0x1000015e
kMessage_0x1000015f, // 0x1000015f
kMessage_0x10000160, // 0x10000160
kMessage_0x10000161, // 0x10000161
kMessage_0x10000162, // 0x10000162
kMessage_0x10000163, // 0x10000163
kMessage_0x10000164, // 0x10000164
kTradeSessionStart, // 0x10000165, wparam = { trade_state, player_number }
kMessage_0x10000166, // 0x10000166
kMessage_0x10000167, // 0x10000167
kMessage_0x10000168, // 0x10000168
kMessage_0x10000169, // 0x10000169
kMessage_0x1000016a, // 0x1000016a
kTradeSessionUpdated, // 0x1000016b, no args
kMessage_0x1000016c, // 0x1000016c
kMessage_0x1000016d, // 0x1000016d
kMessage_0x1000016e, // 0x1000016e
kMessage_0x1000016f, // 0x1000016f
kMessage_0x10000170, // 0x10000170
kMessage_0x10000171, // 0x10000171
kMessage_0x10000172, // 0x10000172
kMessage_0x10000173, // 0x10000173
kMessage_0x10000174, // 0x10000174
kCheckUIState, // 0x10000175
kMessage_0x10000176, // 0x10000176
kMessage_0x10000177, // 0x10000177
kMessage_0x10000178, // 0x10000178
kMessage_0x10000178_1, // 0x10000179, added to GW 2026-02-26
kMessage_0x10000178_2, // 0x1000017a, added to GW 2026-02-26
kMessage_0x10000178_3, // 0x1000017b, added to GW 2026-02-26
kDestroyUIPositionOverlay, // 0x1000017c
kEnableUIPositionOverlay, // 0x1000017d, wparam = uint32_t enable
kMessage_0x1000017b, // 0x1000017e
kGuildHall, // 0x1000017f, wparam = gh key (uint32_t[4])
kMessage_0x1000017d, // 0x10000180
kLeaveGuildHall, // 0x10000181
kTravel, // 0x10000182
kOpenWikiUrl, // 0x10000183, wparam = char* url
kMessage_0x10000181, // 0x10000184
kMessage_0x10000182, // 0x10000185
kSetPreGameContext_Value0, // 0x10000186, wparam = uint32_t value
kMessage_0x10000184, // 0x10000187
kGetPreGameContext_Value0, // 0x10000188, lparam = *uint32_t value_out
kSetPreGameContext_Value1, // 0x10000189, wparam = uint32_t value , added to GW 2026-02-06
kGetPreGameContext_Value1, // 0x1000018a, lparam = *uint32_t value_out, added to GW 2026-02-06
kMessage_0x10000186, // 0x1000018b
kMessage_0x10000187, // 0x1000018c
kMessage_0x10000188, // 0x1000018d
kMessage_0x10000189, // 0x1000018e
kMessage_0x1000018a, // 0x1000018f
kMessage_0x1000018b, // 0x10000190
kMessage_0x1000018c, // 0x10000191
kMessage_0x1000018d, // 0x10000192
kAppendMessageToChat, // 0x10000193, wparam = wchar_t* message
kMessage_0x1000018f, // 0x10000194
kMessage_0x10000190, // 0x10000195
kMessage_0x10000191, // 0x10000196
kMessage_0x10000192, // 0x10000197
kMessage_0x10000193, // 0x10000198
kMessage_0x10000194, // 0x10000199
kMessage_0x10000195, // 0x1000019a
kMessage_0x10000196, // 0x1000019b
kMessage_0x10000197, // 0x1000019c
kMessage_0x10000198, // 0x1000019d
kMessage_0x10000199, // 0x1000019e
kMessage_0x1000019a, // 0x1000019f
kMessage_0x1000019b, // 0x100001a0
kHideHeroPanel, // 0x100001a1, wparam = hero_id
kShowHeroPanel, // 0x100001a2, wparam = hero_id
kMessage_0x1000019e, // 0x100001a3
kMessage_0x1000019f, // 0x100001a4
kMessage_0x100001a0, // 0x100001a5
kGetInventoryAgentId, // 0x100001a6, wparam = 0, lparam = uint32_t* agent_id_out. Used to fetch which agent is selected
kInventoryRelated1, // 0x100001a7, added to GW 2026-02-26
kInventoryRelated2, // 0x100001a8, added to GW 2026-02-26
kInventoryRelated3, // 0x100001a9, added to GW 2026-02-26
kEquipItem, // 0x100001aa, wparam = { item_id, agent_id }
kMoveItem, // 0x100001ab, wparam = { item_id, to_bag, to_slot, bool prompt }
kItemRelated_1, // 0x100001ac
kItemTooltip, // 0x100001ad
kItemRelated_3, // 0x100001ae, added to GW 2026-02-26
kItemRelated_4, // 0x100001af, added to GW 2026-02-26
kInitiateTrade, // 0x100001b0
kMessage_0x100001a7, // 0x100001b1
kMessage_0x100001a8, // 0x100001b2
kMessage_0x100001a9, // 0x100001b3
kMessage_0x100001aa, // 0x100001b4
kMessage_0x100001ab, // 0x100001b5
kMessage_0x100001ac, // 0x100001b6
kMessage_0x100001ad, // 0x100001b7
kMessage_0x100001ae, // 0x100001b8
kMessage_0x100001af, // 0x100001b9
kMessage_0x100001b0, // 0x100001ba
kMessage_0x100001b1, // 0x100001bb
kMessage_0x100001b2, // 0x100001bc
kMessage_0x100001b3, // 0x100001bd
kMessage_0x100001b4, // 0x100001be
kMessage_0x100001b5, // 0x100001bf
kInventoryAgentChanged, // 0x100001c0, Triggered when inventory needs updating due to agent change; no args
kInventoryRelated_1, // 0x100001c1
kInventoryRelated_2, // 0x100001c2
kMissionStatusRelated, // 0x100001c3
kUnused_1c2, // 0x100001c4
kCollapseExpandSkillListSection, // 0x100001c5
kTemplateRelated_1, // 0x100001c6
kTemplateRelated_2, // 0x100001c7
kPromptSaveTemplate, // 0x100001c8
kOpenTemplate, // 0x100001c9, wparam = GW::UI::ChatTemplate*
kTemplateRelated_3, // 0x100001ca
kTemplateRelated_4, // 0x100001cb
// GWCA Client to Server commands. Only added the ones that are used for hooks, everything else goes straight into GW
kSendLoadSkillTemplate = 0x30000000 | 0x3, // 0x30000003, wparam = SkillbarMgr::SkillTemplate*
kSendPingWeaponSet = 0x30000000 | 0x4, // 0x30000004, wparam = UIPacket::kSendPingWeaponSet*
kSendMoveItem = 0x30000000 | 0x5, // 0x30000005, wparam = UIPacket::kSendMoveItem*
kSendMerchantRequestQuote = 0x30000000 | 0x6, // 0x30000006, wparam = UIPacket::kSendMerchantRequestQuote*
kSendMerchantTransactItem = 0x30000000 | 0x7, // 0x30000007, wparam = UIPacket::kSendMerchantTransactItem*
kSendUseItem = 0x30000000 | 0x8, // 0x30000008, wparam = UIPacket::kSendUseItem*
kSendSetActiveQuest = 0x30000000 | 0x9, // 0x30000009, wparam = uint32_t quest_id
kSendAbandonQuest = 0x30000000 | 0xA, // 0x3000000a, wparam = uint32_t quest_id
kSendChangeTarget = 0x30000000 | 0xB, // 0x3000000b, wparam = UIPacket::kSendChangeTarget* // e.g. tell the gw client to focus on a different target
kSendCallTarget = 0x30000000 | 0x13, // 0x30000013, wparam = { uint32_t call_type, uint32_t agent_id } // also used to broadcast morale, death penalty, "I'm following X", etc
kSendDialog = 0x30000000 | 0x16, // 0x30000016, wparam = dialog_id // internal use
kStartWhisper = 0x30000000 | 0x17, // 0x30000017, wparam = UIPacket::kStartWhisper*
kGetSenderColor = 0x30000000 | 0x18, // 0x30000018, wparam = UIPacket::kGetColor* // Get chat sender color depending on channel, output object passed by reference
kGetMessageColor = 0x30000000 | 0x19, // 0x30000019, wparam = UIPacket::kGetColor* // Get chat message color depending on channel, output object passed by reference
kSendChatMessage = 0x30000000 | 0x1B, // 0x3000001b, wparam = UIPacket::kSendChatMessage*
kLogChatMessage = 0x30000000 | 0x1D, // 0x3000001d, wparam = UIPacket::kLogChatMessage*. Triggered when a message wants to be added to the persistent chat log.
kRecvWhisper = 0x30000000 | 0x1E, // 0x3000001e, wparam = UIPacket::kRecvWhisper*
kPrintChatMessage = 0x30000000 | 0x1F, // 0x3000001f, wparam = UIPacket::kPrintChatMessage*. Triggered when a message wants to be added to the in-game chat window.
kSendWorldAction = 0x30000000 | 0x20, // 0x30000020, wparam = UIPacket::kSendWorldAction*
kSetRendererValue = 0x30000000 | 0x21, // 0x30000021, wparam = UIPacket::kSetRendererValue
kIdentifyItem = 0x30000000 | 0x22, // 0x30000022, wparam = UIPacket::kUseKitOnItem
kSalvageItem = 0x30000000 | 0x23 // 0x30000023, wparam = UIPacket::kUseKitOnItem
};
//static_assert(GW::UI::UIMessage::kOpenTemplate == (GW::UI::UIMessage)0x100001c4);
namespace UIPacket {
struct kDialogueMessage {
uint32_t agent_id;
wchar_t* sender;
wchar_t* message;
uint32_t duration;
uint32_t is_dialogue_1_or_2;
};
struct kErrorMessage {
uint32_t error_id;
wchar_t* message;
};
struct kAgentSkillPacket {
uint32_t agent_id;
GW::Constants::SkillID skill_id;
};
struct kLoadMapContext {
const wchar_t* file_name;
Constants::MapID map_id;
Constants::InstanceType map_type;
Vec2f spawn_point;
uint32_t h0014;
uint32_t h0018;
bool* success;
};
static_assert(sizeof(kLoadMapContext) == 0x20);
struct kMouseCoordsClick {
float offset_x;
float offset_y;
uint32_t h0008;
uint32_t h000c;
uint32_t* h0010;
uint32_t h0014;
};
struct kUseKitOnItem {
uint32_t item_id;
uint32_t kit_id;
};
struct kShowXunlaiChest {
uint32_t h0000 = 0;
bool storage_pane_unlocked = true;
bool anniversary_pane_unlocked = true;
};
struct kMoveItem {
uint32_t item_id;
uint32_t to_bag_index;
uint32_t to_slot;
uint32_t prompt;
};
struct kResize {
uint32_t h0000 = 0xffffffff;
float content_width;
float content_height;
float h000c = 0;
float h0010 = 0;
float content_width2;
float content_height2;
float margin_x;
float margin_y;
// ...
};
struct kTomeSkillSelection {
uint32_t item_id;
uint32_t h0004;
uint32_t h0008;
};
struct kMeasureContent {
float max_width; // Maximum width constraint
float max_height; // Maximum height constraint
float* size_output; // Pointer to output buffer for calculated size
uint32_t flags; // Layout flags (similar to the 0x100 flag we saw)
};
struct kSetLayout {
float field_0x0;
float field_0x4;
float field_0x8;
float field_0xc;
float available_width;
float available_height;
};
struct kSetAgentProfession {
AgentID agent_id;
uint32_t primary;
uint32_t secondary;
};
struct kWeaponSwap {
uint32_t weapon_bar_frame_id;
uint32_t weapon_set_id;
};
struct kWeaponSetChanged {
uint32_t h0000;
uint32_t h0004;
uint32_t h0008;
uint32_t h000c;
};
struct kChangeTarget {
uint32_t evaluated_target_id;
bool has_evaluated_target_changed;
uint32_t auto_target_id;
bool has_auto_target_changed;
uint32_t manual_target_id;
bool has_manual_target_changed;
};
struct kSendLoadSkillTemplate {
uint32_t agent_id;
SkillbarMgr::SkillTemplate* skill_template;
};
struct kVendorWindow {
Merchant::TransactionType transaction_type;
uint32_t unk;
uint32_t merchant_agent_id;
uint32_t is_pending;
};
struct kVendorQuote {
uint32_t item_id;
uint32_t price;
};
struct kVendorItems {
Merchant::TransactionType transaction_type;
uint32_t item_ids_count;
uint32_t* item_ids_buffer1; // world->merchant_items.buffer
uint32_t* item_ids_buffer2; // world->merchant_items2.buffer
};
struct kSetRendererValue {
uint32_t renderer_mode; // 0 for window, 2 for full screen
uint32_t metric_id; // TODO: Enum this!
uint32_t value;
};
struct kEffectAdd {
uint32_t agent_id;
Effect* effect;
};
struct kAgentSpeechBubble {
uint32_t agent_id;
wchar_t* message;
uint32_t h0008;
uint32_t h000c;
};
struct kAgentSkillStartedCast {
uint32_t agent_id;
Constants::SkillID skill_id;
float duration;
uint32_t h000c;
};
struct kPreStartSalvage {
uint32_t item_id;
uint32_t kit_id;
};
struct kServerActiveQuestChanged {
GW::Constants::QuestID quest_id;
GW::GamePos marker;
uint32_t h0024;
GW::Constants::MapID map_id;
uint32_t log_state;
};
struct kPrintChatMessage {
GW::Chat::Channel channel;
wchar_t* message;
FILETIME timestamp;
uint32_t is_reprint;
};
struct kPartyShowConfirmDialog {
uint32_t ui_message_to_send_to_party_frame;
uint32_t prompt_identitifier;
wchar_t* prompt_enc_str;
};
struct kUIPositionChanged {
uint32_t window_id;
UI::WindowPosition* position;
};
struct kPreferenceFlagChanged {
UI::FlagPreference preference_id;
uint32_t new_value;
};
struct kPreferenceValueChanged {
UI::NumberPreference preference_id;
uint32_t new_value;
};
struct kPreferenceEnumChanged {
UI::EnumPreference preference_id;
uint32_t enum_index;
};
struct kPartySearchInvite {
uint32_t source_party_search_id;
uint32_t dest_party_search_id;
};
struct kPostProcessingEffect {
uint32_t tint;
float amount;
};
struct kLogout {
uint32_t unknown;
uint32_t character_select;
};
static_assert(sizeof(kLogout) == 0x8);
struct kKeyAction {
uint32_t gw_key;
uint32_t modifiers = 0;
uint32_t state_flags = 0; // shift held = 0x4, ctrl = 0x2, alt = 0x1
};
struct kMouseClick {
uint32_t mouse_button; // 0x0 = left, 0x1 = middle, 0x2 = right
uint32_t is_doubleclick;
uint32_t unknown_type_screen_pos;
uint32_t h000c;
uint32_t h0010;
};
enum ActionState : uint32_t {
MouseDown = 0x6, // 0x6
MouseUp = 0x7, // 0x7
MouseClick = 0x8, // 0x8
MouseDoubleClick = 0x9, // 0x9
DragRelease = 0xb, // 0xb
KeyDown = 0xe // 0xe
};
struct kMouseAction {
uint32_t frame_id;
uint32_t child_offset_id;
ActionState current_state;
void* wparam = 0;
void* lparam = 0;
};
struct kWriteToChatLog {
GW::Chat::Channel channel;
wchar_t* message;
GW::Chat::Channel channel_dupe;
};
struct kPlayerChatMessage {
GW::Chat::Channel channel;
wchar_t* message;
uint32_t player_number;
};
struct kInteractAgent {
uint32_t agent_id;
bool call_target;
};
struct kSendChangeTarget {
uint32_t target_id;
uint32_t auto_target_id;
};
struct kSendCallTarget {
CallTargetType call_type;
uint32_t agent_id;
};
struct kGetColor {
Chat::Color* color;
GW::Chat::Channel channel;
};
struct kWriteToChatLogWithSender {
uint32_t channel;
wchar_t* message;
wchar_t* sender_enc;
};
struct kSendPingWeaponSet {
uint32_t agent_id;
uint32_t weapon_item_id;
uint32_t offhand_item_id;
};
struct kSendMoveItem {
uint32_t item_id;
uint32_t quantity;
uint32_t bag_index;
uint32_t slot;
};
struct kSendMerchantRequestQuote {
Merchant::TransactionType type;
uint32_t item_id;
};
struct kSendMerchantTransactItem {
Merchant::TransactionType type;
uint32_t h0004;
Merchant::QuoteInfo give;
uint32_t gold_recv;
Merchant::QuoteInfo recv;
};
struct kSendUseItem {
uint32_t item_id;
uint16_t quantity; // Unused, but would be cool
};
struct kSendChatMessage {
wchar_t* message;
uint32_t agent_id;
};
struct kLogChatMessage {
wchar_t* message;
GW::Chat::Channel channel;
};
struct kRecvWhisper {
uint32_t transaction_id;
wchar_t* from;
wchar_t* message;
};
struct kStartWhisper {
wchar_t* player_name;
};
struct kCompassDraw {
uint32_t player_number;
uint32_t session_id;
uint32_t number_of_points;
CompassPoint* points;
};
struct kObjectiveAdd {
uint32_t objective_id;
wchar_t* name;
uint32_t type;
};
struct kObjectiveComplete {
uint32_t objective_id;
};
struct kObjectiveUpdated {
uint32_t objective_id;
};
// Straight passthru of GW::Packets::StoC::ItemGeneral
struct kItemUpdated {
uint32_t item_id;
uint32_t model_file_id;
uint32_t type;
uint32_t unk1;
uint32_t extra_id; // Dye color
uint32_t materials;
uint32_t unk2;
uint32_t interaction; // Flags
uint32_t price;
uint32_t model_id;
uint32_t quantity;
wchar_t* enc_name;
uint32_t mod_struct_size;
uint32_t* mod_struct;
};
struct kInventorySlotUpdated {
uint32_t unk;
uint32_t item_id;
uint32_t bag_index;
uint32_t slot_id;
};
struct kSendWorldAction {
WorldActionId action_id;
GW::AgentID agent_id;
bool suppress_call_target; // 1 to block "I'm targetting X", but will also only trigger if the key thing is down
};
struct kAllyOrGuildMessage {
GW::Chat::Channel channel;
wchar_t* message;
wchar_t* sender;
wchar_t* guild_tag;
};
}
}
}
+2 -2
View File
@@ -2,9 +2,9 @@
#define GWCA_VERSION_MAJOR 3
#define GWCA_VERSION_MINOR 2
#define GWCA_VERSION_PATCH 1
#define GWCA_VERSION_PATCH 2
#define GWCA_VERSION_BUILD 1
#define GWCA_VERSION "3.2.1.1"
#define GWCA_VERSION "3.2.2.1"
namespace GWCA {
constexpr int VersionMajor = GWCA_VERSION_MAJOR;
+1 -1
View File
@@ -240,7 +240,7 @@ namespace GW {
return res;
}
Vec2f Rotate(Vec2f v, float rotation);
GWCA_API Vec2f Rotate(Vec2f v, float rotation);
struct GamePos {
float x;
+96 -96
View File
@@ -3,120 +3,120 @@
#include <GWCA/Utilities/Export.h>
namespace GW {
struct Module;
extern Module ChatModule;
struct Module;
extern Module ChatModule;
struct HookStatus;
struct HookEntry;
struct HookStatus;
struct HookEntry;
namespace Chat {
typedef uint32_t Color;
namespace Chat {
typedef uint32_t Color;
#pragma warning(push)
#pragma warning(disable: 4200)
struct ChatMessage {
uint32_t channel;
uint32_t unk1;
FILETIME timestamp;
wchar_t message[0];
};
struct ChatMessage {
uint32_t channel;
uint32_t unk1;
FILETIME timestamp;
wchar_t message[0];
};
#pragma warning(pop)
const size_t CHAT_LOG_LENGTH = 0x200;
struct ChatBuffer {
uint32_t next;
uint32_t unk1;
uint32_t unk2;
ChatMessage* messages[CHAT_LOG_LENGTH];
};
namespace TextColor {
constexpr uint32_t ColorItemCommon = 0xFFFFFFFF;
constexpr uint32_t ColorItemEnhance = 0xFFA0F5F8;
constexpr uint32_t ColorItemUncommon = 0xFFB38AEC;
constexpr uint32_t ColorItemRare = 0xFFFFFD24;
constexpr uint32_t ColorItemUnique = 0xFF00FF00;
constexpr uint32_t ColorItemUniquePvp = 0xFFED1C24;
constexpr uint32_t ColorItemDull = 0xFFA0A0A0;
constexpr uint32_t ColorItemBasic = 0xFFFFFFFF;
constexpr uint32_t ColorItemBonus = 0xFFA0F5F8;
constexpr uint32_t ColorItemAssign = 0xFF6CC16D;
constexpr uint32_t ColorItemCustom = 0xFFA0A0A0;
constexpr uint32_t ColorItemRestrict = 0xFFF67D4D;
constexpr uint32_t ColorItemSell = 0xFFFFFF00;
constexpr uint32_t ColorLabel = 0xFFFFEAB8;
constexpr uint32_t ColorQuest = 0xFF00FF00;
constexpr uint32_t ColorSkillDull = 0xFFA0A0A0;
constexpr uint32_t ColorWarning = 0xFFED0002;
}
const size_t CHAT_LOG_LENGTH = 0x200;
struct ChatBuffer {
uint32_t next;
uint32_t unk1;
uint32_t unk2;
ChatMessage* messages[CHAT_LOG_LENGTH];
};
namespace TextColor {
constexpr uint32_t ColorItemCommon = 0xFFFFFFFF;
constexpr uint32_t ColorItemEnhance = 0xFFA0F5F8;
constexpr uint32_t ColorItemUncommon = 0xFFB38AEC;
constexpr uint32_t ColorItemRare = 0xFFFFD24F;
constexpr uint32_t ColorItemUnique = 0xFF00FF00;
constexpr uint32_t ColorItemUniquePvp = 0xFFED1C24;
constexpr uint32_t ColorItemDull = 0xFFA0A0A0;
constexpr uint32_t ColorItemBasic = 0xFFFFFFFF;
constexpr uint32_t ColorItemBonus = 0xFFA0F5F8;
constexpr uint32_t ColorItemAssign = 0xFF6CC16D;
constexpr uint32_t ColorItemCustom = 0xFFA0A0A0;
constexpr uint32_t ColorItemRestrict = 0xFFF67D4D;
constexpr uint32_t ColorItemSell = 0xFFFFFF00;
constexpr uint32_t ColorLabel = 0xFFFFEAB8;
constexpr uint32_t ColorQuest = 0xFF00FF00;
constexpr uint32_t ColorSkillDull = 0xFFA0A0A0;
constexpr uint32_t ColorWarning = 0xFFED0002;
}
enum Channel : int {
CHANNEL_ALLIANCE = 0,
CHANNEL_ALLIES = 1,
CHANNEL_GWCA1 = 2,
CHANNEL_ALL = 3,
CHANNEL_GWCA2 = 4,
CHANNEL_MODERATOR = 5,
CHANNEL_EMOTE = 6,
CHANNEL_WARNING = 7,
CHANNEL_GWCA3 = 8,
CHANNEL_GUILD = 9,
CHANNEL_GLOBAL = 10,
CHANNEL_GROUP = 11,
CHANNEL_TRADE = 12,
CHANNEL_ADVISORY = 13,
CHANNEL_WHISPER = 14,
CHANNEL_COUNT,
CHANNEL_COMMAND,
CHANNEL_UNKNOW = -1
};
enum Channel : int {
CHANNEL_ALLIANCE = 0,
CHANNEL_ALLIES = 1,
CHANNEL_GWCA1 = 2,
CHANNEL_ALL = 3,
CHANNEL_GWCA2 = 4,
CHANNEL_MODERATOR = 5,
CHANNEL_EMOTE = 6,
CHANNEL_WARNING = 7,
CHANNEL_GWCA3 = 8,
CHANNEL_GUILD = 9,
CHANNEL_GLOBAL = 10,
CHANNEL_GROUP = 11,
CHANNEL_TRADE = 12,
CHANNEL_ADVISORY = 13,
CHANNEL_WHISPER = 14,
CHANNEL_COUNT,
CHANNEL_COMMAND,
CHANNEL_UNKNOW = -1
};
GWCA_API Chat::ChatBuffer* GetChatLog();
GWCA_API bool AddToChatLog(wchar_t* message, uint32_t channel);
GWCA_API bool GetIsTyping();
GWCA_API Chat::ChatBuffer* GetChatLog();
GWCA_API bool AddToChatLog(wchar_t* message, uint32_t channel);
GWCA_API bool GetIsTyping();
GWCA_API bool SendChat(char channel, const char* msg);
GWCA_API bool SendChat(char channel, const wchar_t* msg);
GWCA_API bool SendChat(const wchar_t* to, const wchar_t* msg);
GWCA_API bool SendChat(char channel, const char* msg);
GWCA_API bool SendChat(char channel, const wchar_t* msg);
GWCA_API bool SendChat(const wchar_t* to, const wchar_t* msg);
GWCA_API void WriteChatF(Channel channel, const wchar_t* format, ...);
GWCA_API void WriteChat(Channel channel, const wchar_t* message, const wchar_t* sender = nullptr, bool transient = false);
GWCA_API void WriteChatEnc(Channel channel, const wchar_t* message, const wchar_t* sender = nullptr, bool transient = false);
GWCA_API void WriteChatF(Channel channel, const wchar_t* format, ...);
GWCA_API void WriteChat(Channel channel, const wchar_t* message, const wchar_t* sender = nullptr, bool transient = false);
GWCA_API void WriteChatEnc(Channel channel, const wchar_t* message, const wchar_t* sender = nullptr, bool transient = false);
typedef void(__cdecl* ChatCommandCallback)(GW::HookStatus*, const wchar_t* cmd, int argc, const LPWSTR* argv);
GWCA_API void CreateCommand(GW::HookEntry* entry, const wchar_t* cmd, ChatCommandCallback callback);
GWCA_API void DeleteCommand(GW::HookEntry* entry, const wchar_t* cmd = 0);
typedef void(__cdecl* ChatCommandCallback)(GW::HookStatus*, const wchar_t* cmd, int argc, const LPWSTR* argv);
GWCA_API void CreateCommand(GW::HookEntry* entry, const wchar_t* cmd, ChatCommandCallback callback);
GWCA_API void DeleteCommand(GW::HookEntry* entry, const wchar_t* cmd = 0);
GWCA_API void ToggleTimestamps(bool enable);
GWCA_API void SetTimestampsFormat(bool use_24h, bool show_timestamp_seconds = false);
GWCA_API void SetTimestampsColor(Color color);
GWCA_API void ToggleTimestamps(bool enable);
GWCA_API void SetTimestampsFormat(bool use_24h, bool show_timestamp_seconds = false);
GWCA_API void SetTimestampsColor(Color color);
GWCA_API Color SetSenderColor(Channel chan, Color col);
GWCA_API Color SetMessageColor(Channel chan, Color col);
GWCA_API void GetChannelColors(Channel chan, Color* sender, Color* message);
GWCA_API void GetDefaultColors(Channel chan, Color* sender, Color* message);
GWCA_API Color SetSenderColor(Channel chan, Color col);
GWCA_API Color SetMessageColor(Channel chan, Color col);
GWCA_API void GetChannelColors(Channel chan, Color* sender, Color* message);
GWCA_API void GetDefaultColors(Channel chan, Color* sender, Color* message);
GWCA_API Channel GetChannel(wchar_t opcode);
GWCA_API Channel GetChannel(char opcode);
};
GWCA_API Channel GetChannel(wchar_t opcode);
GWCA_API Channel GetChannel(char opcode);
};
}
// ============================================================
// C Interop API
// ============================================================
extern "C" {
GWCA_API void* GetChatLog();
GWCA_API bool AddToChatLog(wchar_t* message, uint32_t channel);
GWCA_API bool GetIsTyping();
GWCA_API bool SendChat(char channel, const wchar_t* message);
GWCA_API bool SendWhisper(const wchar_t* to, const wchar_t* message);
GWCA_API void WriteChat(int channel, const wchar_t* message, const wchar_t* sender, bool transient);
GWCA_API void WriteChatEnc(int channel, const wchar_t* message, const wchar_t* sender, bool transient);
GWCA_API void ToggleTimestamps(bool enable);
GWCA_API void SetTimestampsFormat(bool use_24h, bool show_timestamp_seconds);
GWCA_API void SetTimestampsColor(uint32_t color);
GWCA_API uint32_t SetSenderColor(int channel, uint32_t color);
GWCA_API uint32_t SetMessageColor(int channel, uint32_t color);
GWCA_API void GetChannelColors(int channel, uint32_t* sender, uint32_t* message);
GWCA_API void GetDefaultColors(int channel, uint32_t* sender, uint32_t* message);
GWCA_API int GetChannelFromWChar(wchar_t opcode);
GWCA_API int GetChannelFromChar(char opcode);
GWCA_API void* GetChatLog();
GWCA_API bool AddToChatLog(wchar_t* message, uint32_t channel);
GWCA_API bool GetIsTyping();
GWCA_API bool SendChat(char channel, const wchar_t* message);
GWCA_API bool SendWhisper(const wchar_t* to, const wchar_t* message);
GWCA_API void WriteChat(int channel, const wchar_t* message, const wchar_t* sender, bool transient);
GWCA_API void WriteChatEnc(int channel, const wchar_t* message, const wchar_t* sender, bool transient);
GWCA_API void ToggleTimestamps(bool enable);
GWCA_API void SetTimestampsFormat(bool use_24h, bool show_timestamp_seconds);
GWCA_API void SetTimestampsColor(uint32_t color);
GWCA_API uint32_t SetSenderColor(int channel, uint32_t color);
GWCA_API uint32_t SetMessageColor(int channel, uint32_t color);
GWCA_API void GetChannelColors(int channel, uint32_t* sender, uint32_t* message);
GWCA_API void GetDefaultColors(int channel, uint32_t* sender, uint32_t* message);
GWCA_API int GetChannelFromWChar(wchar_t opcode);
GWCA_API int GetChannelFromChar(char opcode);
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<NoWarn>$(NoWarn);IL2104;IL3053;IL3000;IL3002;NU1701;CS0108</NoWarn>
<Version>0.9.10.4</Version>
<Version>0.9.10.5</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>