Updated Filestore. Finished Auth and Install Services. Introduced GameOptions and BaseConfig. Adjusted Settings Service to reflect.

This commit is contained in:
2026-04-06 11:41:07 +01:00
parent c148672c08
commit 7b66d8ceac
38 changed files with 2684 additions and 197 deletions

View File

@@ -1,7 +1,7 @@
namespace AlayaCore.Abstractions.Configuration namespace AlayaCore.Abstractions.Configuration
{ {
public class BaseConfig public abstract class BaseConfig
{ {
public abstract string FileName { get; }
} }
} }

View File

@@ -1,7 +1,12 @@
using AlayaCore.Utilities.Enums;
namespace AlayaCore.Abstractions.Interfaces namespace AlayaCore.Abstractions.Interfaces
{ {
public interface IFileStore public interface IFileStore
{ {
string Get(FolderLocation location);
string GetOrCreate(FolderLocation location);
string Combine(FolderLocation location, params string[] paths);
bool Exists(FolderLocation location);
} }
} }

View File

@@ -1,7 +1,10 @@
using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Models; using AlayaCore.Models;
using AlayaCore.States; using AlayaCore.States;
using CmlLib.Core;
using CmlLib.Core.Installers;
namespace AlayaCore.Abstractions.Interfaces namespace AlayaCore.Abstractions.Interfaces
{ {
@@ -9,7 +12,11 @@ namespace AlayaCore.Abstractions.Interfaces
{ {
Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default); Task<LaunchPlan> EvaluateAsync(CancellationToken cancellationToken = default);
Task InstallOrUpdateAsync(CancellationToken cancellationToken = default); Task InstallOrUpdateAsync(CancellationToken cancellationToken = default,
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgess = null,
EventHandler<ByteProgress>? byteProgress = null,
IProgress<InstallerProgressChangedEventArgs>? neoForgeProgress = null,
IProgress<ByteProgress>? neoForgeByteProgress = null);
Task LaunchAsync(CancellationToken cancellationToken = default); Task LaunchAsync(CancellationToken cancellationToken = default);

View File

@@ -1,7 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using CmlLib.Core.Auth;
namespace AlayaCore.Abstractions.Interfaces.Services namespace AlayaCore.Abstractions.Interfaces.Services
{ {
public interface IAuthService public interface IAuthService
{ {
Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default);
Task AuthenticateAsync(CancellationToken cancellationToken = default);
Task SignOutAsync(CancellationToken cancellationToken = default);
Task<MSession> GetSessionAsync(CancellationToken cancellationToken = default);
} }
} }

View File

@@ -1,13 +1,31 @@
using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Installation; using AlayaCore.Installation;
using AlayaCore.Models.Manifests; using AlayaCore.Models.Manifests;
using CmlLib.Core;
using CmlLib.Core.Installers;
namespace AlayaCore.Abstractions.Interfaces.Services namespace AlayaCore.Abstractions.Interfaces.Services
{ {
public interface IMinecraftService public interface IGameInstallService
{ {
Task EnsureMinecraftInstalledAsync(ManifestModel manifest, InstallEnvironment environment, CancellationToken cancellationToken); Task EnsureMinecraftInstalledAsync(
Task EnsureNeoForgeInstalledAsync(ManifestModel manifest, InstallEnvironment environment, CancellationToken cancellationToken); ManifestModel manifest,
InstallEnvironment environment,
CancellationToken cancellationToken = default,
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgess = null,
EventHandler<ByteProgress>? byteProgress = null);
Task EnsureNeoForgeInstalledAsync(
ManifestModel manifest,
InstallEnvironment environment,
CancellationToken cancellationToken = default,
IProgress<InstallerProgressChangedEventArgs>? progress = null,
IProgress<ByteProgress>? byteProgress = null);
Task VerifyFilesAsync(
ManifestModel manifest,
CancellationToken cancellationToken = default);
} }
} }

View File

@@ -7,11 +7,18 @@ namespace AlayaCore.Abstractions.Interfaces.Services
public interface ISettingsService public interface ISettingsService
{ {
LauncherOptions LauncherOptions { get; } LauncherOptions LauncherOptions { get; }
GameOptions GameOptions { get; }
Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default); Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default);
Task UpdateLaunchVersionAsync(string newVersion, CancellationToken cancellationToken = default);
Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default); Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default);
Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default); Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default);
Task SaveGameOptionsAsync(CancellationToken cancellationToken = default);
Task LoadGameOptionsAsync(CancellationToken cancellationToken = default);
Task LoadAllAsync(CancellationToken cancellationToken = default);
Task SaveAllAsync(CancellationToken cancellationToken = default);
} }
} }

View File

@@ -7,7 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CmlLib.Core" Version="4.0.6" />
<PackageReference Include="CmlLib.Core.Auth.Microsoft" Version="3.3.1" />
<PackageReference Include="CmlLib.Core.Installer.NeoForge" Version="4.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="XboxAuthNet.Game.Msal" Version="0.1.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -9,6 +9,9 @@ using AlayaCore.Installation;
using AlayaCore.Models.Configuration; using AlayaCore.Models.Configuration;
using AlayaCore.Models.Manifests; using AlayaCore.Models.Manifests;
using AlayaCore.States; using AlayaCore.States;
using AlayaCore.Utilities.Enums;
using CmlLib.Core;
using CmlLib.Core.Installers;
namespace AlayaCore namespace AlayaCore
{ {
@@ -20,12 +23,13 @@ namespace AlayaCore
private readonly IJavaService _javaService; private readonly IJavaService _javaService;
private readonly IModService _modService; private readonly IModService _modService;
private readonly IGameLaunchService _gameLaunchService; private readonly IGameLaunchService _gameLaunchService;
private readonly IGameInstallService _gameInstallService;
private readonly ISettingsService _settingsService;
private readonly IAuthService _authService;
private readonly LauncherOptions _options; private readonly LauncherOptions _options;
public bool CanRun { get; private set; } public bool CanRun { get; private set; }
public bool NeedsUpdating { get; private set; } public bool NeedsUpdating { get; private set; }
public LaunchPlan? CurrentPlan { get; private set; } public LaunchPlan? CurrentPlan { get; private set; }
public LaunchDirector( public LaunchDirector(
@@ -35,6 +39,9 @@ namespace AlayaCore
IJavaService javaService, IJavaService javaService,
IModService modService, IModService modService,
IGameLaunchService gameLaunchService, IGameLaunchService gameLaunchService,
IGameInstallService gameInstallService,
ISettingsService settingsService,
IAuthService authService,
LauncherOptions options) LauncherOptions options)
{ {
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService)); _manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
@@ -43,6 +50,9 @@ namespace AlayaCore
_javaService = javaService ?? throw new ArgumentNullException(nameof(javaService)); _javaService = javaService ?? throw new ArgumentNullException(nameof(javaService));
_modService = modService ?? throw new ArgumentNullException(nameof(modService)); _modService = modService ?? throw new ArgumentNullException(nameof(modService));
_gameLaunchService = gameLaunchService ?? throw new ArgumentNullException(nameof(gameLaunchService)); _gameLaunchService = gameLaunchService ?? throw new ArgumentNullException(nameof(gameLaunchService));
_gameInstallService = gameInstallService ?? throw new ArgumentNullException(nameof(gameInstallService));
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
_options = options ?? throw new ArgumentNullException(nameof(options)); _options = options ?? throw new ArgumentNullException(nameof(options));
} }
@@ -61,7 +71,8 @@ namespace AlayaCore
javaNeedsInstallOrUpdate: false, javaNeedsInstallOrUpdate: false,
minecraftNeedsInstallOrUpdate: false, minecraftNeedsInstallOrUpdate: false,
neoforgeNeedsInstallOrUpdate: false, neoforgeNeedsInstallOrUpdate: false,
modsNeedSync: false); modsNeedSync: false,
needAuthenticating: false);
ApplyPlan(launcherUpdatePlan); ApplyPlan(launcherUpdatePlan);
return launcherUpdatePlan; return launcherUpdatePlan;
@@ -78,6 +89,10 @@ namespace AlayaCore
!environment.JavaInstalled || !environment.JavaInstalled ||
!string.Equals(environment.JavaVersion, manifest.RequiredJavaVersion, StringComparison.OrdinalIgnoreCase); !string.Equals(environment.JavaVersion, manifest.RequiredJavaVersion, StringComparison.OrdinalIgnoreCase);
bool needAuthenticating = !await _authService
.IsAuthenticatedAsync(cancellationToken)
.ConfigureAwait(false);
bool minecraftNeedsInstallOrUpdate = bool minecraftNeedsInstallOrUpdate =
_options.ForceReinstall || _options.ForceReinstall ||
!environment.MinecraftInstalled || !environment.MinecraftInstalled ||
@@ -97,13 +112,18 @@ namespace AlayaCore
javaNeedsInstallOrUpdate: javaNeedsInstallOrUpdate, javaNeedsInstallOrUpdate: javaNeedsInstallOrUpdate,
minecraftNeedsInstallOrUpdate: minecraftNeedsInstallOrUpdate, minecraftNeedsInstallOrUpdate: minecraftNeedsInstallOrUpdate,
neoforgeNeedsInstallOrUpdate: neoforgeNeedsInstallOrUpdate, neoforgeNeedsInstallOrUpdate: neoforgeNeedsInstallOrUpdate,
modsNeedSync: modsNeedSync); modsNeedSync: modsNeedSync,
needAuthenticating: needAuthenticating);
ApplyPlan(plan); ApplyPlan(plan);
return plan; return plan;
} }
public async Task InstallOrUpdateAsync(CancellationToken cancellationToken = default) public async Task InstallOrUpdateAsync(CancellationToken cancellationToken = default,
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgess = null,
EventHandler<ByteProgress>? byteProgress = null,
IProgress<InstallerProgressChangedEventArgs>? neoForgeProgress = null,
IProgress<ByteProgress>? neoForgeByteProgress = null)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -113,8 +133,8 @@ namespace AlayaCore
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
ManifestModel? manifest = null; ManifestModel? manifest;
InstallEnvironment? environment = null; InstallEnvironment? environment;
switch (plan.State) switch (plan.State)
{ {
@@ -135,7 +155,6 @@ namespace AlayaCore
case LaunchState.InstallJava: case LaunchState.InstallJava:
{ {
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false); manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
environment = await _installStateService environment = await _installStateService
.GetCurrentEnvironmentAsync(cancellationToken) .GetCurrentEnvironmentAsync(cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -147,20 +166,43 @@ namespace AlayaCore
break; break;
} }
case LaunchState.NeedAuthenticating:
{
await _authService.AuthenticateAsync(cancellationToken);
break;
}
case LaunchState.InstallMinecraft: case LaunchState.InstallMinecraft:
{ {
throw new NotImplementedException("Minecraft install/update flow has not been implemented yet."); manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
environment = await _installStateService
.GetCurrentEnvironmentAsync(cancellationToken)
.ConfigureAwait(false);
await _gameInstallService
.EnsureMinecraftInstalledAsync(manifest, environment, cancellationToken, minecraftProgess, byteProgress)
.ConfigureAwait(false);
break;
} }
case LaunchState.InstallNeoforge: case LaunchState.InstallNeoforge:
{ {
throw new NotImplementedException("NeoForge install/update flow has not been implemented yet."); manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
environment = await _installStateService
.GetCurrentEnvironmentAsync(cancellationToken)
.ConfigureAwait(false);
await _gameInstallService
.EnsureNeoForgeInstalledAsync(manifest, environment, cancellationToken, neoForgeProgress, neoForgeByteProgress)
.ConfigureAwait(false);
break;
} }
case LaunchState.SyncMods: case LaunchState.SyncMods:
{ {
manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false); manifest = await EnsureCurrentManifestAsync(cancellationToken).ConfigureAwait(false);
environment = await _installStateService environment = await _installStateService
.GetCurrentEnvironmentAsync(cancellationToken) .GetCurrentEnvironmentAsync(cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@@ -185,6 +227,18 @@ namespace AlayaCore
plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false); plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
} }
if (_options.ForceReinstall)
{
await _settingsService.SetForceReinstallAsync(false, cancellationToken).ConfigureAwait(false);
}
plan = await EvaluateAsync(cancellationToken).ConfigureAwait(false);
if (!plan.CanRun)
{
throw new InvalidOperationException("Install/update completed, but the launcher is still not in a runnable state.");
}
} }
public async Task LaunchAsync(CancellationToken cancellationToken = default) public async Task LaunchAsync(CancellationToken cancellationToken = default)
@@ -250,7 +304,7 @@ namespace AlayaCore
} }
var requiredMods = manifest.Files var requiredMods = manifest.Files
.Where(file => file.Type == AlayaCore.Utilities.Enums.FileType.Mod) .Where(file => file.Type == FileType.Mod)
.ToList(); .ToList();
var installedMods = environment.InstalledModsManifest.Mods; var installedMods = environment.InstalledModsManifest.Mods;

