- add: Content manipulation think
This commit is contained in:
BIN
Nebula.Launcher/Assets/refresh.png
Normal file
BIN
Nebula.Launcher/Assets/refresh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -4,22 +4,22 @@ namespace Nebula.Launcher;
|
||||
|
||||
public static class CurrentConVar
|
||||
{
|
||||
public static readonly ConVar EngineManifestUrl =
|
||||
ConVar.Build<string>("engine.manifestUrl", "https://robust-builds.cdn.spacestation14.com/manifest.json");
|
||||
public static readonly ConVar EngineModuleManifestUrl =
|
||||
ConVar.Build<string>("engine.moduleManifestUrl", "https://robust-builds.cdn.spacestation14.com/modules.json");
|
||||
public static readonly ConVar ManifestDownloadProtocolVersion =
|
||||
ConVar.Build<int>("engine.manifestDownloadProtocolVersion", 1);
|
||||
public static readonly ConVar RobustAssemblyName =
|
||||
ConVar.Build("engine.robustAssemblyName", "Robust.Client");
|
||||
public static readonly ConVar<string> EngineManifestUrl =
|
||||
ConVarBuilder.Build("engine.manifestUrl", "https://robust-builds.cdn.spacestation14.com/manifest.json");
|
||||
public static readonly ConVar<string> EngineModuleManifestUrl =
|
||||
ConVarBuilder.Build("engine.moduleManifestUrl", "https://robust-builds.cdn.spacestation14.com/modules.json");
|
||||
public static readonly ConVar<int> ManifestDownloadProtocolVersion =
|
||||
ConVarBuilder.Build("engine.manifestDownloadProtocolVersion", 1);
|
||||
public static readonly ConVar<string> RobustAssemblyName =
|
||||
ConVarBuilder.Build("engine.robustAssemblyName", "Robust.Client");
|
||||
|
||||
public static readonly ConVar Hub = ConVar.Build<string[]>("launcher.hub", [
|
||||
public static readonly ConVar<string[]> Hub = ConVarBuilder.Build<string[]>("launcher.hub", [
|
||||
"https://hub.spacestation14.com/api/servers"
|
||||
]);
|
||||
public static readonly ConVar AuthServers = ConVar.Build<string[]>("launcher.authServers", [
|
||||
public static readonly ConVar<string[]> AuthServers = ConVarBuilder.Build<string[]>("launcher.authServers", [
|
||||
"https://auth.spacestation14.com/api/auth"
|
||||
]);
|
||||
|
||||
public static readonly ConVar AuthProfiles = ConVar.Build<AuthLoginPassword[]>("auth.profiles", []);
|
||||
public static readonly ConVar AuthCurrent = ConVar.Build<AuthLoginPassword>("auth.current");
|
||||
public static readonly ConVar<AuthLoginPassword[]> AuthProfiles = ConVarBuilder.Build<AuthLoginPassword[]>("auth.profiles", []);
|
||||
public static readonly ConVar<AuthLoginPassword> AuthCurrent = ConVarBuilder.Build<AuthLoginPassword>("auth.current");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
15
Nebula.Launcher/Models/DownloadStreamHeaderFlags.cs
Normal file
15
Nebula.Launcher/Models/DownloadStreamHeaderFlags.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Nebula.Launcher.Models;
|
||||
|
||||
[Flags]
|
||||
public enum DownloadStreamHeaderFlags
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
PreCompressed = 1 << 0
|
||||
}
|
||||
8
Nebula.Launcher/Models/RobustBuildInfo.cs
Normal file
8
Nebula.Launcher/Models/RobustBuildInfo.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Nebula.Launcher.Models;
|
||||
|
||||
public class RobustBuildInfo
|
||||
{
|
||||
public ServerInfo BuildInfo;
|
||||
public RobustManifestInfo RobustManifestInfo;
|
||||
public RobustUrl Url;
|
||||
}
|
||||
@@ -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<ServerLink> 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<ServerLink> Links);
|
||||
|
||||
public sealed record EngineVersionInfo(
|
||||
bool Insecure,
|
||||
[property: JsonPropertyName("redirect")]
|
||||
string? RedirectVersion,
|
||||
Dictionary<string, EngineBuildInfo> Platforms);
|
||||
[property: JsonPropertyName("insecure")] bool Insecure,
|
||||
[property: JsonPropertyName("redirect")] string? RedirectVersion,
|
||||
[property: JsonPropertyName("platforms")] Dictionary<string, EngineBuildInfo> 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<string> InferredTags);
|
||||
public sealed record ServerHubInfo(
|
||||
[property: JsonPropertyName("address")] string Address,
|
||||
[property: JsonPropertyName("statusData")] ServerStatus StatusData,
|
||||
[property: JsonPropertyName("inferredTags")] List<string> InferredTags);
|
||||
|
||||
public sealed record ServerStatus(
|
||||
string Map,
|
||||
string Name,
|
||||
List<string> 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<string> 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<string, Module> Modules);
|
||||
public sealed record ModulesInfo(
|
||||
[property: JsonPropertyName("modules")] Dictionary<string, Module> Modules);
|
||||
|
||||
public sealed record Module(Dictionary<string, ModuleVersionInfo> Versions);
|
||||
public sealed record Module(
|
||||
[property: JsonPropertyName("versions")] Dictionary<string, ModuleVersionInfo> Versions);
|
||||
|
||||
public sealed record ModuleVersionInfo(Dictionary<string, EngineBuildInfo> Platforms);
|
||||
public sealed record ModuleVersionInfo(
|
||||
[property: JsonPropertyName("platforms")] Dictionary<string, EngineBuildInfo> Platforms);
|
||||
|
||||
64
Nebula.Launcher/Models/RobustUrl.cs
Normal file
64
Nebula.Launcher/Models/RobustUrl.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,14 @@
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||
<PackageReference Include="libsodium" Version="1.0.20" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Robust.Natives" Version="0.1.1" />
|
||||
<PackageReference Include="SharpZstd.Interop" Version="1.5.6" />
|
||||
<EmbeddedResource Include="Utils\runtime.json">
|
||||
<LogicalName>Utility.runtime.json</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
107
Nebula.Launcher/Services/AssemblyService.cs
Normal file
107
Nebula.Launcher/Services/AssemblyService.cs
Normal file
@@ -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<Assembly> _assemblies = new();
|
||||
private readonly DebugService _debugService;
|
||||
|
||||
public AssemblyService(DebugService debugService)
|
||||
{
|
||||
_debugService = debugService;
|
||||
}
|
||||
|
||||
public IReadOnlyList<Assembly> 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<LoaderEntryPointAttribute>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<AuthenticateResponse, AuthenticateRequest>(
|
||||
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);
|
||||
|
||||
@@ -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<T>
|
||||
{
|
||||
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<T>(string name, T? defaultValue = default)
|
||||
public static class ConVarBuilder
|
||||
{
|
||||
public static ConVar<T> Build<T>(string name, T? defaultValue = default)
|
||||
{
|
||||
return new ConVar(name, typeof(T), defaultValue);
|
||||
return new ConVar<T>(name, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +40,10 @@ public class ConfigurationService
|
||||
_debugService = debugService;
|
||||
}
|
||||
|
||||
public object? GetConfigValue(ConVar conVar)
|
||||
public T? GetConfigValue<T>(ConVar<T> 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<T>(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<T>(ConVar<T> 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<T>(Stream stream,[NotNullWhen(true)] out T? obj)
|
||||
{
|
||||
obj = null;
|
||||
obj = default;
|
||||
try
|
||||
{
|
||||
obj = JsonSerializer.Deserialize(stream, JsonTypeInfo.CreateJsonTypeInfo(type, JsonSerializerOptions.Default));
|
||||
obj = JsonSerializer.Deserialize<T>(stream);
|
||||
return obj != null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_debugService.Error(e.Message);
|
||||
_debugService.Error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteStream<T>(ConVar<T> 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<T>(ConVar<T> conVar)
|
||||
{
|
||||
return conVar.Name + ".json";
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConfigExt
|
||||
{
|
||||
public static T? GetConfigValue<T>(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<T>(this ConfigurationService configurationService,ConVar<T> conVar, [NotNullWhen(true)] out T? value)
|
||||
{
|
||||
value = configurationService.GetConfigValue(conVar);
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public static bool TryGetConfigValue<T>(this ConfigurationService configurationService,ConVar conVar, [NotNullWhen(true)] out T? value)
|
||||
{
|
||||
value = configurationService.GetConfigValue<T>(conVar);
|
||||
return value != null;
|
||||
}
|
||||
}
|
||||
259
Nebula.Launcher/Services/ContentService.Download.cs
Normal file
259
Nebula.Launcher/Services/ContentService.Download.cs
Normal file
@@ -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<List<RobustManifestItem>> EnsureItems(ManifestReader manifestReader, Uri downloadUri,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<RobustManifestItem> allItems = [];
|
||||
List<RobustManifestItem> 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<List<RobustManifestItem>> 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<RobustManifestItem> 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:
|
||||
// <int32> uncompressed length
|
||||
// When preCompressed is set, we add:
|
||||
// <int32> 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];
|
||||
}
|
||||
}
|
||||
48
Nebula.Launcher/Services/ContentService.cs
Normal file
48
Nebula.Launcher/Services/ContentService.cs
Normal file
@@ -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<RobustBuildInfo> GetBuildInfo(RobustUrl url, CancellationToken cancellationToken)
|
||||
{
|
||||
var info = new RobustBuildInfo();
|
||||
info.Url = url;
|
||||
var bi = await _restService.GetAsync<ServerInfo>(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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
189
Nebula.Launcher/Services/EngineService.cs
Normal file
189
Nebula.Launcher/Services/EngineService.cs
Normal file
@@ -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<string, Module> ModuleInfos;
|
||||
|
||||
public Dictionary<string, EngineVersionInfo> 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<Dictionary<string, EngineVersionInfo>>(
|
||||
new Uri(_varService.GetConfigValue(CurrentConVar.EngineManifestUrl)!), cancellationToken);
|
||||
var moduleInfo = await _restService.GetAsync<ModulesInfo>(
|
||||
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<AssemblyApi?> 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<AssemblyApi?> 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;
|
||||
}
|
||||
}
|
||||
@@ -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>? HubServerChangedEventArgs;
|
||||
|
||||
public readonly ObservableCollection<string> HubList = new();
|
||||
|
||||
private readonly Dictionary<string, List<ServerHubInfo>> _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<string[]>(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<List<ServerHubInfo>>(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<List<ServerHubInfo>>(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,
|
||||
}
|
||||
@@ -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<RestResult<T>> ReadResult<T>(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<RawResult>(new RawResult(content), null, response.StatusCode) as RestResult<T>)!;
|
||||
|
||||
return new RestResult<T>(JsonSerializer.Deserialize<T>(content, _serializerOptions), null,
|
||||
return new RestResult<T>(await response.Content.AsJson<T>(), null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -121,14 +121,14 @@ public class RestService
|
||||
|
||||
public class RestResult<T>
|
||||
{
|
||||
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<T> AsJson<T>(this HttpContent content) where T : notnull
|
||||
{
|
||||
var str = await content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<T>(str, JsonWebOptions) ??
|
||||
throw new JsonException("AsJson: did not expect null response");
|
||||
}
|
||||
}
|
||||
124
Nebula.Launcher/Services/RunnerService.cs
Normal file
124
Nebula.Launcher/Services/RunnerService.cs
Normal file
@@ -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<ApiMount>
|
||||
{
|
||||
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<string>
|
||||
{
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
||||
private readonly AuthService _authService;
|
||||
|
||||
public ObservableCollection<AuthLoginPasswordModel> Accounts { get; } = new();
|
||||
public ObservableCollection<string> 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<string> 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<AuthLoginPasswordModel>(a => Accounts.Remove(a));
|
||||
var onSelect = new DelegateCommand<AuthLoginPasswordModel>(AuthByALP);
|
||||
var onDelete = new DelegateCommand<AuthLoginPasswordModel>(OnDeleteProfile);
|
||||
var onSelect = new DelegateCommand<AuthLoginPasswordModel>(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<AuthLoginPassword[]>(CurrentConVar.AuthProfiles)!)
|
||||
_configurationService.GetConfigValue(CurrentConVar.AuthProfiles)!)
|
||||
{
|
||||
AddAccount(profile);
|
||||
}
|
||||
|
||||
var currProfile = _configurationService.GetConfigValue<AuthLoginPassword>(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<string[]>(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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
33
Nebula.Launcher/ViewModels/ServerEntryModelView.cs
Normal file
33
Nebula.Launcher/ViewModels/ServerEntryModelView.cs
Normal file
@@ -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<RunnerService>()!;
|
||||
_popupMessageService = serviceProvider.GetService<PopupMessageService>()!;
|
||||
_restService = serviceProvider.GetService<RestService>()!;
|
||||
ServerHubInfo = serverHubInfo;
|
||||
}
|
||||
|
||||
public async void OnConnectRequired()
|
||||
{
|
||||
_popupMessageService.PopupInfo("Running server: " + ServerHubInfo.StatusData.Name);
|
||||
await _runnerService.RunGame(ServerHubInfo.Address);
|
||||
}
|
||||
}
|
||||
@@ -13,26 +13,26 @@ namespace Nebula.Launcher.ViewModels;
|
||||
[ViewRegister(typeof(ServerListView))]
|
||||
public partial class ServerListViewModel : ViewModelBase
|
||||
{
|
||||
public ObservableCollection<ServerHubInfo> ServerInfos { get; } = new();
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly HubService _hubService;
|
||||
public ObservableCollection<ServerEntryModelView> ServerInfos { get; } = new();
|
||||
|
||||
public Action? OnSearchChange;
|
||||
|
||||
[ObservableProperty] private string _searchText;
|
||||
|
||||
[ObservableProperty]
|
||||
private ServerHubInfo? _selectedListItem;
|
||||
|
||||
private List<ServerHubInfo> UnsortedServers { get; } = new List<ServerHubInfo>();
|
||||
private List<ServerHubInfo> 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<ServerHubInfo>
|
||||
|
||||
@@ -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">
|
||||
<Design.DataContext>
|
||||
@@ -21,7 +20,7 @@
|
||||
ColumnDefinitions="65,*"
|
||||
IsEnabled="{Binding IsEnabled}"
|
||||
Margin="0"
|
||||
RowDefinitions="*,40">
|
||||
RowDefinitions="*,30">
|
||||
|
||||
<TransitioningContentControl
|
||||
Content="{Binding CurrentPage}"
|
||||
@@ -84,10 +83,12 @@
|
||||
Grid.Row="1"
|
||||
Margin="0,0,0,0"
|
||||
Padding="5">
|
||||
<Panel>
|
||||
<Label HorizontalAlignment="Left" VerticalAlignment="Center">cinka.ru</Label>
|
||||
<Label HorizontalAlignment="Right" VerticalAlignment="Center">v0.01</Label>
|
||||
</Panel>
|
||||
<Label FontSize="10" Foreground="#777777">
|
||||
<Panel>
|
||||
<TextBlock HorizontalAlignment="Left" VerticalAlignment="Center">cinka.ru</TextBlock>
|
||||
<TextBlock HorizontalAlignment="Right" VerticalAlignment="Center">v0.01</TextBlock>
|
||||
</Panel>
|
||||
</Label>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
@@ -104,16 +105,16 @@
|
||||
Grid.Row="0">
|
||||
<Panel Margin="12,0,0,0" VerticalAlignment="Center">
|
||||
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<Label VerticalAlignment="Center" Content="{Binding CurrentTitle}"/>
|
||||
<Label Content="{Binding CurrentTitle}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Button
|
||||
Command="{Binding ClosePopupCommand}"
|
||||
Content="X"
|
||||
Margin="0"
|
||||
CornerRadius="0,10,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0"
|
||||
Padding="10,8,10,8"
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding ClosePopupCommand}"/>
|
||||
VerticalAlignment="Stretch" />
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -7,60 +7,15 @@
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels" IsEnabled="{Binding PageEnabled}">
|
||||
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Design.DataContext>
|
||||
<viewModels:AccountInfoViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid ColumnDefinitions="*,1.5*" RowDefinitions="*" Margin="15">
|
||||
<StackPanel Grid.Column="0" Grid.Row="0">
|
||||
<Border
|
||||
CornerRadius="10"
|
||||
Margin="5"
|
||||
Padding="15">
|
||||
<StackPanel HorizontalAlignment="Center" Spacing="15">
|
||||
<Image
|
||||
Height="100"
|
||||
Margin="0,0,0,20"
|
||||
Source="/Assets/account.png" />
|
||||
<Grid ColumnDefinitions="100, 100" RowDefinitions="Auto, Auto, Auto, Auto" VerticalAlignment="Center">
|
||||
<Label Grid.Column="0" Grid.Row="0" VerticalAlignment="Center">Login:</Label>
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Grid.Row="0"
|
||||
Text="{Binding CurrentLogin}"/>
|
||||
<Label Grid.Column="0" Grid.Row="1" VerticalAlignment="Center">Password:</Label>
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
PasswordChar="#"
|
||||
Text="{Binding CurrentPassword}" />
|
||||
<Label Grid.Column="0" Grid.Row="2" VerticalAlignment="Center">Auth server:</Label>
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Grid.Row="2"
|
||||
Text="{Binding CurrentAuthServer}"/>
|
||||
<ScrollViewer Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2" Height="80">
|
||||
<ListBox Margin="15" Background="#00000000" ItemsSource="{Binding AuthUrls}" SelectedItem="{Binding AuthItemSelect}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Label><TextBlock Text="{Binding}"/></Label>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
Margin="5" Spacing="5"
|
||||
Orientation="Horizontal">
|
||||
<Button Command="{Binding OnSaveProfile}"><Label>Save profile</Label></Button>
|
||||
<Button Command="{Binding DoAuth}"><Label>Auth</Label></Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,1.5*"
|
||||
Margin="15"
|
||||
RowDefinitions="*">
|
||||
<StackPanel Grid.Column="1" Grid.Row="0">
|
||||
<Border
|
||||
CornerRadius="10,10,0,0"
|
||||
@@ -71,28 +26,36 @@
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<ListBox
|
||||
Background="#00000000"
|
||||
Classes="AccountSelector"
|
||||
ItemsSource="{Binding Accounts}"
|
||||
Padding="0" Classes="AccountSelector">
|
||||
Padding="0">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type viewModels:AuthLoginPasswordModel}">
|
||||
<Border Margin="5,5,5,0"
|
||||
<Border
|
||||
CornerRadius="0,10,0,10"
|
||||
Margin="5,5,5,0"
|
||||
VerticalAlignment="Center">
|
||||
<Panel>
|
||||
<StackPanel Margin="10,5,5,5" Orientation="Horizontal">
|
||||
<Label>Name:</Label>
|
||||
<Label><TextBlock Text="{Binding Login}"/></Label>
|
||||
<Label>
|
||||
<TextBlock Text="{Binding Login}" />
|
||||
</Label>
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button CornerRadius="0,0,0,10" Padding="5" Command="{Binding OnSelect}">
|
||||
<Button
|
||||
Command="{Binding OnSelect}"
|
||||
CornerRadius="0,0,0,10"
|
||||
Padding="5">
|
||||
<Label>
|
||||
Select
|
||||
</Label>
|
||||
</Button>
|
||||
<Button
|
||||
BorderThickness="2,0,0,0"
|
||||
Command="{Binding OnDelete}"
|
||||
CornerRadius="0,10,0,0"
|
||||
Padding="5" Command="{Binding OnDelete}">
|
||||
Padding="5">
|
||||
<Label>
|
||||
Delete
|
||||
</Label>
|
||||
@@ -102,8 +65,107 @@
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</ListBox>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="{Binding AuthViewSpan}"
|
||||
Grid.Row="0">
|
||||
<Border
|
||||
CornerRadius="10"
|
||||
Margin="5"
|
||||
Padding="15">
|
||||
<Panel>
|
||||
<StackPanel IsVisible="{Binding !IsLogged}" Spacing="15">
|
||||
<Image
|
||||
Height="100"
|
||||
Margin="0,0,0,20"
|
||||
Source="/Assets/account.png" />
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Label VerticalAlignment="Center">
|
||||
Login:
|
||||
</Label>
|
||||
<TextBox Text="{Binding CurrentLogin}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Label HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
Password:
|
||||
</Label>
|
||||
<TextBox PasswordChar="#" Text="{Binding CurrentPassword}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Label VerticalAlignment="Center">
|
||||
Auth server:
|
||||
</Label>
|
||||
<TextBox Text="{Binding CurrentAuthServer}" />
|
||||
<Button Command="{Binding ExpandAuthUrlCommand}" VerticalAlignment="Stretch">
|
||||
<Label>+</Label>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Border
|
||||
Background="#333333"
|
||||
CornerRadius="10"
|
||||
IsVisible="{Binding AuthUrlConfigExpand}">
|
||||
<ScrollViewer Height="80">
|
||||
<ListBox
|
||||
Background="#00000000"
|
||||
ItemsSource="{Binding AuthUrls}"
|
||||
Margin="5"
|
||||
SelectedItem="{Binding AuthItemSelect}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Label>
|
||||
<TextBlock Text="{Binding}" />
|
||||
</Label>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
<Button
|
||||
Command="{Binding DoAuth}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center">
|
||||
<Label>Auth</Label>
|
||||
</Button>
|
||||
<Button Command="{Binding ExpandAuthViewCommand}" HorizontalAlignment="Right">
|
||||
<Label>
|
||||
>
|
||||
</Label>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel IsVisible="{Binding IsLogged}">
|
||||
<Image
|
||||
Height="100"
|
||||
Margin="0,0,0,20"
|
||||
Source="/Assets/account.png" />
|
||||
<Label>
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<TextBlock>Hello:</TextBlock>
|
||||
<TextBlock Text="{Binding CurrentLogin}" />
|
||||
</StackPanel>
|
||||
</Label>
|
||||
<StackPanel
|
||||
HorizontalAlignment="Center"
|
||||
Margin="5"
|
||||
Orientation="Horizontal"
|
||||
Spacing="5">
|
||||
<Button Command="{Binding Logout}">
|
||||
<Label>Logout</Label>
|
||||
</Button>
|
||||
<Button Command="{Binding SaveProfileCommand}">
|
||||
<Label>Save profile</Label>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
ItemsSource="{Binding ServerInfos}"
|
||||
Padding="0">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type models:ServerHubInfo}">
|
||||
<DataTemplate DataType="{x:Type viewModels:ServerEntryModelView}">
|
||||
<Grid
|
||||
ColumnDefinitions="*,90"
|
||||
Margin="0,5,0,5"
|
||||
@@ -34,7 +34,7 @@
|
||||
Grid.Row="0"
|
||||
Padding="10,0,0,0">
|
||||
<Label VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding StatusData.Name}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.Name}" />
|
||||
</Label>
|
||||
</Border>
|
||||
<Border
|
||||
@@ -48,11 +48,11 @@
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<Label>
|
||||
<TextBlock Text="{Binding StatusData.Players}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.Players}" />
|
||||
</Label>
|
||||
<Label>/</Label>
|
||||
<Label>
|
||||
<TextBlock Text="{Binding StatusData.SoftMaxPlayers}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.SoftMaxPlayers}" />
|
||||
</Label>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
@@ -83,37 +83,37 @@
|
||||
<Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0,0,5,0">RoundStart:</TextBlock>
|
||||
<TextBlock Text="{Binding StatusData.RoundStartTime}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.RoundStartTime}" />
|
||||
</StackPanel>
|
||||
</Label>
|
||||
<Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0,0,5,0">Map:</TextBlock>
|
||||
<TextBlock Text="{Binding StatusData.Map}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.Map}" />
|
||||
</StackPanel>
|
||||
</Label>
|
||||
<Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0,0,5,0">Preset:</TextBlock>
|
||||
<TextBlock Text="{Binding StatusData.Preset}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.Preset}" />
|
||||
</StackPanel>
|
||||
</Label>
|
||||
<Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0,0,5,0">PanicBunker:</TextBlock>
|
||||
<TextBlock Text="{Binding StatusData.PanicBunker}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.PanicBunker}" />
|
||||
</StackPanel>
|
||||
</Label>
|
||||
<Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0,0,5,0">Round Id:</TextBlock>
|
||||
<TextBlock Text="{Binding StatusData.RoundId}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.RoundId}" />
|
||||
</StackPanel>
|
||||
</Label>
|
||||
<Label>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Margin="0,0,5,0">Run Level:</TextBlock>
|
||||
<TextBlock Text="{Binding StatusData.RunLevel}" />
|
||||
<TextBlock Text="{Binding ServerHubInfo.StatusData.RunLevel}" />
|
||||
</StackPanel>
|
||||
</Label>
|
||||
</UniformGrid>
|
||||
@@ -123,6 +123,7 @@
|
||||
<Panel Grid.Column="1" Grid.Row="1">
|
||||
<Border Classes="ButtonBack" CornerRadius="0,0,10,0">
|
||||
<Button
|
||||
Command="{Binding OnConnectRequired}"
|
||||
CornerRadius="0,0,10,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="0"
|
||||
@@ -143,14 +144,25 @@
|
||||
BorderThickness="2,0,0,0"
|
||||
CornerRadius="10"
|
||||
Grid.Row="1">
|
||||
<Grid ColumnDefinitions="*,40" RowDefinitions="*">
|
||||
<Grid ColumnDefinitions="*,40,40" RowDefinitions="*">
|
||||
<TextBox
|
||||
Margin="0"
|
||||
Text="{Binding SearchText}"
|
||||
TextChanged="TextBox_OnTextChanged"
|
||||
VerticalAlignment="Center"
|
||||
Watermark="Server name..." Text="{Binding SearchText}" TextChanged="TextBox_OnTextChanged"/>
|
||||
<Button Grid.Column="1" Padding="10">
|
||||
Watermark="Server name..." />
|
||||
<Button
|
||||
Command="{Binding FilterRequired}"
|
||||
Grid.Column="1"
|
||||
Padding="10">
|
||||
<Image Source="/Assets/filter.png" />
|
||||
</Button>
|
||||
<Button
|
||||
Command="{Binding UpdateRequired}"
|
||||
Grid.Column="2"
|
||||
Padding="10">
|
||||
<Image Source="/Assets/refresh.png" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user