mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-09-18 14:24:59 +00:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
310dbd59b8 | ||
|
|
eae44fb576 | ||
|
|
8d847159f3 | ||
|
|
82514d1707 | ||
|
|
84ee5a9faf | ||
|
|
2634c32b22 | ||
|
|
538e56d0a9 | ||
|
|
59b1acf9df | ||
|
|
9c3f644869 | ||
|
|
0240acb37a | ||
|
|
02c24e6e86 | ||
|
|
53da02d440 | ||
|
|
06c27da672 | ||
|
|
54bf57e6c1 | ||
|
|
733d6fc9f6 | ||
|
|
46943e940b | ||
|
|
13985d703f | ||
|
|
e01aa28e01 | ||
|
|
f5eb67fd1c | ||
|
|
d380949b45 | ||
|
|
b23811604d | ||
|
|
7a665491f1 | ||
|
|
a966906d6e | ||
|
|
5b554707bf | ||
|
|
aeb2c9cc99 | ||
|
|
c1aaf2d1e7 | ||
|
|
98fa8ffa3f | ||
|
|
19c7b6a852 | ||
|
|
0905bf05cb | ||
|
|
132d032546 | ||
|
|
78646fd1b5 | ||
|
|
d8f92b4b91 |
@@ -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/
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csharp"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-dotnettools.csdevkit"
|
||||
]
|
||||
}
|
||||
Vendored
+6
-2
@@ -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",
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "Daybreak.slnx",
|
||||
"omnisharp.projectLoadTimeout": 120,
|
||||
"dotnet.preferCSharpExtension": true,
|
||||
"csharp.semanticHighlighting.enabled": true,
|
||||
"search.followSymlinks": false,
|
||||
"search.exclude": {
|
||||
|
||||
Vendored
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+633
-515
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed record AddressHealth(string Name, string Address, bool Hooked);
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -892,7 +892,7 @@ namespace GW {
|
||||
Lakeside_County_1070_AE,
|
||||
Ashford_Catacombs_1070_AE,
|
||||
|
||||
Count = 0x370,
|
||||
Count = 0x373,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+574
-567
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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user