View File

@@ -1,7 +1,27 @@
using System;
using System.IO;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Utilities.Enums;
using CmlLib.Core;
namespace AlayaCore.Models namespace AlayaCore.Models
{ {
public class AlayaPath public class AlayaPath : MinecraftPath
{ {
public AlayaPath(IFileStore fileStore)
{
BasePath = NormalizePath(fileStore.Get(FolderLocation.Game));
Library = NormalizePath(BasePath + "/libraries");
Versions = NormalizePath(BasePath + "/versions");
Resource = NormalizePath(BasePath + "/resources");
Runtime = NormalizePath(fileStore.GetOrCreate(FolderLocation.JavaRuntime));
Assets = NormalizePath(BasePath + "/assets");
CreateDirs();
}
} }
} }

View File

@@ -1,7 +1,30 @@
using AlayaCore.Abstractions.Configuration;
namespace AlayaCore.Models.Configuration namespace AlayaCore.Models.Configuration
{ {
public class GameOptions public class GameOptions : BaseConfig
{ {
public override string FileName => "Game.json";
public string? LaunchVersion { get; set; }
public int MinimumRamMB { get; set; }
public int MaximumRamMB { get; set; }
public int ScreenWidth { get; set; }
public int ScreenHeight { get; set; }
public bool Fullscreen { get; set; }
public static GameOptions Default { get; } = new GameOptions
{
LaunchVersion = null,
MinimumRamMB = 1024,
MaximumRamMB = 2048,
ScreenWidth = 1920,
ScreenHeight = 1080,
Fullscreen = false,
};
} }
} }

View File

@@ -1,7 +1,16 @@
using AlayaCore.Abstractions.Configuration;
namespace AlayaCore.Models.Configuration namespace AlayaCore.Models.Configuration
{ {
public sealed class LauncherOptions public sealed class LauncherOptions : BaseConfig
{ {
public bool ForceReinstall { get; set; } public bool ForceReinstall { get; set; }
public override string FileName => "Launcher.json";
public static LauncherOptions Default { get; } = new LauncherOptions
{
ForceReinstall = false,
};
} }
} }

View File

@@ -23,16 +23,15 @@ namespace AlayaCore.Models.Manifests.DTO
[JsonProperty("minecraftVersion", Required = Required.Always)] [JsonProperty("minecraftVersion", Required = Required.Always)]
public string MinecraftVersion { get; set; } = string.Empty; public string MinecraftVersion { get; set; } = string.Empty;
[JsonProperty("minecraftUrl", Required = Required.Always)]
[JsonConverter(typeof(UriConverter))]
public Uri MinecraftUrl { get; set; } = null!;
[JsonProperty("neoforgedVersion", Required = Required.Always)] [JsonProperty("neoforgedVersion", Required = Required.Always)]
public string NeoforgedVersion { get; set; } = string.Empty; public string NeoforgedVersion { get; set; } = string.Empty;
[JsonProperty("neoforgedUrl", Required = Required.Always)] [JsonProperty("serverUrl", Required = Required.Always)]
[JsonConverter(typeof(UriConverter))] [JsonConverter(typeof(UriConverter))]
public Uri NeoforgedUrl { get; set; } = null!; public Uri ServerUrl { get; set; } = null!;
[JsonProperty("serverPort", Required = Required.Always)]
public int ServerPort { get; }
[JsonProperty("files", Required = Required.Always)] [JsonProperty("files", Required = Required.Always)]
public List<ModFileEntryDto> Files { get; set; } = new List<ModFileEntryDto>(); public List<ModFileEntryDto> Files { get; set; } = new List<ModFileEntryDto>();

View File

