From bdb8915932f99b04fe15b1a62d1d11f6725ebc4f Mon Sep 17 00:00:00 2001 From: Cinka Date: Fri, 27 Dec 2024 19:15:33 +0300 Subject: [PATCH] - add: Content manipulation think --- Nebula.Launcher/Assets/refresh.png | Bin 0 -> 12044 bytes Nebula.Launcher/CurrentConVar.cs | 24 +- Nebula.Launcher/FileApis/FileApi.cs | 1 - .../Models/DownloadStreamHeaderFlags.cs | 15 + Nebula.Launcher/Models/RobustBuildInfo.cs | 8 + Nebula.Launcher/Models/RobustServerEntry.cs | 80 +++--- Nebula.Launcher/Models/RobustUrl.cs | 64 +++++ Nebula.Launcher/Nebula.Launcher.csproj | 5 + Nebula.Launcher/Services/AssemblyService.cs | 107 ++++++++ Nebula.Launcher/Services/AuthService.cs | 24 +- .../Services/ConfigurationService.cs | 91 +++--- .../Services/ContentService.Download.cs | 259 ++++++++++++++++++ Nebula.Launcher/Services/ContentService.cs | 48 ++++ Nebula.Launcher/Services/DebugService.cs | 7 + Nebula.Launcher/Services/EngineService.cs | 189 +++++++++++++ Nebula.Launcher/Services/HubService.cs | 52 ++-- Nebula.Launcher/Services/RestService.cs | 21 +- Nebula.Launcher/Services/RunnerService.cs | 124 +++++++++ .../ViewModels/AccountInfoViewModel.cs | 92 +++++-- Nebula.Launcher/ViewModels/MainViewModel.cs | 7 +- .../ViewModels/ServerEntryModelView.cs | 33 +++ .../ViewModels/ServerListViewModel.cs | 37 ++- Nebula.Launcher/Views/MainView.axaml | 21 +- .../Views/Pages/AccountInfoView.axaml | 176 ++++++++---- .../Views/Pages/ServerListView.axaml | 38 ++- 25 files changed, 1275 insertions(+), 248 deletions(-) create mode 100644 Nebula.Launcher/Assets/refresh.png create mode 100644 Nebula.Launcher/Models/DownloadStreamHeaderFlags.cs create mode 100644 Nebula.Launcher/Models/RobustBuildInfo.cs create mode 100644 Nebula.Launcher/Models/RobustUrl.cs create mode 100644 Nebula.Launcher/Services/AssemblyService.cs create mode 100644 Nebula.Launcher/Services/ContentService.Download.cs create mode 100644 Nebula.Launcher/Services/ContentService.cs create mode 100644 Nebula.Launcher/Services/EngineService.cs create mode 100644 Nebula.Launcher/Services/RunnerService.cs create mode 100644 Nebula.Launcher/ViewModels/ServerEntryModelView.cs diff --git a/Nebula.Launcher/Assets/refresh.png b/Nebula.Launcher/Assets/refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..f5610409ca270f98ba4de540f4f2ca19bcf31514 GIT binary patch literal 12044 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 literal 0 HcmV?d00001 diff --git a/Nebula.Launcher/CurrentConVar.cs b/Nebula.Launcher/CurrentConVar.cs index 28099ad..06cb901 100644 --- a/Nebula.Launcher/CurrentConVar.cs +++ b/Nebula.Launcher/CurrentConVar.cs @@ -4,22 +4,22 @@ namespace Nebula.Launcher; public static class CurrentConVar { - public static readonly ConVar EngineManifestUrl = - ConVar.Build("engine.manifestUrl", "https://robust-builds.cdn.spacestation14.com/manifest.json"); - public static readonly ConVar EngineModuleManifestUrl = - ConVar.Build("engine.moduleManifestUrl", "https://robust-builds.cdn.spacestation14.com/modules.json"); - public static readonly ConVar ManifestDownloadProtocolVersion = - ConVar.Build("engine.manifestDownloadProtocolVersion", 1); - public static readonly ConVar RobustAssemblyName = - ConVar.Build("engine.robustAssemblyName", "Robust.Client"); + public static readonly ConVar EngineManifestUrl = + ConVarBuilder.Build("engine.manifestUrl", "https://robust-builds.cdn.spacestation14.com/manifest.json"); + public static readonly ConVar EngineModuleManifestUrl = + ConVarBuilder.Build("engine.moduleManifestUrl", "https://robust-builds.cdn.spacestation14.com/modules.json"); + public static readonly ConVar ManifestDownloadProtocolVersion = + ConVarBuilder.Build("engine.manifestDownloadProtocolVersion", 1); + public static readonly ConVar RobustAssemblyName = + ConVarBuilder.Build("engine.robustAssemblyName", "Robust.Client"); - public static readonly ConVar Hub = ConVar.Build("launcher.hub", [ + public static readonly ConVar Hub = ConVarBuilder.Build("launcher.hub", [ "https://hub.spacestation14.com/api/servers" ]); - public static readonly ConVar AuthServers = ConVar.Build("launcher.authServers", [ + public static readonly ConVar AuthServers = ConVarBuilder.Build("launcher.authServers", [ "https://auth.spacestation14.com/api/auth" ]); - public static readonly ConVar AuthProfiles = ConVar.Build("auth.profiles", []); - public static readonly ConVar AuthCurrent = ConVar.Build("auth.current"); + public static readonly ConVar AuthProfiles = ConVarBuilder.Build("auth.profiles", []); + public static readonly ConVar AuthCurrent = ConVarBuilder.Build("auth.current"); } \ No newline at end of file diff --git a/Nebula.Launcher/FileApis/FileApi.cs b/Nebula.Launcher/FileApis/FileApi.cs index d12881a..5cc6a58 100644 --- a/Nebula.Launcher/FileApis/FileApi.cs +++ b/Nebula.Launcher/FileApis/FileApi.cs @@ -36,7 +36,6 @@ public class FileApi : IReadWriteFileApi using var stream = File.OpenWrite(currPath); input.CopyTo(stream); stream.Flush(true); - Console.WriteLine(input.Length + " " + stream.Length); stream.Close(); return true; } diff --git a/Nebula.Launcher/Models/DownloadStreamHeaderFlags.cs b/Nebula.Launcher/Models/DownloadStreamHeaderFlags.cs new file mode 100644 index 0000000..c043e71 --- /dev/null +++ b/Nebula.Launcher/Models/DownloadStreamHeaderFlags.cs @@ -0,0 +1,15 @@ +using System; + +namespace Nebula.Launcher.Models; + +[Flags] +public enum DownloadStreamHeaderFlags +{ + None = 0, + + /// + /// If this flag is set on the download stream, individual files have been pre-compressed by the server. + /// This means each file has a compression header, and the launcher should not attempt to compress files itself. + /// + PreCompressed = 1 << 0 +} \ No newline at end of file diff --git a/Nebula.Launcher/Models/RobustBuildInfo.cs b/Nebula.Launcher/Models/RobustBuildInfo.cs new file mode 100644 index 0000000..98c4e8f --- /dev/null +++ b/Nebula.Launcher/Models/RobustBuildInfo.cs @@ -0,0 +1,8 @@ +namespace Nebula.Launcher.Models; + +public class RobustBuildInfo +{ + public ServerInfo BuildInfo; + public RobustManifestInfo RobustManifestInfo; + public RobustUrl Url; +} \ No newline at end of file diff --git a/Nebula.Launcher/Models/RobustServerEntry.cs b/Nebula.Launcher/Models/RobustServerEntry.cs index e1cfc9a..7aa94a8 100644 --- a/Nebula.Launcher/Models/RobustServerEntry.cs +++ b/Nebula.Launcher/Models/RobustServerEntry.cs @@ -4,26 +4,37 @@ using System.Text.Json.Serialization; namespace Nebula.Launcher.Models; -public sealed record AuthInfo(string Mode, string PublicKey); +public sealed record AuthInfo( + [property: JsonPropertyName("mode")] string Mode, + [property: JsonPropertyName("public_key")] string PublicKey); public sealed record BuildInfo( - string EngineVersion, - string ForkId, - string Version, - string DownloadUrl, - string ManifestUrl, - bool Acz, - string Hash, - string ManifestHash); + [property: JsonPropertyName("engine_version")] string EngineVersion, + [property: JsonPropertyName("fork_id")] string ForkId, + [property: JsonPropertyName("version")] string Version, + [property: JsonPropertyName("download_url")] string DownloadUrl, + [property: JsonPropertyName("manifest_download_url")] string ManifestDownloadUrl, + [property: JsonPropertyName("manifest_url")] string ManifestUrl, + [property: JsonPropertyName("acz")] bool Acz, + [property: JsonPropertyName("hash")] string Hash, + [property: JsonPropertyName("manifest_hash")] string ManifestHash); -public sealed record ServerLink(string Name, string Icon, string Url); -public sealed record ServerInfo(string ConnectAddress, AuthInfo Auth, BuildInfo Build, string Desc, List Links); +public sealed record ServerLink( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("icon")] string Icon, + [property: JsonPropertyName("url")] string Url); + +public sealed record ServerInfo( + [property: JsonPropertyName("connect_address")] string ConnectAddress, + [property: JsonPropertyName("auth")] AuthInfo Auth, + [property: JsonPropertyName("build")] BuildInfo Build, + [property: JsonPropertyName("desc")] string Desc, + [property: JsonPropertyName("links")] List Links); public sealed record EngineVersionInfo( - bool Insecure, - [property: JsonPropertyName("redirect")] - string? RedirectVersion, - Dictionary Platforms); + [property: JsonPropertyName("insecure")] bool Insecure, + [property: JsonPropertyName("redirect")] string? RedirectVersion, + [property: JsonPropertyName("platforms")] Dictionary Platforms); public sealed class EngineBuildInfo { @@ -37,27 +48,28 @@ public sealed class EngineBuildInfo public string Url = default!; } -public sealed record ServerHubInfo(string Address, ServerStatus StatusData, List InferredTags); +public sealed record ServerHubInfo( + [property: JsonPropertyName("address")] string Address, + [property: JsonPropertyName("statusData")] ServerStatus StatusData, + [property: JsonPropertyName("inferredTags")] List InferredTags); public sealed record ServerStatus( - string Map, - string Name, - List Tags, - string Preset, - int Players, - [property: JsonPropertyName("round_id")] - int RoundId, - [property: JsonPropertyName("run_level")] - int RunLevel, - [property: JsonPropertyName("panic_bunker")] - bool PanicBunker, - [property: JsonPropertyName("round_start_time")] - DateTime? RoundStartTime, - [property: JsonPropertyName("soft_max_players")] - int SoftMaxPlayers); + [property: JsonPropertyName("map")] string Map, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("tags")] List Tags, + [property: JsonPropertyName("preset")] string Preset, + [property: JsonPropertyName("players")] int Players, + [property: JsonPropertyName("round_id")] int RoundId, + [property: JsonPropertyName("run_level")] int RunLevel, + [property: JsonPropertyName("panic_bunker")] bool PanicBunker, + [property: JsonPropertyName("round_start_time")] DateTime? RoundStartTime, + [property: JsonPropertyName("soft_max_players")] int SoftMaxPlayers); -public sealed record ModulesInfo(Dictionary Modules); +public sealed record ModulesInfo( + [property: JsonPropertyName("modules")] Dictionary Modules); -public sealed record Module(Dictionary Versions); +public sealed record Module( + [property: JsonPropertyName("versions")] Dictionary Versions); -public sealed record ModuleVersionInfo(Dictionary Platforms); \ No newline at end of file +public sealed record ModuleVersionInfo( + [property: JsonPropertyName("platforms")] Dictionary Platforms); diff --git a/Nebula.Launcher/Models/RobustUrl.cs b/Nebula.Launcher/Models/RobustUrl.cs new file mode 100644 index 0000000..d4e024c --- /dev/null +++ b/Nebula.Launcher/Models/RobustUrl.cs @@ -0,0 +1,64 @@ +using System; +using Nebula.Launcher.Utils; + +namespace Nebula.Launcher.Models; + +public class RobustUrl +{ + public RobustUrl(string url) + { + if (!UriHelper.TryParseSs14Uri(url, out var uri)) + throw new Exception("Invalid scheme"); + + Uri = uri; + + HttpUri = UriHelper.GetServerApiAddress(Uri); + } + + public Uri Uri { get; } + public Uri HttpUri { get; } + public RobustPath InfoUri => new(this, "info"); + public RobustPath StatusUri => new(this, "status"); + + public override string ToString() + { + return HttpUri.ToString(); + } + + public static implicit operator Uri(RobustUrl url) + { + return url.HttpUri; + } + + public static explicit operator RobustUrl(string url) + { + return new RobustUrl(url); + } + + public static explicit operator RobustUrl(Uri uri) + { + return new RobustUrl(uri.ToString()); + } +} + +public class RobustPath +{ + public string Path; + public RobustUrl Url; + + public RobustPath(RobustUrl url, string path) + { + Url = url; + Path = path; + } + + public override string ToString() + { + return ((Uri)this).ToString(); + } + + public static implicit operator Uri(RobustPath path) + { + return new Uri(path.Url, path.Url.HttpUri.PathAndQuery + path.Path); + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Nebula.Launcher.csproj b/Nebula.Launcher/Nebula.Launcher.csproj index 1dffa58..161a840 100644 --- a/Nebula.Launcher/Nebula.Launcher.csproj +++ b/Nebula.Launcher/Nebula.Launcher.csproj @@ -25,9 +25,14 @@ All + + + + Utility.runtime.json + diff --git a/Nebula.Launcher/Services/AssemblyService.cs b/Nebula.Launcher/Services/AssemblyService.cs new file mode 100644 index 0000000..331a1ec --- /dev/null +++ b/Nebula.Launcher/Services/AssemblyService.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; +using Nebula.Launcher.FileApis; +using Robust.LoaderApi; + +namespace Nebula.Launcher.Services; + +[ServiceRegister] +public class AssemblyService +{ + private readonly List _assemblies = new(); + private readonly DebugService _debugService; + + public AssemblyService(DebugService debugService) + { + _debugService = debugService; + } + + public IReadOnlyList Assemblies => _assemblies; + + public AssemblyApi Mount(IFileApi fileApi) + { + var asmApi = new AssemblyApi(fileApi); + AssemblyLoadContext.Default.Resolving += (context, name) => OnAssemblyResolving(context, name, asmApi); + AssemblyLoadContext.Default.ResolvingUnmanagedDll += LoadContextOnResolvingUnmanaged; + + return asmApi; + } + + public bool TryGetLoader(Assembly clientAssembly, [NotNullWhen(true)] out ILoaderEntryPoint? loader) + { + loader = null; + // Find ILoaderEntryPoint with the LoaderEntryPointAttribute + var attrib = clientAssembly.GetCustomAttribute(); + if (attrib == null) + { + Console.WriteLine("No LoaderEntryPointAttribute found on Robust.Client assembly!"); + return false; + } + + var type = attrib.LoaderEntryPointType; + if (!type.IsAssignableTo(typeof(ILoaderEntryPoint))) + { + Console.WriteLine("Loader type '{0}' does not implement ILoaderEntryPoint!", type); + return false; + } + + loader = (ILoaderEntryPoint)Activator.CreateInstance(type)!; + return true; + } + + public bool TryOpenAssembly(string name, AssemblyApi assemblyApi, [NotNullWhen(true)] out Assembly? assembly) + { + if (!TryOpenAssemblyStream(name, assemblyApi, out var asm, out var pdb)) + { + assembly = null; + return false; + } + + assembly = AssemblyLoadContext.Default.LoadFromStream(asm, pdb); + _debugService.Log("LOADED ASSEMBLY " + name); + + + if (!_assemblies.Contains(assembly)) _assemblies.Add(assembly); + + asm.Dispose(); + pdb?.Dispose(); + return true; + } + + public bool TryOpenAssemblyStream(string name, AssemblyApi assemblyApi, [NotNullWhen(true)] out Stream? asm, + out Stream? pdb) + { + asm = null; + pdb = null; + + if (!assemblyApi.TryOpen($"{name}.dll", out asm)) + return false; + + assemblyApi.TryOpen($"{name}.pdb", out pdb); + return true; + } + + private Assembly? OnAssemblyResolving(AssemblyLoadContext context, AssemblyName name, AssemblyApi assemblyApi) + { + _debugService.Debug("Resolving assembly from FileAPI: " + name.Name); + return TryOpenAssembly(name.Name!, assemblyApi, out var assembly) ? assembly : null; + } + + private IntPtr LoadContextOnResolvingUnmanaged(Assembly assembly, string unmanaged) + { + var ourDir = Path.GetDirectoryName(typeof(AssemblyApi).Assembly.Location); + var a = Path.Combine(ourDir!, unmanaged); + + _debugService.Debug($"Loading dll lib: {a}"); + + if (NativeLibrary.TryLoad(a, out var handle)) + return handle; + + return IntPtr.Zero; + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Services/AuthService.cs b/Nebula.Launcher/Services/AuthService.cs index b4f8ca4..2389bd5 100644 --- a/Nebula.Launcher/Services/AuthService.cs +++ b/Nebula.Launcher/Services/AuthService.cs @@ -4,18 +4,22 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; using Nebula.Launcher.Models.Auth; namespace Nebula.Launcher.Services; [ServiceRegister] -public class AuthService +public partial class AuthService : ObservableObject { private readonly HttpClient _httpClient = new(); private readonly RestService _restService; private readonly DebugService _debugService; - public CurrentAuthInfo? SelectedAuth; + [ObservableProperty] + private CurrentAuthInfo? _selectedAuth; + + public string Reason = ""; public AuthService(RestService restService, DebugService debugService) { @@ -36,11 +40,15 @@ public class AuthService var result = await _restService.PostAsync( new AuthenticateRequest(login, password), authUrl, CancellationToken.None); - _debugService.Debug("RESULT " + result.Value); - if (result.Value is null) return false; + + if (result.Value is null) + { + Reason = result.Message; + return false; + } - SelectedAuth = new CurrentAuthInfo(result.Value.UserId, result.Value.Username, - new LoginToken(result.Value.Token, result.Value.ExpireTime), authServer); + SelectedAuth = new CurrentAuthInfo(result.Value.UserId, + new LoginToken(result.Value.Token, result.Value.ExpireTime), authLoginPassword); return true; } @@ -49,7 +57,7 @@ public class AuthService { if (SelectedAuth is null) return false; - var authUrl = new Uri($"{SelectedAuth.AuthServer}/ping"); + var authUrl = new Uri($"{SelectedAuth.AuthLoginPassword.AuthServer}/ping"); using var requestMessage = new HttpRequestMessage(HttpMethod.Get, authUrl); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", SelectedAuth.Token.Token); @@ -61,5 +69,5 @@ public class AuthService } } -public sealed record CurrentAuthInfo(Guid UserId, string Username, LoginToken Token, string AuthServer); +public sealed record CurrentAuthInfo(Guid UserId, LoginToken Token, AuthLoginPassword AuthLoginPassword); public record AuthLoginPassword(string Login, string Password, string AuthServer); diff --git a/Nebula.Launcher/Services/ConfigurationService.cs b/Nebula.Launcher/Services/ConfigurationService.cs index e12f927..ec04a43 100644 --- a/Nebula.Launcher/Services/ConfigurationService.cs +++ b/Nebula.Launcher/Services/ConfigurationService.cs @@ -3,25 +3,28 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using Nebula.Launcher.FileApis; namespace Nebula.Launcher.Services; -public class ConVar +public class ConVar { public string Name { get; } - public Type Type { get; } - public object? DefaultValue { get; } - - private ConVar(string name, Type type, object? defaultValue) + public Type Type => typeof(T); + public T? DefaultValue { get; } + + public ConVar(string name, T? defaultValue) { Name = name; - Type = type; DefaultValue = defaultValue; } +} - public static ConVar Build(string name, T? defaultValue = default) +public static class ConVarBuilder +{ + public static ConVar Build(string name, T? defaultValue = default) { - return new ConVar(name, typeof(T), defaultValue); + return new ConVar(name, defaultValue); } } @@ -37,10 +40,10 @@ public class ConfigurationService _debugService = debugService; } - public object? GetConfigValue(ConVar conVar) + public T? GetConfigValue(ConVar conVar) { - if(!_fileService.ConfigurationApi.TryOpen(conVar.Name, out var stream) || - !ReadStream(stream, conVar.Type, out var obj)) + if(!_fileService.ConfigurationApi.TryOpen(GetFileName(conVar), out var stream) || + !ReadStream(stream, out var obj)) return conVar.DefaultValue; _debugService.Log("Loading config file: " + conVar.Name); @@ -48,7 +51,7 @@ public class ConfigurationService return obj; } - public void SetConfigValue(ConVar conVar, object value) + public void SetConfigValue(ConVar conVar, object value) { if(conVar.Type != value.GetType()) { @@ -57,57 +60,55 @@ public class ConfigurationService } _debugService.Log("Saving config file: " + conVar.Name); - - var stream = new MemoryStream(); - try - { - using var st = new StreamWriter(stream); - st.Write(JsonSerializer.Serialize(value)); - st.Flush(); - _fileService.ConfigurationApi.Save(conVar.Name, st.BaseStream); - } - catch (Exception e) - { - _debugService.Error(e.Message); - } - - stream.Close(); + WriteStream(conVar, value); } - private bool ReadStream(Stream stream, Type type,[NotNullWhen(true)] out object? obj) + private bool ReadStream(Stream stream,[NotNullWhen(true)] out T? obj) { - obj = null; + obj = default; try { - obj = JsonSerializer.Deserialize(stream, JsonTypeInfo.CreateJsonTypeInfo(type, JsonSerializerOptions.Default)); + obj = JsonSerializer.Deserialize(stream); return obj != null; } catch (Exception e) { - _debugService.Error(e.Message); + _debugService.Error(e); return false; } } + + private void WriteStream(ConVar conVar, object value) + { + using var stream = new MemoryStream(); + + try + { + using var st = new StreamWriter(stream); + var ser = JsonSerializer.Serialize(value); + st.Write(ser); + st.Flush(); + stream.Seek(0, SeekOrigin.Begin); + _fileService.ConfigurationApi.Save(GetFileName(conVar), stream); + } + catch (Exception e) + { + _debugService.Error(e); + } + } + + private string GetFileName(ConVar conVar) + { + return conVar.Name + ".json"; + } } public static class ConfigExt { - public static T? GetConfigValue(this ConfigurationService configurationService,ConVar conVar) - { - var value = configurationService.GetConfigValue(conVar); - if (value is not T tv) return default; - return tv; - } - - public static bool TryGetConfigValue(this ConfigurationService configurationService,ConVar conVar,[NotNullWhen(true)] out object? value) + + public static bool TryGetConfigValue(this ConfigurationService configurationService,ConVar conVar, [NotNullWhen(true)] out T? value) { value = configurationService.GetConfigValue(conVar); return value != null; } - - public static bool TryGetConfigValue(this ConfigurationService configurationService,ConVar conVar, [NotNullWhen(true)] out T? value) - { - value = configurationService.GetConfigValue(conVar); - return value != null; - } } \ No newline at end of file diff --git a/Nebula.Launcher/Services/ContentService.Download.cs b/Nebula.Launcher/Services/ContentService.Download.cs new file mode 100644 index 0000000..2834483 --- /dev/null +++ b/Nebula.Launcher/Services/ContentService.Download.cs @@ -0,0 +1,259 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Numerics; +using System.Threading; +using System.Threading.Tasks; +using Nebula.Launcher.FileApis.Interfaces; +using Nebula.Launcher.Models; +using Nebula.Launcher.Utils; + +namespace Nebula.Launcher.Services; + +public partial class ContentService +{ + public bool CheckManifestExist(RobustManifestItem item) + { + return _fileService.ContentFileApi.Has(item.Hash); + } + + public async Task> EnsureItems(ManifestReader manifestReader, Uri downloadUri, + CancellationToken cancellationToken) + { + List allItems = []; + List items = []; + + while (manifestReader.TryReadItem(out var item)) + { + if (cancellationToken.IsCancellationRequested) + { + _debugService.Log("ensuring is cancelled!"); + return []; + } + + if (!CheckManifestExist(item.Value)) + items.Add(item.Value); + allItems.Add(item.Value); + } + + _debugService.Log("Download Count:" + items.Count); + + await Download(downloadUri, items, cancellationToken); + + _fileService.ManifestItems = allItems; + + return allItems; + } + + public async Task> EnsureItems(RobustManifestInfo info, + CancellationToken cancellationToken) + { + _debugService.Log("Getting manifest: " + info.Hash); + + if (_fileService.ManifestFileApi.TryOpen(info.Hash, out var stream)) + { + _debugService.Log("Loading manifest from: " + info.Hash); + return await EnsureItems(new ManifestReader(stream), info.DownloadUri, cancellationToken); + } + + _debugService.Log("Fetching manifest from: " + info.ManifestUri); + + var response = await _http.GetAsync(info.ManifestUri, cancellationToken); + if (!response.IsSuccessStatusCode) throw new Exception(); + + await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken); + _fileService.ManifestFileApi.Save(info.Hash, streamContent); + streamContent.Seek(0, SeekOrigin.Begin); + using var manifestReader = new ManifestReader(streamContent); + return await EnsureItems(manifestReader, info.DownloadUri, cancellationToken); + } + + public async Task Unpack(RobustManifestInfo info, IWriteFileApi otherApi, CancellationToken cancellationToken) + { + _debugService.Log("Unpack manifest files"); + var items = await EnsureItems(info, cancellationToken); + foreach (var item in items) + if (_fileService.ContentFileApi.TryOpen(item.Hash, out var stream)) + { + _debugService.Log($"Unpack {item.Hash} to: {item.Path}"); + otherApi.Save(item.Path, stream); + stream.Close(); + } + else + { + _debugService.Error("OH FUCK!! " + item.Path); + } + } + + public async Task Download(Uri contentCdn, List toDownload, CancellationToken cancellationToken) + { + if (toDownload.Count == 0 || cancellationToken.IsCancellationRequested) + { + _debugService.Log("Nothing to download! Fuck this!"); + return; + } + + _debugService.Log("Downloading from: " + contentCdn); + + var requestBody = new byte[toDownload.Count * 4]; + var reqI = 0; + foreach (var item in toDownload) + { + BinaryPrimitives.WriteInt32LittleEndian(requestBody.AsSpan(reqI, 4), item.Id); + reqI += 4; + } + + var request = new HttpRequestMessage(HttpMethod.Post, contentCdn); + request.Headers.Add( + "X-Robust-Download-Protocol", + _varService.GetConfigValue(CurrentConVar.ManifestDownloadProtocolVersion).ToString(CultureInfo.InvariantCulture)); + + request.Content = new ByteArrayContent(requestBody); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + + request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("zstd")); + var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + + if (cancellationToken.IsCancellationRequested) + { + _debugService.Log("Downloading is cancelled!"); + return; + } + + response.EnsureSuccessStatusCode(); + + var stream = await response.Content.ReadAsStreamAsync(); + var bandwidthStream = new BandwidthStream(stream); + stream = bandwidthStream; + if (response.Content.Headers.ContentEncoding.Contains("zstd")) + stream = new ZStdDecompressStream(stream); + + await using var streamDispose = stream; + + // Read flags header + var streamHeader = await stream.ReadExactAsync(4, null); + var streamFlags = (DownloadStreamHeaderFlags)BinaryPrimitives.ReadInt32LittleEndian(streamHeader); + var preCompressed = (streamFlags & DownloadStreamHeaderFlags.PreCompressed) != 0; + + // compressContext.SetParameter(ZSTD_cParameter.ZSTD_c_nbWorkers, 4); + // If the stream is pre-compressed we need to decompress the blobs to verify BLAKE2B hash. + // If it isn't, we need to manually try re-compressing individual files to store them. + var compressContext = preCompressed ? null : new ZStdCCtx(); + var decompressContext = preCompressed ? new ZStdDCtx() : null; + + // Normal file header: + // uncompressed length + // When preCompressed is set, we add: + // compressed length + var fileHeader = new byte[preCompressed ? 8 : 4]; + + + try + { + // Buffer for storing compressed ZStd data. + var compressBuffer = new byte[1024]; + + // Buffer for storing uncompressed data. + var readBuffer = new byte[1024]; + + var i = 0; + foreach (var item in toDownload) + { + if (cancellationToken.IsCancellationRequested) + { + _debugService.Log("Downloading is cancelled!"); + decompressContext?.Dispose(); + compressContext?.Dispose(); + return; + } + + // Read file header. + await stream.ReadExactAsync(fileHeader, null); + + var length = BinaryPrimitives.ReadInt32LittleEndian(fileHeader.AsSpan(0, 4)); + + EnsureBuffer(ref readBuffer, length); + var data = readBuffer.AsMemory(0, length); + + // Data to write to database. + var compression = ContentCompressionScheme.None; + var writeData = data; + + if (preCompressed) + { + // Compressed length from extended header. + var compressedLength = BinaryPrimitives.ReadInt32LittleEndian(fileHeader.AsSpan(4, 4)); + + if (compressedLength > 0) + { + EnsureBuffer(ref compressBuffer, compressedLength); + var compressedData = compressBuffer.AsMemory(0, compressedLength); + await stream.ReadExactAsync(compressedData, null); + + // Decompress so that we can verify hash down below. + + var decompressedLength = decompressContext!.Decompress(data.Span, compressedData.Span); + + if (decompressedLength != data.Length) + throw new Exception($"Compressed blob {i} had incorrect decompressed size!"); + + // Set variables so that the database write down below uses them. + compression = ContentCompressionScheme.ZStd; + writeData = compressedData; + } + else + { + await stream.ReadExactAsync(data, null); + } + } + else + { + await stream.ReadExactAsync(data, null); + } + + if (!preCompressed) + { + // File wasn't pre-compressed. We should try to manually compress it to save space in DB. + + + EnsureBuffer(ref compressBuffer, ZStd.CompressBound(data.Length)); + var compressLength = compressContext!.Compress(compressBuffer, data.Span); + + // Don't bother saving compressed data if it didn't save enough space. + if (compressLength + 10 < length) + { + // Set variables so that the database write down below uses them. + compression = ContentCompressionScheme.ZStd; + writeData = compressBuffer.AsMemory(0, compressLength); + } + } + + using var fileStream = new MemoryStream(data.ToArray()); + _fileService.ContentFileApi.Save(item.Hash, fileStream); + + _debugService.Log("file saved:" + item.Path); + i += 1; + } + } + finally + { + decompressContext?.Dispose(); + compressContext?.Dispose(); + } + } + + + private static void EnsureBuffer(ref byte[] buf, int needsFit) + { + if (buf.Length >= needsFit) + return; + + var newLen = 2 << BitOperations.Log2((uint)needsFit - 1); + + buf = new byte[newLen]; + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Services/ContentService.cs b/Nebula.Launcher/Services/ContentService.cs new file mode 100644 index 0000000..77824bf --- /dev/null +++ b/Nebula.Launcher/Services/ContentService.cs @@ -0,0 +1,48 @@ +using System; +using System.Data; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Nebula.Launcher.Models; + +namespace Nebula.Launcher.Services; + +[ServiceRegister] +public partial class ContentService +{ + private readonly AssemblyService _assemblyService; + private readonly DebugService _debugService; + private readonly EngineService _engineService; + private readonly FileService _fileService; + private readonly HttpClient _http = new(); + private readonly RestService _restService; + private readonly ConfigurationService _varService; + + public ContentService(RestService restService, DebugService debugService, ConfigurationService varService, + FileService fileService, EngineService engineService, AssemblyService assemblyService) + { + _restService = restService; + _debugService = debugService; + _varService = varService; + _fileService = fileService; + _engineService = engineService; + _assemblyService = assemblyService; + } + + public async Task GetBuildInfo(RobustUrl url, CancellationToken cancellationToken) + { + var info = new RobustBuildInfo(); + info.Url = url; + var bi = await _restService.GetAsync(url.InfoUri, cancellationToken); + if (bi.Value is null) throw new NoNullAllowedException(); + info.BuildInfo = bi.Value; + Console.WriteLine(info.BuildInfo); + info.RobustManifestInfo = info.BuildInfo.Build.Acz + ? new RobustManifestInfo(new RobustPath(info.Url, "manifest.txt"), new RobustPath(info.Url, "download"), + bi.Value.Build.ManifestHash) + : new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl), + new Uri(info.BuildInfo.Build.ManifestDownloadUrl), bi.Value.Build.ManifestHash); + + return info; + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Services/DebugService.cs b/Nebula.Launcher/Services/DebugService.cs index 425f489..b903d82 100644 --- a/Nebula.Launcher/Services/DebugService.cs +++ b/Nebula.Launcher/Services/DebugService.cs @@ -43,6 +43,13 @@ public class DebugService : IDisposable Log(LoggerCategory.Log, message); } + public void Error(Exception e) + { + Error(e.Message + "\r\n" + e.StackTrace); + if(e.InnerException != null) + Error(e.InnerException); + } + public void Dispose() { LogWriter.Dispose(); diff --git a/Nebula.Launcher/Services/EngineService.cs b/Nebula.Launcher/Services/EngineService.cs new file mode 100644 index 0000000..1b7590d --- /dev/null +++ b/Nebula.Launcher/Services/EngineService.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Nebula.Launcher.FileApis; +using Nebula.Launcher.Models; +using Nebula.Launcher.Utils; + +namespace Nebula.Launcher.Services; + +[ServiceRegister] +public class EngineService +{ + private readonly AssemblyService _assemblyService; + private readonly DebugService _debugService; + private readonly FileService _fileService; + private readonly RestService _restService; + private readonly IServiceProvider _serviceProvider; + private readonly ConfigurationService _varService; + public Dictionary ModuleInfos; + + public Dictionary VersionInfos; + + public EngineService(RestService restService, DebugService debugService, ConfigurationService varService, + FileService fileService, IServiceProvider serviceProvider, AssemblyService assemblyService) + { + _restService = restService; + _debugService = debugService; + _varService = varService; + _fileService = fileService; + _serviceProvider = serviceProvider; + _assemblyService = assemblyService; + + var loadTask = Task.Run(() => LoadEngineManifest(CancellationToken.None)); + loadTask.Wait(); + } + + public async Task LoadEngineManifest(CancellationToken cancellationToken) + { + var info = await _restService.GetAsync>( + new Uri(_varService.GetConfigValue(CurrentConVar.EngineManifestUrl)!), cancellationToken); + var moduleInfo = await _restService.GetAsync( + new Uri(_varService.GetConfigValue(CurrentConVar.EngineModuleManifestUrl)!), cancellationToken); + + if (info.Value is null) return; + VersionInfos = info.Value; + + if (moduleInfo.Value is null) return; + ModuleInfos = moduleInfo.Value.Modules; + + foreach (var f in ModuleInfos.Keys) _debugService.Debug(f); + } + + public EngineBuildInfo? GetVersionInfo(string version) + { + if (!VersionInfos.TryGetValue(version, out var foundVersion)) + return null; + + if (foundVersion.RedirectVersion != null) + return GetVersionInfo(foundVersion.RedirectVersion); + + var bestRid = RidUtility.FindBestRid(foundVersion.Platforms.Keys); + if (bestRid == null) bestRid = "linux-x64"; + + _debugService.Log("Selecting RID" + bestRid); + + return foundVersion.Platforms[bestRid]; + } + + public bool TryGetVersionInfo(string version, [NotNullWhen(true)] out EngineBuildInfo? info) + { + info = GetVersionInfo(version); + return info != null; + } + + public async Task EnsureEngine(string version) + { + _debugService.Log("Ensure engine " + version); + + if (!TryOpen(version)) await DownloadEngine(version); + + try + { + return _assemblyService.Mount(_fileService.OpenZip(version, _fileService.EngineFileApi)); + } + catch (Exception e) + { + _fileService.EngineFileApi.Remove(version); + throw; + } + } + + public async Task DownloadEngine(string version) + { + if (!TryGetVersionInfo(version, out var info)) + return; + + _debugService.Log("Downloading engine version " + version); + using var client = new HttpClient(); + var s = await client.GetStreamAsync(info.Url); + _fileService.EngineFileApi.Save(version, s); + await s.DisposeAsync(); + } + + public bool TryOpen(string version, [NotNullWhen(true)] out Stream? stream) + { + return _fileService.EngineFileApi.TryOpen(version, out stream); + } + + public bool TryOpen(string version) + { + var a = TryOpen(version, out var stream); + if (a) stream!.Close(); + return a; + } + + public EngineBuildInfo? GetModuleBuildInfo(string moduleName, string version) + { + if (!ModuleInfos.TryGetValue(moduleName, out var module) || + !module.Versions.TryGetValue(version, out var value)) + return null; + + var bestRid = RidUtility.FindBestRid(value.Platforms.Keys); + if (bestRid == null) throw new Exception("No engine version available for our platform!"); + + return value.Platforms[bestRid]; + } + + public bool TryGetModuleBuildInfo(string moduleName, string version, [NotNullWhen(true)] out EngineBuildInfo? info) + { + info = GetModuleBuildInfo(moduleName, version); + return info != null; + } + + public string ResolveModuleVersion(string moduleName, string engineVersion) + { + var engineVersionObj = Version.Parse(engineVersion); + var module = ModuleInfos[moduleName]; + var selectedVersion = module.Versions.Select(kv => new { Version = Version.Parse(kv.Key), kv.Key, kv.Value }) + .Where(kv => engineVersionObj >= kv.Version) + .MaxBy(kv => kv.Version); + + if (selectedVersion == null) throw new Exception(); + + return selectedVersion.Key; + } + + public async Task EnsureEngineModules(string moduleName, string engineVersion) + { + var moduleVersion = ResolveModuleVersion(moduleName, engineVersion); + if (!TryGetModuleBuildInfo(moduleName, moduleVersion, out var buildInfo)) + return null; + + var fileName = ConcatName(moduleName, moduleVersion); + + if (!TryOpen(fileName)) await DownloadEngineModule(moduleName, moduleVersion); + + try + { + return _assemblyService.Mount(_fileService.OpenZip(fileName, _fileService.EngineFileApi)); + } + catch (Exception e) + { + _fileService.EngineFileApi.Remove(fileName); + throw; + } + } + + public async Task DownloadEngineModule(string moduleName, string moduleVersion) + { + if (!TryGetModuleBuildInfo(moduleName, moduleVersion, out var info)) + return; + + _debugService.Log("Downloading engine module version " + moduleVersion); + using var client = new HttpClient(); + var s = await client.GetStreamAsync(info.Url); + _fileService.EngineFileApi.Save(ConcatName(moduleName, moduleVersion), s); + await s.DisposeAsync(); + } + + public string ConcatName(string moduleName, string moduleVersion) + { + return moduleName + "" + moduleVersion; + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Services/HubService.cs b/Nebula.Launcher/Services/HubService.cs index 661531c..0374b07 100644 --- a/Nebula.Launcher/Services/HubService.cs +++ b/Nebula.Launcher/Services/HubService.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.Threading; using Nebula.Launcher.Models; @@ -10,52 +8,38 @@ namespace Nebula.Launcher.Services; [ServiceRegister] public class HubService { + private readonly ConfigurationService _configurationService; private readonly RestService _restService; public Action? HubServerChangedEventArgs; - public readonly ObservableCollection HubList = new(); - - private readonly Dictionary> _servers = new(); - - + private bool _isUpdating = false; public HubService(ConfigurationService configurationService, RestService restService) { + _configurationService = configurationService; _restService = restService; - HubList.CollectionChanged += HubListCollectionChanged; - foreach (var hubUrl in configurationService.GetConfigValue(CurrentConVar.Hub)!) - { - HubList.Add(hubUrl); - } + UpdateHub(); } - private async void HubListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + public async void UpdateHub() { - if (e.NewItems is not null) - { - foreach (var hubUri in e.NewItems) - { - var urlStr = (string)hubUri; - var servers = await _restService.GetAsyncDefault>(new Uri(urlStr), [], CancellationToken.None); - _servers[urlStr] = servers; - HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs(servers, HubServerChangeAction.Add)); - } - } + if(_isUpdating) return; - if (e.OldItems is not null) + _isUpdating = true; + + + HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs([], HubServerChangeAction.Clear)); + + foreach (var urlStr in _configurationService.GetConfigValue(CurrentConVar.Hub)!) { - foreach (var hubUri in e.OldItems) - { - var urlStr = (string)hubUri; - if (_servers.TryGetValue(urlStr, out var serverInfos)) - { - _servers.Remove(urlStr); - HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs(serverInfos, HubServerChangeAction.Remove)); - } - } + var servers = await _restService.GetAsyncDefault>(new Uri(urlStr), [], CancellationToken.None); + HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs(servers, HubServerChangeAction.Add)); } + + _isUpdating = false; } + } public class HubServerChangedEventArgs : EventArgs @@ -72,5 +56,5 @@ public class HubServerChangedEventArgs : EventArgs public enum HubServerChangeAction { - Add, Remove, + Add, Remove, Clear, } \ No newline at end of file diff --git a/Nebula.Launcher/Services/RestService.cs b/Nebula.Launcher/Services/RestService.cs index f811cd2..c1ce229 100644 --- a/Nebula.Launcher/Services/RestService.cs +++ b/Nebula.Launcher/Services/RestService.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Threading; @@ -102,7 +103,6 @@ public class RestService private async Task> ReadResult(HttpResponseMessage response, CancellationToken cancellationToken) { var content = await response.Content.ReadAsStringAsync(cancellationToken); - //_debug.Debug("CONTENT:" + content); if (response.IsSuccessStatusCode) { @@ -110,7 +110,7 @@ public class RestService if (typeof(T) == typeof(RawResult)) return (new RestResult(new RawResult(content), null, response.StatusCode) as RestResult)!; - return new RestResult(JsonSerializer.Deserialize(content, _serializerOptions), null, + return new RestResult(await response.Content.AsJson(), null, response.StatusCode); } @@ -121,14 +121,14 @@ public class RestService public class RestResult { - public string? Message; + public string Message = "Ok"; public HttpStatusCode StatusCode; public T? Value; public RestResult(T? value, string? message, HttpStatusCode statusCode) { Value = value; - Message = message; + if (message != null) Message = message; StatusCode = statusCode; } @@ -151,4 +151,17 @@ public class RawResult { return result.Result; } +} + + +public static class HttpExt +{ + public static readonly JsonSerializerOptions JsonWebOptions = new(JsonSerializerDefaults.Web); + + public static async Task AsJson(this HttpContent content) where T : notnull + { + var str = await content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(str, JsonWebOptions) ?? + throw new JsonException("AsJson: did not expect null response"); + } } \ No newline at end of file diff --git a/Nebula.Launcher/Services/RunnerService.cs b/Nebula.Launcher/Services/RunnerService.cs new file mode 100644 index 0000000..213c999 --- /dev/null +++ b/Nebula.Launcher/Services/RunnerService.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Nebula.Launcher.Models; +using Robust.LoaderApi; + +namespace Nebula.Launcher.Services; + +[ServiceRegister] +public class RunnerService: IRedialApi +{ + private readonly AssemblyService _assemblyService; + private readonly AuthService _authService; + private readonly PopupMessageService _popupMessageService; + private readonly ContentService _contentService; + private readonly DebugService _debugService; + private readonly EngineService _engineService; + private readonly FileService _fileService; + private readonly ConfigurationService _varService; + + public RunnerService(ContentService contentService, DebugService debugService, ConfigurationService varService, + FileService fileService, EngineService engineService, AssemblyService assemblyService, AuthService authService, + PopupMessageService popupMessageService) + { + _contentService = contentService; + _debugService = debugService; + _varService = varService; + _fileService = fileService; + _engineService = engineService; + _assemblyService = assemblyService; + _authService = authService; + _popupMessageService = popupMessageService; + } + + public async Task Run(string[] runArgs, RobustBuildInfo buildInfo, IRedialApi redialApi, + CancellationToken cancellationToken) + { + _debugService.Log("Start Content!"); + + var engine = await _engineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion); + + if (engine is null) + throw new Exception("Engine version is not usable: " + buildInfo.BuildInfo.Build.EngineVersion); + + await _contentService.EnsureItems(buildInfo.RobustManifestInfo, cancellationToken); + + var extraMounts = new List + { + new(_fileService.HashApi, "/") + }; + + var module = + await _engineService.EnsureEngineModules("Robust.Client.WebView", buildInfo.BuildInfo.Build.EngineVersion); + if (module is not null) + extraMounts.Add(new ApiMount(module, "/")); + + var args = new MainArgs(runArgs, engine, redialApi, extraMounts); + + if (!_assemblyService.TryOpenAssembly(_varService.GetConfigValue(CurrentConVar.RobustAssemblyName)!, engine, out var clientAssembly)) + throw new Exception("Unable to locate Robust.Client.dll in engine build!"); + + if (!_assemblyService.TryGetLoader(clientAssembly, out var loader)) + return; + + await Task.Run(() => loader.Main(args), cancellationToken); + } + + public async Task RunGame(string urlraw) + { + var url = new RobustUrl(urlraw); + + using var cancelTokenSource = new CancellationTokenSource(); + var buildInfo = await _contentService.GetBuildInfo(url, cancelTokenSource.Token); + + var account = _authService.SelectedAuth; + if (account is null) + { + _popupMessageService.PopupInfo("Error! Auth is required!"); + return; + } + + if (buildInfo.BuildInfo.Auth.Mode != "Disabled") + { + Environment.SetEnvironmentVariable("ROBUST_AUTH_TOKEN", account.Token.Token); + Environment.SetEnvironmentVariable("ROBUST_AUTH_USERID", account.UserId.ToString()); + Environment.SetEnvironmentVariable("ROBUST_AUTH_PUBKEY", buildInfo.BuildInfo.Auth.PublicKey); + Environment.SetEnvironmentVariable("ROBUST_AUTH_SERVER", account.AuthLoginPassword.AuthServer); + } + + var args = new List + { + // Pass username to launched client. + // We don't load username from client_config.toml when launched via launcher. + "--username", account.AuthLoginPassword.Login, + + // Tell game we are launcher + "--cvar", "launch.launcher=true" + }; + + var connectionString = url.ToString(); + if (!string.IsNullOrEmpty(buildInfo.BuildInfo.ConnectAddress)) + connectionString = buildInfo.BuildInfo.ConnectAddress; + + // We are using the launcher. Don't show main menu etc.. + // Note: --launcher also implied --connect. + // For this reason, content bundles do not set --launcher. + args.Add("--launcher"); + + args.Add("--connect-address"); + args.Add(connectionString); + + args.Add("--ss14-address"); + args.Add(url.ToString()); + _debugService.Debug("Connect to " + url.ToString()); + + await Run(args.ToArray(), buildInfo, this, cancelTokenSource.Token); + } + + public async void Redial(Uri uri, string text = "") + { + await RunGame(uri.ToString()); + } +} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/AccountInfoViewModel.cs b/Nebula.Launcher/ViewModels/AccountInfoViewModel.cs index ad0d6ca..558d7e9 100644 --- a/Nebula.Launcher/ViewModels/AccountInfoViewModel.cs +++ b/Nebula.Launcher/ViewModels/AccountInfoViewModel.cs @@ -19,6 +19,7 @@ public partial class AccountInfoViewModel : ViewModelBase private readonly AuthService _authService; public ObservableCollection Accounts { get; } = new(); + public ObservableCollection AuthUrls { get; } = new(); [ObservableProperty] private string _currentLogin = String.Empty; @@ -29,9 +30,15 @@ public partial class AccountInfoViewModel : ViewModelBase [ObservableProperty] private string _currentAuthServer = String.Empty; - public ObservableCollection AuthUrls { get; } = new(); + [ObservableProperty] private bool _authUrlConfigExpand; - [ObservableProperty] private bool _pageEnabled = true; + [ObservableProperty] private int _authViewSpan = 1; + + [ObservableProperty] private bool _authMenuExpand; + + private bool _isProfilesEmpty; + + [ObservableProperty] private bool _isLogged; private AuthLoginPassword CurrentAlp { @@ -64,12 +71,9 @@ public partial class AccountInfoViewModel : ViewModelBase ReadAuthConfig(); } - public void AuthByALP(AuthLoginPassword authLoginPassword) + public void AuthByAlp(AuthLoginPassword authLoginPassword) { - CurrentLogin = authLoginPassword.Login; - CurrentPassword = authLoginPassword.Password; - CurrentAuthServer = authLoginPassword.AuthServer; - + CurrentAlp = authLoginPassword; DoAuth(); } @@ -80,19 +84,41 @@ public partial class AccountInfoViewModel : ViewModelBase if(await _authService.Auth(CurrentAlp)) { _popupMessageService.ClosePopup(); - _popupMessageService.PopupInfo("Hello, " + _authService.SelectedAuth!.Username); + _popupMessageService.PopupInfo("Hello, " + _authService.SelectedAuth!.AuthLoginPassword.Login); + IsLogged = true; + _configurationService.SetConfigValue(CurrentConVar.AuthCurrent, CurrentAlp); } else { _popupMessageService.ClosePopup(); - _popupMessageService.PopupInfo("Well, shit is happened"); + Logout(); + _popupMessageService.PopupInfo("Well, shit is happened: " + _authService.Reason); + } + } + + public void Logout() + { + IsLogged = false; + CurrentAlp = new AuthLoginPassword("", "", ""); + _authService.SelectedAuth = null; + } + + private void UpdateAuthMenu() + { + if (AuthMenuExpand || _isProfilesEmpty) + { + AuthViewSpan = 2; + } + else + { + AuthViewSpan = 1; } } private void AddAccount(AuthLoginPassword authLoginPassword) { - var onDelete = new DelegateCommand(a => Accounts.Remove(a)); - var onSelect = new DelegateCommand(AuthByALP); + var onDelete = new DelegateCommand(OnDeleteProfile); + var onSelect = new DelegateCommand(AuthByAlp); var alpm = new AuthLoginPasswordModel( authLoginPassword.Login, @@ -110,12 +136,17 @@ public partial class AccountInfoViewModel : ViewModelBase private void ReadAuthConfig() { foreach (var profile in - _configurationService.GetConfigValue(CurrentConVar.AuthProfiles)!) + _configurationService.GetConfigValue(CurrentConVar.AuthProfiles)!) { AddAccount(profile); } - var currProfile = _configurationService.GetConfigValue(CurrentConVar.AuthProfiles); + if (Accounts.Count == 0) + { + UpdateAuthMenu(); + } + + var currProfile = _configurationService.GetConfigValue(CurrentConVar.AuthCurrent); if (currProfile != null) { @@ -124,7 +155,7 @@ public partial class AccountInfoViewModel : ViewModelBase } AuthUrls.Clear(); - var authUrls = _configurationService.GetConfigValue(CurrentConVar.AuthServers)!; + var authUrls = _configurationService.GetConfigValue(CurrentConVar.AuthServers)!; foreach (var url in authUrls) { AuthUrls.Add(url); @@ -132,10 +163,39 @@ public partial class AccountInfoViewModel : ViewModelBase } [RelayCommand] - public void OnSaveProfile() + private void OnSaveProfile() { AddAccount(CurrentAlp); - _configurationService.SetConfigValue(CurrentConVar.AuthProfiles, Accounts.Select(a => (AuthLoginPassword) a).ToArray()); + _isProfilesEmpty = Accounts.Count == 0; + UpdateAuthMenu(); + DirtyProfile(); + } + + private void OnDeleteProfile(AuthLoginPasswordModel account) + { + Accounts.Remove(account); + _isProfilesEmpty = Accounts.Count == 0; + UpdateAuthMenu(); + DirtyProfile(); + } + + [RelayCommand] + private void OnExpandAuthUrl() + { + AuthUrlConfigExpand = !AuthUrlConfigExpand; + } + + [RelayCommand] + private void OnExpandAuthView() + { + AuthMenuExpand = !AuthMenuExpand; + UpdateAuthMenu(); + } + + private void DirtyProfile() + { + _configurationService.SetConfigValue(CurrentConVar.AuthProfiles, + Accounts.Select(a => (AuthLoginPassword) a).ToArray()); } public string AuthItemSelect diff --git a/Nebula.Launcher/ViewModels/MainViewModel.cs b/Nebula.Launcher/ViewModels/MainViewModel.cs index 3083c3d..11b7e59 100644 --- a/Nebula.Launcher/ViewModels/MainViewModel.cs +++ b/Nebula.Launcher/ViewModels/MainViewModel.cs @@ -2,17 +2,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using Avalonia.Controls; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; -using CommunityToolkit.Mvvm.Messaging; -using Microsoft.Extensions.DependencyInjection; +using JetBrains.Annotations; using Nebula.Launcher.Models; using Nebula.Launcher.Services; using Nebula.Launcher.ViewHelper; using Nebula.Launcher.Views; -using Nebula.Launcher.Views.Pages; namespace Nebula.Launcher.ViewModels; @@ -29,6 +25,7 @@ public partial class MainViewModel : ViewModelBase SelectedListItem = Items.First(vm => vm.ModelType == typeof(AccountInfoViewModel)); } + [UsedImplicitly] public MainViewModel(AccountInfoViewModel accountInfoViewModel, PopupMessageService popupMessageService, IServiceProvider serviceProvider): base(serviceProvider) { diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs new file mode 100644 index 0000000..12ceb7e --- /dev/null +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -0,0 +1,33 @@ +using System; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Nebula.Launcher.Models; +using Nebula.Launcher.Services; + +namespace Nebula.Launcher.ViewModels; + +public partial class ServerEntryModelView : ViewModelBase +{ + + private readonly IServiceProvider _serviceProvider; + private readonly RunnerService _runnerService; + private readonly PopupMessageService _popupMessageService; + private readonly RestService _restService; + + public ServerHubInfo ServerHubInfo { get; } + + public ServerEntryModelView(IServiceProvider serviceProvider, ServerHubInfo serverHubInfo) : base(serviceProvider) + { + _serviceProvider = serviceProvider; + _runnerService = serviceProvider.GetService()!; + _popupMessageService = serviceProvider.GetService()!; + _restService = serviceProvider.GetService()!; + ServerHubInfo = serverHubInfo; + } + + public async void OnConnectRequired() + { + _popupMessageService.PopupInfo("Running server: " + ServerHubInfo.StatusData.Name); + await _runnerService.RunGame(ServerHubInfo.Address); + } +} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/ServerListViewModel.cs b/Nebula.Launcher/ViewModels/ServerListViewModel.cs index e79a9f4..fa36ebb 100644 --- a/Nebula.Launcher/ViewModels/ServerListViewModel.cs +++ b/Nebula.Launcher/ViewModels/ServerListViewModel.cs @@ -13,26 +13,26 @@ namespace Nebula.Launcher.ViewModels; [ViewRegister(typeof(ServerListView))] public partial class ServerListViewModel : ViewModelBase { - public ObservableCollection ServerInfos { get; } = new(); + private readonly IServiceProvider _serviceProvider; + private readonly HubService _hubService; + public ObservableCollection ServerInfos { get; } = new(); public Action? OnSearchChange; [ObservableProperty] private string _searchText; - - [ObservableProperty] - private ServerHubInfo? _selectedListItem; - - private List UnsortedServers { get; } = new List(); + private List UnsortedServers { get; } = new(); //Design think public ServerListViewModel() { - ServerInfos.Add(new ServerHubInfo("ss14://localhost",new ServerStatus("Nebula","TestCraft", ["16+","RU"], "super", 12,55,1,false,DateTime.Now, 20),[])); + ServerInfos.Add(CreateServerView(new ServerHubInfo("ss14://localhost",new ServerStatus("Nebula","TestCraft", ["16+","RU"], "super", 12,55,1,false,DateTime.Now, 20),[]))); } //real think public ServerListViewModel(IServiceProvider serviceProvider, HubService hubService) : base(serviceProvider) { + _serviceProvider = serviceProvider; + _hubService = hubService; hubService.HubServerChangedEventArgs += HubServerChangedEventArgs; OnSearchChange += OnChangeSearch; } @@ -51,13 +51,17 @@ public partial class ServerListViewModel : ViewModelBase UnsortedServers.Add(info); } } - else + if(obj.Action == HubServerChangeAction.Remove) { foreach (var info in obj.Items) { UnsortedServers.Remove(info); } } + if(obj.Action == HubServerChangeAction.Clear) + { + UnsortedServers.Clear(); + } SortServers(); } @@ -68,7 +72,7 @@ public partial class ServerListViewModel : ViewModelBase UnsortedServers.Sort(new ServerComparer()); foreach (var server in UnsortedServers.Where(CheckServerThink)) { - ServerInfos.Add(server); + ServerInfos.Add(CreateServerView(server)); } } @@ -77,6 +81,21 @@ public partial class ServerListViewModel : ViewModelBase if (string.IsNullOrEmpty(SearchText)) return true; return hubInfo.StatusData.Name.ToLower().Contains(SearchText.ToLower()); } + + private ServerEntryModelView CreateServerView(ServerHubInfo serverHubInfo) + { + return new ServerEntryModelView(_serviceProvider, serverHubInfo); + } + + public void FilterRequired() + { + + } + + public void UpdateRequired() + { + _hubService.UpdateHub(); + } } public class ServerComparer : IComparer diff --git a/Nebula.Launcher/Views/MainView.axaml b/Nebula.Launcher/Views/MainView.axaml index 54da439..5ea0102 100644 --- a/Nebula.Launcher/Views/MainView.axaml +++ b/Nebula.Launcher/Views/MainView.axaml @@ -9,7 +9,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="clr-namespace:Nebula.Launcher.Models" - xmlns:popup="clr-namespace:Nebula.Launcher.Views.Popup" xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -21,7 +20,7 @@ ColumnDefinitions="65,*" IsEnabled="{Binding IsEnabled}" Margin="0" - RowDefinitions="*,40"> + RowDefinitions="*,30"> - - - - + @@ -104,16 +105,16 @@ Grid.Row="0"> - - - - - - - + + Padding="0"> - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Nebula.Launcher/Views/Pages/ServerListView.axaml b/Nebula.Launcher/Views/Pages/ServerListView.axaml index c3a564b..dae53af 100644 --- a/Nebula.Launcher/Views/Pages/ServerListView.axaml +++ b/Nebula.Launcher/Views/Pages/ServerListView.axaml @@ -23,7 +23,7 @@ ItemsSource="{Binding ServerInfos}" Padding="0"> - + @@ -83,37 +83,37 @@ @@ -123,6 +123,7 @@ +