mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
Implement Daybreak.API. Reorganize FocusView components (#995)
This commit is contained in:
@@ -82,21 +82,6 @@ jobs:
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
|
||||
- name: Create publish installer files
|
||||
run: dotnet publish .\Daybreak.Installer\Daybreak.Installer.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained true -o .\Publish
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
|
||||
- name: Create publish extractor files
|
||||
run: dotnet publish .\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
|
||||
- name: Create publish API files
|
||||
run: dotnet publish .\Daybreak.API\Daybreak.API.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishSingleFile=false --self-contained true -o .\Publish
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
|
||||
- name: Pack publish files
|
||||
run: |
|
||||
Write-Host $env
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<RootNamespace>Daybreak._7ZipExtractor</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<Platforms>x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Daybreak.API.Configuration;
|
||||
|
||||
public static class BuildInfo
|
||||
{
|
||||
public const string Configuration =
|
||||
#if DEBUG
|
||||
"Debug";
|
||||
#else
|
||||
"Release";
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace Daybreak.API.Configuration;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithConfiguration(this WebApplicationBuilder builder)
|
||||
{
|
||||
var config = BuildInfo.Configuration;
|
||||
var appSettingsStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.json");
|
||||
if (appSettingsStream is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to load appsettings.json");
|
||||
}
|
||||
|
||||
var appSettingsConfigStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.{config}.json");
|
||||
if (appSettingsConfigStream is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load appsettings.{config}.json");
|
||||
}
|
||||
|
||||
builder.Configuration
|
||||
.AddJsonStream(appSettingsStream)
|
||||
.AddJsonStream(appSettingsConfigStream)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
builder.WebHost.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true");
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static Stream? GetManifestResourceStream(string name)
|
||||
{
|
||||
var assembly = typeof(WebApplicationBuilderExtensions).Assembly;
|
||||
var resourceName = assembly.GetManifestResourceNames()
|
||||
.FirstOrDefault(n => n.EndsWith(name, StringComparison.OrdinalIgnoreCase));
|
||||
if (resourceName is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return assembly.GetManifestResourceStream(resourceName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Debug",
|
||||
"Override": {
|
||||
"Microsoft": "Debug"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Correlation": {
|
||||
"Header": "X-Correlation-Vector"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Services;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Net.Sdk.Web;
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace Daybreak.API.Controllers.Rest;
|
||||
|
||||
[GenerateController("api/v1/rest/character-select")]
|
||||
[Tags("Character Select")]
|
||||
public sealed class CharacterSelectController(
|
||||
CharacterSelectService characterSelectService)
|
||||
{
|
||||
private readonly CharacterSelectService characterSelectService = characterSelectService.ThrowIfNull();
|
||||
|
||||
[GenerateGet]
|
||||
[EndpointName("GetCharacterSelectInformation")]
|
||||
[EndpointSummary("Get Character Select Information")]
|
||||
[EndpointDescription("Get the character select information for the current player. Returns a serialized CharacterSelectInformation object")]
|
||||
[ProducesResponseType<CharacterSelectInformation>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetCharacterSelectInformation(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var characterSelectInfo = await this.characterSelectService.GetCharacterSelectInformation(cancellationToken);
|
||||
return characterSelectInfo is null ?
|
||||
Results.StatusCode(StatusCodes.Status503ServiceUnavailable) :
|
||||
Results.Ok(characterSelectInfo);
|
||||
}
|
||||
|
||||
[GeneratePost("{identifier}")]
|
||||
[EndpointName("ChangeCharacter")]
|
||||
[EndpointSummary("Change Character by identifier")]
|
||||
[EndpointDescription("Logs out from the current character and tries to log in with the desired character. Identifier can be either uuid or character name")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> ChangeCharacter(
|
||||
[FromRoute] string identifier,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var isUuid = Uuid.TryParse(identifier, out _);
|
||||
var result = isUuid
|
||||
? await this.characterSelectService.ChangeCharacterByUuid(identifier, cancellationToken)
|
||||
: await this.characterSelectService.ChangeCharacterByName(identifier, cancellationToken);
|
||||
return result ?
|
||||
Results.Ok() :
|
||||
Results.StatusCode(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using Daybreak.API.Services;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Net.Sdk.Web;
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace Daybreak.API.Controllers.Rest;
|
||||
|
||||
[GenerateController("api/v1/rest/main-player")]
|
||||
[Tags("Main Player")]
|
||||
public sealed class MainPlayerController(
|
||||
MainPlayerService mainPlayerService,
|
||||
ILogger<MainPlayerController> logger)
|
||||
{
|
||||
private readonly MainPlayerService mainPlayerService = mainPlayerService.ThrowIfNull();
|
||||
private readonly ILogger<MainPlayerController> logger = logger.ThrowIfNull();
|
||||
|
||||
[GenerateGet("state")]
|
||||
[EndpointName("GetMainPlayerState")]
|
||||
[EndpointSummary("Get the current MainPlayerState")]
|
||||
[EndpointDescription("Get the current MainPlayerState. Returns a json serialized MainPlayerState object. You can use the websocket endpoint to subscribe to MainPlayerState updates on each game thread proc")]
|
||||
[ProducesResponseType<MainPlayerState>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetMainPlayerState(CancellationToken cancellationToken)
|
||||
{
|
||||
var mainPlayerState = await this.mainPlayerService.GetMainPlayerState(cancellationToken);
|
||||
return mainPlayerState is not null ? Results.Ok(mainPlayerState) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GenerateGet("quest-log")]
|
||||
[EndpointName("GetQuestLog")]
|
||||
[EndpointSummary("Get the current Quest Log")]
|
||||
[EndpointDescription("Get the current Quest Log. Returns a json serialized QuestLogInformation object")]
|
||||
[ProducesResponseType<QuestLogInformation>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetQuestLog(CancellationToken cancellationToken)
|
||||
{
|
||||
var questLog = await this.mainPlayerService.GetQuestLog(cancellationToken);
|
||||
return questLog is not null ? Results.Ok(questLog) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GenerateGet("info")]
|
||||
[EndpointName("GetMainPlayerInformation")]
|
||||
[EndpointSummary("Get the current MainPlayerInformation")]
|
||||
[EndpointDescription("Get the current MainPlayerInformation. Returns a json serialized MainPlayerInformation object")]
|
||||
[ProducesResponseType<MainPlayerInformation>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetMainPlayerInformation(CancellationToken cancellationToken)
|
||||
{
|
||||
var mainPlayerInformation = await this.mainPlayerService.GetMainPlayerInformation(cancellationToken);
|
||||
return mainPlayerInformation is not null ? Results.Ok(mainPlayerInformation) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GenerateGet("build")]
|
||||
[EndpointName("GetMainPlayerBuild")]
|
||||
[EndpointSummary("Get the current build")]
|
||||
[EndpointDescription("Get the current build. Returns a json serialized BuildEntry object")]
|
||||
[ProducesResponseType<BuildEntry>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetMainPlayerBuild(CancellationToken cancellationToken)
|
||||
{
|
||||
var build = await this.mainPlayerService.GetCurrentBuild(cancellationToken);
|
||||
return build is not null ? Results.Ok(build) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GeneratePost("build")]
|
||||
[EndpointName("SetMainPlayerBuild")]
|
||||
[EndpointSummary("Set the current build")]
|
||||
[EndpointDescription("Set the current build. Returns 200 is the operation has succeeded")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> SetMainPlayerBuild([FromQuery(Name = "code")] string? code, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await this.mainPlayerService.SetCurrentBuild(code ?? string.Empty, cancellationToken);
|
||||
return result ? Results.Ok() : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GenerateGet("instance-info")]
|
||||
[EndpointName("GetMainPlayerInstanceInfo")]
|
||||
[EndpointSummary("Get the current instance info")]
|
||||
[EndpointDescription("Get the current instance info. Returns a json serialized MainPlayerInstanceInfo object")]
|
||||
[ProducesResponseType<InstanceInfo>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetMainPlayerInstanceInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var instanceInfo = await this.mainPlayerService.GetMainPlayerInstance(cancellationToken);
|
||||
return instanceInfo is not null ? Results.Ok(instanceInfo) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GenerateGet("title")]
|
||||
[EndpointName("GetMainPlayerTitleInfo")]
|
||||
[EndpointSummary("Get the current title info")]
|
||||
[EndpointDescription("Get the current title info. Returns a json serialized TitleInfo object")]
|
||||
[ProducesResponseType<TitleInfo>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetMainPlayerTitleInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var titleInfo = await this.mainPlayerService.GetTitleInfo(cancellationToken);
|
||||
return titleInfo is not null ? Results.Ok(titleInfo) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Daybreak.API.Services;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Net.Sdk.Web;
|
||||
|
||||
namespace Daybreak.API.Controllers.Rest;
|
||||
|
||||
[GenerateController("api/v1/rest/party")]
|
||||
[Tags("Party")]
|
||||
public sealed class PartyController(PartyService partyService)
|
||||
{
|
||||
private readonly PartyService 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)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> GetPartyLoadout(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var partyLoadout = await this.partyService.GetPartyLoadout(cancellationToken);
|
||||
return partyLoadout is not null ? Results.Ok(partyLoadout) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
[GeneratePost("loadout")]
|
||||
[EndpointName("SetPartyLoadout")]
|
||||
[EndpointSummary("Set the current party loadout")]
|
||||
[EndpointDescription("Set the current party loadout")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IResult> SetPartyLoadout([FromBody] PartyLoadout partyLoadout, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await this.partyService.SetPartyLoadout(partyLoadout, cancellationToken);
|
||||
return result ? Results.Ok() : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using Net.Sdk.Web;
|
||||
|
||||
namespace Daybreak.API.Controllers;
|
||||
|
||||
[GenerateController("api/test")]
|
||||
public sealed class TestController
|
||||
{
|
||||
[GenerateGet("testing")]
|
||||
public IResult GetTest(CancellationToken token)
|
||||
{
|
||||
return Results.Text("Hello from injected ASP-NET Core!", "text/plain");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services;
|
||||
using Daybreak.API.WebSockets;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using System.Buffers;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace Daybreak.API.Controllers.WebSocket.MainPlayer;
|
||||
|
||||
public sealed class MainPlayerStateRoute(
|
||||
MainPlayerService mainPlayerStateService,
|
||||
ChatService chatService,
|
||||
ILogger<MainPlayerStateRoute> logger)
|
||||
: UpdateWebSocketRoute
|
||||
{
|
||||
private const int DefaultFrequency = 1000;
|
||||
|
||||
private readonly MainPlayerService mainPlayerStateService = mainPlayerStateService.ThrowIfNull();
|
||||
private readonly ChatService chatService = chatService.ThrowIfNull();
|
||||
private readonly ILogger<MainPlayerStateRoute> logger = logger.ThrowIfNull();
|
||||
|
||||
private CallbackRegistration? mainPlayerStateRegistration;
|
||||
|
||||
protected override int BufferSize => 256;
|
||||
|
||||
public override Task ExecuteAsync(WebSocketMessageType type, ReadOnlySequence<byte> data, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task SocketAccepted(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (!int.TryParse(this.Context?.Request.Query["f"].FirstOrDefault(), out var freq))
|
||||
{
|
||||
freq = DefaultFrequency;
|
||||
}
|
||||
|
||||
var freqTimeSpan = TimeSpan.FromMilliseconds(freq);
|
||||
scopedLogger.LogInformation("WebSocket accepted {id} with frequency {frequency}", this.Context?.Connection.Id ?? string.Empty, freqTimeSpan);
|
||||
|
||||
this.mainPlayerStateRegistration = this.mainPlayerStateService.RegisterMainStateConsumer(freqTimeSpan, this.SendUpdate);
|
||||
|
||||
await this.chatService.AddMessageAsync(
|
||||
message: $"{nameof(MainPlayerState)} subscriber added: {this.Context?.Connection.RemoteIpAddress?.ToString()}:{this.Context?.Connection.RemotePort} with frequency {freq}ms",
|
||||
sender: "Daybreak.API",
|
||||
channel: Channel.Whisper,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public override async Task SocketClosed()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
|
||||
this.mainPlayerStateRegistration?.Dispose();
|
||||
this.mainPlayerStateRegistration = default;
|
||||
|
||||
scopedLogger.LogInformation("WebSocket closed {id}", this.Context?.Connection.Id ?? string.Empty);
|
||||
await this.chatService.AddMessageAsync(
|
||||
message: $"{nameof(MainPlayerState)} subscriber removed: {this.Context?.Connection.RemoteIpAddress?.ToString()}:{this.Context?.Connection.RemotePort}",
|
||||
sender: "Daybreak.API",
|
||||
channel: Channel.Whisper,
|
||||
cancellationToken: CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Daybreak.API.Interop;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.API.Converters;
|
||||
|
||||
public sealed class PointerValueConverter : JsonConverter<PointerValue>
|
||||
{
|
||||
public override PointerValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var s = reader.GetString() ?? throw new JsonException("Expected string");
|
||||
return PointerValue.Parse(s);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, PointerValue value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
@@ -9,33 +9,49 @@
|
||||
<StripSymbols>true</StripSymbols>
|
||||
<InteropExports>true</InteropExports>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
|
||||
|
||||
<PublishAot>true</PublishAot>
|
||||
<SelfContained>true</SelfContained>
|
||||
|
||||
|
||||
|
||||
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
|
||||
<UseAppHost>false</UseAppHost>
|
||||
<EnableIISSupport>false</EnableIISSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<Platforms>x86</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Net.Sdk.Web.Extensions.SourceGenerators" Version="0.9.0" />
|
||||
<EmbeddedResource Include="Configuration\appsettings.Debug.json" />
|
||||
<EmbeddedResource Include="Configuration\appsettings.json" />
|
||||
<EmbeddedResource Include="Configuration\appsettings.Release.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<RdXmlFile Include="rd.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
|
||||
<PackageReference Include="MinHook.NET" Version="1.1.1" />
|
||||
<PackageReference Include="Net.Sdk.Web.Extensions" Version="0.8.10" />
|
||||
<PackageReference Include="Net.Sdk.Web.Extensions.SourceGenerators" Version="0.9.3" />
|
||||
<PackageReference Include="PeNet" Version="5.1.0" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.1" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="8.1.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="8.1.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.2" />
|
||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageReference Include="SystemExtensions.NetStandard.Generators" Version="0.1.5" PrivateAssets="all" />
|
||||
<PackageReference Include="ZLinq" Version="1.4.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<DaybreakHostOutputDir>$(MSBuildProjectDirectory)\..\Daybreak\bin\x86\$(Configuration)\$(TargetFramework)\</DaybreakHostOutputDir>
|
||||
<ShouldPublishAot>true</ShouldPublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PublishIntoDaybreak" AfterTargets="PostBuild" Condition="'$(ShouldPublishAot)'=='true'">
|
||||
<Message Text="📦 dotnet-publish Daybreak.API → $(DaybreakHostOutputDir)" Importance="High" />
|
||||
|
||||
<Exec Command="dotnet publish "$(MSBuildProjectFullPath)" -c $(Configuration) -r win-x86 --self-contained true -o "$(DaybreakHostOutputDir)"" WorkingDirectory="$(ProjectDir)" />
|
||||
</Target>
|
||||
</Project>
|
||||
+73
-21
@@ -1,47 +1,99 @@
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Diagnostics;
|
||||
using System.Extensions.Core;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
using Daybreak.API.Configuration;
|
||||
using Daybreak.API.Extensions;
|
||||
using Daybreak.API.Health;
|
||||
using Daybreak.API.Hosting;
|
||||
using Daybreak.API.Logging;
|
||||
using Daybreak.API.Serialization;
|
||||
using Daybreak.API.Services;
|
||||
using Daybreak.API.Swagger;
|
||||
using Daybreak.API.WebSockets;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Net.Sdk.Web;
|
||||
|
||||
namespace Daybreak.API;
|
||||
|
||||
public static class EntryPoint
|
||||
public class EntryPoint
|
||||
{
|
||||
private const int StartPort = 5080;
|
||||
private static readonly TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly CancellationTokenSource CancellationTokenSource = new();
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "ThreadInit"), STAThread]
|
||||
public static int ThreadInit(IntPtr _, int __)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", null, EnvironmentVariableTarget.Process);
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_PREVENTHOSTINGSTARTUP", "true", EnvironmentVariableTarget.Process);
|
||||
#if DEBUG
|
||||
ConsoleExtensions.AllocateAnsiConsole();
|
||||
#endif
|
||||
var port = FindAvailablePort(StartPort);
|
||||
if (port <= 0)
|
||||
var app = CreateApplication(port);
|
||||
var runTask = Task.Run(() => StartServer(app), CancellationTokenSource.Token);
|
||||
var scopedLogger = app.Services.GetRequiredService<ILogger<EntryPoint>>().CreateScopedLogger();
|
||||
var healthCheck = app.Services.GetRequiredService<HealthCheckService>();
|
||||
var sw = Stopwatch.StartNew();
|
||||
var healthy = false;
|
||||
while(sw.Elapsed < InitializationTimeout)
|
||||
{
|
||||
Console.WriteLine($"No available port found starting from {StartPort}");
|
||||
return -1;
|
||||
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}")));
|
||||
if (status.Status is HealthStatus.Healthy)
|
||||
{
|
||||
healthy = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"Starting Daybreak API on port {port}");
|
||||
Task.Run(() => StartServer(port));
|
||||
return port;
|
||||
if (healthy)
|
||||
{
|
||||
scopedLogger.LogInformation("Daybreak API is healthy on port {port}. Initialization succeeded in {duration}", port, sw.Elapsed);
|
||||
return port;
|
||||
}
|
||||
else
|
||||
{
|
||||
scopedLogger.LogError("Daybreak API failed to initialize in {duration}", sw.Elapsed);
|
||||
CancellationTokenSource.Cancel();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task StartServer(int port)
|
||||
private static WebApplication CreateApplication(int port)
|
||||
{
|
||||
var app = WebApplication.CreateBuilder()
|
||||
.WithConfiguration()
|
||||
.WithHosting(port)
|
||||
.WithSwagger()
|
||||
.WithSerializationContext()
|
||||
.WithLogging()
|
||||
.WithDaybreakServices()
|
||||
.WithWebSocketRoutes()
|
||||
.WithRoutes()
|
||||
.WithHealthChecks()
|
||||
.Build();
|
||||
|
||||
app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(30) });
|
||||
|
||||
return app
|
||||
.UseHealthChecks()
|
||||
.UseLogging()
|
||||
.UseRoutes()
|
||||
.UseWebSocketRoutes()
|
||||
.UseSwaggerWithUI();
|
||||
|
||||
}
|
||||
|
||||
private static async Task StartServer(WebApplication app)
|
||||
{
|
||||
try
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.UseUrls($"http://127.0.0.1:{port}");
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
{
|
||||
options.SerializerOptions.TypeInfoResolverChain.Insert(0, new ApiJsonSerializerContext());
|
||||
});
|
||||
builder.Logging.AddConsole();
|
||||
builder.WithRoutes();
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseRoutes();
|
||||
|
||||
await app.RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using ZLinq;
|
||||
|
||||
namespace Daybreak.API.Extensions;
|
||||
|
||||
public static class AttributeContextExtensions
|
||||
{
|
||||
public static List<AttributeEntry> GetAttributeEntryList(this AttributeContext[] attributes)
|
||||
{
|
||||
return attributes
|
||||
.AsValueEnumerable()
|
||||
.Take(45) // There are only 45 maximum attributes in game
|
||||
.Where(a => a.LevelBase > 0 && a.Level >= a.LevelBase) // Select only attributes with points in them
|
||||
.Select(a => new AttributeEntry(a.Id, a.LevelBase, a.Level)).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Daybreak.API.Extensions;
|
||||
|
||||
public static unsafe class GameContextExtensions
|
||||
{
|
||||
public static bool TryGetPlayerId(
|
||||
this WrappedPointer<GameContext> gameContext,
|
||||
[NotNullWhen(true)] out uint? playerId)
|
||||
{
|
||||
playerId = 0;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null ||
|
||||
gameContext.Pointer->WorldContext->PlayerControlledChar is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
playerId = gameContext.Pointer->WorldContext->PlayerControlledChar->AgentId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetBuildContext(
|
||||
this WrappedPointer<GameContext> gameContext,
|
||||
[NotNullWhen(true)] out GuildWarsArray<SkillbarContext>? skillbars,
|
||||
[NotNullWhen(true)] out GuildWarsArray<PartyAttribute>? attributes,
|
||||
[NotNullWhen(true)] out GuildWarsArray<ProfessionsContext>? professions,
|
||||
[NotNullWhen(true)] out GuildWarsArray<uint>? unlockedSkills)
|
||||
{
|
||||
skillbars = default;
|
||||
attributes = default;
|
||||
professions = default;
|
||||
unlockedSkills = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
skillbars = gameContext.Pointer->WorldContext->Skillbars;
|
||||
attributes = gameContext.Pointer->WorldContext->Attributes;
|
||||
professions = gameContext.Pointer->WorldContext->Professions;
|
||||
unlockedSkills = gameContext.Pointer->WorldContext->UnlockedCharacterSkills;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetPlayerParty(
|
||||
this WrappedPointer<GameContext> gameContext,
|
||||
[NotNullWhen(true)] out uint? partyId,
|
||||
[NotNullWhen(true)] out GuildWarsArray<PlayerPartyMember>? players,
|
||||
[NotNullWhen(true)] out GuildWarsArray<HeroPartyMember>? heroes,
|
||||
[NotNullWhen(true)] out GuildWarsArray<HenchmanPartyMember>? henchmen)
|
||||
{
|
||||
partyId = default;
|
||||
players = default;
|
||||
heroes = default;
|
||||
henchmen = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->PartyContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
partyId = gameContext.Pointer->PartyContext->PlayerParty->PartyId;
|
||||
players = gameContext.Pointer->PartyContext->PlayerParty->Players;
|
||||
heroes = gameContext.Pointer->PartyContext->PlayerParty->Heroes;
|
||||
henchmen = gameContext.Pointer->PartyContext->PlayerParty->Henchmen;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetHeroFlags(
|
||||
this WrappedPointer<GameContext> gameContext,
|
||||
[NotNullWhen(true)]out GuildWarsArray<HeroFlag>? heroFlags)
|
||||
{
|
||||
heroFlags = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
heroFlags = gameContext.Pointer->WorldContext->HeroFlags;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetAccountContext(
|
||||
this WrappedPointer<GameContext> gameContext,
|
||||
out WrappedPointer<AccountGameContext> accountContext)
|
||||
{
|
||||
accountContext = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->AccountContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
accountContext = gameContext.Pointer->AccountContext;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Daybreak.API.Serialization;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using System.Text.Json;
|
||||
using ZLinq;
|
||||
|
||||
namespace Daybreak.API.Health;
|
||||
|
||||
public sealed class AddressHealthCheck(IEnumerable<IAddressHealthService> addressHealthServices)
|
||||
: IHealthCheck
|
||||
{
|
||||
private readonly IEnumerable<IAddressHealthService> addressHealthServices = addressHealthServices;
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var states = this.addressHealthServices.AsValueEnumerable().SelectMany(addressService => addressService.GetAddressStates());
|
||||
var healthy = !states.Any(s => s.Address == 0x0);
|
||||
using var statesArray = states.ToArrayPool();
|
||||
var serialized = JsonSerializer.SerializeToElement(statesArray.ArraySegment, ApiJsonSerializerContext.Default.ArraySegmentAddressState);
|
||||
var meta = new Dictionary<string, object>
|
||||
{
|
||||
["Count"] = statesArray.Size,
|
||||
["Invalid"] = states.Count(s => s.Address == 0x0),
|
||||
["States"] = serialized,
|
||||
};
|
||||
|
||||
return Task.FromResult(healthy
|
||||
? HealthCheckResult.Healthy("Addresses are initialized", meta)
|
||||
: HealthCheckResult.Unhealthy("Addresses are not initialized", data: meta));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Daybreak.API.Serialization;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using System.Text.Json;
|
||||
using ZLinq;
|
||||
|
||||
namespace Daybreak.API.Health;
|
||||
|
||||
public sealed class HooksHealthCheck(IEnumerable<IHookHealthService> hookHealthServices)
|
||||
: IHealthCheck
|
||||
{
|
||||
private readonly IEnumerable<IHookHealthService> hookHealthServices = hookHealthServices;
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var states = this.hookHealthServices.AsValueEnumerable().SelectMany(hookService => hookService.GetHookStates());
|
||||
var healthy = !states.Any(s => !s.Hooked);
|
||||
var statesArray = states.ToArrayPool();
|
||||
var serialized = JsonSerializer.SerializeToElement(statesArray.ArraySegment, ApiJsonSerializerContext.Default.ArraySegmentHookState);
|
||||
var meta = new Dictionary<string, object>
|
||||
{
|
||||
["Count"] = statesArray.Size,
|
||||
["Unhooked"] = states.Count(s => !s.Hooked),
|
||||
["States"] = serialized,
|
||||
};
|
||||
|
||||
return Task.FromResult(healthy
|
||||
? HealthCheckResult.Healthy("Hooks are initialized", meta)
|
||||
: HealthCheckResult.Unhealthy("Hooks are not initialized", data: meta));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Daybreak.API.Health;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithHealthChecks(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck<HooksHealthCheck>(nameof(HooksHealthCheck))
|
||||
.AddCheck<AddressHealthCheck>(nameof(AddressHealthCheck));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Daybreak.API.Models;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using ZLinq;
|
||||
|
||||
namespace Daybreak.API.Health;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
[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")]
|
||||
public static WebApplication UseHealthChecks(this WebApplication app)
|
||||
{
|
||||
app.MapGet("/api/v1/rest/health", static async (HealthCheckService healthCheckService, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var report = await healthCheckService.CheckHealthAsync(cancellationToken);
|
||||
|
||||
var response = new HealthCheckResponse
|
||||
{
|
||||
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)
|
||||
})
|
||||
};
|
||||
|
||||
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")
|
||||
.Produces<HealthCheckResponse>(StatusCodes.Status200OK)
|
||||
.WithOpenApi();
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Daybreak.API.Hosting;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithHosting(this WebApplicationBuilder builder, int port)
|
||||
{
|
||||
builder.WebHost.ConfigureKestrel(o => o.ListenAnyIP(port));
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma warning disable CS0436 // Type conflicts with imported type
|
||||
using System.Extensions;
|
||||
|
||||
[assembly: GenerateFixedArray<uint>(Size = 4)]
|
||||
[assembly: GenerateFixedArray<char>(Size = 0x14)]
|
||||
[assembly: GenerateFixedArray<char>(Size = 0x40)]
|
||||
[assembly: GenerateFixedArray<byte>(Size = 0x18)]
|
||||
[assembly: GenerateFixedArray<char>(Size = 256)]
|
||||
[assembly: GenerateFixedArray<char>(Size = 32)]
|
||||
[assembly: GenerateFixedArray<char>(Size = 5)]
|
||||
[assembly: GenerateFixedArray<char>(Size = 8)]
|
||||
[assembly: GenerateFixedArray<byte>(Size = 56)]
|
||||
[assembly: GenerateFixedArray<uint>(Size = 17)]
|
||||
[assembly: GenerateFixedArray<uint>(Size = 12)]
|
||||
[assembly: GenerateFixedArray<uint>(Size = 13)]
|
||||
[assembly: GenerateFixedArray<uint>(Size = 8)]
|
||||
#pragma warning restore CS0436 // Type conflicts with imported type
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public static class FixedArrays
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public sealed class GWDelegateCache<TDelegate>(GWAddressCache cache)
|
||||
where TDelegate : Delegate
|
||||
{
|
||||
public GWAddressCache Cache { get; } = cache;
|
||||
|
||||
public TDelegate? GetDelegate()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint address)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return Marshal.GetDelegateForFunctionPointer<TDelegate>((nint)address);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
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();
|
||||
|
||||
protected unsafe static nint BuildFastCallThunk(nuint target)
|
||||
{
|
||||
if (target is 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var code = stackalloc byte[13]
|
||||
{
|
||||
0x58, // pop eax (ret)
|
||||
0x59, // pop ecx (ctx)
|
||||
0x5A, // pop edx (edxVal)
|
||||
0x5B, // pop ebx (wparam)
|
||||
0x53, // push ebx (wparam)
|
||||
0x50, // push eax (ret)
|
||||
0xB8,0,0,0,0, // mov eax, TARGET
|
||||
0xFF,0xE0 // jmp eax
|
||||
}; // 13 bytes total
|
||||
|
||||
const int TARGET_OFFSET = 7;
|
||||
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
|
||||
|
||||
var mem = NativeMethods.VirtualAlloc(0, 13, 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, 13, 13);
|
||||
return mem;
|
||||
}
|
||||
}
|
||||
|
||||
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 = BuildFastCallThunk(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 = BuildFastCallThunk(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 = BuildFastCallThunk(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 = BuildFastCallThunk(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 = BuildFastCallThunk(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7>)call;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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) : IHook<T>
|
||||
where T : Delegate
|
||||
{
|
||||
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
|
||||
private HookEngine? engine;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.semaphore.Wait();
|
||||
try
|
||||
{
|
||||
if (!this.Hooked ||
|
||||
this.engine is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.engine.DisableHooks();
|
||||
this.engine.Dispose();
|
||||
|
||||
this.cont = null;
|
||||
this.Hooked = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
byte op = Marshal.ReadByte((IntPtr)addr);
|
||||
if (op != 0xE8 && op != 0xE9) // not CALL/JMP rel32
|
||||
return addr;
|
||||
|
||||
int rel = Marshal.ReadInt32((IntPtr)(addr + 1));
|
||||
long dst = (long)addr + 5 + rel; // 5-byte instruction
|
||||
return (nuint)dst;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
|
||||
public readonly struct AccountUnlockedCount
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint Id;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x138)]
|
||||
public readonly struct AccountGameContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly GuildWarsArray<AccountUnlockedCount> AccountUnlockedCounts;
|
||||
|
||||
[FieldOffset(0x00b4)]
|
||||
public readonly GuildWarsArray<uint> UnlockedPvpHeroes;
|
||||
|
||||
[FieldOffset(0x0124)]
|
||||
public readonly GuildWarsArray<uint> UnlockedAccountSkills;
|
||||
|
||||
[FieldOffset(0x0134)]
|
||||
public readonly uint AccountFlags;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x1C)]
|
||||
public readonly unsafe struct AccountInfoContext
|
||||
{
|
||||
public readonly char* AccountName;
|
||||
public readonly uint Wins;
|
||||
public readonly uint Losses;
|
||||
public readonly uint Rating;
|
||||
public readonly uint QualifierPoints;
|
||||
public readonly uint Rank;
|
||||
public readonly uint TournamentRewardPoints;
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum AgentType : uint
|
||||
{
|
||||
Living = 0xDB,
|
||||
Gadget = 0x200,
|
||||
Item = 0x400
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AgentModelType : ushort
|
||||
{
|
||||
NPC = 0x2000,
|
||||
Player = 0x3000
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum LivingAgentState : uint
|
||||
{
|
||||
None = 0,
|
||||
InCombatStance = 0x000001,
|
||||
HasQuest = 0x000002,
|
||||
Dead = 0x000008,
|
||||
Female = 0x000200,
|
||||
HasBossGlow = 0x000400,
|
||||
HidingCape = 0x001000,
|
||||
CanBeViewedInPartyWindow = 0x020000,
|
||||
SpawnedSpirit = 0x040000,
|
||||
BeingObservedOrPlayer = 0x400000
|
||||
}
|
||||
|
||||
public enum LivingAgentAllegiance : byte
|
||||
{
|
||||
Ally_NonAttackable = 0x1,
|
||||
Neutral = 0x2,
|
||||
Enemy = 0x3,
|
||||
Spirit_Pet = 0x4,
|
||||
Minion = 0x5,
|
||||
Npc_Minipet = 0x6
|
||||
};
|
||||
|
||||
|
||||
[Flags]
|
||||
public enum LivingAgentEffects : uint
|
||||
{
|
||||
None = 0,
|
||||
Bleeding = 0x0001,
|
||||
Conditioned = 0x0002,
|
||||
Crippled1 = 0x0008, // Part of the Crippled check
|
||||
Crippled2 = 0x0002, // Part of the Crippled check
|
||||
Crippled = 0x000A, // Combined flag for crippled (0x0008 | 0x0002)
|
||||
Dead = 0x0010,
|
||||
DeepWound = 0x0020,
|
||||
Poisoned = 0x0040,
|
||||
Enchanted = 0x0080,
|
||||
DegenHexed = 0x0400,
|
||||
Hexed = 0x0800,
|
||||
WeaponSpelled = 0x8000
|
||||
}
|
||||
|
||||
public enum LivingAgentModelState : ushort
|
||||
{
|
||||
// Idle states
|
||||
Idle1 = 64,
|
||||
Idle2 = 68,
|
||||
Idle3 = 100,
|
||||
|
||||
// Casting states
|
||||
Casting1 = 65,
|
||||
Casting2 = 581,
|
||||
|
||||
// Moving states
|
||||
Moving1 = 12,
|
||||
Moving2 = 76,
|
||||
Moving3 = 204,
|
||||
|
||||
// Attacking states
|
||||
Attacking1 = 96,
|
||||
Attacking2 = 1088,
|
||||
Attacking3 = 1120,
|
||||
|
||||
// Other states
|
||||
KnockedDown = 1104
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct AgentEffects
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly GuildWarsArray<Buff> Buffs;
|
||||
public readonly GuildWarsArray<Effect> Effects;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct AgentGameContext
|
||||
{
|
||||
[FieldOffset(0x01AC)]
|
||||
public readonly uint InstanceTimer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 196)]
|
||||
public readonly struct AgentContext
|
||||
{
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint AgentInstanceTimer;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint AgentId;
|
||||
|
||||
[FieldOffset(0x0074)]
|
||||
public readonly GamePos Pos;
|
||||
|
||||
[FieldOffset(0x009C)]
|
||||
public readonly AgentType Type;
|
||||
|
||||
[FieldOffset(0x00A0)]
|
||||
public readonly Vector2 Velocity;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 212)]
|
||||
public readonly struct AgentItemContext
|
||||
{
|
||||
[FieldOffset(0x00C4)]
|
||||
public readonly uint OwnerId;
|
||||
|
||||
[FieldOffset(0x00C8)]
|
||||
public readonly uint ItemId;
|
||||
|
||||
[FieldOffset(0x00D0)]
|
||||
public readonly uint ExtraType;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 228)]
|
||||
public readonly struct AgentGadgetContext
|
||||
{
|
||||
[FieldOffset(0x00CC)]
|
||||
public readonly uint ExtraType;
|
||||
|
||||
[FieldOffset(0x00D0)]
|
||||
public readonly uint GadgetId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C0)]
|
||||
public readonly unsafe struct AgentLivingContext
|
||||
{
|
||||
[FieldOffset(0x00C4)]
|
||||
public readonly uint OwnerId;
|
||||
|
||||
[FieldOffset(0x00F4)]
|
||||
public readonly ushort AgentType; // All non-player have an identifier. Two of the same mob have the same identifier;
|
||||
|
||||
[FieldOffset(0x00F6)]
|
||||
public readonly AgentModelType AgentModelType;
|
||||
|
||||
[FieldOffset(0x0104)]
|
||||
public readonly TagInfo* Tags;
|
||||
|
||||
[FieldOffset(0x010A)]
|
||||
public readonly byte Primary;
|
||||
|
||||
[FieldOffset(0x010B)]
|
||||
public readonly byte Secondary;
|
||||
|
||||
[FieldOffset(0x010C)]
|
||||
public readonly byte Level;
|
||||
|
||||
[FieldOffset(0x010D)]
|
||||
public readonly byte TeamId;
|
||||
|
||||
[FieldOffset(0x0114)]
|
||||
public readonly float EnergyRegen;
|
||||
|
||||
[FieldOffset(0x011C)]
|
||||
public readonly float Energy;
|
||||
|
||||
[FieldOffset(0x0120)]
|
||||
public readonly uint MaxEnergy;
|
||||
|
||||
[FieldOffset(0x0128)]
|
||||
public readonly float HealthRegen;
|
||||
|
||||
[FieldOffset(0x0130)]
|
||||
public readonly float Health;
|
||||
|
||||
[FieldOffset(0x0134)]
|
||||
public readonly uint MaxHealth;
|
||||
|
||||
[FieldOffset(0x0138)]
|
||||
public readonly uint Effects;
|
||||
|
||||
[FieldOffset(0x0154)]
|
||||
public readonly LivingAgentModelState ModelState;
|
||||
|
||||
[FieldOffset(0x0158)]
|
||||
public readonly LivingAgentState State;
|
||||
|
||||
[FieldOffset(0x0180)]
|
||||
public readonly uint LoginNumber;
|
||||
|
||||
[FieldOffset(0x01B1)]
|
||||
public readonly LivingAgentAllegiance Allegiance;
|
||||
|
||||
public readonly bool IsAlive => (!this.State.HasFlag(LivingAgentState.Dead)) && this.Health > 0f;
|
||||
|
||||
public readonly bool IsPlayer => this.LoginNumber is not 0;
|
||||
|
||||
public readonly bool IsNpc => this.LoginNumber is 0;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 52)]
|
||||
public readonly struct MapAgentContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly float CurrentEnergy;
|
||||
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly float MaxEnergy;
|
||||
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly float EnergyRegen;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint SkillTimestamp;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly float CurrentHealth;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly float MaxHealth;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly float HealthRegen;
|
||||
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly LivingAgentEffects Effects;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x38)]
|
||||
public readonly unsafe struct AgentInfo
|
||||
{
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly char* NameEncoded;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct AttributeContext
|
||||
{
|
||||
public readonly uint Id;
|
||||
public readonly uint LevelBase;
|
||||
public readonly uint Level;
|
||||
public readonly uint DecrementPoints;
|
||||
public readonly uint IncrementPoints;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct AttributeInfo
|
||||
{
|
||||
public readonly uint ProfessionId;
|
||||
public readonly uint AttributeId;
|
||||
public readonly uint NameId;
|
||||
public readonly uint DescriptionId;
|
||||
public readonly uint IsPve;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct PartyAttribute
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly AttributeContext Attribute0;
|
||||
public readonly AttributeContext Attribute1;
|
||||
public readonly AttributeContext Attribute2;
|
||||
public readonly AttributeContext Attribute3;
|
||||
public readonly AttributeContext Attribute4;
|
||||
public readonly AttributeContext Attribute5;
|
||||
public readonly AttributeContext Attribute6;
|
||||
public readonly AttributeContext Attribute7;
|
||||
public readonly AttributeContext Attribute8;
|
||||
public readonly AttributeContext Attribute9;
|
||||
public readonly AttributeContext Attribute10;
|
||||
public readonly AttributeContext Attribute11;
|
||||
public readonly AttributeContext Attribute12;
|
||||
public readonly AttributeContext Attribute13;
|
||||
public readonly AttributeContext Attribute14;
|
||||
public readonly AttributeContext Attribute15;
|
||||
public readonly AttributeContext Attribute16;
|
||||
public readonly AttributeContext Attribute17;
|
||||
public readonly AttributeContext Attribute18;
|
||||
public readonly AttributeContext Attribute19;
|
||||
public readonly AttributeContext Attribute20;
|
||||
public readonly AttributeContext Attribute21;
|
||||
public readonly AttributeContext Attribute22;
|
||||
public readonly AttributeContext Attribute23;
|
||||
public readonly AttributeContext Attribute24;
|
||||
public readonly AttributeContext Attribute25;
|
||||
public readonly AttributeContext Attribute26;
|
||||
public readonly AttributeContext Attribute27;
|
||||
public readonly AttributeContext Attribute28;
|
||||
public readonly AttributeContext Attribute29;
|
||||
public readonly AttributeContext Attribute30;
|
||||
public readonly AttributeContext Attribute31;
|
||||
public readonly AttributeContext Attribute32;
|
||||
public readonly AttributeContext Attribute33;
|
||||
public readonly AttributeContext Attribute34;
|
||||
public readonly AttributeContext Attribute35;
|
||||
public readonly AttributeContext Attribute36;
|
||||
public readonly AttributeContext Attribute37;
|
||||
public readonly AttributeContext Attribute38;
|
||||
public readonly AttributeContext Attribute39;
|
||||
public readonly AttributeContext Attribute40;
|
||||
public readonly AttributeContext Attribute41;
|
||||
public readonly AttributeContext Attribute42;
|
||||
public readonly AttributeContext Attribute43;
|
||||
public readonly AttributeContext Attribute44;
|
||||
public readonly AttributeContext Attribute45;
|
||||
public readonly AttributeContext Attribute46;
|
||||
public readonly AttributeContext Attribute47;
|
||||
public readonly AttributeContext Attribute48;
|
||||
public readonly AttributeContext Attribute49;
|
||||
public readonly AttributeContext Attribute50;
|
||||
public readonly AttributeContext Attribute51;
|
||||
public readonly AttributeContext Attribute52;
|
||||
public readonly AttributeContext Attribute53;
|
||||
|
||||
public readonly AttributeContext[] Attributes =>
|
||||
[
|
||||
this.Attribute0, this.Attribute1, this.Attribute2, this.Attribute3, this.Attribute4, this.Attribute5, this.Attribute6, this.Attribute7, this.Attribute8, this.Attribute9,
|
||||
this.Attribute10, this.Attribute11, this.Attribute12, this.Attribute13, this.Attribute14, this.Attribute15, this.Attribute16, this.Attribute17, this.Attribute18, this.Attribute19,
|
||||
this.Attribute20, this.Attribute21, this.Attribute22, this.Attribute23, this.Attribute24, this.Attribute25, this.Attribute26, this.Attribute27, this.Attribute28, this.Attribute29,
|
||||
this.Attribute30, this.Attribute31, this.Attribute32, this.Attribute33, this.Attribute34, this.Attribute35, this.Attribute36, this.Attribute37, this.Attribute38, this.Attribute39,
|
||||
this.Attribute40, this.Attribute41, this.Attribute42, this.Attribute43, this.Attribute44, this.Attribute45, this.Attribute46, this.Attribute47, this.Attribute48, this.Attribute49,
|
||||
this.Attribute50, this.Attribute51, this.Attribute52, this.Attribute53
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum Behavior
|
||||
{
|
||||
Fight,
|
||||
Guard,
|
||||
AvoidCombat
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct CharContext
|
||||
{
|
||||
[FieldOffset(0x0064)]
|
||||
public readonly Uuid PlayerUuid;
|
||||
|
||||
[FieldOffset(0x0074)]
|
||||
public readonly Array20Char PlayerName;
|
||||
|
||||
[FieldOffset(0x0198)]
|
||||
public readonly uint MapId;
|
||||
|
||||
[FieldOffset(0x019C)]
|
||||
public readonly uint IsExporable;
|
||||
|
||||
[FieldOffset(0x01A0)]
|
||||
public readonly Array24Byte Host;
|
||||
|
||||
[FieldOffset(0x01B8)]
|
||||
public readonly uint PlayerId;
|
||||
|
||||
[FieldOffset(0x0220)]
|
||||
public readonly uint DistrictNumber;
|
||||
|
||||
[FieldOffset(0x0224)]
|
||||
public readonly Language Language;
|
||||
|
||||
[FieldOffset(0x0228)]
|
||||
public readonly uint ObserveMapId;
|
||||
|
||||
[FieldOffset(0x022C)]
|
||||
public readonly uint CurrentMapId;
|
||||
|
||||
[FieldOffset(0x0230)]
|
||||
public readonly uint ObserveMapType;
|
||||
|
||||
[FieldOffset(0x0234)]
|
||||
public readonly uint CurrentMapType;
|
||||
|
||||
[FieldOffset(0x02A4)]
|
||||
public readonly uint PlayerNumber;
|
||||
|
||||
[FieldOffset(0x03B8)]
|
||||
public readonly Array64Char PlayerEmail;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct CharInfoContext
|
||||
{
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly Uuid Uuid;
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly Array20Char Name;
|
||||
[FieldOffset(0x0040)]
|
||||
public readonly Array17Uint Props;
|
||||
|
||||
public readonly uint MapId => (this.Props[0] >> 16) & 0xFFFF;
|
||||
public readonly uint Primary => (this.Props[2] >> 20) & 0xF;
|
||||
public readonly uint Secondary => (this.Props[7] >> 10) & 0xF;
|
||||
public readonly uint Campaign => this.Props[7] & 0xF;
|
||||
public readonly uint Level => (this.Props[7] >> 4) & 0x3F;
|
||||
public readonly bool IsPvp => ((this.Props[7] >> 9) & 0x1) == 0x1;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum District
|
||||
{
|
||||
Current,
|
||||
International,
|
||||
American,
|
||||
EuropeEnglish,
|
||||
EuropeFrench,
|
||||
EuropeGerman,
|
||||
EuropeItalian,
|
||||
EuropeSpanish,
|
||||
EuropePolish,
|
||||
EuropeRussian,
|
||||
AsiaKorean,
|
||||
AsiaChinese,
|
||||
AsiaJapanese,
|
||||
Unknown = 0xff
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Daybreak.API.Models;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
// Very incomplete. GWCA contains a much better definition
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1AC)]
|
||||
public readonly struct Frame
|
||||
{
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint FrameLayout;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint VisibilityFlags;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly uint Type;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly uint TemplateType;
|
||||
|
||||
[FieldOffset(0x00A0)]
|
||||
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
|
||||
|
||||
[FieldOffset(0x00b0)]
|
||||
public readonly uint ChildOffsetId;
|
||||
|
||||
[FieldOffset(0x00b4)]
|
||||
public readonly uint FrameId;
|
||||
|
||||
[FieldOffset(0x0120)]
|
||||
public readonly FrameRelation Relation;
|
||||
|
||||
[FieldOffset(0x0184)]
|
||||
public readonly uint Field91;
|
||||
|
||||
public bool IsCreated => (this.Field91 & 0x4) != 0;
|
||||
public bool IsHidden => (this.Field91 & 0x200) != 0;
|
||||
public bool IsVisible => !this.IsHidden;
|
||||
public bool IsDIsabled => (this.Field91 & 0x10) != 0;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
|
||||
public readonly unsafe struct FrameRelation
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly FrameRelation* Parent;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint FrameHashId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack =1)]
|
||||
public readonly unsafe struct InteractionMessage
|
||||
{
|
||||
public readonly uint FrameId;
|
||||
public readonly UIMessage MessageId;
|
||||
public readonly void** WParam;
|
||||
}
|
||||
|
||||
public unsafe delegate void UIInteractionCallback(InteractionMessage* message, void* wParam, void* lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
public unsafe readonly struct FrameInteractionCallback
|
||||
{
|
||||
public readonly void* Callback; // UIInteractionCallback
|
||||
public readonly void* UiCtl_Context;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly unsafe struct GameContext
|
||||
{
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly AgentGameContext* AgentGameContext;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly MapContext* MapContext;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly AccountGameContext* AccountContext;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly WorldContext* WorldContext;
|
||||
|
||||
[FieldOffset(0x003C)]
|
||||
public readonly GuildContext* GuildContext;
|
||||
|
||||
[FieldOffset(0x0044)]
|
||||
public readonly CharContext* CharContext;
|
||||
|
||||
[FieldOffset(0x004C)]
|
||||
public readonly PartyContext* PartyContext;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct GamePos
|
||||
{
|
||||
public readonly float X;
|
||||
public readonly float Y;
|
||||
public readonly uint Plane;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly unsafe struct GuildContext
|
||||
{
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly Array20Char PlayerName;
|
||||
|
||||
[FieldOffset(0x0060)]
|
||||
public readonly uint PlayerBuildIndex;
|
||||
|
||||
[FieldOffset(0x0078)]
|
||||
public readonly Array256Char Announcement;
|
||||
|
||||
[FieldOffset(0x0287)]
|
||||
public readonly Array20Char AnnouncementAuthor;
|
||||
|
||||
[FieldOffset(0x02A0)]
|
||||
public readonly uint PlayerGuildRank;
|
||||
|
||||
[FieldOffset(0x02A8)]
|
||||
public readonly GuildWarsArray<TownAlliance> FactionOutpostGuilds;
|
||||
|
||||
[FieldOffset(0x02B8)]
|
||||
public readonly uint KurzickTownCount;
|
||||
|
||||
[FieldOffset(0x02BC)]
|
||||
public readonly uint LuxonTownCount;
|
||||
|
||||
[FieldOffset(0x02F8)]
|
||||
public readonly GuildWarsArray<WrappedPointer<Guild>> Guilds;
|
||||
|
||||
[FieldOffset(0x0358)]
|
||||
public readonly GuildWarsArray<WrappedPointer<GuildPlayer>> PlayerRoster;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct TownAlliance
|
||||
{
|
||||
public readonly uint Rank;
|
||||
public readonly uint Allegiance;
|
||||
public readonly uint Faction;
|
||||
public readonly Array32Char Name;
|
||||
public readonly Array5Char Tag;
|
||||
public readonly CapeDesign Cape;
|
||||
public readonly uint MapId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct CapeDesign
|
||||
{
|
||||
public readonly uint CapeBgColor;
|
||||
public readonly uint CapeDetailColor;
|
||||
public readonly uint CapeEmblemColor;
|
||||
public readonly uint CapeShape;
|
||||
public readonly uint CapeDetail;
|
||||
public readonly uint CapeEmblem;
|
||||
public readonly uint CapeTrim;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x174)]
|
||||
public readonly unsafe struct GuildPlayer
|
||||
{
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly char* NamePtr;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly Array20Char InvitedName;
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly Array20Char CurrentName;
|
||||
[FieldOffset(0x0050)]
|
||||
public readonly Array20Char InviterName;
|
||||
[FieldOffset(0x0080)]
|
||||
public readonly uint InviteTime;
|
||||
[FieldOffset(0x0084)]
|
||||
public readonly Array20Char PromoterName;
|
||||
[FieldOffset(0x00DC)]
|
||||
public readonly uint Offline;
|
||||
[FieldOffset(0x00E0)]
|
||||
public readonly uint MemberType;
|
||||
[FieldOffset(0x00E4)]
|
||||
public readonly uint Status;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xAC)]
|
||||
public readonly struct Guild
|
||||
{
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly uint Index;
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly uint Rank;
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint Features;
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly Array32Char Name;
|
||||
[FieldOffset(0x0070)]
|
||||
public readonly uint Rating;
|
||||
[FieldOffset(0x0074)]
|
||||
public readonly uint Faction;
|
||||
[FieldOffset(0x0078)]
|
||||
public readonly uint FactionPoints;
|
||||
[FieldOffset(0x007C)]
|
||||
public readonly uint QualifierPoints;
|
||||
[FieldOffset(0x0080)]
|
||||
public readonly Array8Char Tag;
|
||||
[FieldOffset(0x0090)]
|
||||
public readonly CapeDesign Cape;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public readonly T* Buffer;
|
||||
public readonly uint Capacity;
|
||||
public readonly uint Size;
|
||||
public readonly uint Param;
|
||||
|
||||
public Enumerator GetEnumerator() => new(this.Buffer, this.Size);
|
||||
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
public unsafe struct Enumerator : IEnumerator, IEnumerator<T>
|
||||
{
|
||||
private readonly T* buffer;
|
||||
private readonly uint size;
|
||||
private int index;
|
||||
|
||||
internal Enumerator(T* buffer, uint size)
|
||||
{
|
||||
this.buffer = buffer;
|
||||
this.size = size;
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
int next = this.index + 1;
|
||||
if (next >= this.size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.index = next;
|
||||
return true;
|
||||
}
|
||||
|
||||
public T Current => this.buffer[this.index];
|
||||
object IEnumerator.Current => this.Current;
|
||||
|
||||
public void Reset() => this.index = -1;
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Extensions;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x24)]
|
||||
public readonly struct HeroFlag
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint HeroId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint Level;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly Behavior Behavior;
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly Vector2 Flag;
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly uint LockedTargetId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x78)]
|
||||
public readonly struct HeroInfo
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint HeroId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint Level;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly Profession Primary;
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly Profession Secondary;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint HeroFileId;
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint ModelFileId;
|
||||
[FieldOffset(0x0050)]
|
||||
public readonly Array20Char Name;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum InstanceType
|
||||
{
|
||||
Outpost,
|
||||
Explorable,
|
||||
Loading
|
||||
};
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly unsafe struct InstanceInfoContext
|
||||
{
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly InstanceType InstanceType;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly AreaInfo* CurrentMapInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct AreaInfo
|
||||
{
|
||||
public readonly uint Campaign;
|
||||
public readonly uint Continent;
|
||||
public readonly uint Region;
|
||||
public readonly uint RegionType;
|
||||
public readonly uint Flags;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum Language
|
||||
{
|
||||
English,
|
||||
Korean,
|
||||
French,
|
||||
German,
|
||||
Italian,
|
||||
Spanish,
|
||||
TraditionalChinese,
|
||||
Japanese = 8,
|
||||
Polish,
|
||||
Russian,
|
||||
BorkBorkBork = 17,
|
||||
Unknown = 0xff
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct MapContext
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public readonly unsafe struct NpcContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint ModelId;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint Sex;
|
||||
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly NpcFlags Flags;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint Primary;
|
||||
|
||||
[FieldOffset(0x001C)]
|
||||
public readonly byte DefaultLevel;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly char* EncName;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly int FilesCount;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly int FilesCapacity;
|
||||
|
||||
[Flags]
|
||||
public enum NpcFlags : uint
|
||||
{
|
||||
None = 0x0000,
|
||||
Henchman = 0x0010,
|
||||
Hero = 0x0020,
|
||||
Spirit = 0x4000,
|
||||
Minion = 0x0100,
|
||||
Pet = 0x000D
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly unsafe struct PetContext
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly uint OwnerAgentId;
|
||||
public readonly char* PetName;
|
||||
public readonly uint ModelFileId1;
|
||||
public readonly uint ModelFileId2;
|
||||
public readonly Behavior Behavior;
|
||||
public readonly uint LockedTargetId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct ControlledMinion
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly uint MinionCount;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum PlayerPartyMemberState : uint
|
||||
{
|
||||
None = 0,
|
||||
Connected = 1,
|
||||
Ticked = 2
|
||||
}
|
||||
|
||||
public enum PartySearchType
|
||||
{
|
||||
PartySearchType_Hunting = 0,
|
||||
PartySearchType_Mission = 1,
|
||||
PartySearchType_Quest = 2,
|
||||
PartySearchType_Trade = 3,
|
||||
PartySearchType_Guild = 4,
|
||||
};
|
||||
|
||||
[Flags]
|
||||
public enum PartyFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
HardMode = 0x10, // Bit 4
|
||||
Defeated = 0x20, // Bit 5
|
||||
PartyLeader = 0x80 // Bit 7 (0x7th position)
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
|
||||
public readonly unsafe struct PartyMoraleContext
|
||||
{
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly PartyMemberMoraleInfo* MoraleInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct PartyMemberMoraleInfo
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint Morale;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x58)]
|
||||
public readonly unsafe struct PartyContext
|
||||
{
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly PartyFlags Flags;
|
||||
[FieldOffset(0x0040)]
|
||||
public readonly GuildWarsArray<WrappedPointer<PartyInfo>> Parties;
|
||||
[FieldOffset(0x0054)]
|
||||
public readonly PartyInfo* PlayerParty;
|
||||
[FieldOffset(0x00C0)]
|
||||
public readonly GuildWarsArray<WrappedPointer<PartySearch>> PartySearches;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
public readonly struct PlayerPartyMember
|
||||
{
|
||||
public readonly uint LogingNumber;
|
||||
public readonly uint CalledTargetId;
|
||||
public readonly PlayerPartyMemberState State;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x18)]
|
||||
public readonly struct HeroPartyMember
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint OwnerPlayerId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint HeroId;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint Level;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
|
||||
public readonly struct HenchmanPartyMember
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly Profession Profession;
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly uint Level;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x84)]
|
||||
public readonly struct PartyInfo
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint PartyId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly GuildWarsArray<PlayerPartyMember> Players;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly GuildWarsArray<HenchmanPartyMember> Henchmen;
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly GuildWarsArray<HeroPartyMember> Heroes;
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly GuildWarsArray<uint> Others;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct PartySearch
|
||||
{
|
||||
public readonly uint PartySearchId;
|
||||
public readonly PartySearchType PartySearchType;
|
||||
public readonly uint HardMode;
|
||||
public readonly uint District;
|
||||
public readonly uint Language;
|
||||
public readonly uint PartySize;
|
||||
public readonly uint HeroCount;
|
||||
public readonly Array32Char Message;
|
||||
public readonly Array20Char PartyLeader;
|
||||
public readonly Profession Primary;
|
||||
public readonly Profession Secondary;
|
||||
public readonly uint Level;
|
||||
public readonly uint Timestamp;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum PlayerContextFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
PvP = 0x800
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x4C)]
|
||||
public readonly unsafe struct PlayerContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly int AgentId;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly PlayerContextFlags Flags;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint PrimaryProfession;
|
||||
|
||||
[FieldOffset(0x001C)]
|
||||
public readonly uint SecondaryProfession;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly char* NameEncoded;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly char* Name;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint PartyLeaderPlayerNumber;
|
||||
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly uint ActiveTitleTier;
|
||||
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly uint PlayerNumber;
|
||||
|
||||
[FieldOffset(0x0038)]
|
||||
public readonly uint PartySize;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct PlayerControlledCharContext
|
||||
{
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint AgentId;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct PreGameContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint FrameId;
|
||||
|
||||
[FieldOffset(0x0124)]
|
||||
public readonly uint ChosenCharacterIndex;
|
||||
|
||||
[FieldOffset(0x0140)]
|
||||
public readonly uint Index1;
|
||||
|
||||
[FieldOffset(0x0144)]
|
||||
public readonly uint Index2;
|
||||
|
||||
[FieldOffset(0x00148)]
|
||||
public readonly GuildWarsArray<LoginCharacterContext> LoginCharacters;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct LoginCharacterContext
|
||||
{
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly Array20Char CharacterName;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum Profession : uint
|
||||
{
|
||||
None,
|
||||
Warrior,
|
||||
Ranger,
|
||||
Monk,
|
||||
Necromancer,
|
||||
Mesmer,
|
||||
Elementalist,
|
||||
Assassin,
|
||||
Ritualist,
|
||||
Paragon,
|
||||
Dervish
|
||||
}
|
||||
|
||||
public enum ProfessionByte : byte
|
||||
{
|
||||
None,
|
||||
Warrior,
|
||||
Ranger,
|
||||
Monk,
|
||||
Necromancer,
|
||||
Mesmer,
|
||||
Elementalist,
|
||||
Assassin,
|
||||
Ritualist,
|
||||
Paragon,
|
||||
Dervish
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x0014)]
|
||||
public readonly struct ProfessionsContext
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly Profession CurrentPrimary;
|
||||
public readonly Profession CurrentSecondary;
|
||||
public readonly uint UnlockedProfessionsFlags;
|
||||
|
||||
public bool ProfessionUnlocked(int professionId)
|
||||
{
|
||||
return (this.UnlockedProfessionsFlags & (1U << professionId)) != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x0034)]
|
||||
public readonly struct QuestContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint QuestId;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint MapFrom;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly Vector3 Marker;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly uint MapTo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
public readonly unsafe struct MissionObjectiveContext
|
||||
{
|
||||
public readonly uint ObjectiveId;
|
||||
public readonly char* EncodedString;
|
||||
public readonly uint Type;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum ServerRegion
|
||||
{
|
||||
International = -2,
|
||||
America = 0,
|
||||
Korea,
|
||||
Europe,
|
||||
China,
|
||||
Japan,
|
||||
Unknown = 0xff
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct SkillContext
|
||||
{
|
||||
public readonly uint Adrenaline1;
|
||||
public readonly uint Adrenaline2;
|
||||
public readonly uint Recharge;
|
||||
public readonly uint Id;
|
||||
public readonly uint Event;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xBC)]
|
||||
public readonly struct SkillbarContext
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly SkillContext Skill0;
|
||||
public readonly SkillContext Skill1;
|
||||
public readonly SkillContext Skill2;
|
||||
public readonly SkillContext Skill3;
|
||||
public readonly SkillContext Skill4;
|
||||
public readonly SkillContext Skill5;
|
||||
public readonly SkillContext Skill6;
|
||||
public readonly SkillContext Skill7;
|
||||
public readonly uint Disabled;
|
||||
public readonly uint Casting;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x10)]
|
||||
public readonly struct Buff
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint SkillId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint BuffId;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint TargetAgentId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)]
|
||||
public readonly struct Effect
|
||||
{
|
||||
public readonly uint SkillId;
|
||||
public readonly uint AttributeLevel;
|
||||
public readonly uint EffectId;
|
||||
public readonly uint AgentId;
|
||||
public readonly float Duration;
|
||||
public readonly uint Timestamp;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct DupeSkill
|
||||
{
|
||||
public readonly uint SkillId;
|
||||
public readonly uint Count;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public readonly struct SkillTemplate(uint primary, uint secondary, uint attributeCount, Array12Uint attributeIds, Array12Uint attributeValues, Array8Uint skills)
|
||||
{
|
||||
public readonly uint Primary = primary;
|
||||
public readonly uint Secondary = secondary;
|
||||
public readonly uint AttributesCount = attributeCount;
|
||||
public readonly Array12Uint AttributeIds = attributeIds;
|
||||
public readonly Array12Uint AttributeValues = attributeValues;
|
||||
public readonly Array8Uint Skills = skills;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct TagInfo
|
||||
{
|
||||
public readonly ushort GuildId;
|
||||
public readonly byte Primary;
|
||||
public readonly byte Secondary;
|
||||
public readonly ushort Level;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum TitleProps : uint
|
||||
{
|
||||
None = 0,
|
||||
PercentageBased = 1,
|
||||
HasTiers = 2
|
||||
// Note: The HasTiers check uses (props & 3) == 2, which means bit 1 is set but bit 0 is not
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 40)]
|
||||
public readonly struct TitleContext
|
||||
{
|
||||
public readonly TitleProps Props;
|
||||
public readonly uint CurrentPoints;
|
||||
public readonly uint CurrentTitleTierIndex;
|
||||
public readonly uint PointsNeededForCurrentRank;
|
||||
public readonly uint NextTitleTierIndex;
|
||||
public readonly uint PointsNeededForNextRank;
|
||||
public readonly uint MaxTitleRank;
|
||||
public readonly uint MaxTitleTierIndex;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
public readonly unsafe struct TitleTier
|
||||
{
|
||||
public readonly TitleProps Props;
|
||||
public readonly uint TierNumber;
|
||||
public readonly char* TierNameEncoded;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct TitleClientData
|
||||
{
|
||||
public readonly uint TitleId;
|
||||
public readonly uint NameId;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum UIAction : uint
|
||||
{
|
||||
None = 0,
|
||||
Screenshot = 0xAE,
|
||||
|
||||
// Panels
|
||||
CloseAllPanels = 0x85,
|
||||
ToggleInventoryWindow = 0x8B,
|
||||
OpenScoreChart = 0xBD,
|
||||
OpenTemplateManager = 0xD3,
|
||||
OpenSaveEquipmentTemplate = 0xD4,
|
||||
OpenSaveSkillTemplate = 0xD5,
|
||||
OpenParty = 0xBF,
|
||||
OpenGuild = 0xBA,
|
||||
OpenFriends = 0xB9,
|
||||
ToggleAllBags = 0xB8,
|
||||
OpenMissionMap = 0xB6,
|
||||
OpenBag2 = 0xB5,
|
||||
OpenBag1 = 0xB4,
|
||||
OpenBelt = 0xB3,
|
||||
OpenBackpack = 0xB2,
|
||||
OpenSkillsAndAttributes = 0x8F,
|
||||
OpenQuestLog = 0x8E,
|
||||
OpenWorldMap = 0x8C,
|
||||
OpenOptions = 0x8D,
|
||||
OpenHero = 0x8A,
|
||||
|
||||
// Weapon sets
|
||||
CycleEquipment = 0x86,
|
||||
ActivateWeaponSet1 = 0x81,
|
||||
ActivateWeaponSet2,
|
||||
ActivateWeaponSet3,
|
||||
ActivateWeaponSet4,
|
||||
|
||||
DropItem = 0xCD, // drops bundle item >> flags, ashes, etc
|
||||
|
||||
// Chat
|
||||
CharReply = 0xBE,
|
||||
OpenChat = 0xA1,
|
||||
OpenAlliance = 0x88,
|
||||
|
||||
ReverseCamera = 0x90,
|
||||
StrafeLeft = 0x91,
|
||||
StrafeRight = 0x92,
|
||||
TurnLeft = 0xA2,
|
||||
TurnRight = 0xA3,
|
||||
MoveBackward = 0xAC,
|
||||
MoveForward = 0xAD,
|
||||
CancelAction = 0xAF,
|
||||
Interact = 0x80,
|
||||
ReverseDirection = 0xB1,
|
||||
Autorun = 0xB7,
|
||||
Follow = 0xCC,
|
||||
|
||||
// Targeting
|
||||
TargetPartyMember1 = 0x96,
|
||||
TargetPartyMember2,
|
||||
TargetPartyMember3,
|
||||
TargetPartyMember4,
|
||||
TargetPartyMember5,
|
||||
TargetPartyMember6,
|
||||
TargetPartyMember7,
|
||||
TargetPartyMember8,
|
||||
TargetPartyMember9 = 0xC6,
|
||||
TargetPartyMember10,
|
||||
TargetPartyMember11,
|
||||
TargetPartyMember12,
|
||||
|
||||
TargetNearestItem = 0xC3,
|
||||
TargetNextItem = 0xC4,
|
||||
TargetPreviousItem = 0xC5,
|
||||
TargetPartyMemberNext = 0xCA,
|
||||
TargetPartyMemberPrevious = 0xCB,
|
||||
TargetAllyNearest = 0xBC,
|
||||
ClearTarget = 0xE3,
|
||||
TargetSelf = 0xA0, // also 0x96
|
||||
TargetPriorityTarget = 0x9F,
|
||||
TargetNearestEnemy = 0x93,
|
||||
TargetNextEnemy = 0x95,
|
||||
TargetPreviousEnemy = 0x9E,
|
||||
|
||||
ShowOthers = 0x89,
|
||||
ShowTargets = 0x94,
|
||||
|
||||
CameraZoomIn = 0xCE,
|
||||
CameraZoomOut = 0xCF,
|
||||
|
||||
// Party/Hero commands
|
||||
ClearPartyCommands = 0xDB,
|
||||
CommandParty = 0xD6,
|
||||
CommandHero1,
|
||||
CommandHero2,
|
||||
CommandHero3,
|
||||
CommandHero4 = 0x102,
|
||||
CommandHero5,
|
||||
CommandHero6,
|
||||
CommandHero7,
|
||||
|
||||
OpenHero1PetCommander = 0xE0,
|
||||
OpenHero2PetCommander,
|
||||
OpenHero3PetCommander,
|
||||
OpenHero4PetCommander = 0xFE,
|
||||
OpenHero5PetCommander,
|
||||
OpenHero6PetCommander,
|
||||
OpenHero7PetCommander,
|
||||
OpenHeroCommander1 = 0xDC,
|
||||
OpenHeroCommander2,
|
||||
OpenHeroCommander3,
|
||||
OpenPetCommander,
|
||||
OpenHeroCommander4 = 0x126,
|
||||
OpenHeroCommander5,
|
||||
OpenHeroCommander6,
|
||||
OpenHeroCommander7,
|
||||
|
||||
Hero1Skill1 = 0xE5,
|
||||
Hero1Skill2,
|
||||
Hero1Skill3,
|
||||
Hero1Skill4,
|
||||
Hero1Skill5,
|
||||
Hero1Skill6,
|
||||
Hero1Skill7,
|
||||
Hero1Skill8,
|
||||
Hero2Skill1,
|
||||
Hero2Skill2,
|
||||
Hero2Skill3,
|
||||
Hero2Skill4,
|
||||
Hero2Skill5,
|
||||
Hero2Skill6,
|
||||
Hero2Skill7,
|
||||
Hero2Skill8,
|
||||
Hero3Skill1,
|
||||
Hero3Skill2,
|
||||
Hero3Skill3,
|
||||
Hero3Skill4,
|
||||
Hero3Skill5,
|
||||
Hero3Skill6,
|
||||
Hero3Skill7,
|
||||
Hero3Skill8,
|
||||
Hero4Skill1 = 0x106,
|
||||
Hero4Skill2,
|
||||
Hero4Skill3,
|
||||
Hero4Skill4,
|
||||
Hero4Skill5,
|
||||
Hero4Skill6,
|
||||
Hero4Skill7,
|
||||
Hero4Skill8,
|
||||
Hero5Skill1,
|
||||
Hero5Skill2,
|
||||
Hero5Skill3,
|
||||
Hero5Skill4,
|
||||
Hero5Skill5,
|
||||
Hero5Skill6,
|
||||
Hero5Skill7,
|
||||
Hero5Skill8,
|
||||
Hero6Skill1,
|
||||
Hero6Skill2,
|
||||
Hero6Skill3,
|
||||
Hero6Skill4,
|
||||
Hero6Skill5,
|
||||
Hero6Skill6,
|
||||
Hero6Skill7,
|
||||
Hero6Skill8,
|
||||
Hero7Skill1,
|
||||
Hero7Skill2,
|
||||
Hero7Skill3,
|
||||
Hero7Skill4,
|
||||
Hero7Skill5,
|
||||
Hero7Skill6,
|
||||
Hero7Skill7,
|
||||
Hero7Skill8,
|
||||
|
||||
// Skills
|
||||
UseSkill1 = 0xA4,
|
||||
UseSkill2,
|
||||
UseSkill3,
|
||||
UseSkill4,
|
||||
UseSkill5,
|
||||
UseSkill6,
|
||||
UseSkill7,
|
||||
UseSkill8
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public readonly struct Uuid(
|
||||
uint timeLow,
|
||||
ushort timeMid,
|
||||
ushort timeHighAndVersion,
|
||||
byte clockSqHi,
|
||||
byte clockSqLow,
|
||||
byte node0,
|
||||
byte node1,
|
||||
byte node2,
|
||||
byte node3,
|
||||
byte node4,
|
||||
byte node5) : IEquatable<Uuid>
|
||||
{
|
||||
public const uint NodeLength = 6;
|
||||
|
||||
public readonly uint TimeLow = timeLow;
|
||||
public readonly ushort TimeMid = timeMid;
|
||||
public readonly ushort TimeHighAndVersion = timeHighAndVersion;
|
||||
public readonly byte ClockSqHiAndRes = clockSqHi;
|
||||
public readonly byte ClockSqLow = clockSqLow;
|
||||
public readonly byte Node0 = node0;
|
||||
public readonly byte Node1 = node1;
|
||||
public readonly byte Node2 = node2;
|
||||
public readonly byte Node3 = node3;
|
||||
public readonly byte Node4 = node4;
|
||||
public readonly byte Node5 = node5;
|
||||
|
||||
public bool Equals(Uuid other)
|
||||
{
|
||||
return this.TimeLow == other.TimeLow &&
|
||||
this.TimeMid == other.TimeMid &&
|
||||
this.TimeHighAndVersion == other.TimeHighAndVersion &&
|
||||
this.ClockSqHiAndRes == other.ClockSqHiAndRes &&
|
||||
this.ClockSqLow == other.ClockSqLow &&
|
||||
this.Node0 == other.Node0 &&
|
||||
this.Node1 == other.Node1 &&
|
||||
this.Node2 == other.Node2 &&
|
||||
this.Node3 == other.Node3 &&
|
||||
this.Node4 == other.Node4 &&
|
||||
this.Node5 == other.Node5;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Uuid uuid && this.Equals(uuid);
|
||||
}
|
||||
|
||||
public static bool operator ==(Uuid left, Uuid right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(Uuid left, Uuid right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static bool TryParse(string uuidString, out Uuid uuid)
|
||||
{
|
||||
if (uuidString.Length != 36)
|
||||
{
|
||||
uuid = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var parts = uuidString.Split('-');
|
||||
if (parts.Length != 5)
|
||||
{
|
||||
uuid = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(parts[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var timeLow) ||
|
||||
!ushort.TryParse(parts[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var timeMid) ||
|
||||
!ushort.TryParse(parts[2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var timeHighAndVersion) ||
|
||||
!byte.TryParse(parts[3].AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clockSqHi) ||
|
||||
!byte.TryParse(parts[3].AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clockSqLow) ||
|
||||
!byte.TryParse(parts[4].AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node0) ||
|
||||
!byte.TryParse(parts[4].AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node1) ||
|
||||
!byte.TryParse(parts[4].AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node2) ||
|
||||
!byte.TryParse(parts[4].AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node3) ||
|
||||
!byte.TryParse(parts[4].AsSpan(8, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node4) ||
|
||||
!byte.TryParse(parts[4].AsSpan(10, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node5))
|
||||
{
|
||||
uuid = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
uuid = new Uuid(timeLow, timeMid, timeHighAndVersion, clockSqHi, clockSqLow, node0, node1, node2, node3, node4, node5);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine<uint, ushort, ushort, byte, byte, byte[]>(this.TimeLow, this.TimeMid, this.TimeHighAndVersion, this.ClockSqHiAndRes, this.ClockSqLow, [this.Node0, this.Node1, this.Node2, this.Node3, this.Node4, this.Node5]);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.TimeLow:X8}-{this.TimeMid:X4}-{this.TimeHighAndVersion:X4}-{this.ClockSqHiAndRes:X2}{this.ClockSqLow:X2}-{this.Node0:X2}{this.Node1:X2}{this.Node2:X2}{this.Node3:X2}{this.Node4:X2}{this.Node5:X2}";
|
||||
}
|
||||
|
||||
public readonly static Uuid Zero = new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly unsafe struct WorldContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly AccountInfoContext* AccountInfo;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly GuildWarsArray<uint> MerchItems;
|
||||
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly GuildWarsArray<uint> MerchItems2;
|
||||
|
||||
[FieldOffset(0x007C)]
|
||||
public readonly GuildWarsArray<MapAgentContext> MapAgents;
|
||||
|
||||
[FieldOffset(0x009C)]
|
||||
public readonly Vector3 AllFlag;
|
||||
|
||||
[FieldOffset(0x00AC)]
|
||||
public readonly GuildWarsArray<PartyAttribute> Attributes;
|
||||
|
||||
[FieldOffset(0x0508)]
|
||||
public readonly GuildWarsArray<AgentEffects> PartyEffects;
|
||||
|
||||
[FieldOffset(0x0528)]
|
||||
public readonly uint ActiveQuestId;
|
||||
|
||||
[FieldOffset(0x052C)]
|
||||
public readonly GuildWarsArray<QuestContext> QuestLog;
|
||||
|
||||
[FieldOffset(0x0564)]
|
||||
public readonly GuildWarsArray<MissionObjectiveContext> MissionObjectives;
|
||||
|
||||
[FieldOffset(0x0574)]
|
||||
public readonly GuildWarsArray<uint> HenchmenAgentIds;
|
||||
|
||||
[FieldOffset(0x0584)]
|
||||
public readonly GuildWarsArray<HeroFlag> HeroFlags;
|
||||
|
||||
[FieldOffset(0x0594)]
|
||||
public readonly GuildWarsArray<HeroInfo> HeroInfos;
|
||||
|
||||
[FieldOffset(0x05A4)]
|
||||
public readonly GuildWarsArray<nuint> CartographedAreas;
|
||||
|
||||
[FieldOffset(0x05BC)]
|
||||
public readonly GuildWarsArray<ControlledMinion> ControlledMinions;
|
||||
|
||||
[FieldOffset(0x05CC)]
|
||||
public readonly GuildWarsArray<uint> MissionsCompleted;
|
||||
|
||||
[FieldOffset(0x05DC)]
|
||||
public readonly GuildWarsArray<uint> MissionsBonus;
|
||||
|
||||
[FieldOffset(0x05EC)]
|
||||
public readonly GuildWarsArray<uint> MissionsCompletedHardMode;
|
||||
|
||||
[FieldOffset(0x05FC)]
|
||||
public readonly GuildWarsArray<uint> MissionsBonusHardMode;
|
||||
|
||||
[FieldOffset(0x060C)]
|
||||
public readonly GuildWarsArray<uint> UnlockedMap;
|
||||
|
||||
[FieldOffset(0x062C)]
|
||||
public readonly GuildWarsArray<PartyMoraleContext> PartyMorale;
|
||||
|
||||
[FieldOffset(0x067C)]
|
||||
public readonly uint PlayerNumber;
|
||||
|
||||
[FieldOffset(0x0680)]
|
||||
public readonly PlayerControlledCharContext* PlayerControlledChar;
|
||||
|
||||
[FieldOffset(0x0684)]
|
||||
public readonly uint HardModeUnlocked;
|
||||
|
||||
[FieldOffset(0x06AC)]
|
||||
public readonly GuildWarsArray<PetContext> Pets;
|
||||
|
||||
[FieldOffset(0x06BC)]
|
||||
public readonly GuildWarsArray<ProfessionsContext> Professions;
|
||||
|
||||
[FieldOffset(0x06F0)]
|
||||
public readonly GuildWarsArray<SkillbarContext> Skillbars;
|
||||
|
||||
[FieldOffset(0x0700)]
|
||||
public readonly GuildWarsArray<uint> LearnableCharacterSkills;
|
||||
|
||||
[FieldOffset(0x0710)]
|
||||
public readonly GuildWarsArray<uint> UnlockedCharacterSkills;
|
||||
|
||||
[FieldOffset(0x0720)]
|
||||
public readonly GuildWarsArray<DupeSkill> DupeSkills;
|
||||
|
||||
[FieldOffset(0x0740)]
|
||||
public readonly uint Experience;
|
||||
|
||||
[FieldOffset(0x0748)]
|
||||
public readonly uint CurrentKurzick;
|
||||
|
||||
[FieldOffset(0x0750)]
|
||||
public readonly uint TotalKurzick;
|
||||
|
||||
[FieldOffset(0x0758)]
|
||||
public readonly uint CurrentLuxon;
|
||||
|
||||
[FieldOffset(0x0760)]
|
||||
public readonly uint TotalLuxon;
|
||||
|
||||
[FieldOffset(0x0768)]
|
||||
public readonly uint CurrentImperial;
|
||||
|
||||
[FieldOffset(0x0770)]
|
||||
public readonly uint TotalImperial;
|
||||
|
||||
[FieldOffset(0x0788)]
|
||||
public readonly uint Level;
|
||||
|
||||
[FieldOffset(0x0790)]
|
||||
public readonly uint Morale;
|
||||
|
||||
[FieldOffset(0x0798)]
|
||||
public readonly uint CurrentBalthazar;
|
||||
|
||||
[FieldOffset(0x07A0)]
|
||||
public readonly uint TotalBalthazar;
|
||||
|
||||
[FieldOffset(0x07A8)]
|
||||
public readonly uint CurrentSkillPoints;
|
||||
|
||||
[FieldOffset(0x07B0)]
|
||||
public readonly uint TotalSkillPoints;
|
||||
|
||||
[FieldOffset(0x07B8)]
|
||||
public readonly uint MaxKurzick;
|
||||
|
||||
[FieldOffset(0x07BC)]
|
||||
public readonly uint MaxLuxon;
|
||||
|
||||
[FieldOffset(0x07C0)]
|
||||
public readonly uint MaxBalthazar;
|
||||
|
||||
[FieldOffset(0x07C4)]
|
||||
public readonly uint MaxImperial;
|
||||
|
||||
[FieldOffset(0x07CC)]
|
||||
public readonly GuildWarsArray<AgentInfo> AgentInfos;
|
||||
|
||||
[FieldOffset(0x07FC)]
|
||||
public readonly GuildWarsArray<NpcContext> Npcs;
|
||||
|
||||
[FieldOffset(0x080C)]
|
||||
public readonly GuildWarsArray<PlayerContext> Players;
|
||||
|
||||
[FieldOffset(0x081C)]
|
||||
public readonly GuildWarsArray<TitleContext> Titles;
|
||||
|
||||
[FieldOffset(0x082C)]
|
||||
public readonly GuildWarsArray<TitleTier> TitleTiers;
|
||||
|
||||
[FieldOffset(0x083C)]
|
||||
public readonly GuildWarsArray<uint> VanquishedAreas;
|
||||
|
||||
[FieldOffset(0x084C)]
|
||||
public readonly uint FoesKilled;
|
||||
|
||||
[FieldOffset(0x0850)]
|
||||
public readonly uint FoesToKill;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public readonly unsafe struct WrappedPointer<T>(T* pointer)
|
||||
where T : unmanaged
|
||||
{
|
||||
public readonly T* Pointer = pointer;
|
||||
|
||||
public bool IsNull => this.Pointer is null;
|
||||
|
||||
public static implicit operator WrappedPointer<T>(T* pointer) => new(pointer);
|
||||
|
||||
public static implicit operator T*(WrappedPointer<T> wrappedPointer) => wrappedPointer.Pointer;
|
||||
|
||||
public static bool operator ==(T* left, WrappedPointer<T> right) => left == right.Pointer;
|
||||
|
||||
public static bool operator !=(T* left, WrappedPointer<T> right) => left != right.Pointer;
|
||||
|
||||
public static bool operator ==(WrappedPointer<T> left, T* right) => left.Pointer == right;
|
||||
|
||||
public static bool operator !=(WrappedPointer<T> left, T* right) => left.Pointer != right;
|
||||
|
||||
public static bool operator ==(WrappedPointer<T> left, WrappedPointer<T> right) => left.Pointer == right.Pointer;
|
||||
|
||||
public static bool operator !=(WrappedPointer<T> left, WrappedPointer<T> right) => left.Pointer != right.Pointer;
|
||||
|
||||
public override int GetHashCode() => this.Pointer is null ? 0 : this.Pointer->GetHashCode();
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is WrappedPointer<T> wrappedPointer)
|
||||
{
|
||||
return this.Pointer == wrappedPointer.Pointer;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public interface IHook<T>
|
||||
: IDisposable where T : Delegate
|
||||
{
|
||||
public T Continue { get; }
|
||||
|
||||
public nuint ContinueAddress { get; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public readonly struct ManagedFrame(
|
||||
WrappedPointer<Frame> frame,
|
||||
Action closeFrame) : IDisposable
|
||||
{
|
||||
private readonly Action closeFrame = closeFrame;
|
||||
|
||||
public static ManagedFrame Null => new(null, () => { });
|
||||
|
||||
public readonly WrappedPointer<Frame> Frame = frame;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.closeFrame();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using MemoryPack;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public static class MemoryPackExtensions
|
||||
{
|
||||
/// <summary>Serialize <paramref name="value"/> into <paramref name="destination"/>.</summary>
|
||||
/// <returns>Number of bytes written.</returns>
|
||||
public static int SerializeToSpan<T>(T value, Span<byte> destination)
|
||||
{
|
||||
var dummy = DummyBufferWriter.Instance;
|
||||
|
||||
using var state = MemoryPackWriterOptionalStatePool.Rent(null);
|
||||
|
||||
var writer = new MemoryPackWriter<DummyBufferWriter>(ref dummy, destination, state);
|
||||
|
||||
writer.WriteValue(value);
|
||||
writer.Flush();
|
||||
|
||||
return writer.WrittenCount;
|
||||
}
|
||||
|
||||
private sealed class DummyBufferWriter : IBufferWriter<byte>
|
||||
{
|
||||
public static readonly DummyBufferWriter Instance = new();
|
||||
public void Advance(int count) { /* ignore – we never overflow destination */ }
|
||||
public Memory<byte> GetMemory(int sizeHint = 0) => throw new NotSupportedException();
|
||||
public Span<byte> GetSpan(int sizeHint = 0) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Daybreak.API.Converters;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
[JsonConverter(typeof(PointerValueConverter))]
|
||||
public readonly struct PointerValue(nuint address) : IEquatable<PointerValue>, IEquatable<nuint>, IEquatable<nint>
|
||||
{
|
||||
public readonly nuint Address = address;
|
||||
|
||||
public bool Equals(PointerValue other)
|
||||
{
|
||||
return this.Address.Equals(other.Address);
|
||||
}
|
||||
|
||||
public bool Equals(nuint other)
|
||||
{
|
||||
return this.Address.Equals(other);
|
||||
}
|
||||
|
||||
public bool Equals(nint other)
|
||||
{
|
||||
return this.Address.Equals((nuint)other);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"0x{this.Address:X8}";
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is PointerValue value && this.Equals(value);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Address);
|
||||
}
|
||||
|
||||
public static bool operator ==(PointerValue left, PointerValue right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(PointerValue left, PointerValue right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(nuint address)
|
||||
{
|
||||
return new PointerValue(address);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(nint address)
|
||||
{
|
||||
return new PointerValue((nuint)address);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(int address)
|
||||
{
|
||||
return new PointerValue((nuint)address);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(uint address)
|
||||
{
|
||||
return new PointerValue(address);
|
||||
}
|
||||
|
||||
public static PointerValue Parse(string s)
|
||||
{
|
||||
if (s.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)
|
||||
&& nuint.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out var v))
|
||||
{
|
||||
return new PointerValue(v);
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid pointer format: {s}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public sealed class UnmanagedStruct<T> : IDisposable
|
||||
where T : struct
|
||||
{
|
||||
public nint Address { get; private set; }
|
||||
|
||||
public UnmanagedStruct(T value)
|
||||
{
|
||||
this.Address = Marshal.AllocHGlobal(Marshal.SizeOf<T>());
|
||||
Marshal.StructureToPtr(value, this.Address, false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Marshal.DestroyStructure<T>(this.Address);
|
||||
Marshal.FreeHGlobal(this.Address);
|
||||
this.Address = 0x0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.CorrelationVector;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Net.Sdk.Web;
|
||||
using Net.Sdk.Web.Options;
|
||||
using Serilog.Context;
|
||||
|
||||
namespace Daybreak.API.Logging;
|
||||
|
||||
public sealed class LoggingEnrichmentMiddleware(IOptions<CorrelationVectorOptions> options)
|
||||
: IMiddleware
|
||||
{
|
||||
private readonly CorrelationVectorOptions options = options.Value;
|
||||
|
||||
public Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue(this.options.Header, out var value) ||
|
||||
value.FirstOrDefault() is not string cvString ||
|
||||
CorrelationVector.Parse(cvString) is not CorrelationVector cv)
|
||||
{
|
||||
cv = new CorrelationVector();
|
||||
}
|
||||
|
||||
cv = CorrelationVector.Extend(cv.ToString());
|
||||
context.SetCorrelationVector(cv);
|
||||
LogContext.PushProperty("CorrelationVector", cv);
|
||||
HeaderDictionaryExtensions.Append(context.Response.Headers, this.options.Header, context.GetCorrelationVector().ToString());
|
||||
return next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Serilog.Settings.Configuration;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
using Serilog;
|
||||
using Net.Sdk.Web.Options;
|
||||
|
||||
namespace Daybreak.API.Logging;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithLogging(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddSerilog(new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration, new ConfigurationReaderOptions
|
||||
{
|
||||
SectionName = "Logging"
|
||||
})
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(
|
||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{SourceContext}] [{CorrelationVector}] {NewLine}{Message:lj}{NewLine}{Exception}",
|
||||
theme: AnsiConsoleTheme.Sixteen)
|
||||
.WriteTo.File(
|
||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{SourceContext}] [{CorrelationVector}] {NewLine}{Message:lj}{NewLine}{Exception}",
|
||||
path: "Daybreak.API.log")
|
||||
.CreateLogger());
|
||||
builder.Services.AddScoped<LoggingEnrichmentMiddleware>();
|
||||
builder.Services.AddOptions<CorrelationVectorOptions>()
|
||||
.Bind(builder.Configuration.GetSection("Correlation"));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Daybreak.API.Logging;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static WebApplication UseLogging(this WebApplication app)
|
||||
{
|
||||
app.UseMiddleware<LoggingEnrichmentMiddleware>();
|
||||
return app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Daybreak.API.Interop;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class AddressState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required PointerValue Address { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class AsyncRefreshCache<T>(TimeSpan refreshRate, Func<CancellationToken, ValueTask<T>> refreshFunc)
|
||||
{
|
||||
private readonly Func<CancellationToken, ValueTask<T>> refreshFunc = refreshFunc.ThrowIfNull();
|
||||
|
||||
private readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
|
||||
private T? cache;
|
||||
private DateTime lastUpdateDateTime = DateTime.MinValue;
|
||||
private TimeSpan refreshRate = refreshRate;
|
||||
|
||||
public async ValueTask<T> GetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.cache is not null && DateTime.UtcNow - this.lastUpdateDateTime < this.refreshRate)
|
||||
{
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
using var ctx = await this.semaphoreSlim.Acquire(cancellationToken);
|
||||
if (DateTime.UtcNow - this.lastUpdateDateTime > this.refreshRate ||
|
||||
this.cache is null)
|
||||
{
|
||||
this.lastUpdateDateTime = DateTime.UtcNow;
|
||||
this.cache = await this.refreshFunc(cancellationToken);
|
||||
}
|
||||
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
public void UpdateRefreshRate(TimeSpan newRefreshRate)
|
||||
{
|
||||
this.refreshRate = newRefreshRate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class ByteConsumerEntry(
|
||||
Guid id, TimeSpan freq, Action<ReadOnlySpan<byte>> handler)
|
||||
{
|
||||
private readonly Action<ReadOnlySpan<byte>> handler = handler.ThrowIfNull();
|
||||
|
||||
private DateTimeOffset lastConsume = DateTimeOffset.MinValue;
|
||||
|
||||
public Guid Id { get; } = id;
|
||||
public TimeSpan Frequency { get; } = freq;
|
||||
|
||||
public void TryConsume(DateTimeOffset currentTime, ReadOnlySpan<byte> value)
|
||||
{
|
||||
if (currentTime - this.lastConsume >= this.Frequency)
|
||||
{
|
||||
this.handler(value);
|
||||
this.lastConsume = currentTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class CallbackRegistration(Guid uid, Action onDispose) : IDisposable
|
||||
{
|
||||
private readonly Guid uid = uid;
|
||||
private readonly Action onDispose = onDispose.ThrowIfNull();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.onDispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public enum Channel : uint
|
||||
{
|
||||
Alliance = 0,
|
||||
Allies = 1, // coop with two groups for instance.
|
||||
GWCA1 = 2,
|
||||
All = 3,
|
||||
GWCA2 = 4,
|
||||
Moderator = 5,
|
||||
Emote = 6,
|
||||
Warning = 7, // shows in the middle of the screen and does not parse <c> tags
|
||||
GWCA3 = 8,
|
||||
Guild = 9,
|
||||
Global = 10,
|
||||
Group = 11,
|
||||
Trade = 12,
|
||||
Advisory = 13,
|
||||
Whisper = 14,
|
||||
Count,
|
||||
|
||||
// non-standard channel, but useful.
|
||||
Command
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class HealthCheckResponse
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<HealthStatus>))]
|
||||
public required HealthStatus Status { get; set; }
|
||||
public required TimeSpan TotalDuration { get; set; }
|
||||
public required Dictionary<string, HealthCheckEntryResponse> Entries { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Daybreak.API.Interop;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class HookState
|
||||
{
|
||||
public required bool Hooked { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required PointerValue TargetAddress { get; init; }
|
||||
public required PointerValue ContinueAddress { get; init; }
|
||||
public required PointerValue DetourAddress { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public interface IWorkItem
|
||||
{
|
||||
CancellationToken CancellationToken { get; }
|
||||
void Execute();
|
||||
void Cancel();
|
||||
void Exception(Exception ex);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public enum UIMessage : uint
|
||||
{
|
||||
None = 0x0,
|
||||
Resize = 0x8,
|
||||
InitFrame = 0x9,
|
||||
DestroyFrame = 0xb,
|
||||
KeyDown = 0x1e, // wparam = UIPacket::kKeyAction*
|
||||
KeyUp = 0x20, // wparam = UIPacket::kKeyAction*
|
||||
MouseClick = 0x22, // wparam = UIPacket::kMouseClick*
|
||||
MouseClick2 = 0x2e, // wparam = UIPacket::kMouseAction*
|
||||
MouseAction = 0x2f, // wparam = UIPacket::kMouseAction*
|
||||
SetLayout = 0x33,
|
||||
FrameMessage_0x47 = 0x47, // Multiple uses depending on frame
|
||||
UpdateAgentEffects = 0x10000000 | 0x9,
|
||||
RerenderAgentModel = 0x10000000 | 0x7, // wparam = uint32_t agent_id
|
||||
AgentSpeechBubble = 0x10000000 | 0x17,
|
||||
ShowAgentNameTag = 0x10000000 | 0x19, // wparam = AgentNameTagInfo*
|
||||
HideAgentNameTag = 0x10000000 | 0x1A,
|
||||
SetAgentNameTagAttribs = 0x10000000 | 0x1B, // wparam = AgentNameTagInfo*
|
||||
ChangeTarget = 0x10000000 | 0x20, // wparam = UIPacket::kChangeTarget*
|
||||
AgentStartCasting = 0x10000000 | 0x27, // wparam = UIPacket::kAgentStartCasting*
|
||||
ShowMapEntryMessage = 0x10000000 | 0x29, // wparam = { wchar_t* title, wchar_t* subtitle }
|
||||
SetCurrentPlayerData = 0x10000000 | 0x2A, // fired after setting the worldcontext player name
|
||||
PostProcessingEffect = 0x10000000 | 0x34, // Triggered when drunk. wparam = UIPacket::kPostProcessingEffect
|
||||
HeroAgentAdded = 0x10000000 | 0x38, // hero assigned to agent/inventory/ai mode
|
||||
HeroDataAdded = 0x10000000 | 0x39, // hero info received from server (name, level etc)
|
||||
ShowXunlaiChest = 0x10000000 | 0x40,
|
||||
MinionCountUpdated = 0x10000000 | 0x46,
|
||||
MoraleChange = 0x10000000 | 0x47, // wparam = {agent id, morale percent }
|
||||
LoginStateChanged = 0x10000000 | 0x50, // wparam = {bool is_logged_in, bool unk }
|
||||
EffectAdd = 0x10000000 | 0x55, // wparam = {agent_id, GW::Effect*}
|
||||
EffectRenew = 0x10000000 | 0x56, // wparam = GW::Effect*
|
||||
EffectRemove = 0x10000000 | 0x57, // wparam = effect id
|
||||
SkillActivated = 0x10000000 | 0x5b, // wparam ={ uint32_t agent_id , uint32_t skill_id }
|
||||
UpdateSkillbar = 0x10000000 | 0x5E, // wparam ={ uint32_t agent_id , ... }
|
||||
UpdateSkillsAvailable = 0x10000000 | 0x5f, // Triggered on a skill unlock, profession change or map load
|
||||
TitleProgressUpdated = 0x10000000 | 0x65, // wparam = title_id
|
||||
ExperienceGained = 0x10000000 | 0x66, // wparam = experience amount
|
||||
WriteToChatLog = 0x10000000 | 0x7E, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
|
||||
WriteToChatLogWithSender = 0x10000000 | 0x7f, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
|
||||
AllyOrGuildMessage = 0x10000000 | 0x80, // wparam = UIPacket::kAllyOrGuildMessage*
|
||||
PlayerChatMessage = 0x10000000 | 0x81, // wparam = UIPacket::kPlayerChatMessage*
|
||||
FloatingWindowMoved = 0x10000000 | 0x83, // wparam = frame_id
|
||||
FriendUpdated = 0x10000000 | 0x89, // wparam = { GW::Friend*, ... }
|
||||
MapLoaded = 0x10000000 | 0x8A,
|
||||
OpenWhisper = 0x10000000 | 0x90, // wparam = wchar* name
|
||||
Logout = 0x10000000 | 0x9b, // wparam = { bool unknown, bool character_select }
|
||||
CompassDraw = 0x10000000 | 0x9c, // wparam = UIPacket::kCompassDraw*
|
||||
OnScreenMessage = 0x10000000 | 0xA0, // wparam = wchar_** encoded_string
|
||||
DialogBody = 0x10000000 | 0xA4, // wparam = DialogBodyInfo*
|
||||
DialogButton = 0x10000000 | 0xA1, // wparam = DialogButtonInfo*
|
||||
TargetNPCPartyMember = 0x10000000 | 0xB1, // wparam = { uint32_t unk, uint32_t agent_id }
|
||||
TargetPlayerPartyMember = 0x10000000 | 0xB2, // wparam = { uint32_t unk, uint32_t player_number }
|
||||
VendorWindow = 0x10000000 | 0xB3, // wparam = UIPacket::kVendorWindow
|
||||
VendorItems = 0x10000000 | 0xB7, // wparam = UIPacket::kVendorItems
|
||||
VendorTransComplete = 0x10000000 | 0xB9, // wparam = *TransactionType
|
||||
VendorQuote = 0x10000000 | 0xBB, // wparam = UIPacket::kVendorQuote
|
||||
StartMapLoad = 0x10000000 | 0xC0, // wparam = { uint32_t map_id, ...}
|
||||
WorldMapUpdated = 0x10000000 | 0xC5, // Triggered when an area in the world map has been discovered/updated
|
||||
GuildMemberUpdated = 0x10000000 | 0xD8, // wparam = { GuildPlayer::name_ptr }
|
||||
ShowHint = 0x10000000 | 0xDF, // wparam = { uint32_t icon_type, wchar_t* message_enc }
|
||||
WeaponSetSwapComplete = 0x10000000 | 0xE7, // wparam = UIPacket::kWeaponSwap*
|
||||
WeaponSetSwapCancel = 0x10000000 | 0xE8,
|
||||
WeaponSetUpdated = 0x10000000 | 0xE9,
|
||||
UpdateGoldCharacter = 0x10000000 | 0xEA, // wparam = { uint32_t unk, uint32_t gold_character }
|
||||
UpdateGoldStorage = 0x10000000 | 0xEB, // wparam = { uint32_t unk, uint32_t gold_storage }
|
||||
InventorySlotUpdated = 0x10000000 | 0xEC, // undocumented. Triggered when an item is moved into a slot
|
||||
EquipmentSlotUpdated = 0x10000000 | 0xED, // undocumented. Triggered when an item is moved into a slot
|
||||
InventorySlotCleared = 0x10000000 | 0xEF, // undocumented. Triggered when an item has been removed from a slot
|
||||
EquipmentSlotCleared = 0x10000000 | 0xF0, // undocumented. Triggered when an item has been removed from a slot
|
||||
PvPWindowContent = 0x10000000 | 0xF8,
|
||||
PreStartSalvage = 0x10000000 | 0x100, // { uint32_t item_id, uint32_t kit_id }
|
||||
TradePlayerUpdated = 0x10000000 | 0x103, // wparam = GW::TraderPlayer*
|
||||
ItemUpdated = 0x10000000 | 0x104, // wparam = UIPacket::kItemUpdated*
|
||||
MapChange = 0x10000000 | 0x10F, // wparam = map id
|
||||
CalledTargetChange = 0x10000000 | 0x113, // wparam = { player_number, target_id }
|
||||
ErrorMessage = 0x10000000 | 0x117, // wparam = { int error_index, wchar_t* error_encoded_string }
|
||||
PartyHardModeChanged = 0x10000000 | 0x118, // wparam = { int is_hard_mode }
|
||||
PartyAddHenchman = 0x10000000 | 0x119,
|
||||
PartyRemoveHenchman = 0x10000000 | 0x11a,
|
||||
PartyAddHero = 0x10000000 | 0x11c,
|
||||
PartyRemoveHero = 0x10000000 | 0x11d,
|
||||
PartyAddPlayer = 0x10000000 | 0x122,
|
||||
PartyRemovePlayer = 0x10000000 | 0x124,
|
||||
DisableEnterMissionBtn = 0x10000000 | 0x128, // wparam = boolean (1 = disabled, 0 = enabled)
|
||||
ShowCancelEnterMissionBtn = 0x10000000 | 0x12b,
|
||||
PartyDefeated = 0x10000000 | 0x12d,
|
||||
PartySearchInviteReceived = 0x10000000 | 0x135, // wparam = UIPacket::kPartySearchInviteReceived*
|
||||
PartySearchInviteSent = 0x10000000 | 0x137,
|
||||
PartyShowConfirmDialog = 0x10000000 | 0x138, // wparam = UIPacket::kPartyShowConfirmDialog
|
||||
PreferenceEnumChanged = 0x10000000 | 0x13E, // wparam = UiPacket::kPreferenceEnumChanged
|
||||
PreferenceFlagChanged = 0x10000000 | 0x13F, // wparam = UiPacket::kPreferenceFlagChanged
|
||||
PreferenceValueChanged = 0x10000000 | 0x140, // wparam = UiPacket::kPreferenceValueChanged
|
||||
UIPositionChanged = 0x10000000 | 0x141, // wparam = UIPacket::kUIPositionChanged
|
||||
QuestAdded = 0x10000000 | 0x149, // wparam = { quest_id, ... }
|
||||
QuestDetailsChanged = 0x10000000 | 0x14A, // wparam = { quest_id, ... }
|
||||
ClientActiveQuestChanged = 0x10000000 | 0x14C, // wparam = { quest_id, ... }. Triggered when the game requests the current quest to change
|
||||
ServerActiveQuestChanged = 0x10000000 | 0x14E, // wparam = UIPacket::kServerActiveQuestChanged*. Triggered when the server requests the current quest to change
|
||||
UnknownQuestRelated = 0x10000000 | 0x14F,
|
||||
DungeonComplete = 0x10000000 | 0x151, // undocumented
|
||||
MissionComplete = 0x10000000 | 0x152, // undocumented
|
||||
VanquishComplete = 0x10000000 | 0x154, // undocumented
|
||||
ObjectiveAdd = 0x10000000 | 0x155, // wparam = UIPacket::kObjectiveAdd*
|
||||
ObjectiveComplete = 0x10000000 | 0x156, // wparam = UIPacket::kObjectiveComplete*
|
||||
ObjectiveUpdated = 0x10000000 | 0x157, // wparam = UIPacket::kObjectiveUpdated*
|
||||
TradeSessionStart = 0x10000000 | 0x160, // wparam = { trade_state, player_number }
|
||||
TradeSessionUpdated = 0x10000000 | 0x166, // no args
|
||||
TriggerLogoutPrompt = 0x10000000 | 0x16C, // no args
|
||||
ToggleOptionsWindow = 0x10000000 | 0x16D, // no args
|
||||
CheckUIState = 0x10000000 | 0x170, // Undocumented
|
||||
RedrawItem = 0x10000000 | 0x172, // wparam = uint32_t item_id
|
||||
CloseSettings = 0x10000000 | 0x174, // Undocumented
|
||||
ChangeSettingsTab = 0x10000000 | 0x175, // wparam = uint32_t is_interface_tab
|
||||
GuildHall = 0x10000000 | 0x177, // wparam = gh key (uint32_t[4])
|
||||
LeaveGuildHall = 0x10000000 | 0x179,
|
||||
Travel = 0x10000000 | 0x17A,
|
||||
OpenWikiUrl = 0x10000000 | 0x17B, // wparam = char* url
|
||||
AppendMessageToChat = 0x10000000 | 0x189, // wparam = wchar_t* message
|
||||
HideHeroPanel = 0x10000000 | 0x197, // wparam = hero_id
|
||||
ShowHeroPanel = 0x10000000 | 0x198, // wparam = hero_id
|
||||
GetInventoryAgentId = 0x10000000 | 0x19c, // wparam = 0, lparam = uint32_t* agent_id_out. Used to fetch which agent is selected
|
||||
EquipItem = 0x10000000 | 0x19d, // wparam = { item_id, agent_id }
|
||||
MoveItem = 0x10000000 | 0x19e, // wparam = { item_id, to_bag, to_slot, bool prompt }
|
||||
InitiateTrade = 0x10000000 | 0x1A0,
|
||||
InventoryAgentChanged = 0x10000000 | 0x1b0, // Triggered when inventory needs updating due to agent change; no args
|
||||
OpenTemplate = 0x10000000 | 0x1B9, // wparam = GW::UI::ChatTemplate*
|
||||
|
||||
// GWCA Client to Server commands. Only added the ones that are used for hooks, everything else goes straight into GW
|
||||
|
||||
SendLoadSkillTemplate = 0x30000000 | 0x3, // wparam = SkillbarMgr::SkillTemplate*
|
||||
SendPingWeaponSet = 0x30000000 | 0x4, // wparam = UIPacket::kSendPingWeaponSet*
|
||||
SendMoveItem = 0x30000000 | 0x5, // wparam = UIPacket::kSendMoveItem*
|
||||
SendMerchantRequestQuote = 0x30000000 | 0x6, // wparam = UIPacket::kSendMerchantRequestQuote*
|
||||
SendMerchantTransactItem = 0x30000000 | 0x7, // wparam = UIPacket::kSendMerchantTransactItem*
|
||||
SendUseItem = 0x30000000 | 0x8, // wparam = UIPacket::kSendUseItem*
|
||||
SendSetActiveQuest = 0x30000000 | 0x9, // wparam = uint32_t quest_id
|
||||
SendAbandonQuest = 0x30000000 | 0xA, // wparam = uint32_t quest_id
|
||||
SendChangeTarget = 0x30000000 | 0xB, // wparam = UIPacket::kSendChangeTarget* // e.g. tell the gw client to focus on a different target
|
||||
SendCallTarget = 0x30000000 | 0x13, // wparam = { uint32_t call_type, uint32_t agent_id } // also used to broadcast morale, death penalty, "I'm following X", etc
|
||||
SendDialog = 0x30000000 | 0x16, // wparam = dialog_id // internal use
|
||||
|
||||
|
||||
StartWhisper = 0x30000000 | 0x17, // wparam = UIPacket::kStartWhisper*
|
||||
GetSenderColor = 0x30000000 | 0x18, // wparam = UIPacket::kGetColor* // Get chat sender color depending on channel, output object passed by reference
|
||||
GetMessageColor = 0x30000000 | 0x19, // wparam = UIPacket::kGetColor* // Get chat message color depending on channel, output object passed by reference
|
||||
SendChatMessage = 0x30000000 | 0x1B, // wparam = UIPacket::kSendChatMessage*
|
||||
LogChatMessage = 0x30000000 | 0x1D, // wparam = UIPacket::kLogChatMessage*. Triggered when a message wants to be added to the persistent chat log.
|
||||
RecvWhisper = 0x30000000 | 0x1E, // wparam = UIPacket::kRecvWhisper*
|
||||
PrintChatMessage = 0x30000000 | 0x1F, // wparam = UIPacket::kPrintChatMessage*. Triggered when a message wants to be added to the in-game chat window.
|
||||
SendWorldAction = 0x30000000 | 0x20, // wparam = UIPacket::kSendWorldAction*
|
||||
SetRendererValue = 0x30000000 | 0x21 // wparam = UIPacket::kSetRendererValue
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public static class UIPackets
|
||||
{
|
||||
public enum MouseButtons
|
||||
{
|
||||
Left = 0x0,
|
||||
Middle = 0x1,
|
||||
Right = 0x2
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct LogChatMessage(string message, Channel channel)
|
||||
{
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public readonly string Message = message;
|
||||
public readonly Channel Channel = channel;
|
||||
};
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct UIChatMessage(Channel channel, string message, Channel channel2)
|
||||
{
|
||||
public readonly Channel Channel = channel;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public readonly string Message = message;
|
||||
public readonly Channel Channel2 = channel2;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct KeyAction(uint key)
|
||||
{
|
||||
public readonly uint Key = key;
|
||||
public readonly uint WParam = 0x4000;
|
||||
public readonly uint LParam = 0x0006;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct MouseClick(MouseButtons mouseButton, uint isDoubleClick, uint unknownTypeScreenPos)
|
||||
{
|
||||
public readonly MouseButtons MouseButton = mouseButton;
|
||||
public readonly uint IsDoubleClick = isDoubleClick;
|
||||
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class WorkItem(
|
||||
Action action,
|
||||
TaskCompletionSource tcs,
|
||||
CancellationToken token) : IWorkItem
|
||||
{
|
||||
private readonly Action action = action;
|
||||
private readonly TaskCompletionSource tcs = tcs;
|
||||
|
||||
public CancellationToken CancellationToken { get; } = token;
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
if (this.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
this.tcs.TrySetCanceled(this.CancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.action();
|
||||
this.tcs.TrySetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
this.tcs.TrySetCanceled(this.CancellationToken);
|
||||
}
|
||||
|
||||
public void Exception(Exception ex)
|
||||
{
|
||||
this.tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WorkItem<T>(
|
||||
Func<T> func,
|
||||
TaskCompletionSource<T> tcs,
|
||||
CancellationToken token) : IWorkItem
|
||||
{
|
||||
private readonly Func<T> func = func;
|
||||
private readonly TaskCompletionSource<T> tcs = tcs;
|
||||
|
||||
public CancellationToken CancellationToken { get; } = token;
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
if (this.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
this.tcs.TrySetCanceled(this.CancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.tcs.TrySetResult(this.func());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
this.tcs.TrySetCanceled(this.CancellationToken);
|
||||
}
|
||||
|
||||
public void Exception(Exception ex)
|
||||
{
|
||||
this.tcs.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,21 @@
|
||||
|
||||
namespace Daybreak.API;
|
||||
|
||||
internal static partial class NativeMethods
|
||||
internal unsafe static partial class NativeMethods
|
||||
{
|
||||
public const int STD_OUTPUT_HANDLE = -11;
|
||||
public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
|
||||
public const uint ENABLE_PROCESSED_OUTPUT = 0x0001;
|
||||
|
||||
public const int WM_KEYDOWN = 0x0100;
|
||||
public const int WM_KEYUP = 0x0101;
|
||||
public const int WM_CHAR = 0x0102;
|
||||
public const int VK_RIGHT = 0x27;
|
||||
|
||||
public const uint MEM_COMMIT = 0x1000;
|
||||
public const uint MEM_RESERVE = 0x2000;
|
||||
public const uint PAGE_EXECUTE_READWRITE = 0x40;
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool AllocConsole();
|
||||
@@ -25,4 +34,18 @@ internal static partial class NativeMethods
|
||||
[LibraryImport("kernel32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
public static partial nint SendMessageW(nint hWnd, int Msg, nint wParam, nint lParam);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
public static partial nint VirtualAlloc(
|
||||
nint lpAddress, nuint dwSize,
|
||||
uint flAllocationType, uint flProtect);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool VirtualProtect(
|
||||
void* lpAddress, nuint dwSize,
|
||||
uint flNewProtect, out uint lpflOldProtect);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,39 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.API.Serialization;
|
||||
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(byte[]))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(uint))]
|
||||
[JsonSerializable(typeof(nuint))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(Task<IResult>))]
|
||||
[JsonSerializable(typeof(Task))]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(HealthCheckResponse))]
|
||||
[JsonSerializable(typeof(HealthCheckEntryResponse))]
|
||||
[JsonSerializable(typeof(HookState))]
|
||||
[JsonSerializable(typeof(ArraySegment<HookState>))]
|
||||
[JsonSerializable(typeof(AddressState))]
|
||||
[JsonSerializable(typeof(ArraySegment<AddressState>))]
|
||||
[JsonSerializable(typeof(MainPlayerState))]
|
||||
[JsonSerializable(typeof(QuestLogInformation))]
|
||||
[JsonSerializable(typeof(QuestInformation))]
|
||||
[JsonSerializable(typeof(MainPlayerInformation))]
|
||||
[JsonSerializable(typeof(CharacterSelectInformation))]
|
||||
[JsonSerializable(typeof(CharacterSelectEntry))]
|
||||
[JsonSerializable(typeof(BuildEntry))]
|
||||
[JsonSerializable(typeof(List<BuildEntry>))]
|
||||
[JsonSerializable(typeof(PartyLoadoutEntry))]
|
||||
[JsonSerializable(typeof(List<PartyLoadout>))]
|
||||
[JsonSerializable(typeof(InstanceInfo))]
|
||||
[JsonSerializable(typeof(TitleInfo))]
|
||||
public partial class ApiJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Daybreak.API.Serialization;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithSerializationContext(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
{
|
||||
options.SerializerOptions.TypeInfoResolverChain.Insert(0, new ApiJsonSerializerContext());
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Services.MDns;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Extensions.Core;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class ApiAdvertisingService(
|
||||
IServer server,
|
||||
IMDnsService mDnsService,
|
||||
IHostApplicationLifetime lifetime,
|
||||
ILogger<ApiAdvertisingService> logger) : IHostedService
|
||||
{
|
||||
private const string ProcessIdPlaceholder = "{PID}";
|
||||
private const string ServiceName = $"daybreak-api-{ProcessIdPlaceholder}";
|
||||
private const string ServiceSubType = "daybreak-api";
|
||||
private readonly IServerAddressesFeature serverAddressesFeature = server.Features.Get<IServerAddressesFeature>() ?? throw new InvalidOperationException("Server does not support addresses feature.");
|
||||
private readonly IMDnsService mDnsService = mDnsService;
|
||||
private readonly IHostApplicationLifetime lifetime = lifetime;
|
||||
private readonly ILogger<ApiAdvertisingService> logger = logger;
|
||||
|
||||
private DnsRegistrationToken? dnsToken;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.lifetime.ApplicationStarted.Register(this.RegisterDomain);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.dnsToken?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void RegisterDomain()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var addresses = this.serverAddressesFeature?.Addresses.ToArray() ?? [];
|
||||
var port = addresses
|
||||
.Select(a => new Uri(a).Port)
|
||||
.Distinct()
|
||||
.FirstOrDefault();
|
||||
|
||||
scopedLogger.LogInformation("Found port {port}", port);
|
||||
if (port is 0)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to start advertising service. No port found");
|
||||
}
|
||||
|
||||
var dnsEntry = ServiceName.Replace(ProcessIdPlaceholder, Environment.ProcessId.ToString());
|
||||
scopedLogger.LogInformation("Advertising service {service}:{port}", dnsEntry, port);
|
||||
this.dnsToken = this.mDnsService.RegisterDomain(dnsEntry, (ushort)port, ServiceSubType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using ZLinq;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class CharacterSelectService(
|
||||
InstanceContextService instanceContextService,
|
||||
PlatformContextService platformContextService,
|
||||
UIContextService uiContextService,
|
||||
GameThreadService gameThreadService,
|
||||
GameContextService gameContextService,
|
||||
ILogger<CharacterSelectService> logger)
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private readonly struct LogOutMessage(uint unknown, uint characterSelect)
|
||||
{
|
||||
public readonly uint Unknown = unknown;
|
||||
public readonly uint CharacterSelect = characterSelect;
|
||||
}
|
||||
|
||||
private readonly InstanceContextService instanceContextService = instanceContextService.ThrowIfNull();
|
||||
private readonly PlatformContextService platformContextService = platformContextService.ThrowIfNull();
|
||||
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
|
||||
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
|
||||
private readonly GameContextService gameContextService = gameContextService.ThrowIfNull();
|
||||
private readonly ILogger<CharacterSelectService> logger = logger.ThrowIfNull();
|
||||
|
||||
public Task<CharacterSelectInformation?> GetCharacterSelectInformation(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var availableCharsContext = this.gameContextService.GetAvailableChars();
|
||||
if (availableCharsContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Available characters context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
|
||||
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
|
||||
foreach(var charContext in *availableCharsContext.Pointer)
|
||||
{
|
||||
var nameSpan = charContext.Name.AsSpan();
|
||||
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
|
||||
availableChars.Add(new CharacterSelectEntry(
|
||||
charContext.Uuid.ToString(),
|
||||
name,
|
||||
charContext.Primary,
|
||||
charContext.Secondary,
|
||||
charContext.Campaign,
|
||||
charContext.MapId,
|
||||
charContext.Level,
|
||||
charContext.IsPvp));
|
||||
}
|
||||
|
||||
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
|
||||
return new CharacterSelectInformation(currentCharacter, availableChars);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> ChangeCharacterByName(string characterName, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (await this.ValidateState(cancellationToken) is false)
|
||||
{
|
||||
scopedLogger.LogError("Cannot change character while in loading state");
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.TriggerLogOut(cancellationToken);
|
||||
|
||||
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(characterName, cancellationToken);
|
||||
if (indexResult is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find index by name {name}", characterName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentIndex = indexResult.Value.CurrentIndex;
|
||||
var desiredIndex = indexResult.Value.DesiredIndex;
|
||||
scopedLogger.LogInformation("Changing character to {name} with index {index}", characterName, desiredIndex);
|
||||
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
|
||||
if (!navigateResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to navigate to character {name} with index {index}", characterName, desiredIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ChangeCharacterByUuid(string uuid, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (await this.ValidateState(cancellationToken) is false)
|
||||
{
|
||||
scopedLogger.LogError("Cannot change character while in loading state");
|
||||
return false;
|
||||
}
|
||||
|
||||
var desiredCharName = await this.GetCharNameByUuid(uuid, cancellationToken);
|
||||
if (desiredCharName is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find character with UUID: {uuid}", uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.TriggerLogOut(cancellationToken);
|
||||
|
||||
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(desiredCharName, cancellationToken);
|
||||
if (indexResult is null)
|
||||
{
|
||||
scopedLogger.LogError("Resolved character by uuid {uuid} but failed to find index by name {name}", uuid, desiredCharName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentIndex = indexResult.Value.CurrentIndex;
|
||||
var desiredIndex = indexResult.Value.DesiredIndex;
|
||||
scopedLogger.LogInformation("Changing character to {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
|
||||
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
|
||||
if (!navigateResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to navigate to character {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateState(CancellationToken cancellationToken)
|
||||
{
|
||||
return await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<bool> NavigateToCharAndPlay(uint currentIndex, uint desiredIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
var result = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var preGameContext = this.gameContextService.GetPreGameContext();
|
||||
if (preGameContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Pre-game context is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
var hwnd = this.platformContextService.GetWindowHandle();
|
||||
if (hwnd is null or 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get window handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preGameContext.Pointer->Index1 == currentIndex)
|
||||
{
|
||||
return false; //Not moved yet
|
||||
}
|
||||
|
||||
if (preGameContext.Pointer->Index1 == desiredIndex)
|
||||
{
|
||||
// We're on the desired character. Trigger play
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
|
||||
return true;
|
||||
}
|
||||
|
||||
currentIndex = preGameContext.Pointer->Index1;
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, NativeMethods.VK_RIGHT, 0x014D0001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, NativeMethods.VK_RIGHT, 0x014D0001);
|
||||
return false;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (result)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var availableCharsContext = this.gameContextService.GetAvailableChars();
|
||||
if (availableCharsContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Available characters context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
foreach (var charContext in *availableCharsContext.Pointer)
|
||||
{
|
||||
if (charContext.Uuid.ToString() != uuid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameSpan = charContext.Name.AsSpan();
|
||||
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
|
||||
return name;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task TriggerLogOut(CancellationToken cancellationToken)
|
||||
{
|
||||
await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
var logoutMessage = new LogOutMessage(0, 0);
|
||||
unsafe
|
||||
{
|
||||
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<(uint DesiredIndex, uint CurrentIndex)?> WaitForLoginScreenAndGetCurrentAndDesiredIndex(string desiredCharName, CancellationToken cancellationToken)
|
||||
{
|
||||
uint? desiredIndex = default;
|
||||
uint? currentIndex = default;
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
var ready = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var preGameContext = this.gameContextService.GetPreGameContext();
|
||||
if (preGameContext.IsNull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var uiState = 10U;
|
||||
this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
|
||||
var loginScreen = uiState == 2;
|
||||
if (!loginScreen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0U; i < preGameContext.Pointer->LoginCharacters.Size; i++)
|
||||
{
|
||||
var loginCharacter = preGameContext.Pointer->LoginCharacters.Buffer[i];
|
||||
var charNameSpan = loginCharacter.CharacterName.AsSpan();
|
||||
var charName = new string(charNameSpan[..charNameSpan.IndexOf('\0')]);
|
||||
if (charName == desiredCharName)
|
||||
{
|
||||
desiredIndex = i;
|
||||
currentIndex = 0xffffffdd;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (ready)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (desiredIndex is not null && currentIndex is not null)
|
||||
{
|
||||
return (desiredIndex.Value, currentIndex.Value);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class ChatService(
|
||||
InstanceContextService instanceContextService,
|
||||
GameThreadService gameThreadService,
|
||||
UIContextService uiContextService)
|
||||
{
|
||||
private readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
private readonly InstanceContextService instanceContextService = instanceContextService.ThrowIfNull();
|
||||
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
|
||||
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a message to the chat. It does not send the message, it only shows it to the player.
|
||||
/// </summary>
|
||||
public async ValueTask AddMessageAsync(string message, string? sender, Channel channel, CancellationToken cancellationToken)
|
||||
{
|
||||
using var ctx = await this.semaphoreSlim.Acquire(cancellationToken);
|
||||
await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var encodedMessage = string.Create(
|
||||
3 + message.Length,
|
||||
message,
|
||||
static (span, msg) =>
|
||||
{
|
||||
span[0] = '\u0108';
|
||||
span[1] = '\u0107';
|
||||
msg.AsSpan().CopyTo(span[2..]);
|
||||
span[^1] = '\u0001';
|
||||
});
|
||||
|
||||
var encodedSender = sender is not null
|
||||
? string.Create(
|
||||
3 + sender.Length,
|
||||
sender,
|
||||
static (span, msg) =>
|
||||
{
|
||||
span[0] = '\u0108';
|
||||
span[1] = '\u0107';
|
||||
msg.AsSpan().CopyTo(span[2..]);
|
||||
span[^1] = '\u0001';
|
||||
})
|
||||
: default;
|
||||
|
||||
var encoded = encodedSender is null
|
||||
? encodedMessage
|
||||
: string.Create(
|
||||
encodedMessage.Length + encodedSender.Length + 6,
|
||||
(encodedMessage, encodedSender),
|
||||
static (span, state) =>
|
||||
{
|
||||
span[0] = '\u076b';
|
||||
span[1] = '\u010a';
|
||||
state.encodedSender.AsSpan().CopyTo(span[2..]);
|
||||
span[2 + state.encodedSender.Length] = '\u0001';
|
||||
span[3 + state.encodedSender.Length] = '\u010b';
|
||||
state.encodedMessage.AsSpan().CopyTo(span[(4 + state.encodedSender.Length)..]);
|
||||
span[^2] = '\u0001';
|
||||
});
|
||||
|
||||
using var packet = new UnmanagedStruct<UIPackets.UIChatMessage>(new UIPackets.UIChatMessage(channel, encoded, channel));
|
||||
this.uiContextService.SendMessage(UIMessage.WriteToChatLog, (nuint)packet.Address, 0x0);
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public unsafe sealed class AgentContextService : IAddressHealthService
|
||||
{
|
||||
private const string AgentArrayMask = "xxxxxxx";
|
||||
private const int AgentArrayOffset = -0x4;
|
||||
private static readonly byte[] AgentArrayAddressPattern = [0x8b, 0x0c, 0x90, 0x85, 0xc9, 0x74, 0x19];
|
||||
|
||||
private const string PlayerAgentIdMask = "xx????xxxx";
|
||||
private const int PlayerAgentIdOffset = -0xE;
|
||||
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache agentArrayAddress;
|
||||
private readonly GWAddressCache playerAgentIdAddress;
|
||||
private readonly ILogger<AgentContextService> logger;
|
||||
|
||||
public AgentContextService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<AgentContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.agentArrayAddress = new GWAddressCache(this.GetAgentArrayAddress);
|
||||
this.playerAgentIdAddress = new GWAddressCache(this.GetPreGameContextAddress);
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return [
|
||||
new AddressState
|
||||
{
|
||||
Address = this.agentArrayAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.agentArrayAddress),
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
Address = this.playerAgentIdAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.playerAgentIdAddress),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public WrappedPointer<GuildWarsArray<WrappedPointer<AgentContext>>> GetAgentArray()
|
||||
{
|
||||
var agentArrayAddress = this.agentArrayAddress.GetAddress();
|
||||
if (agentArrayAddress is null or 0x0)
|
||||
{
|
||||
this.logger.LogError("Failed to get agent array address");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (GuildWarsArray<WrappedPointer<AgentContext>>*)agentArrayAddress;
|
||||
}
|
||||
|
||||
public uint GetPlayerAgentId()
|
||||
{
|
||||
var playerAgentIdAddress = this.playerAgentIdAddress.GetAddress();
|
||||
if (playerAgentIdAddress is null or 0x0)
|
||||
{
|
||||
this.logger.LogError("Failed to get player agent id address");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return *(uint*)playerAgentIdAddress;
|
||||
}
|
||||
|
||||
private nuint GetAgentArrayAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var address = this.memoryScanningService.FindAndResolveAddress(AgentArrayAddressPattern, AgentArrayMask, AgentArrayOffset);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent array address");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Agent array address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetPreGameContextAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var address = this.memoryScanningService.FindAndResolveAddress(PlayerAgentIdAddressPattern, PlayerAgentIdMask, PlayerAgentIdOffset);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find player agent id address");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Player agent id address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class ChatHandlingService
|
||||
: IHostedService, IHookHealthService
|
||||
{
|
||||
private const string AddToChatLogMask = "xxxxxx";
|
||||
private static readonly byte[] AddToChatLogSeq = [0x40, 0x25, 0xFF, 0x01, 0x00, 0x00];
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate bool AddToChatLog([MarshalAs(UnmanagedType.LPWStr)] string message, Channel channel);
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWHook<AddToChatLog> addToChatLogHook;
|
||||
private readonly ILogger<ChatHandlingService> logger;
|
||||
|
||||
private CancellationTokenSource? cts = default;
|
||||
|
||||
public ChatHandlingService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<ChatHandlingService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.addToChatLogHook = new GWHook<AddToChatLog>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
this.memoryScanningService.FindAddress(AddToChatLogSeq, AddToChatLogMask))),
|
||||
this.OnAddToChatLog);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.cts?.Dispose();
|
||||
this.cts = new CancellationTokenSource();
|
||||
return Task.Factory.StartNew(() => this.InitializeHooks(this.cts.Token), this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.cts?.Cancel();
|
||||
this.cts?.Dispose();
|
||||
this.addToChatLogHook.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public List<HookState> GetHookStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new HookState
|
||||
{
|
||||
Hooked = this.addToChatLogHook.Hooked,
|
||||
Name = nameof(this.addToChatLogHook),
|
||||
TargetAddress = new PointerValue(this.addToChatLogHook.TargetAddress),
|
||||
ContinueAddress = new PointerValue(this.addToChatLogHook.ContinueAddress),
|
||||
DetourAddress = new PointerValue(this.addToChatLogHook.DetourAddress)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
scopedLogger.LogInformation("Initializing hooks");
|
||||
while (!this.addToChatLogHook.EnsureInitialized())
|
||||
{
|
||||
scopedLogger.LogDebug("Waiting for hook to initialize: {hook}", nameof(this.addToChatLogHook));
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Hooks initialized");
|
||||
}
|
||||
|
||||
private bool OnAddToChatLog(string message, Channel channel)
|
||||
{
|
||||
return this.addToChatLogHook.Continue(message, channel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public unsafe sealed class GameContextService : IAddressHealthService
|
||||
{
|
||||
private const string BaseAddressMask = "xxxxxxx";
|
||||
private const int BaseAddressOffset = +7;
|
||||
private static readonly byte[] BaseAddressPattern = [0x50, 0x6A, 0x0F, 0x6A, 0x00, 0xFF, 0x35];
|
||||
|
||||
private const string PreGameContextFile = "UiPregame.cpp";
|
||||
private const string PreGameContextAssertion = "!s_scene";
|
||||
private const int PreGameContextOffset = 0x34;
|
||||
|
||||
private const string AvailableCharsMask = "xx????xxxxxxx";
|
||||
private const int AvailableCharsOffset = 0x2;
|
||||
private static readonly byte[] AvailableCharsPattern = [0x8B, 0x35, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0xF8, 0x84, 0x00, 0x00, 0x00];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache baseAddress;
|
||||
private readonly GWAddressCache preGameContextAddress;
|
||||
private readonly GWAddressCache availableCharsAddress;
|
||||
private readonly ILogger<GameContextService> logger;
|
||||
|
||||
public GameContextService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<GameContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
|
||||
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
|
||||
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return [
|
||||
new AddressState
|
||||
{
|
||||
Address = this.baseAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.baseAddress),
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
Address = this.preGameContextAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.preGameContextAddress),
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
Address = this.availableCharsAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.availableCharsAddress),
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public WrappedPointer<GameContext> GetGameContext()
|
||||
{
|
||||
var gameContextAddress = this.GetGameContextAddress();
|
||||
if (gameContextAddress is 0x0)
|
||||
{
|
||||
this.logger.LogError("Failed to get game context address");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (GameContext*)gameContextAddress;
|
||||
}
|
||||
|
||||
public WrappedPointer<PreGameContext> GetPreGameContext()
|
||||
{
|
||||
var preGameContextAddress = this.preGameContextAddress.GetAddress();
|
||||
if (preGameContextAddress is null or 0x0)
|
||||
{
|
||||
this.logger.LogError("Failed to get pre-game context address");
|
||||
return null;
|
||||
}
|
||||
|
||||
return *(PreGameContext**)preGameContextAddress;
|
||||
}
|
||||
|
||||
public WrappedPointer<GuildWarsArray<CharInfoContext>> GetAvailableChars()
|
||||
{
|
||||
var availableCharsAddress = this.availableCharsAddress.GetAddress();
|
||||
if (availableCharsAddress is null or 0x0)
|
||||
{
|
||||
this.logger.LogError("Failed to get available chars address");
|
||||
return null;
|
||||
}
|
||||
|
||||
return *(GuildWarsArray<CharInfoContext>**)availableCharsAddress;
|
||||
}
|
||||
|
||||
public bool IsMapLoaded()
|
||||
{
|
||||
var gameContext = this.GetGameContext();
|
||||
if (gameContext.IsNull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gameContext.Pointer->MapContext is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private nuint GetGameContextAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (this.baseAddress.GetAddress() is not nuint basePtr)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get base address");
|
||||
return default;
|
||||
}
|
||||
|
||||
nuint** baseContext = *(nuint***)basePtr;
|
||||
if (baseContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get base context address");
|
||||
return default;
|
||||
}
|
||||
|
||||
var globalContextPtr = baseContext[0x6];
|
||||
if (globalContextPtr is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game context address");
|
||||
return default;
|
||||
}
|
||||
|
||||
return (nuint)globalContextPtr;
|
||||
}
|
||||
|
||||
private nuint GetBaseAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
|
||||
if (baseAddress is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find base address");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
|
||||
return baseAddress;
|
||||
}
|
||||
|
||||
private unsafe nuint GetPreGameContextAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var preGameContextAddress = this.memoryScanningService.FindAssertion(PreGameContextFile, PreGameContextAssertion, 0, PreGameContextOffset);
|
||||
if (preGameContextAddress is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find pre-game context address");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
preGameContextAddress = *(nuint*)preGameContextAddress;
|
||||
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", preGameContextAddress);
|
||||
return preGameContextAddress;
|
||||
}
|
||||
|
||||
private unsafe nuint GetAvailableCharactersAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var availableCharsAddress = this.memoryScanningService.FindAddress(AvailableCharsPattern, AvailableCharsMask, AvailableCharsOffset);
|
||||
if (availableCharsAddress is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find available chars address");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Available chars address: 0x{address:X8}", availableCharsAddress);
|
||||
return availableCharsAddress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class GameThreadService
|
||||
: IHostedService, IHookHealthService
|
||||
{
|
||||
private const string LeaveGameThreadFile = "FrApi.cpp";
|
||||
private const string LeaveGameThreadAssertion = "renderElapsed >= 0";
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void LeaveGameThread(nint ctx);
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWHook<LeaveGameThread> leaveGameThreadHook;
|
||||
private readonly ILogger<GameThreadService> logger;
|
||||
private readonly ConcurrentQueue<IWorkItem> queuedItems = [];
|
||||
private readonly ConcurrentDictionary<Guid, Action> registeredCallbacks = [];
|
||||
|
||||
private CancellationTokenSource? cts = default;
|
||||
|
||||
public GameThreadService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<GameThreadService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.leaveGameThreadHook = new GWHook<LeaveGameThread>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
this.memoryScanningService.FindAssertion(LeaveGameThreadFile, LeaveGameThreadAssertion, 0, 0), 0x300)),
|
||||
this.OnLeaveGameThread);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.cts?.Dispose();
|
||||
this.cts = new CancellationTokenSource();
|
||||
return Task.Factory.StartNew(() => this.InitializeHooks(this.cts.Token), this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.cts?.Cancel();
|
||||
this.cts?.Dispose();
|
||||
this.leaveGameThreadHook.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task QueueOnGameThread(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource();
|
||||
this.queuedItems.Enqueue(new WorkItem(action, taskCompletionSource, cancellationToken));
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
public Task<T> QueueOnGameThread<T>(Func<T> action, CancellationToken cancellationToken)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
this.queuedItems.Enqueue(new WorkItem<T>(action, taskCompletionSource, cancellationToken));
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
public CallbackRegistration RegisterCallback(Action action)
|
||||
{
|
||||
var uid = new Guid();
|
||||
var registration = new CallbackRegistration(uid, () => this.registeredCallbacks.TryRemove(uid, out _));
|
||||
this.registeredCallbacks[uid] = action;
|
||||
return registration;
|
||||
}
|
||||
|
||||
public List<HookState> GetHookStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new HookState
|
||||
{
|
||||
Hooked = this.leaveGameThreadHook.Hooked,
|
||||
Name = nameof(this.leaveGameThreadHook),
|
||||
TargetAddress = new PointerValue(this.leaveGameThreadHook.TargetAddress),
|
||||
ContinueAddress = new PointerValue(this.leaveGameThreadHook.ContinueAddress),
|
||||
DetourAddress = new PointerValue(this.leaveGameThreadHook.DetourAddress)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
scopedLogger.LogInformation("Initializing hooks");
|
||||
while (!this.leaveGameThreadHook.EnsureInitialized())
|
||||
{
|
||||
scopedLogger.LogDebug("Waiting for hook to initialize: {hook}", nameof(this.leaveGameThreadHook));
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Hooks initialized");
|
||||
}
|
||||
|
||||
private void OnLeaveGameThread(nint ctx)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
while(this.queuedItems.TryDequeue(out var item))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (item.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
item.Cancel();
|
||||
continue;
|
||||
}
|
||||
|
||||
item.Execute();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "Error executing action on game thread");
|
||||
item.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach(var callback in this.registeredCallbacks)
|
||||
{
|
||||
try
|
||||
{
|
||||
callback.Value();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "Error executing registered callback {uid}", callback.Key);
|
||||
}
|
||||
}
|
||||
|
||||
this.leaveGameThreadHook.Continue(ctx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Daybreak.API.Models;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public interface IAddressHealthService
|
||||
{
|
||||
List<AddressState> GetAddressStates();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Daybreak.API.Models;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public interface IHookHealthService
|
||||
{
|
||||
List<HookState> GetHookStates();
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed unsafe class InstanceContextService : IAddressHealthService
|
||||
{
|
||||
private const string InstanceInfoAddressMask = "xxxx????xxxx";
|
||||
private const int InstanceInfoAddressOffset = +0xD;
|
||||
private static readonly byte[] InstanceInfoAddressPattern = [0x6A, 0x2C, 0x50, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x08, 0xC7];
|
||||
private const string AreaInfoAddressMask = "xxxxx";
|
||||
private const int AreaInfoAddressOffset = +0x5;
|
||||
private static readonly byte[] AreaInfoAddressPattern = [0x6B, 0xC6, 0x7C, 0x5E, 0x05];
|
||||
private const string ServerRegionAddressMask = "xxxxxxx";
|
||||
private const int ServerRegionAddressOffset = -0x4;
|
||||
private static readonly byte[] ServerRegionAddressPattern = [0x6A, 0x54, 0x8D, 0x46, 0x24, 0x89, 0x08];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache instanceInfoAddress;
|
||||
private readonly GWAddressCache areaInfoAddress;
|
||||
private readonly GWAddressCache serverRegionAddress;
|
||||
private readonly ILogger<InstanceContextService> logger;
|
||||
|
||||
public InstanceContextService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<InstanceContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.instanceInfoAddress = new GWAddressCache(() => this.memoryScanningService.FindAndResolveAddress(InstanceInfoAddressPattern, InstanceInfoAddressMask, InstanceInfoAddressOffset));
|
||||
this.areaInfoAddress = new GWAddressCache(() => this.memoryScanningService.FindAndResolveAddress(AreaInfoAddressPattern, AreaInfoAddressMask, AreaInfoAddressOffset));
|
||||
this.serverRegionAddress = new GWAddressCache(() => this.memoryScanningService.FindAndResolveAddress(ServerRegionAddressPattern, ServerRegionAddressMask, ServerRegionAddressOffset));
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return [
|
||||
new AddressState
|
||||
{
|
||||
Address = this.instanceInfoAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.instanceInfoAddress),
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
Address = this.areaInfoAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.areaInfoAddress),
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
Address = this.serverRegionAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.serverRegionAddress),
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public WrappedPointer<InstanceInfoContext> GetInstanceInfoContext()
|
||||
{
|
||||
var instanceInfoAddress = this.instanceInfoAddress.GetAddress();
|
||||
if (instanceInfoAddress is null)
|
||||
{
|
||||
this.logger.LogError("Failed to get instance info address");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (InstanceInfoContext*)instanceInfoAddress;
|
||||
}
|
||||
|
||||
public WrappedPointer<AreaInfo> GetAreaInfo()
|
||||
{
|
||||
var areaInfoAddress = this.areaInfoAddress.GetAddress();
|
||||
if (areaInfoAddress is null)
|
||||
{
|
||||
this.logger.LogError("Failed to get area info address");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (AreaInfo*)areaInfoAddress;
|
||||
}
|
||||
|
||||
public ServerRegion GetServerRegion()
|
||||
{
|
||||
var serverRegionAddress = this.serverRegionAddress.GetAddress();
|
||||
if (serverRegionAddress is null)
|
||||
{
|
||||
this.logger.LogError("Failed to get server region address");
|
||||
return ServerRegion.Unknown;
|
||||
}
|
||||
|
||||
return *(ServerRegion*)serverRegionAddress;
|
||||
}
|
||||
|
||||
public InstanceType GetInstanceType()
|
||||
{
|
||||
var instanceInfo = this.GetInstanceInfoContext();
|
||||
if (instanceInfo.IsNull)
|
||||
{
|
||||
return InstanceType.Loading;
|
||||
}
|
||||
|
||||
return instanceInfo.Pointer->InstanceType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
using Daybreak.Shared.Models.Interop;
|
||||
using PeNet;
|
||||
using PeNet.Header.Pe;
|
||||
using System.Core.Extensions;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Extensions.Core;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed unsafe class MemoryScanningService
|
||||
{
|
||||
private readonly ILogger<MemoryScanningService> logger;
|
||||
private readonly (nuint BaseAddress, ImageSectionHeader Section) textSection = GetSectionHeader(".text");
|
||||
private readonly (nuint BaseAddress, ImageSectionHeader Section) dataSection = GetSectionHeader(".rdata");
|
||||
|
||||
public MemoryScanningService(
|
||||
ILogger<MemoryScanningService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
}
|
||||
|
||||
public T? ReadPointer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(GuildwarsPointer<T> ptr)
|
||||
where T : struct
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
scopedLogger.LogInformation("Reading pointer: 0x{ptr:X8}", ptr.Address);
|
||||
if (ptr.Address is 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return Marshal.PtrToStructure<T>((nint)ptr.Address);
|
||||
}
|
||||
|
||||
public nuint FunctionFromNearCall(nuint callInstructionAddress, bool checkValidPtr = true)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (callInstructionAddress is 0)
|
||||
{
|
||||
scopedLogger.LogError("Invalid call instruction address: 0x{address:X8}", callInstructionAddress);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var opcode = Marshal.ReadByte((nint)callInstructionAddress);
|
||||
var functionAddress = opcode switch
|
||||
{
|
||||
0xE8 => (nuint)((nint)callInstructionAddress + 5 + Marshal.ReadInt32((nint)(callInstructionAddress + 1))),
|
||||
0xE9 => (nuint)((nint)callInstructionAddress + 5 + Marshal.ReadInt32((nint)(callInstructionAddress + 1))),
|
||||
0xEB => (nuint)((nint)callInstructionAddress + 2 + (sbyte)Marshal.ReadByte((nint)(callInstructionAddress + 1))),
|
||||
_ => (nuint)0
|
||||
};
|
||||
|
||||
if (checkValidPtr && functionAddress is 0)
|
||||
{
|
||||
scopedLogger.LogError("Invalid function address: 0x{address:X8}", functionAddress);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Recursively resolve nested JMPs
|
||||
var nestedCall = this.FunctionFromNearCall(functionAddress, checkValidPtr);
|
||||
if (nestedCall != 0)
|
||||
{
|
||||
scopedLogger.LogInformation("Resolved nested call to: 0x{address:X8}", nestedCall);
|
||||
return nestedCall;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Resolved function address: 0x{address:X8}", functionAddress);
|
||||
return functionAddress;
|
||||
}
|
||||
|
||||
public nuint FindAssertion(
|
||||
string assertionFile,
|
||||
string assertionMessage,
|
||||
uint lineNumber = 0,
|
||||
int offset = 0)
|
||||
{
|
||||
Span<byte> pattern =
|
||||
[
|
||||
0x68, 0,0,0,0, // push <line> / will be overwritten later
|
||||
0xBA, 0,0,0,0, // mov edx,<file> / will be overwritten later
|
||||
0xB9, 0,0,0,0 // mov ecx,<msg > / will be overwritten later
|
||||
];
|
||||
|
||||
const string maskAll = "xxxxxxxxxxxxxxx"; // 15 × 'x'
|
||||
|
||||
var msgBytes = Encoding.ASCII.GetBytes(assertionMessage + '\0');
|
||||
var msgMask = new string('x', msgBytes.Length);
|
||||
|
||||
var fileOrigBytes = Encoding.ASCII.GetBytes(assertionFile + '\0');
|
||||
var fileOrigMask = new string('x', fileOrigBytes.Length);
|
||||
|
||||
var (rdataBase, rdataHdr) = this.dataSection;
|
||||
var rdataStart = rdataBase + rdataHdr.VirtualAddress;
|
||||
var rdataEnd = rdataStart + rdataHdr.VirtualSize;
|
||||
|
||||
nuint msgSearchPos = rdataStart;
|
||||
while (true)
|
||||
{
|
||||
var msgPtr = FindInRange(msgBytes, msgMask, 0, msgSearchPos, rdataEnd);
|
||||
if (msgPtr is 0)
|
||||
{
|
||||
break; // no more messages → we are done
|
||||
}
|
||||
|
||||
msgSearchPos = msgPtr + 1; // next search starts just after the hit
|
||||
|
||||
// write ECX operand (little-endian) into the pattern
|
||||
BitConverter.TryWriteBytes(pattern.Slice(11, 4), (uint)msgPtr);
|
||||
|
||||
var fileSearchPos = rdataStart;
|
||||
while (true)
|
||||
{
|
||||
var filePtr = FindInRange(fileOrigBytes, fileOrigMask, 0, fileSearchPos, rdataEnd);
|
||||
|
||||
// fallbacks: lower-case and CamelCase, exactly like the C++
|
||||
if (filePtr == 0)
|
||||
{
|
||||
var lower = assertionFile.ToLowerInvariant();
|
||||
var lowerBytes = Encoding.ASCII.GetBytes(lower + '\0');
|
||||
filePtr = FindInRange(lowerBytes, new string('x', lowerBytes.Length), 0, fileSearchPos, rdataEnd);
|
||||
}
|
||||
|
||||
if (filePtr == 0)
|
||||
{
|
||||
var camel = ToCamelCase(assertionFile);
|
||||
var camelBytes = Encoding.ASCII.GetBytes(camel + '\0');
|
||||
filePtr = FindInRange(camelBytes, new string('x', camelBytes.Length), 0, fileSearchPos, rdataEnd);
|
||||
}
|
||||
|
||||
if (filePtr == 0)
|
||||
{
|
||||
break; // try next message
|
||||
}
|
||||
|
||||
fileSearchPos = filePtr + 1;
|
||||
|
||||
// If what we found is “…/file.hpp” (no drive letter) fix it
|
||||
if (Marshal.ReadByte((nint)(filePtr + 1)) != (byte)':')
|
||||
{
|
||||
// look ≤128 bytes backward for ':' and back-up one char
|
||||
nuint colon = FindInRange([(byte)':'], "x", -1, filePtr, filePtr - 128);
|
||||
if (colon == 0)
|
||||
{
|
||||
continue; // failed → try next file hit
|
||||
}
|
||||
|
||||
filePtr = colon;
|
||||
}
|
||||
|
||||
// write EDX operand
|
||||
BitConverter.TryWriteBytes(pattern.Slice(6, 4), (uint)filePtr);
|
||||
|
||||
nuint hit = 0;
|
||||
if (lineNumber != 0 && (lineNumber & 0xff) == lineNumber)
|
||||
{
|
||||
// PUSH imm8 variant – start matching at pattern[3]
|
||||
pattern[3] = 0x6A; // opcode
|
||||
pattern[4] = (byte)lineNumber;
|
||||
hit = FindAddressInternal(
|
||||
this.textSection,
|
||||
pattern[3..].ToArray(),
|
||||
maskAll[3..],
|
||||
offset);
|
||||
}
|
||||
|
||||
if (hit == 0 && lineNumber != 0)
|
||||
{
|
||||
// PUSH imm32 variant – whole pattern
|
||||
pattern[0] = 0x68;
|
||||
BitConverter.TryWriteBytes(pattern.Slice(1, 4), lineNumber);
|
||||
hit = FindAddressInternal(
|
||||
this.textSection,
|
||||
pattern.ToArray(),
|
||||
maskAll,
|
||||
offset);
|
||||
}
|
||||
|
||||
if (hit == 0 && lineNumber == 0)
|
||||
{
|
||||
// No line number – match only from MOV EDX onward
|
||||
hit = FindAddressInternal(
|
||||
this.textSection,
|
||||
pattern[5..].ToArray(),
|
||||
maskAll[5..],
|
||||
offset);
|
||||
}
|
||||
|
||||
if (hit != 0)
|
||||
{
|
||||
return hit; // found exactly what we wanted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // nothing matched
|
||||
}
|
||||
|
||||
public nuint FindAndResolveAddress(byte[] pattern, string mask, int offset = 0)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var address = FindAddressInternal(this.textSection, pattern, mask, offset);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find address");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var ptr = (nuint)Marshal.ReadIntPtr((nint)address);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
public nuint FindAddress(byte[] pattern, string mask, int offset = 0)
|
||||
{
|
||||
var address = FindAddressInternal(this.textSection, pattern, mask, offset);
|
||||
return address;
|
||||
}
|
||||
|
||||
public nuint ToFunctionStart(nuint callInstructionAddress, uint scanRange = 0x500)
|
||||
{
|
||||
if (callInstructionAddress == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// pattern: 55 8B EC – mask: "xxx"
|
||||
ReadOnlySpan<byte> prologue = [0x55, 0x8B, 0xEC];
|
||||
const string mask = "xxx";
|
||||
|
||||
var start = callInstructionAddress; // begin at the CALL itself
|
||||
var end = callInstructionAddress - scanRange; // scan backwards
|
||||
|
||||
return FindInRange(prologue, mask, 0, start, end);
|
||||
}
|
||||
|
||||
private static bool IsValidPtr(nuint address, (nuint BaseAddress, ImageSectionHeader Section) section)
|
||||
{
|
||||
return address > 0 &&
|
||||
(ulong)address > section.Section.ImageBaseAddress &&
|
||||
(ulong)address < (section.Section.ImageBaseAddress + section.Section.VirtualSize);
|
||||
}
|
||||
|
||||
private static unsafe nuint FindInRange(
|
||||
ReadOnlySpan<byte> pattern,
|
||||
string mask,
|
||||
int offset,
|
||||
nuint start,
|
||||
nuint end)
|
||||
{
|
||||
bool forward = start < end;
|
||||
int patLength = pattern.Length;
|
||||
|
||||
if (forward)
|
||||
{
|
||||
end -= (uint)patLength; // forward scan ⇒ clamp tail
|
||||
}
|
||||
|
||||
var cur = start;
|
||||
while (forward ? cur <= end : cur >= end)
|
||||
{
|
||||
if (Marshal.ReadByte((nint)cur) == pattern[0])
|
||||
{
|
||||
var matched = true;
|
||||
for (int i = 1; i < patLength && matched; ++i)
|
||||
{
|
||||
if (mask[i] == 'x' &&
|
||||
Marshal.ReadByte((nint)(cur + (uint)i)) != pattern[i])
|
||||
{
|
||||
matched = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched)
|
||||
{
|
||||
return (nuint)((long)cur + offset);
|
||||
}
|
||||
}
|
||||
|
||||
cur = forward ? cur + 1 : cur - 1;
|
||||
if (!forward && cur == 0) // prevent unsigned wrap-around
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string ToCamelCase(string text)
|
||||
=> CultureInfo.InvariantCulture.TextInfo.ToTitleCase(text.ToLowerInvariant());
|
||||
|
||||
private static nuint FindAddressInternal((nuint BaseAddress, ImageSectionHeader Section) section, byte[] pattern, string mask, int offset = 0)
|
||||
{
|
||||
var textStart = section.BaseAddress + section.Section.VirtualAddress;
|
||||
var textEnd = textStart + section.Section.VirtualSize - (uint)pattern.Length;
|
||||
|
||||
for (var cur = textStart; cur <= textEnd; ++cur)
|
||||
{
|
||||
if (Marshal.ReadByte((nint)cur) != pattern[0])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var match = true;
|
||||
for (var i = 1; i < pattern.Length && match; ++i)
|
||||
{
|
||||
if (mask[i] is 'x' && Marshal.ReadByte((nint)(cur + (uint)i)) != pattern[i])
|
||||
{
|
||||
match = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (match)
|
||||
{
|
||||
return (nuint)((nint)cur + offset);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static (nuint, ImageSectionHeader) GetSectionHeader(string headerName)
|
||||
{
|
||||
var m = Process.GetCurrentProcess().MainModule ?? throw new InvalidOperationException("Failed to initialize memory scanner. Failed to find main module");
|
||||
var baseAddr = (nuint)m.BaseAddress;
|
||||
var pe = new PeFile(m.FileName);
|
||||
var textHdr = pe.ImageSectionHeaders?.FirstOrDefault(h => h.Name == headerName);
|
||||
return textHdr is null
|
||||
? throw new InvalidOperationException($"Failed to initialize memory scanner. Failed to find {headerName} section")
|
||||
: ((nuint, ImageSectionHeader))(baseAddr, textHdr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class PartyContextService
|
||||
: IAddressHealthService
|
||||
{
|
||||
private const string SearchButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtSearch.cpp";
|
||||
private const string SearchButtonAssertion = "m_activeList == LIST_HEROES";
|
||||
private const string WindowButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtButtons.cpp";
|
||||
private const string WindowButtonAssertion = "m_selection.agentId";
|
||||
|
||||
// Fastcall funcs do not work in .NET for x86 right now. https://github.com/dotnet/runtime/issues/113851
|
||||
// This fastcall equivalent is <void*, uint, uint*, void>
|
||||
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> searchButtonCallback;
|
||||
// This fastcall equivalent is <void*, uint, uint*, void>
|
||||
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> windowButtonCallback;
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly ILogger<PartyContextService> logger;
|
||||
|
||||
public PartyContextService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<PartyContextService> logger)
|
||||
{
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.searchButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(SearchButtonFile, SearchButtonAssertion, 0, 0))));
|
||||
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(WindowButtonFile, WindowButtonAssertion, 0, 0))));
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
return
|
||||
[
|
||||
new AddressState
|
||||
{
|
||||
Address = this.searchButtonCallback.Cache.GetAddress() ?? 0,
|
||||
Name = nameof(this.searchButtonCallback)
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
Address = this.windowButtonCallback.Cache.GetAddress() ?? 0,
|
||||
Name = nameof(this.windowButtonCallback)
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, uint* wparam)
|
||||
{
|
||||
this.searchButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, uint* wparam)
|
||||
{
|
||||
this.windowButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool AddHero(uint heroId)
|
||||
{
|
||||
var wparam = stackalloc uint[4];
|
||||
wparam[2] = 0x6;
|
||||
wparam[1] = 0x1;
|
||||
|
||||
var ctx = stackalloc uint[13];
|
||||
ctx[0xb] = 1;
|
||||
ctx[9] = heroId;
|
||||
return this.CallSearchButtonCallback(ctx, 2, wparam);
|
||||
}
|
||||
|
||||
public unsafe bool KickHero(uint heroId)
|
||||
{
|
||||
var wparam = stackalloc uint[4];
|
||||
wparam[2] = 0x6;
|
||||
wparam[1] = 0x6;
|
||||
|
||||
var ctx = stackalloc uint[13];
|
||||
ctx[0xb] = 1;
|
||||
ctx[9] = heroId;
|
||||
return this.CallSearchButtonCallback(ctx, 0, wparam);
|
||||
}
|
||||
|
||||
public bool KickAllHeroes() => this.KickHero(0x26);
|
||||
|
||||
public unsafe bool LeaveParty()
|
||||
{
|
||||
var ctx = stackalloc uint[14];
|
||||
ctx[0xd] = 1;
|
||||
return this.CallWindowButtonCallback(ctx, 0, (uint*)0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class PlatformContextService
|
||||
: IAddressHealthService
|
||||
{
|
||||
private const string WindowHandleAddressMask = "xxxxx????xxx";
|
||||
private const int WindowHandleAddressOffset = -0xC;
|
||||
private static readonly byte[] WindowHandleAddressPattern = [0x83, 0xC4, 0x04, 0x83, 0x3D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x31];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache windowHandleAddress;
|
||||
private readonly ILogger<PlatformContextService> logger;
|
||||
|
||||
public PlatformContextService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<PlatformContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.windowHandleAddress = new GWAddressCache(this.GetWindowHandleAddress);
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return [
|
||||
new AddressState
|
||||
{
|
||||
Address = this.windowHandleAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.windowHandleAddress),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public unsafe uint? GetWindowHandle()
|
||||
{
|
||||
var windowHandlePtr = this.windowHandleAddress.GetAddress();
|
||||
if (windowHandlePtr is null)
|
||||
{
|
||||
this.logger.LogError("Failed to get window handle");
|
||||
return null;
|
||||
}
|
||||
|
||||
return *(uint*)windowHandlePtr;
|
||||
}
|
||||
|
||||
private nuint GetWindowHandleAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var baseAddress = this.memoryScanningService.FindAndResolveAddress(WindowHandleAddressPattern, WindowHandleAddressMask, WindowHandleAddressOffset);
|
||||
if (baseAddress is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find window handle address");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Window handle address: 0x{address:X8}", baseAddress);
|
||||
return baseAddress;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class SkillbarContextService
|
||||
: IAddressHealthService
|
||||
{
|
||||
private const string LoadSkillTemplateFile = "TemplatesHelpers.cpp";
|
||||
private const string LoadSkillTemplateAssertion = "targetPrimaryProf == templateData.profPrimary";
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate void LoadSkillTemplate(uint agentId, SkillTemplate* template);
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWDelegateCache<LoadSkillTemplate> loadSkillTemplateFunc;
|
||||
private readonly ILogger<SkillbarContextService> logger;
|
||||
|
||||
public SkillbarContextService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<SkillbarContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
unsafe
|
||||
{
|
||||
this.loadSkillTemplateFunc = new GWDelegateCache<LoadSkillTemplate>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
this.memoryScanningService.FindAssertion(LoadSkillTemplateFile, LoadSkillTemplateAssertion, 0, 0), 0x200)));
|
||||
}
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new()
|
||||
{
|
||||
Address = this.loadSkillTemplateFunc.Cache.GetAddress() ?? 0U,
|
||||
Name = nameof(this.loadSkillTemplateFunc)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public unsafe void LoadBuild(uint agentId, SkillTemplate* template)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (this.loadSkillTemplateFunc.GetDelegate() is not LoadSkillTemplate func)
|
||||
{
|
||||
scopedLogger.LogError("Load skill template delegate is not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
func(agentId, template);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class UIContextService
|
||||
: IAddressHealthService, IHostedService, IHookHealthService
|
||||
{
|
||||
private const string GetChildFrameIdFile = "CtlView.cpp";
|
||||
private const string GetChildFrameIdAssertion = "pageId";
|
||||
private const int GetChildFrameIdOffset = 0x19;
|
||||
private const string SetFrameFlagFile = "FrApi.cpp";
|
||||
private const string SetFrameFlagAssertion = "frameId";
|
||||
private const int SetFrameVisibleOffset = 0x4f9;
|
||||
private const int SetFrameDisabledOffset = 0x4e3;
|
||||
private static readonly byte[] SendFrameUiMessageByIdSeq = [0x83, 0xFB, 0x47, 0x73, 0x14];
|
||||
private const string SendFrameUiMessageByIdMask = "xxxxx";
|
||||
private const int SendFrameUiMessageByIdOffset = -0x34;
|
||||
private static readonly byte[] SendUIMessageSeq = [0xB9, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0x89, 0x45, 0x08];
|
||||
private const string SendUIMessageMask = "x????x????xxxxx";
|
||||
private const string CreateHashFromStringMask = "xxxxxxx";
|
||||
private const int CreateHashFromStringOffset = 0x7;
|
||||
private static readonly byte[] CreateHashFromStringSeq = [0x85, 0xC0, 0x74, 0x0D, 0x6A, 0xFF, 0x50];
|
||||
private const string FrameArrayFile = "\\Code\\Engine\\Frame\\FrMsg.cpp";
|
||||
private const string FrameArrayAssertion = "frame";
|
||||
private static readonly byte[] GetRootFrameSeq = [0x05, 0xE0, 0xFE, 0xFF, 0xFF, 0xC3];
|
||||
private const string GetRootFrameMask = "xxxxxx";
|
||||
private const int GetRootFrameOffset = -0x3C;
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate uint CreateHashFromStringFunc(char* value, int seed);
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SendUIMessageFunc(UIMessage uiMessage, nuint wParam, nuint lParam);
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SetFrameFlagFunc(uint frameId, uint flag);
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate uint GetChildFrameIdFunc(uint parentFrameId, uint childOffset);
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate Frame* GetRootFrameFunc();
|
||||
|
||||
private readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWDelegateCache<CreateHashFromStringFunc> createHashFromString;
|
||||
private readonly GWDelegateCache<GetChildFrameIdFunc> getChildFrameId;
|
||||
private readonly GWAddressCache frameArrayAddressCache;
|
||||
private readonly GWHook<SendUIMessageFunc> sendUiMessageHook;
|
||||
private readonly GWDelegateCache<GetRootFrameFunc> getRootFrame;
|
||||
// This fastcall equivalent is <GuildWarsArray<FrameInteractionCallback>*, void*, UIMessage, void*, void*>
|
||||
private readonly GWFastCall<nint, nint, UIMessage, nint, nint, GWFastCall.Void> sendFrameUIMessage;
|
||||
private readonly ILogger<UIContextService> logger;
|
||||
|
||||
private CancellationTokenSource? cts = default;
|
||||
|
||||
public UIContextService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<UIContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.createHashFromString = new GWDelegateCache<CreateHashFromStringFunc>(new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(CreateHashFromStringSeq, CreateHashFromStringMask, CreateHashFromStringOffset))));
|
||||
this.frameArrayAddressCache = new GWAddressCache(() => this.memoryScanningService.FindAssertion(FrameArrayFile, FrameArrayAssertion, 0, -0x14));
|
||||
this.sendUiMessageHook = new GWHook<SendUIMessageFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
this.memoryScanningService.FindAddress(SendUIMessageSeq, SendUIMessageMask))),
|
||||
this.OnSendUIMessage);
|
||||
this.sendFrameUIMessage = new(
|
||||
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(SendFrameUiMessageByIdSeq, SendFrameUiMessageByIdMask, SendFrameUiMessageByIdOffset) + 0x67)));
|
||||
this.getChildFrameId = new GWDelegateCache<GetChildFrameIdFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset))));
|
||||
this.getRootFrame = new GWDelegateCache<GetRootFrameFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, GetRootFrameOffset)));
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.createHashFromString),
|
||||
Address = this.createHashFromString.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.frameArrayAddressCache),
|
||||
Address = this.frameArrayAddressCache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getChildFrameId),
|
||||
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.sendFrameUIMessage),
|
||||
Address = this.sendFrameUIMessage.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getRootFrame),
|
||||
Address = this.getRootFrame.Cache.GetAddress() ?? 0U
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public List<HookState> GetHookStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new HookState
|
||||
{
|
||||
Hooked = this.sendUiMessageHook.Hooked,
|
||||
Name = nameof(this.sendUiMessageHook),
|
||||
TargetAddress = this.sendUiMessageHook.TargetAddress,
|
||||
ContinueAddress = this.sendUiMessageHook.ContinueAddress,
|
||||
DetourAddress = this.sendUiMessageHook.DetourAddress
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.cts?.Dispose();
|
||||
this.cts = new CancellationTokenSource();
|
||||
return Task.Factory.StartNew(() => this.InitializeHooks(this.cts.Token), this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.cts?.Cancel();
|
||||
this.cts?.Dispose();
|
||||
this.sendUiMessageHook.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<Frame> GetChildFrame(WrappedPointer<Frame> parent, uint childOffset)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (parent.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Parent frame is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
var getChildFrameId = this.getChildFrameId.GetDelegate();
|
||||
if (getChildFrameId is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get GetChildFrameId delegate");
|
||||
return null;
|
||||
}
|
||||
|
||||
var childFrameId = getChildFrameId(parent.Pointer->FrameId, childOffset);
|
||||
if (childFrameId == 0)
|
||||
{
|
||||
scopedLogger.LogError("No child frame found for parent frame {parent}", parent.Pointer->FrameId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.GetFrameById(childFrameId);
|
||||
}
|
||||
|
||||
public WrappedPointer<Frame> GetButtonActionFrame()
|
||||
{
|
||||
return this.GetChildFrame(this.GetFrameByLabel("Game"), 6);
|
||||
}
|
||||
|
||||
public unsafe bool SendFrameUIMessage(WrappedPointer<Frame> frame, UIMessage messageId, void* arg1, void* arg2 = null)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (frame.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Frame is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.sendFrameUIMessage.Invoke((nint)(&frame.Pointer->FrameCallbacks), 0, messageId, (nint)arg1, (nint)arg2);
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool SetFrameDisabled(WrappedPointer<Frame> frame, bool disabled)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (frame.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Frame is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
SetFrameDisabledInternal(frame, disabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool SetFrameVisible(WrappedPointer<Frame> frame, bool visible)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (frame.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Frame is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
SetFrameVisibleInternal(frame, visible);
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool KeyDown(UIAction action, WrappedPointer<Frame> frame)
|
||||
{
|
||||
if (frame.IsNull)
|
||||
{
|
||||
frame = this.GetButtonActionFrame();
|
||||
}
|
||||
|
||||
this.semaphoreSlim.Wait();
|
||||
var packet = new UIPackets.KeyAction((uint)action);
|
||||
this.SendFrameUIMessage(frame, UIMessage.KeyDown, &packet);
|
||||
this.semaphoreSlim.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool KeyUp(UIAction action, WrappedPointer<Frame> frame)
|
||||
{
|
||||
if (frame.IsNull)
|
||||
{
|
||||
frame = this.GetButtonActionFrame();
|
||||
}
|
||||
|
||||
this.semaphoreSlim.Wait();
|
||||
var packet = new UIPackets.KeyAction((uint)action);
|
||||
this.SendFrameUIMessage(frame, UIMessage.KeyUp, &packet);
|
||||
this.semaphoreSlim.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<GuildWarsArray<WrappedPointer<Frame>>> GetFrameArray()
|
||||
{
|
||||
if (this.frameArrayAddressCache.GetAddress() is not nuint address)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return *(GuildWarsArray<WrappedPointer<Frame>>**)address;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<Frame> GetFrameByLabel(string label)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var frameArray = this.GetFrameArray();
|
||||
if (frameArray.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Frame array is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
var hash = this.CreateHashFromString(label, -1);
|
||||
if (hash is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get hash for label {label}", label);
|
||||
return null;
|
||||
}
|
||||
|
||||
for(var i = 0; i < frameArray.Pointer->Size; i++)
|
||||
{
|
||||
var frame = frameArray.Pointer->Buffer[i];
|
||||
if (frame.Pointer is null ||
|
||||
(int)frame.Pointer == -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frame.Pointer->Relation.FrameHashId == hash)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
scopedLogger.LogWarning("Frame with label {label} not found", label);
|
||||
return null;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<Frame> GetFrameById(uint frameId)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var frameArray = this.GetFrameArray();
|
||||
if (frameArray.IsNull ||
|
||||
frameArray.Pointer->Size <= frameId)
|
||||
{
|
||||
scopedLogger.LogError("Frame array is null or frameId {frameId} is out of bounds", frameId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var frame = frameArray.Pointer->Skip((int)frameId).FirstOrDefault();
|
||||
if (frame.IsNull)
|
||||
{
|
||||
scopedLogger.LogWarning("Frame with id {frameId} not found", frameId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public unsafe uint CreateHashFromString(string value, int seed)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (this.createHashFromString.GetDelegate() is not CreateHashFromStringFunc del)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get CreateHashFromString delegate");
|
||||
return 0;
|
||||
}
|
||||
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
return del(valuePtr, seed);
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam)
|
||||
{
|
||||
this.semaphoreSlim.Wait();
|
||||
this.sendUiMessageHook.Continue(message, wParam, lParam);
|
||||
this.semaphoreSlim.Release();
|
||||
}
|
||||
|
||||
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
scopedLogger.LogInformation("Initializing hooks");
|
||||
while (!this.sendUiMessageHook.EnsureInitialized())
|
||||
{
|
||||
scopedLogger.LogDebug("Waiting for hook to initialize: {hook}", nameof(this.sendUiMessageHook));
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Hooks initialized");
|
||||
}
|
||||
|
||||
private void OnSendUIMessage(UIMessage uiMessage, nuint wParam, nuint lParam)
|
||||
{
|
||||
this.sendUiMessageHook.Continue(uiMessage, wParam, lParam);
|
||||
}
|
||||
|
||||
private static unsafe void SetFrameVisibleInternal(WrappedPointer<Frame> frame, bool visible)
|
||||
{
|
||||
uint* pState = &frame.Pointer->Field91; // direct field address
|
||||
if (visible)
|
||||
{
|
||||
*pState &= ~0x200u; // clear “hidden”
|
||||
}
|
||||
else
|
||||
{
|
||||
*pState |= 0x200u; // set “hidden”
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static unsafe void SetFrameDisabledInternal(WrappedPointer<Frame> frame, bool disabled)
|
||||
{
|
||||
uint* pState = &frame.Pointer->Field91; // same word (offset 0x184)
|
||||
|
||||
if (disabled)
|
||||
{
|
||||
*pState |= 0x10u; // set “disabled”
|
||||
}
|
||||
else
|
||||
{
|
||||
*pState &= ~0x10u; // clear
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
using Daybreak.API.Extensions;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
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.Services.BuildTemplates;
|
||||
using MemoryPack;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ZLinq;
|
||||
using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class MainPlayerService : IDisposable
|
||||
{
|
||||
private readonly InstanceContextService instanceContextService;
|
||||
private readonly IBuildTemplateManager buildTemplateManager;
|
||||
private readonly SkillbarContextService skillbarContextService;
|
||||
private readonly AgentContextService agentContextService;
|
||||
private readonly CallbackRegistration callbackRegistration;
|
||||
private readonly GameContextService gameContextService;
|
||||
private readonly GameThreadService gameThreadService;
|
||||
private readonly ILogger<MainPlayerService> logger;
|
||||
|
||||
private readonly ConcurrentQueue<TaskCompletionSource<MainPlayerState>> pendingRequests = new();
|
||||
private readonly ConcurrentDictionary<Guid, ByteConsumerEntry> consumers = new();
|
||||
private readonly ArrayBufferWriter<byte> bufferWriter = new();
|
||||
|
||||
private TimeSpan minUpdateFrequency = TimeSpan.MaxValue;
|
||||
private MainPlayerState? mainPlayerState;
|
||||
private DateTimeOffset lastUpdateTime = DateTimeOffset.MinValue;
|
||||
private DateTimeOffset lastFrequencyUpdate = DateTimeOffset.MinValue;
|
||||
|
||||
public MainPlayerService(
|
||||
InstanceContextService instanceContextService,
|
||||
IBuildTemplateManager buildTemplateManager,
|
||||
SkillbarContextService skillbarContextService,
|
||||
GameContextService gameContextService,
|
||||
AgentContextService agentContextService,
|
||||
GameThreadService gameThreadService,
|
||||
ILogger<MainPlayerService> logger)
|
||||
{
|
||||
this.instanceContextService = instanceContextService.ThrowIfNull();
|
||||
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
|
||||
this.skillbarContextService = skillbarContextService.ThrowIfNull();
|
||||
this.gameThreadService = gameThreadService.ThrowIfNull();
|
||||
this.agentContextService = agentContextService.ThrowIfNull();
|
||||
this.gameContextService = gameContextService.ThrowIfNull();
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.callbackRegistration = this.gameThreadService.RegisterCallback(this.OnGameThreadProc);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.callbackRegistration?.Dispose();
|
||||
}
|
||||
|
||||
public Task<bool> SetCurrentBuild(string buildCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (!this.buildTemplateManager.TryDecodeTemplate(buildCode, out var build) ||
|
||||
build is not SingleBuildEntry singleBuild)
|
||||
{
|
||||
scopedLogger.LogError("Failed to decode build template from code {buildCode}", buildCode);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading or InstanceType.Explorable)
|
||||
{
|
||||
scopedLogger.LogError("Not in outpost");
|
||||
return default;
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game context");
|
||||
return false;
|
||||
}
|
||||
|
||||
var playerAgentId = this.agentContextService.GetPlayerAgentId();
|
||||
if (playerAgentId is 0x0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get player agent id");
|
||||
return false;
|
||||
}
|
||||
|
||||
var playerAgent = this.GetAgentContext(playerAgentId);
|
||||
if (playerAgent is null ||
|
||||
playerAgent->Type is not AgentType.Living ||
|
||||
playerAgent->AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Player agent {playerAgentId} not found in agent array", playerAgentId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var livingAgent = (AgentLivingContext*)playerAgent;
|
||||
if (livingAgent->Level != gameContext.Pointer->WorldContext->Level)
|
||||
{
|
||||
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->WorldContext->Level);
|
||||
return false;
|
||||
}
|
||||
|
||||
var agentProfession = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
if (agentProfession.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var validationRequest = new BuildTemplateValidationRequest(
|
||||
(uint)singleBuild.Primary.Id,
|
||||
(uint)singleBuild.Secondary.Id,
|
||||
singleBuild.Skills.AsValueEnumerable().Select(s => (uint)s.Id).ToArray(),
|
||||
livingAgent->Primary,
|
||||
agentProfession.UnlockedProfessionsFlags,
|
||||
[.. gameContext.Pointer->WorldContext->UnlockedCharacterSkills]);
|
||||
|
||||
if (!this.buildTemplateManager.CanTemplateApply(validationRequest))
|
||||
{
|
||||
scopedLogger.LogError("Build template validation failed for player agent id {agentId}", playerAgentId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var attributeIds = new Array12Uint();
|
||||
var attributeValues = new Array12Uint();
|
||||
var skills = new Array8Uint();
|
||||
for(var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
|
||||
{
|
||||
if (singleBuild.Attributes[i].Attribute is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
attributeIds[i] = (uint)singleBuild.Attributes[i].Attribute!.Id;
|
||||
attributeValues[i] = (uint)singleBuild.Attributes[i].Points;
|
||||
}
|
||||
|
||||
for(var i =0; i < singleBuild.Skills.Count && i < 8; i++)
|
||||
{
|
||||
skills[i] = (uint)singleBuild.Skills[i].Id;
|
||||
}
|
||||
|
||||
var skillTemplate = new SkillTemplate(
|
||||
(uint)singleBuild.Primary.Id,
|
||||
(uint)singleBuild.Secondary.Id,
|
||||
(uint)Math.Min(singleBuild.Attributes.Count, 12),
|
||||
attributeIds,
|
||||
attributeValues,
|
||||
skills);
|
||||
|
||||
this.skillbarContextService.LoadBuild(playerAgentId, &skillTemplate);
|
||||
return true;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<BuildEntry?> GetCurrentBuild(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
|
||||
{
|
||||
scopedLogger.LogError("Not loaded");
|
||||
return default;
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game context");
|
||||
return default;
|
||||
}
|
||||
|
||||
var playerAgentId = this.agentContextService.GetPlayerAgentId();
|
||||
if (playerAgentId is 0x0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get player agent id");
|
||||
return default;
|
||||
}
|
||||
|
||||
var skillbarContext = gameContext.Pointer->WorldContext->Skillbars.AsValueEnumerable().FirstOrDefault(s => s.AgentId == playerAgentId);
|
||||
if (skillbarContext.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find skillbar context for player agent id {agentId}", playerAgentId);
|
||||
return default;
|
||||
}
|
||||
|
||||
var agentAttributes = gameContext.Pointer->WorldContext->Attributes.AsValueEnumerable().FirstOrDefault(a => a.AgentId == playerAgentId);
|
||||
if (agentAttributes.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent attributes for player agent id {agentId}", playerAgentId);
|
||||
return default;
|
||||
}
|
||||
|
||||
var professionContext = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
if (professionContext.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
|
||||
return default;
|
||||
}
|
||||
|
||||
return new BuildEntry(
|
||||
(int)professionContext.CurrentPrimary,
|
||||
(int)professionContext.CurrentSecondary,
|
||||
agentAttributes.Attributes.GetAttributeEntryList(),
|
||||
[skillbarContext.Skill0.Id, skillbarContext.Skill1.Id, skillbarContext.Skill2.Id, skillbarContext.Skill3.Id,
|
||||
skillbarContext.Skill4.Id, skillbarContext.Skill5.Id, skillbarContext.Skill6.Id, skillbarContext.Skill7.Id]);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<QuestLogInformation?> GetQuestLog(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
|
||||
{
|
||||
scopedLogger.LogError("Not loaded");
|
||||
return default;
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull || gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
return new QuestLogInformation(
|
||||
gameContext.Pointer->WorldContext->ActiveQuestId,
|
||||
gameContext.Pointer->WorldContext->QuestLog.AsValueEnumerable().Select(q => new QuestInformation(q.QuestId, q.MapFrom, q.MapTo)).ToList());
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<MainPlayerInformation?> GetMainPlayerInformation(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
|
||||
{
|
||||
scopedLogger.LogError("Not loaded");
|
||||
return default;
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull || gameContext.Pointer->CharContext is null ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var playerNameSpan = gameContext.Pointer->CharContext->PlayerName.AsSpan();
|
||||
var playerName = new string(playerNameSpan[..playerNameSpan.IndexOf('\0')]);
|
||||
var emailSpan = gameContext.Pointer->CharContext->PlayerEmail.AsSpan();
|
||||
var email = new string(emailSpan[..emailSpan.IndexOf('\0')]);
|
||||
var accountName = new string(gameContext.Pointer->WorldContext->AccountInfo->AccountName);
|
||||
return new MainPlayerInformation(
|
||||
gameContext.Pointer->CharContext->PlayerUuid.ToString(),
|
||||
email,
|
||||
playerName,
|
||||
accountName,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Wins,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Losses,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Rating,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->QualifierPoints,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Rank,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->TournamentRewardPoints);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<MainPlayerState> GetMainPlayerState(CancellationToken cancellationToken)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<MainPlayerState>();
|
||||
if (cancellationToken.CanBeCanceled)
|
||||
{
|
||||
cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken));
|
||||
}
|
||||
|
||||
this.pendingRequests.Enqueue(tcs);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public Task<InstanceInfo> GetMainPlayerInstance(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var instanceType = this.instanceContextService.GetInstanceType();
|
||||
if (instanceType is InstanceType.Loading)
|
||||
{
|
||||
return new InstanceInfo(0, 0, 0, 0, Shared.Models.Api.InstanceType.Loading, DistrictRegionInfo.Unknown, LanguageInfo.Unknown, CampaignInfo.Unknown, ContinentInfo.Unknown, RegionInfo.Unknown, DifficultyInfo.Unknown);
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
var instanceInfoContext = this.instanceContextService.GetInstanceInfoContext();
|
||||
var serverRegion = this.instanceContextService.GetServerRegion();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->CharContext is null ||
|
||||
gameContext.Pointer->WorldContext is null ||
|
||||
gameContext.Pointer->PartyContext is null ||
|
||||
instanceInfoContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return new InstanceInfo(0, 0, 0, 0, Shared.Models.Api.InstanceType.Loading, DistrictRegionInfo.Unknown, LanguageInfo.Unknown, CampaignInfo.Unknown, ContinentInfo.Unknown, RegionInfo.Unknown, DifficultyInfo.Unknown);
|
||||
}
|
||||
|
||||
var charContext = *gameContext.Pointer->CharContext;
|
||||
var worldContext = *gameContext.Pointer->WorldContext;
|
||||
var partyContext = *gameContext.Pointer->PartyContext;
|
||||
var instanceInfo = *instanceInfoContext.Pointer;
|
||||
var mapId = charContext.MapId;
|
||||
var language = charContext.Language;
|
||||
var districtNumber = charContext.DistrictNumber;
|
||||
var foesKilled = worldContext.FoesKilled;
|
||||
var foesToKill = worldContext.FoesToKill;
|
||||
|
||||
return new InstanceInfo(
|
||||
MapId: charContext.MapId,
|
||||
DistrictNumber: charContext.DistrictNumber,
|
||||
FoesKilled: worldContext.FoesKilled,
|
||||
FoesToKill: worldContext.FoesToKill,
|
||||
Type: (Shared.Models.Api.InstanceType)instanceType,
|
||||
DistrictRegion: (DistrictRegionInfo)serverRegion,
|
||||
Language: (LanguageInfo)charContext.Language,
|
||||
Campaign: (CampaignInfo)instanceInfo.CurrentMapInfo->Campaign,
|
||||
Continent: (ContinentInfo)instanceInfo.CurrentMapInfo->Continent,
|
||||
Region: (RegionInfo)instanceInfo.CurrentMapInfo->Region,
|
||||
Difficulty: partyContext.Flags.HasFlag(PartyFlags.HardMode) ? DifficultyInfo.Hard : DifficultyInfo.Normal);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<TitleInfo?> GetTitleInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
|
||||
{
|
||||
scopedLogger.LogError("Not loaded");
|
||||
return default;
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull || gameContext.Pointer->WorldContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
if (!gameContext.TryGetPlayerId(out var playerId))
|
||||
{
|
||||
scopedLogger.LogError("Failed to get player id");
|
||||
return default;
|
||||
}
|
||||
|
||||
var player = gameContext.Pointer->WorldContext->Players.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerId);
|
||||
if (player.AgentId != playerId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find player with id {playerId}", playerId);
|
||||
return default;
|
||||
}
|
||||
|
||||
var currentTier = player.ActiveTitleTier;
|
||||
TitleContext? currentTitle = default;
|
||||
int? id = -1;
|
||||
for(var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
|
||||
{
|
||||
var title = gameContext.Pointer->WorldContext->Titles.Skip(i).FirstOrDefault();
|
||||
if (title.CurrentTitleTierIndex == currentTier)
|
||||
{
|
||||
currentTitle = title;
|
||||
id = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentTitle.HasValue)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find current title with tier {currentTier}", currentTier);
|
||||
return default;
|
||||
}
|
||||
|
||||
var titleTier = gameContext.Pointer->WorldContext->TitleTiers.Skip((int)currentTitle.Value.CurrentTitleTierIndex).FirstOrDefault();
|
||||
return new TitleInfo
|
||||
(
|
||||
Id: (uint)id,
|
||||
IsPercentage: currentTitle.Value.Props.HasFlag(TitleProps.PercentageBased),
|
||||
PointsForCurrentRank: currentTitle.Value.PointsNeededForCurrentRank,
|
||||
PointsForNextRank: titleTier.TierNumber == currentTitle.Value.MaxTitleRank ?
|
||||
currentTitle.Value.CurrentPoints :
|
||||
currentTitle.Value.PointsNeededForNextRank,
|
||||
CurrentPoints: currentTitle.Value.CurrentPoints,
|
||||
TierNumber: titleTier.TierNumber,
|
||||
MaxTierNumber: currentTitle.Value.MaxTitleTierIndex
|
||||
);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public CallbackRegistration RegisterMainStateConsumer(TimeSpan frequency, Action<ReadOnlySpan<byte>> onUpdate)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(frequency, TimeSpan.Zero);
|
||||
|
||||
var id = Guid.NewGuid();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var entry = new ByteConsumerEntry(id, frequency, onUpdate);
|
||||
|
||||
this.consumers[id] = entry;
|
||||
this.RecalculateMinFrequency();
|
||||
|
||||
return new CallbackRegistration(id, () =>
|
||||
{
|
||||
this.consumers.TryRemove(id, out _);
|
||||
this.RecalculateMinFrequency();
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void OnGameThreadProc()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
/*
|
||||
* Periodically adjust the frequency of updates based on connected clients.
|
||||
* Sometimes clients crash and disconnect in a way that the server wasn't able
|
||||
* to trigger a frequency recalculation, so this periodic recalculation should
|
||||
* take care of inconsistent state.
|
||||
*/
|
||||
if (now - this.lastFrequencyUpdate > TimeSpan.FromSeconds(10))
|
||||
{
|
||||
this.RecalculateMinFrequency();
|
||||
}
|
||||
|
||||
/*
|
||||
* Only rebuild the state when we do not have one yet or the shortest required frequency has elapsed.
|
||||
* If we have any pending request (excluding consumers), we should also rebuild the state
|
||||
* to return the latest data to the request
|
||||
*/
|
||||
if (this.mainPlayerState is not null &&
|
||||
this.minUpdateFrequency is TimeSpan minFreq &&
|
||||
now - this.lastUpdateTime < minFreq &&
|
||||
this.pendingRequests.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
|
||||
{
|
||||
scopedLogger.LogError("Not loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
var agentArray = this.agentContextService.GetAgentArray();
|
||||
var playerAgentId = this.agentContextService.GetPlayerAgentId();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null ||
|
||||
gameContext.Pointer->CharContext is null ||
|
||||
gameContext.Pointer->WorldContext->MapAgents.Buffer is null ||
|
||||
agentArray.IsNull ||
|
||||
agentArray.Pointer->Buffer is null ||
|
||||
playerAgentId is 0x0)
|
||||
{
|
||||
scopedLogger.LogDebug("Game data is not yet initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
var playerMapAgent = gameContext.Pointer->WorldContext->MapAgents.AsValueEnumerable().Skip((int)playerAgentId).FirstOrDefault();
|
||||
var playerAgent = this.GetAgentContext(playerAgentId);
|
||||
if (playerAgent is null ||
|
||||
playerAgent->Type is not AgentType.Living ||
|
||||
playerAgent->AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Player agent {playerAgentId} not found in agent array", playerAgentId);
|
||||
return;
|
||||
}
|
||||
|
||||
var livingAgent = (AgentLivingContext*)playerAgent;
|
||||
if (livingAgent->Level != gameContext.Pointer->WorldContext->Level)
|
||||
{
|
||||
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->WorldContext->Level);
|
||||
return;
|
||||
}
|
||||
|
||||
this.mainPlayerState = new MainPlayerState(
|
||||
gameContext.Pointer->WorldContext->Experience,
|
||||
gameContext.Pointer->WorldContext->Level,
|
||||
|
||||
gameContext.Pointer->WorldContext->CurrentLuxon,
|
||||
gameContext.Pointer->WorldContext->CurrentKurzick,
|
||||
gameContext.Pointer->WorldContext->CurrentImperial,
|
||||
gameContext.Pointer->WorldContext->CurrentBalthazar,
|
||||
gameContext.Pointer->WorldContext->MaxLuxon,
|
||||
gameContext.Pointer->WorldContext->MaxKurzick,
|
||||
gameContext.Pointer->WorldContext->MaxImperial,
|
||||
gameContext.Pointer->WorldContext->MaxBalthazar,
|
||||
gameContext.Pointer->WorldContext->TotalLuxon,
|
||||
gameContext.Pointer->WorldContext->TotalKurzick,
|
||||
gameContext.Pointer->WorldContext->TotalImperial,
|
||||
gameContext.Pointer->WorldContext->TotalBalthazar,
|
||||
|
||||
// Energy and health are percentages of Max
|
||||
livingAgent->Health * livingAgent->MaxHealth,
|
||||
livingAgent->MaxHealth,
|
||||
livingAgent->Energy * livingAgent->MaxEnergy,
|
||||
livingAgent->MaxEnergy,
|
||||
|
||||
livingAgent->Primary,
|
||||
livingAgent->Secondary,
|
||||
|
||||
playerAgent->Pos.X,
|
||||
playerAgent->Pos.Y
|
||||
);
|
||||
|
||||
this.bufferWriter.ResetWrittenCount();
|
||||
MemoryPackSerializer.Serialize(this.bufferWriter, this.mainPlayerState);
|
||||
this.lastUpdateTime = now;
|
||||
|
||||
while (this.pendingRequests.TryDequeue(out var tcs))
|
||||
{
|
||||
tcs.TrySetResult(this.mainPlayerState);
|
||||
}
|
||||
|
||||
foreach (var kvp in this.consumers)
|
||||
{
|
||||
var entry = kvp.Value;
|
||||
entry.TryConsume(now, this.bufferWriter.WrittenSpan);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecalculateMinFrequency()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var noConsumers = this.consumers.IsEmpty;
|
||||
if (noConsumers)
|
||||
{
|
||||
this.minUpdateFrequency = TimeSpan.MaxValue;
|
||||
scopedLogger.LogDebug("No consumers registered, disabling updates");
|
||||
this.lastFrequencyUpdate = DateTimeOffset.UtcNow;
|
||||
return;
|
||||
}
|
||||
|
||||
var minValue = this.consumers.Values.Min(c => c.Frequency);
|
||||
scopedLogger.LogDebug("Adjusted update frequency to {frequency}", minValue);
|
||||
this.lastFrequencyUpdate = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe AgentContext* GetAgentContext(uint agentId)
|
||||
{
|
||||
var agentArray = this.agentContextService.GetAgentArray();
|
||||
if (agentArray.IsNull || agentArray.Pointer->Buffer is null ||
|
||||
agentArray.Pointer->Size <= agentId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return agentArray.Pointer->Buffer[agentId].Pointer;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user