@@ -10,11 +10,12 @@ namespace AlayaCore.Models.Manifests
{ {
public Version AlayaVersion { get; } public Version AlayaVersion { get; }
public string RequiredJavaVersion { get; } public string RequiredJavaVersion { get; }
public Uri RequiredJavaUrl { get; } public Uri RequiredJavaBaseUrl { get; }
public string MinecraftVersion { get; } public string MinecraftVersion { get; }
public Uri MinecraftUrl { get; }
public string NeoforgedVersion { get; } public string NeoforgedVersion { get; }
public Uri NeoforgedUrl { get; }
public Uri ServerUrl { get; }
public int ServerPort { get; }
public IReadOnlyList<ModFileEntry> Files { get; } public IReadOnlyList<ModFileEntry> Files { get; }
public ManifestModel( public ManifestModel(
@@ -22,20 +23,18 @@ namespace AlayaCore.Models.Manifests
string requiredJavaVersion, string requiredJavaVersion,
Uri requiredJavaUrl, Uri requiredJavaUrl,
string minecraftVersion, string minecraftVersion,
Uri minecraftUrl,
string neoforgedVersion, string neoforgedVersion,
Uri neoforgedUrl, Uri serverUrl,
int serverPort,
IEnumerable<ModFileEntry> files) IEnumerable<ModFileEntry> files)
{ {
AlayaVersion = alayaVersion ?? throw new ArgumentNullException(nameof(alayaVersion)); AlayaVersion = alayaVersion ?? throw new ArgumentNullException(nameof(alayaVersion));
RequiredJavaVersion = RequireNonEmpty(requiredJavaVersion, nameof(requiredJavaVersion)); RequiredJavaVersion = RequireNonEmpty(requiredJavaVersion, nameof(requiredJavaVersion));
RequiredJavaUrl = requiredJavaUrl ?? throw new ArgumentNullException(nameof(requiredJavaUrl)); RequiredJavaBaseUrl = requiredJavaUrl ?? throw new ArgumentNullException(nameof(requiredJavaUrl));
MinecraftVersion = RequireNonEmpty(minecraftVersion, nameof(minecraftVersion)); MinecraftVersion = RequireNonEmpty(minecraftVersion, nameof(minecraftVersion));
MinecraftUrl = minecraftUrl ?? throw new ArgumentNullException(nameof(minecraftUrl));
NeoforgedVersion = RequireNonEmpty(neoforgedVersion, nameof(neoforgedVersion)); NeoforgedVersion = RequireNonEmpty(neoforgedVersion, nameof(neoforgedVersion));
NeoforgedUrl = neoforgedUrl ?? throw new ArgumentNullException(nameof(neoforgedUrl));
if (files == null) if (files == null)
{ {

View File

@@ -1,7 +1,113 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Utilities.Enums;
using CmlLib.Core.Auth;
using CmlLib.Core.Auth.Microsoft;
using Microsoft.Identity.Client;
using XboxAuthNet.Game.Msal;
using XboxAuthNet.Game.Msal.OAuth;
namespace AlayaCore.Services namespace AlayaCore.Services
{ {
public class AuthService public sealed class AuthService : IAuthService
{ {
private readonly IFileStore _fileStore;
private MSession? _session;
private IPublicClientApplication? _clientApp;
private JELoginHandler? _loginHandler;
public AuthService(IFileStore fileStore)
{
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
}
public Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
bool authenticated = _session != null && _session.CheckIsValid();
return Task.FromResult(authenticated);
}
public async Task AuthenticateAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
try
{
_session = await loginHandler
.AuthenticateSilently(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
catch
{
_session = await loginHandler
.AuthenticateInteractively(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
if (_session == null || !_session.CheckIsValid())
{
throw new InvalidOperationException("Authentication did not produce a valid Minecraft session.");
}
}
public async Task SignOutAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
JELoginHandler loginHandler = await BuildHandlerAsync().ConfigureAwait(false);
await loginHandler.Signout(cancellationToken).ConfigureAwait(false);
_session = null;
}
public async Task<MSession> GetSessionAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (_session != null && _session.CheckIsValid())
{
return _session;
}
await AuthenticateAsync(cancellationToken).ConfigureAwait(false);
if (_session == null || !_session.CheckIsValid())
{
throw new InvalidOperationException("No valid Minecraft session is available.");
}
return _session;
}
private async Task<JELoginHandler> BuildHandlerAsync()
{
if (_loginHandler != null)
{
return _loginHandler;
}
string accountDirectory = _fileStore.GetOrCreate(FolderLocation.Data);
string accountFilePath = Path.Combine(accountDirectory, "accounts.json");
_clientApp = await MsalClientHelper
.BuildApplicationWithCache("d91042d4-3eb5-43e4-b3ed-600e1d0760ff")
.ConfigureAwait(false);
_loginHandler = new JELoginHandlerBuilder()
.WithOAuthProvider(new MsalCodeFlowProvider(_clientApp))
.WithAccountManager(accountFilePath)
.Build();
return _loginHandler;
}
} }
} }

View File

@@ -1,7 +1,272 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Installation;
using AlayaCore.Models;
using AlayaCore.Models.Manifests;
using AlayaCore.Utilities.Enums;
using CmlLib.Core;
using CmlLib.Core.Installer.NeoForge;
using CmlLib.Core.Installer.NeoForge.Installers;
using CmlLib.Core.Installers;
using CmlLib.Core.Java;
using CmlLib.Core.VersionMetadata;
namespace AlayaCore.Services namespace AlayaCore.Services
{ {
public class GameInstallService public sealed class GameInstallService : IGameInstallService
{ {
private const string InstalledModsManifestFileName = "InstalledModsManifest.json";
private readonly IFileStore _fileStore;
private readonly ISettingsService _settingsService;
private AlayaPath? _gamePath;
private MinecraftLauncher? _minecraftLauncher;
public GameInstallService(
IFileStore fileStore,
ISettingsService settingsService)
{
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public async Task EnsureMinecraftInstalledAsync(
ManifestModel manifest,
InstallEnvironment environment,
CancellationToken cancellationToken = default,
EventHandler<InstallerProgressChangedEventArgs>? minecraftProgess = null,
EventHandler<ByteProgress>? byteProgress = null)
{
if (manifest == null)
{
throw new ArgumentNullException(nameof(manifest));
}
if (environment == null)
{
throw new ArgumentNullException(nameof(environment));
}
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
{
throw new InvalidDataException("Minecraft version is missing.");
}
bool alreadyInstalled =
environment.MinecraftInstalled &&
string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase);
if (alreadyInstalled)
{
return;
}
bool versionMismatch =
environment.MinecraftInstalled &&
!string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase);
if (versionMismatch)
{
await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false);
}
MinecraftLauncher launcher = GetOrCreateLauncher(minecraftProgess, byteProgress);
await launcher
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
.ConfigureAwait(false);
}
public async Task EnsureNeoForgeInstalledAsync(
ManifestModel manifest,
InstallEnvironment environment,
CancellationToken cancellationToken = default,
IProgress<InstallerProgressChangedEventArgs>? progress = null,
IProgress<ByteProgress>? byteProgress = null)
{
if (manifest == null)
{
throw new ArgumentNullException(nameof(manifest));
}
if (environment == null)
{
throw new ArgumentNullException(nameof(environment));
}
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
{
throw new InvalidDataException("Minecraft version is missing.");
}
if (string.IsNullOrWhiteSpace(manifest.NeoforgedVersion))
{
throw new InvalidDataException("NeoForge version is missing.");
}
bool alreadyInstalled =
environment.NeoforgedInstalled &&
string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase);
if (alreadyInstalled)
{
return;
}
if (!environment.MinecraftInstalled ||
!string.Equals(environment.MinecraftVersion, manifest.MinecraftVersion, StringComparison.OrdinalIgnoreCase))
{
await EnsureMinecraftInstalledAsync(manifest, environment, cancellationToken).ConfigureAwait(false);
}
if (string.IsNullOrWhiteSpace(environment.JavaPath))
{
throw new InvalidOperationException("A valid Java installation is required before installing NeoForge.");
}
MinecraftLauncher launcher = GetOrCreateLauncher();
await DownloadAndInstallNeoForgeAsync(
launcher,
manifest,
environment,
cancellationToken,
progress, byteProgress).ConfigureAwait(false);
await launcher
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
.ConfigureAwait(false);
}
public async Task VerifyFilesAsync(
ManifestModel manifest,
CancellationToken cancellationToken = default)
{
if (manifest == null)
{
throw new ArgumentNullException(nameof(manifest));
}
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(manifest.MinecraftVersion))
{
throw new InvalidDataException("Minecraft version is missing.");
}
MinecraftLauncher launcher = GetOrCreateLauncher();
await launcher
.InstallAsync(manifest.MinecraftVersion, cancellationToken)
.ConfigureAwait(false);
}
private async Task CleanOldInstallAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
string gamePath = GetMinecraftPath();
if (Directory.Exists(gamePath))
{
Directory.Delete(gamePath, recursive: true);
}
string installedModsManifestPath = Path.Combine(
_fileStore.Get(FolderLocation.Manifests),
InstalledModsManifestFileName);
if (File.Exists(installedModsManifestPath))
{
File.Delete(installedModsManifestPath);
}
_gamePath = null;
_minecraftLauncher = null;
await _settingsService.UpdateLaunchVersionAsync(string.Empty, cancellationToken).ConfigureAwait(false);
}
private MinecraftLauncher GetOrCreateLauncher(EventHandler<InstallerProgressChangedEventArgs>? minecraftProgess = null,
EventHandler<ByteProgress>? byteProgress = null)
{
if (_minecraftLauncher != null)
{
return _minecraftLauncher;
}
_gamePath = new AlayaPath(_fileStore);
_minecraftLauncher = new MinecraftLauncher(_gamePath);
if(byteProgress != null)
_minecraftLauncher.ByteProgressChanged += byteProgress;
if(minecraftProgess != null)
_minecraftLauncher.FileProgressChanged += minecraftProgess;
return _minecraftLauncher;
}
private async Task DownloadAndInstallNeoForgeAsync(
MinecraftLauncher launcher,
ManifestModel manifest,
InstallEnvironment environment,
CancellationToken cancellationToken,
IProgress<InstallerProgressChangedEventArgs>? progress = null,
IProgress<ByteProgress>? byteProgress = null)
{
if (launcher == null)
{
throw new ArgumentNullException(nameof(launcher));
}
bool neoForgeMismatch =
environment.NeoforgedInstalled &&
!string.Equals(environment.NeoforgedVersion, manifest.NeoforgedVersion, StringComparison.OrdinalIgnoreCase);
if (neoForgeMismatch)
{
await CleanOldInstallAsync(cancellationToken).ConfigureAwait(false);
return;
}
NeoForgeInstaller installer = new NeoForgeInstaller(launcher);
NeoForgeInstallOptions options = new NeoForgeInstallOptions
{
CancellationToken = cancellationToken,
JavaPath = environment.JavaPath,
SkipIfAlreadyInstalled = true
};
if(progress != null)
options.FileProgress = progress;
if(byteProgress != null)
options.ByteProgress = byteProgress;
string version = await installer
.Install(manifest.MinecraftVersion, manifest.NeoforgedVersion, options)
.ConfigureAwait(false);
await _settingsService
.UpdateLaunchVersionAsync(version, cancellationToken)
.ConfigureAwait(false);
}
private string GetMinecraftPath()
{
return _fileStore.GetOrCreate(FolderLocation.Game);
}
} }
} }

View File

@@ -1,7 +1,124 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Installation;
using AlayaCore.Models;
using AlayaCore.Models.Configuration;
using AlayaCore.Models.Manifests;
using AlayaCore.Utilities.Enums;
using CmlLib.Core;
using CmlLib.Core.Auth;
using CmlLib.Core.ProcessBuilder;
namespace AlayaCore.Services namespace AlayaCore.Services
{ {
public class GameLaunchService public sealed class GameLaunchService : IGameLaunchService
{ {
private readonly IAuthService _authService;
private readonly IFileStore _fileStore;
private readonly ILaunchDirector _director;
private readonly GameOptions _gameOptions;
private AlayaPath? _gamePath;
private MinecraftLauncher? _minecraftLauncher;
public GameLaunchService(
IAuthService authService,
IFileStore fileStore,
ILaunchDirector director,
GameOptions gameOptions)
{
_authService = authService ?? throw new ArgumentNullException(nameof(authService));
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
_director = director ?? throw new ArgumentNullException(nameof(director));
_gameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
}
public async Task LaunchAsync(
ManifestModel manifest,
InstallEnvironment environment,
CancellationToken cancellationToken = default)
{
if (!_director.CanRun)
{
throw new InvalidOperationException("The launcher is not in a runnable state.");
}
if (manifest == null)
{
throw new ArgumentNullException(nameof(manifest));
}
if (environment == null)
{
throw new ArgumentNullException(nameof(environment));
}
if (string.IsNullOrWhiteSpace(_gameOptions.LaunchVersion))
{
throw new InvalidDataException("GameOptions.LaunchVersion is not configured.");
}
if (string.IsNullOrWhiteSpace(environment.JavaPath))
{
throw new InvalidOperationException("A valid Java path is required to launch the game.");
}
cancellationToken.ThrowIfCancellationRequested();
MLaunchOption option = await BuildLaunchOptions(manifest, environment);
MinecraftLauncher launcher = GetOrCreateLauncher();
var process = await launcher
.CreateProcessAsync(_gameOptions.LaunchVersion, option)
.ConfigureAwait(false);
var processWrapper = new ProcessWrapper(process);
processWrapper.StartWithEvents();
await processWrapper.WaitForExitTaskAsync().ConfigureAwait(false);
}
private MinecraftLauncher GetOrCreateLauncher()
{
if (_minecraftLauncher != null)
{
return _minecraftLauncher;
}
_gamePath = new AlayaPath(_fileStore);
_minecraftLauncher = new MinecraftLauncher(_gamePath);
return _minecraftLauncher;
}
private string GetMinecraftPath()
{
return _fileStore.GetOrCreate(FolderLocation.Game);
}
private async Task<MLaunchOption> BuildLaunchOptions(ManifestModel manifest, InstallEnvironment environment)
{
if (manifest.ServerUrl == null)
{
throw new InvalidDataException("Manifest Server Url is not configured.");
}
return new MLaunchOption
{
Session = await _authService.GetSessionAsync(),
JavaPath = environment.JavaPath,
MinimumRamMb = _gameOptions.MinimumRamMB,
MaximumRamMb = _gameOptions.MaximumRamMB,
ScreenWidth = _gameOptions.ScreenWidth,
ScreenHeight = _gameOptions.ScreenHeight,
ServerIp = manifest.ServerUrl.Host,
ServerPort = manifest.ServerPort,
DockName = "AlayaCraft"
};
}
} }
} }

View File

@@ -12,13 +12,13 @@ using AlayaCore.Models.Results;
namespace AlayaCore.Services namespace AlayaCore.Services
{ {
public sealed class DownloadService : IDownloadService public sealed class HttpDownloadService : IDownloadService
{ {
private const int BUFFER_SIZE = 81920; private const int BUFFER_SIZE = 81920;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
public DownloadService(IHttpClient httpClient) public HttpDownloadService(IHttpClient httpClient)
{ {
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
} }

View File

@@ -1,24 +1,32 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Installation; using AlayaCore.Installation;
using AlayaCore.Models.Manifests; using AlayaCore.Models.Manifests;
using AlayaCore.Utilities.Enums;
using Newtonsoft.Json.Linq;
namespace AlayaCore.Services namespace AlayaCore.Services
{ {
public sealed class InstallationStateService : IInstallStateService public sealed class InstallationStateService : IInstallStateService
{ {
private const string JAVA_RUNTIME_FOLDER_NAME = "java-runtime-epsilon"; private const string VersionsFolderName = "versions";
private readonly IFileStore _fileStore;
private readonly IManifestService _manifestService; private readonly IManifestService _manifestService;
public InstallationStateService(IManifestService manifestService) public InstallationStateService(
IFileStore fileStore,
IManifestService manifestService)
{ {
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
_manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService)); _manifestService = manifestService ?? throw new ArgumentNullException(nameof(manifestService));
} }
@@ -36,26 +44,20 @@ namespace AlayaCore.Services
javaVersion = GetJavaVersion(javaPath!); javaVersion = GetJavaVersion(javaPath!);
} }
bool minecraftInstalled = IsMinecraftInstalled(); InstalledVersionState versionState = GetInstalledVersionState();
string? minecraftVersion = null;
bool neoforgeInstalled = IsNeoforgeInstalled();
string? neoforgeVersion = null;
InstalledModsManifestModel installedModsManifest = InstalledModsManifestModel installedModsManifest =
await _manifestService.GetInstalledModsManifestAsync(cancellationToken).ConfigureAwait(false) await _manifestService.GetInstalledModsManifestAsync(cancellationToken).ConfigureAwait(false);
?? new InstalledModsManifestModel();
return new InstallEnvironment( return new InstallEnvironment(
osPlatform: platform, osPlatform: platform,
javaInstalled: javaInstalled, javaInstalled: javaInstalled,
javaPath: javaPath, javaPath: javaPath,
javaVersion: javaVersion, javaVersion: javaVersion,
minecraftInstalled: minecraftInstalled, minecraftInstalled: !string.IsNullOrWhiteSpace(versionState.MinecraftVersion),
minecraftVersion: minecraftVersion, minecraftVersion: versionState.MinecraftVersion,
neoforgedInstalled: neoforgeInstalled, neoforgedInstalled: !string.IsNullOrWhiteSpace(versionState.NeoForgeVersion),
neoforgedVersion: neoforgeVersion, neoforgedVersion: versionState.NeoForgeVersion,
installedModsManifest: installedModsManifest); installedModsManifest: installedModsManifest);
} }
@@ -131,18 +133,14 @@ namespace AlayaCore.Services
: null; : null;
} }
private static bool TryGetJavaPath(out string? javaPath) private bool TryGetJavaPath(out string? javaPath)
{ {
string executableName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) string executableName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "java.exe" ? "javaw.exe"
: "java"; : "javaw";
string fullPath = Path.Combine( string runtimePath = _fileStore.GetOrCreate(FolderLocation.JavaRuntime);
AppContext.BaseDirectory, string fullPath = Path.Combine(runtimePath, "bin", executableName);
"Java",
JAVA_RUNTIME_FOLDER_NAME,
"bin",
executableName);
if (!File.Exists(fullPath)) if (!File.Exists(fullPath))
{ {
@@ -154,14 +152,130 @@ namespace AlayaCore.Services
return true; return true;
} }
private static bool IsMinecraftInstalled() private InstalledVersionState GetInstalledVersionState()
{ {
return true; string versionsPath = GetVersionsPath();
if (!Directory.Exists(versionsPath))
{
return InstalledVersionState.Empty();
}
string[] versionDirectories = Directory.GetDirectories(versionsPath);
if (versionDirectories.Length == 0)
{
return InstalledVersionState.Empty();
}
string? minecraftVersion = null;
string? neoForgeVersion = null;
foreach (string versionDirectory in versionDirectories)
{
string? versionFolderName = Path.GetFileName(versionDirectory);
if (string.IsNullOrWhiteSpace(versionFolderName))
{
continue;
}
string versionJsonPath = Path.Combine(versionDirectory, $"{versionFolderName}.json");
if (!File.Exists(versionJsonPath))
{
continue;
}
JObject versionJson = LoadJson(versionJsonPath);
string? id = versionJson.Value<string>("id");
string? inheritsFrom = versionJson.Value<string>("inheritsFrom");
if (string.IsNullOrWhiteSpace(id))
{
continue;
}
if (IsNeoForgeVersion(id, inheritsFrom))
{
neoForgeVersion = id;
if (string.IsNullOrWhiteSpace(minecraftVersion) && !string.IsNullOrWhiteSpace(inheritsFrom))
{
minecraftVersion = inheritsFrom;
}
continue;
}
if (string.IsNullOrWhiteSpace(minecraftVersion))
{
minecraftVersion = id;
}
}
return new InstalledVersionState(minecraftVersion, neoForgeVersion);
} }
private static bool IsNeoforgeInstalled() private string GetVersionsPath()
{ {
return true; return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Game), VersionsFolderName);
}
private static bool IsNeoForgeVersion(string? id, string? inheritsFrom)
{
return ContainsNeoForgeMarker(id) || ContainsNeoForgeMarker(inheritsFrom);
}
private static bool ContainsNeoForgeMarker(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
return value.Contains("neoforge", StringComparison.OrdinalIgnoreCase) ||
value.Contains("neoforged", StringComparison.OrdinalIgnoreCase);
}
private static JObject LoadJson(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("Path cannot be null, empty, or whitespace.", nameof(path));
}
string json = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(json))
{
throw new InvalidDataException($"File '{path}' was empty.");
}
return JObject.Parse(json);
}
private sealed class InstalledVersionState
{
public string? MinecraftVersion { get; }
public string? NeoForgeVersion { get; }
public InstalledVersionState(string? minecraftVersion, string? neoForgeVersion)
{
MinecraftVersion = Normalize(minecraftVersion);
NeoForgeVersion = Normalize(neoForgeVersion);
}
public static InstalledVersionState Empty()
{
return new InstalledVersionState(null, null);
}
private static string? Normalize(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value;
}
} }
} }
} }

