mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
47369851f2
* Mod selection for each launch configuration (Closes #1225) * Store enabled mods with the launched application
247 lines
10 KiB
C#
247 lines
10 KiB
C#
using Daybreak.Configuration.Options;
|
|
using Daybreak.Shared.Models;
|
|
using Daybreak.Shared.Models.Async;
|
|
using Daybreak.Shared.Models.LaunchConfigurations;
|
|
using Daybreak.Shared.Models.Mods;
|
|
using Daybreak.Shared.Services.Api;
|
|
using Daybreak.Shared.Services.Injection;
|
|
using Daybreak.Shared.Services.MDns;
|
|
using Daybreak.Shared.Services.Notifications;
|
|
using Daybreak.Shared.Services.Options;
|
|
using Daybreak.Shared.Utils;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using System.Core.Extensions;
|
|
using System.Diagnostics;
|
|
using System.Extensions.Core;
|
|
|
|
namespace Daybreak.Services.Api;
|
|
|
|
public sealed class DaybreakApiService(
|
|
IOptionsProvider optionsProvider,
|
|
IAttachedApiAccessor attachedApiAccessor,
|
|
IMDomainRegistrar mDomainRegistrar,
|
|
IStubInjector stubInjector,
|
|
INotificationService notificationService,
|
|
IHttpClient<ScopedApiContext> scopedApiClient,
|
|
IOptionsMonitor<DaybreakApiOptions> daybreakApiOptions,
|
|
ILogger<DaybreakApiService> logger,
|
|
ILogger<ScopedApiContext> scopedApiLogger)
|
|
: IDaybreakApiService
|
|
{
|
|
private const string EntryPoint = "ThreadInit";
|
|
private const string LocalHost = "localhost";
|
|
private const string DaybreakApiName = "Api/Daybreak.API.dll";
|
|
private const string ProcessIdPlaceholder = "{PID}";
|
|
private const string DaybreakApiServiceName = $"daybreak-api-{ProcessIdPlaceholder}";
|
|
private const string ServiceSubType = "daybreak-api";
|
|
|
|
private readonly IOptionsProvider optionsProvider = optionsProvider.ThrowIfNull();
|
|
private readonly IAttachedApiAccessor attachedApiAccessor = attachedApiAccessor.ThrowIfNull();
|
|
private readonly IMDomainRegistrar mDomainRegistrar = mDomainRegistrar.ThrowIfNull();
|
|
private readonly IStubInjector stubInjector = stubInjector.ThrowIfNull();
|
|
private readonly INotificationService notificationService = notificationService.ThrowIfNull();
|
|
private readonly IHttpClient<ScopedApiContext> scopedApiClient = scopedApiClient.ThrowIfNull();
|
|
private readonly IOptionsMonitor<DaybreakApiOptions> daybreakApiOptions = daybreakApiOptions.ThrowIfNull();
|
|
private readonly ILogger<DaybreakApiService> logger = logger.ThrowIfNull();
|
|
private readonly ILogger<ScopedApiContext> scopedApiLogger = scopedApiLogger.ThrowIfNull();
|
|
|
|
public string Name { get; } = "Daybreak API";
|
|
|
|
public string Description { get; } = "Daybreak API integration with Guild Wars. Gets injected into Guild Wars to enable extended functionality such as loading builds, character switching, and more";
|
|
|
|
public bool IsEnabled
|
|
{
|
|
get => this.daybreakApiOptions.CurrentValue.Enabled;
|
|
set
|
|
{
|
|
var options = this.daybreakApiOptions.CurrentValue;
|
|
options.Enabled = value;
|
|
this.optionsProvider.SaveOption(options);
|
|
}
|
|
}
|
|
|
|
public bool IsInstalled => true;
|
|
public bool IsVisible => true;
|
|
public bool CanCustomManage => false;
|
|
public bool CanUninstall => false;
|
|
public bool CanDisable => true;
|
|
|
|
public IProgressAsyncOperation<bool> PerformUninstallation(CancellationToken cancellationToken)
|
|
{
|
|
throw new NotImplementedException("DaybreakApi mod does not support manual uninstallation");
|
|
}
|
|
|
|
public IEnumerable<string> GetCustomArguments() => [];
|
|
|
|
public Task<bool> IsUpdateAvailable(CancellationToken cancellationToken) => Task.FromResult(false);
|
|
|
|
public Task<bool> PerformUpdate(CancellationToken cancellationToken)
|
|
{
|
|
throw new NotImplementedException("DaybreakApi mod does not support manual updates");
|
|
}
|
|
|
|
public IProgressAsyncOperation<bool> PerformInstallation(CancellationToken cancellationToken)
|
|
{
|
|
throw new NotImplementedException("DaybreakApi mod does not support manual installation");
|
|
}
|
|
|
|
public Task OnCustomManagement(CancellationToken cancellationToken)
|
|
{
|
|
throw new NotImplementedException("DaybreakApi mod does not support custom management");
|
|
}
|
|
|
|
public Task<ScopedApiContext?> AttachDaybreakApiContext(GuildWarsApplicationLaunchContext launchContext, ScopedApiContext apiContext, CancellationToken _)
|
|
{
|
|
if (this.attachedApiAccessor is AttachedApiAccessor accessor)
|
|
{
|
|
accessor.LaunchContext = launchContext;
|
|
accessor.ApiContext = apiContext;
|
|
}
|
|
|
|
return Task.FromResult<ScopedApiContext?>(apiContext);
|
|
}
|
|
|
|
public async Task<ScopedApiContext?> AttachDaybreakApiContext(GuildWarsApplicationLaunchContext launchContext, CancellationToken cancellationToken)
|
|
{
|
|
var apiContext = await this.GetDaybreakApiContext(launchContext.GuildWarsProcess, cancellationToken);
|
|
if (apiContext is not null)
|
|
{
|
|
return await this.AttachDaybreakApiContext(launchContext, apiContext, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
return apiContext;
|
|
}
|
|
}
|
|
|
|
public async Task<ScopedApiContext?> GetDaybreakApiContext(Process guildWarsProcess, CancellationToken cancellationToken)
|
|
{
|
|
var scopedLogger = this.logger.CreateScopedLogger();
|
|
if (guildWarsProcess.HasExited)
|
|
{
|
|
scopedLogger.LogWarning("Guild Wars process has exited");
|
|
return default;
|
|
}
|
|
|
|
var serviceName = DaybreakApiServiceName.Replace(ProcessIdPlaceholder, guildWarsProcess.Id.ToString());
|
|
var serviceUris = this.mDomainRegistrar.Resolve(serviceName);
|
|
var serviceUri = serviceUris?.Count > 0 ? serviceUris[0] : default;
|
|
if (serviceUri is null)
|
|
{
|
|
scopedLogger.LogWarning("Failed to find Daybreak API service by name {serviceName}", serviceName);
|
|
return default;
|
|
}
|
|
|
|
var uriBuilder = new UriBuilder(serviceUri)
|
|
{
|
|
Host = LocalHost
|
|
};
|
|
var apiContext = new DaybreakAPIContext(uriBuilder.Uri);
|
|
var scopedApiContext = new ScopedApiContext(this.scopedApiLogger, this.scopedApiClient, apiContext);
|
|
if (await scopedApiContext.IsAvailable(cancellationToken))
|
|
{
|
|
return scopedApiContext;
|
|
}
|
|
|
|
return default;
|
|
}
|
|
|
|
public async Task<ScopedApiContext?> FindDaybreakApiContextByCredentials(LoginCredentials loginCredentials, CancellationToken cancellationToken)
|
|
{
|
|
var scopedLogger = this.logger.CreateScopedLogger();
|
|
var serviceUris = this.mDomainRegistrar.QueryByServiceName(n => n.StartsWith(ServiceSubType));
|
|
foreach(var uri in serviceUris ?? [])
|
|
{
|
|
var uriBuilder = new UriBuilder(uri)
|
|
{
|
|
Host = LocalHost
|
|
};
|
|
var apiContext = new DaybreakAPIContext(uriBuilder.Uri);
|
|
var scopedApiContext = new ScopedApiContext(this.scopedApiLogger, this.scopedApiClient, apiContext);
|
|
|
|
var response = await scopedApiContext.GetLoginInfo(cancellationToken);
|
|
if (loginCredentials.Username == response?.Email)
|
|
{
|
|
return scopedApiContext;
|
|
}
|
|
}
|
|
|
|
scopedLogger.LogWarning("Failed to find Daybreak API service by credentials for user {username}", loginCredentials?.Username ?? string.Empty);
|
|
return default;
|
|
}
|
|
|
|
public void RequestInstancesAnnouncement()
|
|
{
|
|
this.mDomainRegistrar.QueryAllServices();
|
|
}
|
|
|
|
public Task OnGuildWarsCreated(GuildWarsCreatedContext guildWarsCreatedContext, CancellationToken cancellationToken) =>
|
|
this.InjectWithStub(guildWarsCreatedContext.ApplicationLauncherContext, cancellationToken);
|
|
|
|
public Task OnGuildWarsStarted(GuildWarsStartedContext guildWarsStartedContext, CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
public Task OnGuildWarsStarting(GuildWarsStartingContext guildWarsStartingContext, CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
public Task OnGuildWarsStartingDisabled(GuildWarsStartingDisabledContext guildWarsStartingDisabledContext, CancellationToken cancellationToken) => Task.CompletedTask;
|
|
|
|
public Task<bool> ShouldRunAgain(GuildWarsRunningContext guildWarsRunningContext, CancellationToken cancellationToken)
|
|
{
|
|
if (!guildWarsRunningContext.LoadedModules.Contains(DaybreakApiName) &&
|
|
this.IsEnabled)
|
|
{
|
|
return Task.FromResult(true);
|
|
}
|
|
|
|
return Task.FromResult(false);
|
|
}
|
|
|
|
public Task OnGuildWarsRunning(GuildWarsRunningContext guildWarsRunningContext, CancellationToken cancellationToken)
|
|
=> this.InjectWithStub(guildWarsRunningContext.ApplicationLauncherContext, cancellationToken);
|
|
|
|
private async Task InjectWithStub(ApplicationLauncherContext context, CancellationToken cancellationToken)
|
|
{
|
|
var scopedLogger = this.logger.CreateScopedLogger();
|
|
var dllName =
|
|
Path.GetFullPath(
|
|
Path.Combine(
|
|
PathUtils.GetRootFolder(),
|
|
DaybreakApiName));
|
|
|
|
if (!File.Exists(dllName))
|
|
{
|
|
this.notificationService.NotifyError(
|
|
"Daybreak API Failure",
|
|
"Failed to inject with stub. Daybreak API dll not found",
|
|
expirationTime: DateTime.UtcNow + TimeSpan.FromSeconds(10));
|
|
scopedLogger.LogError("Failed to inject with stub. Daybreak API DLL not found at {dllPath}", dllName);
|
|
return;
|
|
}
|
|
|
|
scopedLogger.LogInformation("Injecting {dllName} into {processId}", dllName, context.Process.Id);
|
|
var result = await this.stubInjector.Inject(context.Process, dllName, EntryPoint, cancellationToken);
|
|
if (result < 0)
|
|
{
|
|
this.notificationService.NotifyError(
|
|
"Daybreak API Failure",
|
|
"Failed to inject with stub. Injection failed");
|
|
scopedLogger.LogError("Failed to inject with stub. Injection failed");
|
|
return;
|
|
}
|
|
|
|
var port = result;
|
|
if (port <= 0)
|
|
{
|
|
this.notificationService.NotifyError(
|
|
"Daybreak API Failure",
|
|
"Failed to start API. Non-success exit code");
|
|
scopedLogger.LogError($"Failed to start API. Exit code {port}");
|
|
return;
|
|
}
|
|
|
|
this.notificationService.NotifyInformation(
|
|
"Injected Daybreak API",
|
|
$"Daybreak API started on port {port}");
|
|
}
|
|
}
|