- add: Content manipulation think

This commit is contained in:
2024-12-27 19:15:33 +03:00
parent acad001528
commit bdb8915932
25 changed files with 1275 additions and 248 deletions

View 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;
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View 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];
}
}

View 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;
}
}

View File

@@ -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();

View 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;
}
}

View File

@@ -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,
}

View File

@@ -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");
}
}

View 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());
}
}