View File

@@ -1,24 +1,30 @@
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Installation; using AlayaCore.Installation;
using AlayaCore.Models.Manifests; using AlayaCore.Models.Manifests;
using AlayaCore.Utilities.Enums;
namespace AlayaCore.Services namespace AlayaCore.Services
{ {
public sealed class JavaService : IJavaService public sealed class JavaService : IJavaService
{ {
private const string DOWNLOAD_FILE_NAME = "java-runtime.download"; private const string JavaArchiveHashPlaceholder = "REPLACE_WITH_MANIFEST_HASH_SUPPORT";
private const string JAVA_INSTALL_FOLDER_NAME = "Java"; private const string BaseUrl = "https://aka.ms/download-jdk/";
private const string JAVA_ARCHIVE_HASH_PLACEHOLDER = "REPLACE_WITH_MANIFEST_HASH_SUPPORT";
private readonly IDownloadService _downloadService; private readonly IDownloadService _downloadService;
private readonly IFileStore _fileStore;
public JavaService(IDownloadService downloadService) public JavaService(IDownloadService downloadService, IFileStore fileStore)
{ {
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService)); _downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
} }
public async Task EnsureValidJavaInstalledAsync( public async Task EnsureValidJavaInstalledAsync(
@@ -38,26 +44,28 @@ namespace AlayaCore.Services
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
string installDirectory = GetJavaInstallDirectory();
if (environment.JavaInstalled && if (environment.JavaInstalled &&
Directory.Exists(installDirectory) &&
string.Equals(environment.JavaVersion, manifest.RequiredJavaVersion, StringComparison.OrdinalIgnoreCase)) string.Equals(environment.JavaVersion, manifest.RequiredJavaVersion, StringComparison.OrdinalIgnoreCase))
{ {
return; return;
} }
if (environment.JavaInstalled && !string.IsNullOrWhiteSpace(environment.JavaPath)) if (Directory.Exists(installDirectory))
{ {
string javaRootFolder = ResolveJavaRootFolder(environment.JavaPath); await RemoveDirectoryAsync(installDirectory, cancellationToken).ConfigureAwait(false);
await RemoveJavaAsync(javaRootFolder, cancellationToken).ConfigureAwait(false);
} }
string downloadPath = GetJavaDownloadPath(); Uri downloadUri = GetPlatformSpecificJavaUri(manifest.RequiredJavaVersion);
string installDirectory = GetJavaInstallDirectory(); string downloadPath = GetJavaDownloadPath(downloadUri);
Directory.CreateDirectory(Path.GetDirectoryName(downloadPath) ?? AppContext.BaseDirectory); Directory.CreateDirectory(Path.GetDirectoryName(downloadPath) ?? AppContext.BaseDirectory);
Directory.CreateDirectory(installDirectory); Directory.CreateDirectory(installDirectory);
await DownloadJavaAsync( await DownloadJavaAsync(
manifest.RequiredJavaUrl, downloadUri,
downloadPath, downloadPath,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
@@ -67,52 +75,100 @@ namespace AlayaCore.Services
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
private static string GetJavaInstallDirectory() public Uri GetPlatformSpecificJavaUri(string javaVersion)
{ {
return Path.Combine(AppContext.BaseDirectory, JAVA_INSTALL_FOLDER_NAME); if (string.IsNullOrWhiteSpace(javaVersion))
}
private static string GetJavaDownloadPath()
{
return Path.Combine(AppContext.BaseDirectory, "Temp", DOWNLOAD_FILE_NAME);
}
private static string ResolveJavaRootFolder(string javaExecutablePath)
{
if (string.IsNullOrWhiteSpace(javaExecutablePath))
{ {
throw new ArgumentException("Java executable path cannot be null, empty, or whitespace.", nameof(javaExecutablePath)); throw new ArgumentException("Java version must be provided.", nameof(javaVersion));
} }
string? binFolder = Path.GetDirectoryName(javaExecutablePath); string os = GetOsSegment();
if (string.IsNullOrWhiteSpace(binFolder)) string arch = GetArchitectureSegment();
{ string extension = GetFileExtension(os);
throw new InvalidOperationException("Could not resolve the Java bin directory.");
}
string? javaRootFolder = Path.GetDirectoryName(binFolder); string fileName = $"microsoft-jdk-{javaVersion}-{os}-{arch}.{extension}";
if (string.IsNullOrWhiteSpace(javaRootFolder)) return new Uri($"{BaseUrl}{fileName}");
{
throw new InvalidOperationException("Could not resolve the Java installation directory.");
}
return javaRootFolder;
} }
private static Task RemoveJavaAsync( private string GetJavaInstallDirectory()
string oldJavaFolder, {
return _fileStore.GetOrCreate(FolderLocation.Java);
}
private string GetJavaDownloadPath(Uri downloadUri)
{
if (downloadUri == null)
{
throw new ArgumentNullException(nameof(downloadUri));
}
string fileName = Path.GetFileName(downloadUri.AbsolutePath);
if (string.IsNullOrWhiteSpace(fileName))
{
throw new InvalidOperationException("Could not determine Java archive file name from download URI.");
}
return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Downloads), fileName);
}
private static string GetOsSegment()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return "windows";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return "linux";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "macos";
}
throw new PlatformNotSupportedException("Unsupported operating system.");
}
private static string GetArchitectureSegment()
{
return RuntimeInformation.OSArchitecture switch
{
Architecture.X64 => "x64",
Architecture.Arm64 => "aarch64",
Architecture.X86 => throw new NotSupportedException("X86 is not supported."),
Architecture.Arm => throw new NotSupportedException("Arm32 is not supported."),
_ => throw new PlatformNotSupportedException(
$"Unsupported architecture: {RuntimeInformation.OSArchitecture}")
};
}
private static string GetFileExtension(string os)
{
return os switch
{
"windows" => "zip",
"linux" => "tar.gz",
"macos" => "tar.gz",
_ => throw new PlatformNotSupportedException($"Unsupported OS: {os}")
};
}
private static Task RemoveDirectoryAsync(
string directoryPath,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrWhiteSpace(oldJavaFolder)) if (string.IsNullOrWhiteSpace(directoryPath))
{ {
throw new ArgumentException("Old Java folder cannot be null, empty, or whitespace.", nameof(oldJavaFolder)); throw new ArgumentException("Directory path cannot be null, empty, or whitespace.", nameof(directoryPath));
} }
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
if (Directory.Exists(oldJavaFolder)) if (Directory.Exists(directoryPath))
{ {
Directory.Delete(oldJavaFolder, recursive: true); Directory.Delete(directoryPath, recursive: true);
} }
return Task.CompletedTask; return Task.CompletedTask;
@@ -140,22 +196,32 @@ namespace AlayaCore.Services
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Replace this when your manifest includes a Java archive/runtime hash. string directory = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
if (File.Exists(destinationPath))
{
File.Delete(destinationPath);
}
await _downloadService.DownloadFileAsync( await _downloadService.DownloadFileAsync(
javaUri, javaUri,
destinationPath, destinationPath,
JAVA_ARCHIVE_HASH_PLACEHOLDER, JavaArchiveHashPlaceholder,
cancellationToken: cancellationToken).ConfigureAwait(false); cancellationToken: cancellationToken).ConfigureAwait(false);
} }
private static Task InstallJavaAsync( private static async Task InstallJavaAsync(
string installerPath, string archivePath,
string installDirectory, string installDirectory,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrWhiteSpace(installerPath)) if (string.IsNullOrWhiteSpace(archivePath))
{ {
throw new ArgumentException("Installer path cannot be null, empty, or whitespace.", nameof(installerPath)); throw new ArgumentException("Archive path cannot be null, empty, or whitespace.", nameof(archivePath));
} }
if (string.IsNullOrWhiteSpace(installDirectory)) if (string.IsNullOrWhiteSpace(installDirectory))
@@ -165,23 +231,87 @@ namespace AlayaCore.Services
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
if (!File.Exists(installerPath)) if (!File.Exists(archivePath))
{ {
throw new FileNotFoundException("Java installer or archive was not found.", installerPath); throw new FileNotFoundException("Java archive was not found.", archivePath);
} }
Directory.CreateDirectory(installDirectory); Directory.CreateDirectory(installDirectory);
// TODO: if (archivePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
// Implement this based on the actual Java package format. {
// ZipFile.ExtractToDirectory(archivePath, installDirectory);
// Examples: return;
// - If the file is a .zip, extract it into installDirectory }
// - If it is a .tar.gz, unpack it appropriately
// - If it is an installer executable, launch it silently if (archivePath.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
// {
// For now this is intentionally left explicit rather than guessing the wrong install strategy. await ExtractTarGzAsync(archivePath, installDirectory, cancellationToken).ConfigureAwait(false);
throw new NotImplementedException("InstallJavaAsync must be implemented for the actual Java package format."); return;
}
throw new NotSupportedException(
$"Unsupported Java archive format: {Path.GetFileName(archivePath)}");
}
private static async Task ExtractTarGzAsync(
string archivePath,
string destinationDirectory,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(archivePath))
{
throw new ArgumentException("Archive path cannot be null, empty, or whitespace.", nameof(archivePath));
}
if (string.IsNullOrWhiteSpace(destinationDirectory))
{
throw new ArgumentException("Destination directory cannot be null, empty, or whitespace.", nameof(destinationDirectory));
}
Directory.CreateDirectory(destinationDirectory);
var startInfo = new ProcessStartInfo
{
FileName = "tar",
Arguments = $"-xzf \"{archivePath}\" -C \"{destinationDirectory}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = startInfo };
await using (cancellationToken.Register(() =>
{
try
{
if (!process.HasExited)
{
process.Kill();
}
}
catch
{
// Ignore race conditions if the process exits while cancellation is being handled.
}
}))
{
process.Start();
string standardOutput = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
string standardError = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
await Task.Run(() => process.WaitForExit(), cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"tar extraction failed with exit code {process.ExitCode}. Error: {standardError}. Output: {standardOutput}");
}
}
} }
} }
} }

View File

@@ -3,11 +3,13 @@ using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Clients; using AlayaCore.Abstractions.Interfaces.Clients;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Models.Configuration; using AlayaCore.Models.Configuration;
using AlayaCore.Models.Manifests; using AlayaCore.Models.Manifests;
using AlayaCore.Models.Manifests.DTO; using AlayaCore.Models.Manifests.DTO;
using AlayaCore.Utilities.Enums;
using AlayaCore.Utilities.Extensions; using AlayaCore.Utilities.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -21,15 +23,18 @@ namespace AlayaCore.Services
private readonly IDownloadService _downloadService; private readonly IDownloadService _downloadService;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IFileStore _fileStore;
private readonly ManifestServiceOptions _options; private readonly ManifestServiceOptions _options;
public ManifestService( public ManifestService(
IDownloadService downloadService, IDownloadService downloadService,
IHttpClient httpClient, IHttpClient httpClient,
IFileStore fileStore,
ManifestServiceOptions options) ManifestServiceOptions options)
{ {
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService)); _downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
_options = options ?? throw new ArgumentNullException(nameof(options)); _options = options ?? throw new ArgumentNullException(nameof(options));
} }
@@ -168,17 +173,17 @@ namespace AlayaCore.Services
public string GetLauncherManifestPath() public string GetLauncherManifestPath()
{ {
return Path.Combine(_options.ManifestDirectoryPath, LAUNCHER_MANIFEST_FILE_NAME); return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Manifests), LAUNCHER_MANIFEST_FILE_NAME);
} }
public string GetCoreManifestPath() public string GetCoreManifestPath()
{ {
return Path.Combine(_options.ManifestDirectoryPath, CORE_MANIFEST_FILE_NAME); return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Manifests), CORE_MANIFEST_FILE_NAME);
} }
public string GetInstalledModsManifestPath() public string GetInstalledModsManifestPath()
{ {
return Path.Combine(_options.ManifestDirectoryPath, INSTALLED_MODS_MANIFEST_FILE_NAME); return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Manifests), INSTALLED_MODS_MANIFEST_FILE_NAME);
} }
private async Task<TModel?> LoadLocalManifestAsync<TDto, TModel>( private async Task<TModel?> LoadLocalManifestAsync<TDto, TModel>(

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Clients; using AlayaCore.Abstractions.Interfaces.Clients;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Installation; using AlayaCore.Installation;
@@ -21,23 +22,23 @@ namespace AlayaCore.Services
{ {
public sealed class ModService : IModService public sealed class ModService : IModService
{ {
private const string InstalledModsManifestFileName = "InstalledModsManifest.json"; private const string INSTALLED_MODS_MANIFEST_FILE_NAME = "InstalledModsManifest.json";
private readonly IDownloadService _downloadService; private readonly IDownloadService _downloadService;
private readonly ModrinthConnectionOptions _options; private readonly ModrinthConnectionOptions _options;
private readonly ManifestServiceOptions _manifestOptions;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IFileStore _fileStore;
public ModService( public ModService(
IDownloadService downloadService, IDownloadService downloadService,
ModrinthConnectionOptions options, ModrinthConnectionOptions options,
ManifestServiceOptions manifestOptions, IHttpClient httpClient,
IHttpClient httpClient) IFileStore fileStore)
{ {
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService)); _downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
_options = options ?? throw new ArgumentNullException(nameof(options)); _options = options ?? throw new ArgumentNullException(nameof(options));
_manifestOptions = manifestOptions ?? throw new ArgumentNullException(nameof(manifestOptions));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
} }
public async Task ProcessModsAsync( public async Task ProcessModsAsync(
@@ -238,7 +239,7 @@ namespace AlayaCore.Services
return $"{baseUrl}/version_file/{sha512Hash}"; return $"{baseUrl}/version_file/{sha512Hash}";
} }
private static string GetModDestinationPath(ModFileEntry fileEntry) private string GetModDestinationPath(ModFileEntry fileEntry)
{ {
if (fileEntry == null) if (fileEntry == null)
{ {
@@ -251,17 +252,16 @@ namespace AlayaCore.Services
} }
string modsDirectory = GetModsDirectoryPath(); string modsDirectory = GetModsDirectoryPath();
Directory.CreateDirectory(modsDirectory);
return Path.Combine(modsDirectory, fileEntry.FileName); return Path.Combine(modsDirectory, fileEntry.FileName);
} }
private static string GetModsDirectoryPath() private string GetModsDirectoryPath()
{ {
return Path.Combine(AppContext.BaseDirectory, "Game", "mods"); return _fileStore.GetOrCreate(FolderLocation.Mods);
} }
private static void RemoveStaleMods(IEnumerable<ModFileEntry> requiredMods) private void RemoveStaleMods(IEnumerable<ModFileEntry> requiredMods)
{ {
if (requiredMods == null) if (requiredMods == null)
{ {
@@ -269,6 +269,7 @@ namespace AlayaCore.Services
} }
string modsDirectory = GetModsDirectoryPath(); string modsDirectory = GetModsDirectoryPath();
if (!Directory.Exists(modsDirectory)) if (!Directory.Exists(modsDirectory))
{ {
return; return;
@@ -302,10 +303,9 @@ namespace AlayaCore.Services
List<ModFileEntry> entries = installedMods.ToList(); List<ModFileEntry> entries = installedMods.ToList();
InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries); InstalledModsManifestModel manifest = new InstalledModsManifestModel(entries);
string manifestsDirectory = _manifestOptions.ManifestDirectoryPath; string manifestsDirectory = _fileStore.GetOrCreate(FolderLocation.Manifests);
Directory.CreateDirectory(manifestsDirectory);
string manifestPath = Path.Combine(manifestsDirectory, InstalledModsManifestFileName); string manifestPath = Path.Combine(manifestsDirectory, INSTALLED_MODS_MANIFEST_FILE_NAME);
string temporaryManifestPath = manifestPath + ".tmp"; string temporaryManifestPath = manifestPath + ".tmp";
InstalledModsManifestDto dto = manifest.ToDto(); InstalledModsManifestDto dto = manifest.ToDto();

View File

@@ -2,21 +2,30 @@ using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Configuration;
using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Models.Configuration; using AlayaCore.Models.Configuration;
using AlayaCore.Utilities.Enums;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace AlayaCore.Services namespace AlayaCore.Services
{ {
public sealed class SettingsService : ISettingsService public sealed class SettingsService : ISettingsService
{ {
private const string LauncherSettingsFileName = "Launcher.json"; private readonly IFileStore _fileStore;
public LauncherOptions LauncherOptions { get; } public LauncherOptions LauncherOptions { get; }
public GameOptions GameOptions { get; }
public SettingsService(LauncherOptions launcherOptions) public SettingsService(
LauncherOptions launcherOptions,
GameOptions gameOptions,
IFileStore fileStore)
{ {
LauncherOptions = launcherOptions ?? throw new ArgumentNullException(nameof(launcherOptions)); LauncherOptions = launcherOptions ?? throw new ArgumentNullException(nameof(launcherOptions));
GameOptions = gameOptions ?? throw new ArgumentNullException(nameof(gameOptions));
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
} }
public async Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default) public async Task SetForceReinstallAsync(bool value, CancellationToken cancellationToken = default)
@@ -25,10 +34,16 @@ namespace AlayaCore.Services
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false); await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
} }
public async Task UpdateLaunchVersionAsync(string newVersion, CancellationToken cancellationToken = default)
{
GameOptions.LaunchVersion = newVersion ?? string.Empty;
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
}
public async Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default) public async Task SaveLauncherOptionsAsync(CancellationToken cancellationToken = default)
{ {
await SaveAsync( await SaveAsync(
LauncherSettingsFileName, LauncherOptions.FileName,
LauncherOptions, LauncherOptions,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
@@ -36,7 +51,7 @@ namespace AlayaCore.Services
public async Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default) public async Task LoadLauncherOptionsAsync(CancellationToken cancellationToken = default)
{ {
LauncherOptions? loadedOptions = await LoadAsync<LauncherOptions>( LauncherOptions? loadedOptions = await LoadAsync<LauncherOptions>(
LauncherSettingsFileName, LauncherOptions.FileName,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
if (loadedOptions == null) if (loadedOptions == null)
@@ -47,10 +62,49 @@ namespace AlayaCore.Services
LauncherOptions.ForceReinstall = loadedOptions.ForceReinstall; LauncherOptions.ForceReinstall = loadedOptions.ForceReinstall;
} }
public async Task SaveGameOptionsAsync(CancellationToken cancellationToken = default)
{
await SaveAsync(
GameOptions.FileName,
GameOptions,
cancellationToken).ConfigureAwait(false);
}
public async Task LoadGameOptionsAsync(CancellationToken cancellationToken = default)
{
GameOptions? loadedOptions = await LoadAsync<GameOptions>(
GameOptions.FileName,
cancellationToken).ConfigureAwait(false);
if (loadedOptions == null)
{
return;
}
GameOptions.LaunchVersion = loadedOptions.LaunchVersion;
GameOptions.MinimumRamMB = loadedOptions.MinimumRamMB;
GameOptions.MaximumRamMB = loadedOptions.MaximumRamMB;
GameOptions.ScreenWidth = loadedOptions.ScreenWidth;
GameOptions.ScreenHeight = loadedOptions.ScreenHeight;
GameOptions.Fullscreen = loadedOptions.Fullscreen;
}
public async Task LoadAllAsync(CancellationToken cancellationToken = default)
{
await LoadLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
await LoadGameOptionsAsync(cancellationToken).ConfigureAwait(false);
}
public async Task SaveAllAsync(CancellationToken cancellationToken = default)
{
await SaveLauncherOptionsAsync(cancellationToken).ConfigureAwait(false);
await SaveGameOptionsAsync(cancellationToken).ConfigureAwait(false);
}
private async Task SaveAsync<T>( private async Task SaveAsync<T>(
string fileName, string fileName,
T value, T value,
CancellationToken cancellationToken) CancellationToken cancellationToken) where T : BaseConfig
{ {
if (string.IsNullOrWhiteSpace(fileName)) if (string.IsNullOrWhiteSpace(fileName))
{ {
@@ -89,7 +143,7 @@ namespace AlayaCore.Services
private async Task<T?> LoadAsync<T>( private async Task<T?> LoadAsync<T>(
string fileName, string fileName,
CancellationToken cancellationToken) CancellationToken cancellationToken) where T : BaseConfig
{ {
if (string.IsNullOrWhiteSpace(fileName)) if (string.IsNullOrWhiteSpace(fileName))
{ {
@@ -124,9 +178,9 @@ namespace AlayaCore.Services
} }
} }
private static string GetFullPath(string fileName) private string GetFullPath(string fileName)
{ {
return Path.Combine(AppContext.BaseDirectory, "Config", fileName); return Path.Combine(_fileStore.GetOrCreate(FolderLocation.Config), fileName);
} }
} }
} }

View File

@@ -4,6 +4,7 @@ namespace AlayaCore.States
{ {
Ready, Ready,
LauncherNeedsUpdate, LauncherNeedsUpdate,
NeedAuthenticating,
InstallJava, InstallJava,
InstallMinecraft, InstallMinecraft,
InstallNeoforge, InstallNeoforge,
@@ -17,35 +18,46 @@ namespace AlayaCore.States
public bool MinecraftNeedsInstallOrUpdate { get; } public bool MinecraftNeedsInstallOrUpdate { get; }
public bool NeoforgeNeedsInstallOrUpdate { get; } public bool NeoforgeNeedsInstallOrUpdate { get; }
public bool ModsNeedSync { get; } public bool ModsNeedSync { get; }
public bool NeedAuthenticating { get; }
public LaunchState State => ComputeState(); public LaunchState State => ComputeState();
public bool CanRun => public bool CanRun => State == LaunchState.Ready;
State == LaunchState.Ready;
public bool NeedsUpdating => public bool NeedsUpdating =>
State != LaunchState.Ready; LauncherNeedsUpdate ||
JavaNeedsInstallOrUpdate ||
MinecraftNeedsInstallOrUpdate ||
NeoforgeNeedsInstallOrUpdate ||
ModsNeedSync;
public bool NeedsAttention =>
NeedsUpdating || NeedAuthenticating;
public LaunchPlan( public LaunchPlan(
bool launcherNeedsUpdate, bool launcherNeedsUpdate,
bool javaNeedsInstallOrUpdate, bool javaNeedsInstallOrUpdate,
bool minecraftNeedsInstallOrUpdate, bool minecraftNeedsInstallOrUpdate,
bool neoforgeNeedsInstallOrUpdate, bool neoforgeNeedsInstallOrUpdate,
bool modsNeedSync) bool modsNeedSync,
bool needAuthenticating)
{ {
LauncherNeedsUpdate = launcherNeedsUpdate; LauncherNeedsUpdate = launcherNeedsUpdate;
JavaNeedsInstallOrUpdate = javaNeedsInstallOrUpdate; JavaNeedsInstallOrUpdate = javaNeedsInstallOrUpdate;
MinecraftNeedsInstallOrUpdate = minecraftNeedsInstallOrUpdate; MinecraftNeedsInstallOrUpdate = minecraftNeedsInstallOrUpdate;
NeoforgeNeedsInstallOrUpdate = neoforgeNeedsInstallOrUpdate; NeoforgeNeedsInstallOrUpdate = neoforgeNeedsInstallOrUpdate;
ModsNeedSync = modsNeedSync; ModsNeedSync = modsNeedSync;
NeedAuthenticating = needAuthenticating;
} }
private LaunchState ComputeState() private LaunchState ComputeState()
{ {
// Priority order matters a LOT here
if (LauncherNeedsUpdate) if (LauncherNeedsUpdate)
return LaunchState.LauncherNeedsUpdate; return LaunchState.LauncherNeedsUpdate;
if (NeedAuthenticating)
return LaunchState.NeedAuthenticating;
if (JavaNeedsInstallOrUpdate) if (JavaNeedsInstallOrUpdate)
return LaunchState.InstallJava; return LaunchState.InstallJava;
@@ -68,7 +80,8 @@ namespace AlayaCore.States
javaNeedsInstallOrUpdate: false, javaNeedsInstallOrUpdate: false,
minecraftNeedsInstallOrUpdate: false, minecraftNeedsInstallOrUpdate: false,
neoforgeNeedsInstallOrUpdate: false, neoforgeNeedsInstallOrUpdate: false,
modsNeedSync: false); modsNeedSync: false,
needAuthenticating: false);
} }
} }
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Globalization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace AlayaCore.Utilities.Converters namespace AlayaCore.Utilities.Converters
@@ -12,9 +11,9 @@ namespace AlayaCore.Utilities.Converters
{ {
writer.WriteNull(); writer.WriteNull();
} }
else if (value is Uri) else if (value is Uri uri)
{ {
writer.WriteValue(((Uri)value).AbsoluteUri); writer.WriteValue(uri.AbsoluteUri);
} }
else else
{ {
@@ -33,8 +32,7 @@ namespace AlayaCore.Utilities.Converters
{ {
try try
{ {
Uri uri = new Uri((string)reader.Value!); return new Uri((string)reader.Value!, UriKind.Absolute);
return uri;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -47,7 +45,7 @@ namespace AlayaCore.Utilities.Converters
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
{ {
return objectType == typeof(Uri); return typeof(Uri).IsAssignableFrom(objectType);
} }
} }
} }

View File

@@ -1,7 +1,17 @@
namespace AlayaCore.Utilities.Enums namespace AlayaCore.Utilities.Enums
{ {
public class FolderLocation public enum FolderLocation
{ {
BaseDirectory,
Java,
JavaRuntime,
Game,
Mods,
ResourcePacks,
Config,
Downloads,
Manifests,
Plugins,
Data
} }
} }

View File

@@ -50,9 +50,9 @@ namespace AlayaCore.Utilities.Extensions
dto.RequiredJavaVersion, dto.RequiredJavaVersion,
dto.RequiredJavaUrl, dto.RequiredJavaUrl,
dto.MinecraftVersion, dto.MinecraftVersion,
dto.MinecraftUrl,
dto.NeoforgedVersion, dto.NeoforgedVersion,
dto.NeoforgedUrl, dto.ServerUrl,
dto.ServerPort,
dto.Files?.Select(file => file.ToModel()) ?? Array.Empty<ModFileEntry>()); dto.Files?.Select(file => file.ToModel()) ?? Array.Empty<ModFileEntry>());
} }
@@ -81,11 +81,9 @@ namespace AlayaCore.Utilities.Extensions
{ {
AlayaVersion = model.AlayaVersion, AlayaVersion = model.AlayaVersion,
RequiredJavaVersion = model.RequiredJavaVersion, RequiredJavaVersion = model.RequiredJavaVersion,
RequiredJavaUrl = model.RequiredJavaUrl, RequiredJavaUrl = model.RequiredJavaBaseUrl,
MinecraftVersion = model.MinecraftVersion, MinecraftVersion = model.MinecraftVersion,
MinecraftUrl = model.MinecraftUrl,
NeoforgedVersion = model.NeoforgedVersion, NeoforgedVersion = model.NeoforgedVersion,
NeoforgedUrl = model.NeoforgedUrl,
Files = model.Files.Select(file => file.ToDto()).ToList() Files = model.Files.Select(file => file.ToDto()).ToList()
}; };
} }

View File

@@ -6,20 +6,86 @@ using AlayaCore.Utilities.Enums;
namespace AlayaCore.Utilities.Stores namespace AlayaCore.Utilities.Stores
{ {
public class FileStore : IFileStore public sealed class LocalFileStore : IFileStore
{ {
private static readonly string _baseDirectory = AppContext.BaseDirectory; private static readonly string BaseDirectoryPath = AppContext.BaseDirectory;
private static readonly string _javaDirectory = Path.Combine(_baseDirectory, "Java"); private static readonly string JavaDirectoryPath = Path.Combine(BaseDirectoryPath, "Java");
private static readonly string JavaRuntimeDirectoryPath = Path.Combine(JavaDirectoryPath, "java-runtime-epsilon");
private static readonly Dictionary<FolderLocation, string> _folders = new() private static readonly string GameDirectoryPath = Path.Combine(BaseDirectoryPath, "Game");
{ private static readonly string ModsDirectoryPath = Path.Combine(GameDirectoryPath, "mods");
{ FolderLocation.BaseDirectory, _baseDirectory }, private static readonly string ResourcePacksDirectoryPath = Path.Combine(GameDirectoryPath, "resourcepacks");
{ FolderLocation.Java, _javaDirectory} private static readonly string ConfigDirectoryPath = Path.Combine(BaseDirectoryPath, "Config");
}; private static readonly string DownloadsDirectoryPath = Path.Combine(BaseDirectoryPath, "Temp");
private static readonly string ManifestsDirectoryPath = Path.Combine(BaseDirectoryPath, "Manifests");
private static readonly string PluginsDirectoryPath = Path.Combine(GameDirectoryPath, "plugins");
private static readonly string DataDirectoryPath = Path.Combine(BaseDirectoryPath, "Data");
private static readonly IReadOnlyDictionary<FolderLocation, string> Folders =
new Dictionary<FolderLocation, string>
{
{ FolderLocation.BaseDirectory, BaseDirectoryPath },
{ FolderLocation.Java, JavaDirectoryPath },
{ FolderLocation.JavaRuntime, JavaRuntimeDirectoryPath },
{ FolderLocation.Game, GameDirectoryPath },
{ FolderLocation.Mods, ModsDirectoryPath },
{ FolderLocation.ResourcePacks, ResourcePacksDirectoryPath },
{ FolderLocation.Config, ConfigDirectoryPath },
{ FolderLocation.Downloads, DownloadsDirectoryPath },
{ FolderLocation.Manifests, ManifestsDirectoryPath},
{ FolderLocation.Plugins, PluginsDirectoryPath },
{ FolderLocation.Data, DataDirectoryPath}
};
public string Get(FolderLocation location) public string Get(FolderLocation location)
{ {
return _folders[location]; if (!Folders.TryGetValue(location, out string? path))
{
throw new ArgumentOutOfRangeException(nameof(location), location, "Unknown folder location.");
}
return path;
}
public string GetOrCreate(FolderLocation location)
{
string path = Get(location);
Directory.CreateDirectory(path);
return path;
}
public string Combine(FolderLocation location, params string[] paths)
{
if (paths == null)
{
throw new ArgumentNullException(nameof(paths));
}
string rootPath = Get(location);
if (paths.Length == 0)
{
return rootPath;
}
string[] combinedPaths = new string[paths.Length + 1];
combinedPaths[0] = rootPath;
for (int i = 0; i < paths.Length; i++)
{
if (string.IsNullOrWhiteSpace(paths[i]))
{
throw new ArgumentException("Path segments cannot be null, empty, or whitespace.", nameof(paths));
}
combinedPaths[i + 1] = paths[i];
}
return Path.Combine(combinedPaths);
}
public bool Exists(FolderLocation location)
{
return Directory.Exists(Get(location));
} }
} }
} }

View File

@@ -44,9 +44,25 @@
"netstandard2.1": { "netstandard2.1": {
"targetAlias": "netstandard2.1", "targetAlias": "netstandard2.1",
"dependencies": { "dependencies": {
"CmlLib.Core": {
"target": "Package",
"version": "[4.0.6, )"
},
"CmlLib.Core.Auth.Microsoft": {
"target": "Package",
"version": "[3.3.1, )"
},
"CmlLib.Core.Installer.NeoForge": {
"target": "Package",
"version": "[4.0.0, )"
},
"Newtonsoft.Json": { "Newtonsoft.Json": {
"target": "Package", "target": "Package",
"version": "[13.0.4, )" "version": "[13.0.4, )"
},
"XboxAuthNet.Game.Msal": {
"target": "Package",
"version": "[0.1.3, )"
} }
}, },
"imports": [ "imports": [

View File

@@ -1,2 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" /> <Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)system.text.json/8.0.5/buildTransitive/netstandard2.0/System.Text.Json.targets" Condition="Exists('$(NuGetPackageRoot)system.text.json/8.0.5/buildTransitive/netstandard2.0/System.Text.Json.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.1/buildTransitive/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.1/buildTransitive/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.targets')" />
</ImportGroup>
</Project>

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("AlayaCore")] [assembly: System.Reflection.AssemblyCompanyAttribute("AlayaCore")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+823ccf4b877ddf1fc1293f72c6b704d6a449ddaa")]
[assembly: System.Reflection.AssemblyProductAttribute("AlayaCore")] [assembly: System.Reflection.AssemblyProductAttribute("AlayaCore")]
[assembly: System.Reflection.AssemblyTitleAttribute("AlayaCore")] [assembly: System.Reflection.AssemblyTitleAttribute("AlayaCore")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
02d72db7e7301cd87568f68f39a003b2484df3c699c3ceed1d9bd04f25f6e3dd c63b3bf6f0fd998ece1d911c58e61eb584840e2e19d535b62f67f54b1510e49e

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,40 @@
{ {
"version": 2, "version": 2,
"dgSpecHash": "iqHG7z//q/I=", "dgSpecHash": "pXFuC0zr6Zg=",
"success": true, "success": true,
"projectFilePath": "/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj", "projectFilePath": "/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj",
"expectedPackageFiles": [ "expectedPackageFiles": [
"/Users/ryanmacham/.nuget/packages/cmllib.core/4.0.6/cmllib.core.4.0.6.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/cmllib.core.auth.microsoft/3.3.1/cmllib.core.auth.microsoft.3.3.1.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/cmllib.core.commons/4.0.0/cmllib.core.commons.4.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/cmllib.core.installer.neoforge/4.0.0/cmllib.core.installer.neoforge.4.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/htmlagilitypack/1.11.48/htmlagilitypack.1.11.48.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/lzma-sdk/19.0.0/lzma-sdk.19.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/microsoft.bcl.asyncinterfaces/8.0.0/microsoft.bcl.asyncinterfaces.8.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.1/microsoft.extensions.dependencyinjection.abstractions.8.0.1.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.1/microsoft.extensions.logging.abstractions.8.0.1.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/microsoft.identity.client/4.61.3/microsoft.identity.client.4.61.3.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/microsoft.identity.client.extensions.msal/4.61.3/microsoft.identity.client.extensions.msal.4.61.3.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/microsoft.identitymodel.abstractions/6.35.0/microsoft.identitymodel.abstractions.6.35.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/newtonsoft.json/13.0.4/newtonsoft.json.13.0.4.nupkg.sha512", "/Users/ryanmacham/.nuget/packages/newtonsoft.json/13.0.4/newtonsoft.json.13.0.4.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/sharpziplib/1.4.2/sharpziplib.1.4.2.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.buffers/4.5.1/system.buffers.4.5.1.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.diagnostics.diagnosticsource/6.0.1/system.diagnostics.diagnosticsource.6.0.1.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.io.filesystem.accesscontrol/5.0.0/system.io.filesystem.accesscontrol.5.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.memory/4.5.5/system.memory.4.5.5.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.net.http.json/8.0.0/system.net.http.json.8.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.numerics.vectors/4.4.0/system.numerics.vectors.4.4.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.runtime.compilerservices.unsafe/6.0.0/system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.security.accesscontrol/5.0.0/system.security.accesscontrol.5.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.security.cryptography.protecteddata/4.5.0/system.security.cryptography.protecteddata.4.5.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.security.principal.windows/5.0.0/system.security.principal.windows.5.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.text.encodings.web/8.0.0/system.text.encodings.web.8.0.0.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.text.json/8.0.5/system.text.json.8.0.5.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.threading.tasks.dataflow/8.0.1/system.threading.tasks.dataflow.8.0.1.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/system.threading.tasks.extensions/4.5.4/system.threading.tasks.extensions.4.5.4.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/xboxauthnet/3.0.4/xboxauthnet.3.0.4.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/xboxauthnet.game/1.4.1/xboxauthnet.game.1.4.1.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/xboxauthnet.game.msal/0.1.3/xboxauthnet.game.msal.0.1.3.nupkg.sha512",
"/Users/ryanmacham/.nuget/packages/netstandard.library.ref/2.1.0/netstandard.library.ref.2.1.0.nupkg.sha512" "/Users/ryanmacham/.nuget/packages/netstandard.library.ref/2.1.0/netstandard.library.ref.2.1.0.nupkg.sha512"
], ],
"logs": [] "logs": []

View File

@@ -1 +1 @@
"restore":{"projectUniqueName":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","projectName":"AlayaCore","projectPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","packagesPath":"","outputPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/obj/","projectStyle":"PackageReference","originalTargetFrameworks":["netstandard2.1"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.200"}"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","dependencies":{"Newtonsoft.Json":{"target":"Package","version":"[13.0.4, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"downloadDependencies":[{"name":"NETStandard.Library.Ref","version":"[2.1.0, 2.1.0]"}],"frameworkReferences":{"NETStandard.Library":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"/usr/local/share/dotnet/sdk/10.0.201/RuntimeIdentifierGraph.json"}} "restore":{"projectUniqueName":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","projectName":"AlayaCore","projectPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/AlayaCore.csproj","packagesPath":"","outputPath":"/Users/ryanmacham/Documents/Coding/AlayaCore/AlayaCore/obj/","projectStyle":"PackageReference","originalTargetFrameworks":["netstandard2.1"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.200"}"frameworks":{"netstandard2.1":{"targetAlias":"netstandard2.1","dependencies":{"CmlLib.Core":{"target":"Package","version":"[4.0.6, )"},"CmlLib.Core.Auth.Microsoft":{"target":"Package","version":"[3.3.1, )"},"CmlLib.Core.Installer.NeoForge":{"target":"Package","version":"[4.0.0, )"},"Newtonsoft.Json":{"target":"Package","version":"[13.0.4, )"},"XboxAuthNet.Game.Msal":{"target":"Package","version":"[0.1.3, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"downloadDependencies":[{"name":"NETStandard.Library.Ref","version":"[2.1.0, 2.1.0]"}],"frameworkReferences":{"NETStandard.Library":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"/usr/local/share/dotnet/sdk/10.0.201/RuntimeIdentifierGraph.json"}}

View File

@@ -1 +1 @@
17752277078247393 17754069520908862

View File

@@ -1 +1 @@
17752277078247393 17754069520908862