diff --git a/Nebula.Launcher/App.axaml.cs b/Nebula.Launcher/App.axaml.cs index 885f647..1b1ea20 100644 --- a/Nebula.Launcher/App.axaml.cs +++ b/Nebula.Launcher/App.axaml.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; using System.Linq; +using System.Reflection; using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; using Nebula.Launcher.ViewModels; diff --git a/Nebula.Launcher/Nebula.Launcher.csproj b/Nebula.Launcher/Nebula.Launcher.csproj index a6943fb..3392f9d 100644 --- a/Nebula.Launcher/Nebula.Launcher.csproj +++ b/Nebula.Launcher/Nebula.Launcher.csproj @@ -27,6 +27,7 @@ + @@ -35,6 +36,13 @@ Code + + + + + + + diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs index be2bfb5..ecd0cb8 100644 --- a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -3,31 +3,106 @@ using System.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using Nebula.Launcher.ViewHelper; using Nebula.Shared.Models; +using Nebula.Shared.Services; namespace Nebula.Launcher.ViewModels; [ViewModelRegister(isSingleton:false)] public partial class ServerEntryModelView : ViewModelBase { + private readonly AuthService _authService = default!; + private readonly ContentService _contentService = default!; + private readonly CancellationService _cancellationService = default!; + private readonly DebugService _debugService = default!; + private readonly RunnerService _runnerService; + [ObservableProperty] private bool _runVisible = true; - public ServerHubInfo ServerHubInfo { get; set; } + public ServerHubInfo ServerHubInfo { get; set; } = default!; public ServerEntryModelView() : base() { } - public ServerEntryModelView(IServiceProvider serviceProvider) : base(serviceProvider) + public ServerEntryModelView( + IServiceProvider serviceProvider, + AuthService authService, + ContentService contentService, + CancellationService cancellationService, + DebugService debugService, + RunnerService runnerService + ) : base(serviceProvider) { + _authService = authService; + _contentService = contentService; + _cancellationService = cancellationService; + _debugService = debugService; + _runnerService = runnerService; } - public void RunInstance() + private Process? _process; + + public async void RunInstance() { - var p = Process.Start("./Nebula.Runner", "a b c"); - p.BeginOutputReadLine(); - p.BeginErrorReadLine(); + var authProv = _authService.SelectedAuth; + + var buildInfo = await _contentService.GetBuildInfo(new RobustUrl(ServerHubInfo.Address), _cancellationService.Token); + + await _runnerService.PrepareRun(buildInfo, _cancellationService.Token); + + _process = Process.Start(new ProcessStartInfo() + { + FileName = "dotnet.exe", + Arguments = "./Nebula.Runner.dll", + Environment = { + { "ROBUST_AUTH_USERID", authProv?.UserId.ToString() } , + { "ROBUST_AUTH_TOKEN", authProv?.Token.Token } , + { "ROBUST_AUTH_SERVER", authProv?.AuthLoginPassword.AuthServer } , + { "ROBUST_AUTH_PUBKEY", buildInfo.BuildInfo.Auth.PublicKey } , + { "GAME_URL", ServerHubInfo.Address } , + { "AUTH_LOGIN", authProv?.AuthLoginPassword.Login } , + }, + CreateNoWindow = true, UseShellExecute = false + }); + + if (_process is null) + { + return; + } + + _process.OutputDataReceived += OnOutputDataReceived; + _process.ErrorDataReceived += OnErrorDataReceived; + + _process.Exited += OnExited; } - + + private void OnExited(object? sender, EventArgs e) + { + if (_process is null) + { + return; + } + + _process.OutputDataReceived -= OnOutputDataReceived; + _process.ErrorDataReceived -= OnErrorDataReceived; + _process.Exited -= OnExited; + + _debugService.Log("PROCESS EXIT WITH CODE " + _process.ExitCode); + + _process.Dispose(); + _process = null; + } + + private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data != null) _debugService.Error(e.Data); + } + + private void OnOutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (e.Data != null) _debugService.Log(e.Data); + } + public void ReadLog() { @@ -36,6 +111,25 @@ public partial class ServerEntryModelView : ViewModelBase public void StopInstance() { - + _process?.Close(); + } + + static string FindDotnetPath() + { + var pathEnv = Environment.GetEnvironmentVariable("PATH"); + var paths = pathEnv?.Split(System.IO.Path.PathSeparator); + if (paths != null) + { + foreach (var path in paths) + { + var dotnetPath = System.IO.Path.Combine(path, "dotnet"); + if (System.IO.File.Exists(dotnetPath)) + { + return dotnetPath; + } + } + } + + throw new Exception("Dotnet not found!"); } } \ No newline at end of file diff --git a/Nebula.Runner/App.cs b/Nebula.Runner/App.cs index 9a1688e..888cf41 100644 --- a/Nebula.Runner/App.cs +++ b/Nebula.Runner/App.cs @@ -1,14 +1,57 @@ using Nebula.Shared; +using Nebula.Shared.Models; using Nebula.Shared.Services; +using Nebula.Shared.Utils; +using Robust.LoaderApi; namespace Nebula.Runner; [ServiceRegister] -public class App(DebugService debugService) +public sealed class App(DebugService debugService, RunnerService runnerService, ContentService contentService) : IRedialApi { - - public void Run(string[] args) + public async Task Run(string[] args1) { - debugService.Log("HELLO!!! " + string.Join(" ",args)); + debugService.Log("HELLO!!! "); + + var login = Environment.GetEnvironmentVariable("AUTH_LOGIN") ?? "Alexandra"; + var urlraw = Environment.GetEnvironmentVariable("GAME_URL") ?? "ss14://localhost"; + + var url = urlraw.ToRobustUrl(); + + using var cancelTokenSource = new CancellationTokenSource(); + var buildInfo = await contentService.GetBuildInfo(url, cancelTokenSource.Token); + + + var args = new List + { + // Pass username to launched client. + // We don't load username from client_config.toml when launched via launcher. + "--username", 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()); + + await runnerService.Run(args.ToArray(), buildInfo, this, cancelTokenSource.Token); + } + + public void Redial(Uri uri, string text = "") + { + } } \ No newline at end of file diff --git a/Nebula.Runner/Nebula.Runner.csproj b/Nebula.Runner/Nebula.Runner.csproj index 63b96af..60b4687 100644 --- a/Nebula.Runner/Nebula.Runner.csproj +++ b/Nebula.Runner/Nebula.Runner.csproj @@ -1,6 +1,7 @@  + WinExe net8.0 enable enable @@ -12,6 +13,6 @@ + - diff --git a/Nebula.Runner/Program.cs b/Nebula.Runner/Program.cs index ad79ca3..47fb294 100644 --- a/Nebula.Runner/Program.cs +++ b/Nebula.Runner/Program.cs @@ -1,3 +1,4 @@ +using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Nebula.Shared; @@ -11,6 +12,7 @@ public static class Program services.AddServices(); var serviceProvider = services.BuildServiceProvider(); - serviceProvider.GetService()!.Run(args); + var task = serviceProvider.GetService()!.Run(args); + task.Wait(); } } \ No newline at end of file diff --git a/Nebula.Shared/Nebula.Shared.csproj b/Nebula.Shared/Nebula.Shared.csproj index 8a8daa2..66ac89e 100644 --- a/Nebula.Shared/Nebula.Shared.csproj +++ b/Nebula.Shared/Nebula.Shared.csproj @@ -8,12 +8,10 @@ - - - - + + Utility.runtime.json + - diff --git a/Nebula.Shared/ServiceManager.cs b/Nebula.Shared/ServiceManager.cs index 9e1dfc9..c980b10 100644 --- a/Nebula.Shared/ServiceManager.cs +++ b/Nebula.Shared/ServiceManager.cs @@ -7,8 +7,17 @@ public static class ServiceExt { public static void AddServices(this IServiceCollection services) { - foreach (var (type, inference) in GetServicesWithHelpAttribute(Assembly.GetExecutingAssembly())) + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { + AddServices(services, assembly); + } + } + + public static void AddServices(this IServiceCollection services, Assembly assembly) + { + foreach (var (type, inference) in GetServicesWithHelpAttribute(assembly)) + { + Console.WriteLine("[ServiceMng] ADD SERVICE " + type); if (inference is null) { services.AddSingleton(type); diff --git a/Nebula.Shared/Services/AssemblyService.cs b/Nebula.Shared/Services/AssemblyService.cs index 432d115..cbf5035 100644 --- a/Nebula.Shared/Services/AssemblyService.cs +++ b/Nebula.Shared/Services/AssemblyService.cs @@ -10,20 +10,36 @@ namespace Nebula.Shared.Services; [ServiceRegister] public class AssemblyService { - private readonly Dictionary _assemblies = new(); + private readonly List _assemblies = new(); private readonly DebugService _debugService; public AssemblyService(DebugService debugService) { _debugService = debugService; + + SharpZstd.Interop.ZstdImportResolver.ResolveLibrary += (name, assembly1, path) => + { + if (name.Equals("SharpZstd.Native")) + { + _debugService.Debug("RESOLVING SHARPZSTD THINK: " + name + " " + path); + GetRuntimeInfo(out string platform, out string architecture, out string extension); + string fileName = GetDllName(platform, architecture, extension); + + if (NativeLibrary.TryLoad(fileName, assembly1, path, out var nativeLibrary)) + { + return nativeLibrary; + } + } + return IntPtr.Zero; + }; } - //public IReadOnlyList Assemblies => _assemblies; + public IReadOnlyList Assemblies => _assemblies; - public AssemblyApi Mount(IFileApi fileApi, string apiName = "") + public AssemblyApi Mount(IFileApi fileApi) { var asmApi = new AssemblyApi(fileApi); - AssemblyLoadContext.Default.Resolving += (context, name) => OnAssemblyResolving(context, name, asmApi, apiName); + AssemblyLoadContext.Default.Resolving += (context, name) => OnAssemblyResolving(context, name, asmApi); AssemblyLoadContext.Default.ResolvingUnmanagedDll += LoadContextOnResolvingUnmanaged; return asmApi; @@ -53,11 +69,6 @@ public class AssemblyService public bool TryOpenAssembly(string name, AssemblyApi assemblyApi, [NotNullWhen(true)] out Assembly? assembly) { - if (_assemblies.TryGetValue(name, out assembly)) - { - return true; - } - if (!TryOpenAssemblyStream(name, assemblyApi, out var asm, out var pdb)) { assembly = null; @@ -66,8 +77,9 @@ public class AssemblyService assembly = AssemblyLoadContext.Default.LoadFromStream(asm, pdb); _debugService.Log("LOADED ASSEMBLY " + name); - - _assemblies.Add(name, assembly); + + + if (!_assemblies.Contains(assembly)) _assemblies.Add(assembly); asm.Dispose(); pdb?.Dispose(); @@ -86,13 +98,27 @@ public class AssemblyService assemblyApi.TryOpen($"{name}.pdb", out pdb); return true; } + + private readonly HashSet _resolvingAssemblies = new HashSet(); - private Assembly? OnAssemblyResolving(AssemblyLoadContext context, AssemblyName name, AssemblyApi assemblyApi, - string apiName) + private Assembly? OnAssemblyResolving(AssemblyLoadContext context, AssemblyName name, AssemblyApi assemblyApi) { - - _debugService.Debug($"Resolving assembly from {apiName}: {name.Name}"); - return TryOpenAssembly(name.Name!, assemblyApi, out var assembly) ? assembly : null; + if (_resolvingAssemblies.Contains(name.FullName)) + { + _debugService.Debug($"Already resolving {name.Name}, skipping."); + return null; // Prevent recursive resolution + } + + try + { + _resolvingAssemblies.Add(name.FullName); + _debugService.Debug($"Resolving assembly from FileAPI: {name.Name}"); + return TryOpenAssembly(name.Name!, assemblyApi, out var assembly) ? assembly : null; + } + finally + { + _resolvingAssemblies.Remove(name.FullName); + } } private IntPtr LoadContextOnResolvingUnmanaged(Assembly assembly, string unmanaged) @@ -104,7 +130,66 @@ public class AssemblyService if (NativeLibrary.TryLoad(a, out var handle)) return handle; + + _debugService.Error("Loading dll error! Not found"); return IntPtr.Zero; } + + public static string GetDllName( + string platform, + string architecture, + string extension) + { + string name = $"SharpZstd.Native.{extension}"; + return name; + } + + public static void GetRuntimeInfo( + out string platform, + out string architecture, + out string extension) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + platform = "win"; + extension = "dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + platform = "linux"; + extension = "so"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + platform = "osx"; + extension = "dylib"; + } + else + { + platform = "linux"; + extension = "so"; + } + + if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + { + architecture = "x64"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + architecture = "x86"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm) + { + architecture = "arm"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + { + architecture = "arm64"; + } + else + { + throw new PlatformNotSupportedException("Unsupported process architecture."); + } + } } \ No newline at end of file diff --git a/Nebula.Shared/Services/AuthService.cs b/Nebula.Shared/Services/AuthService.cs index 28f5987..d369413 100644 --- a/Nebula.Shared/Services/AuthService.cs +++ b/Nebula.Shared/Services/AuthService.cs @@ -45,6 +45,12 @@ public partial class AuthService( SelectedAuth = null; } + public void SetAuth(Guid guid, string token, string login, string authServer) + { + SelectedAuth = new CurrentAuthInfo(guid, new LoginToken(token, DateTimeOffset.Now), + new AuthLoginPassword(login, "", authServer)); + } + public async Task EnsureToken() { if (SelectedAuth is null) return false; diff --git a/Nebula.Shared/Services/EngineService.cs b/Nebula.Shared/Services/EngineService.cs index f0fc197..c4a3f58 100644 --- a/Nebula.Shared/Services/EngineService.cs +++ b/Nebula.Shared/Services/EngineService.cs @@ -81,7 +81,7 @@ public sealed class EngineService try { - return _assemblyService.Mount(_fileService.OpenZip(version, _fileService.EngineFileApi),$"Engine.Ensure-{version}"); + return _assemblyService.Mount(_fileService.OpenZip(version, _fileService.EngineFileApi)); } catch (Exception e) { @@ -161,7 +161,7 @@ public sealed class EngineService try { - return _assemblyService.Mount(_fileService.OpenZip(fileName, _fileService.EngineFileApi),"Engine.EnsureModule"); + return _assemblyService.Mount(_fileService.OpenZip(fileName, _fileService.EngineFileApi)); } catch (Exception e) { diff --git a/Nebula.Shared/Services/RunnerService.cs b/Nebula.Shared/Services/RunnerService.cs index a0e0ee2..751f3d1 100644 --- a/Nebula.Shared/Services/RunnerService.cs +++ b/Nebula.Shared/Services/RunnerService.cs @@ -10,18 +10,8 @@ public sealed class RunnerService( ConfigurationService varService, FileService fileService, EngineService engineService, - AssemblyService assemblyService, - AuthService authService, - PopupMessageService popupMessageService, - CancellationService cancellationService) - : IRedialApi + AssemblyService assemblyService) { - public async Task PrepareRun(RobustUrl url) - { - var buildInfo = await contentService.GetBuildInfo(url, cancellationService.Token); - await PrepareRun(buildInfo, cancellationService.Token); - } - public async Task PrepareRun(RobustBuildInfo buildInfo, CancellationToken cancellationToken) { debugService.Log("Prepare Content!"); @@ -67,60 +57,4 @@ public sealed class RunnerService( 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.Popup("Error! Auth is required!"); - return; - } - - if (buildInfo.BuildInfo.Auth.Mode != "Disabled") - { - Environment.SetEnvironmentVariable("ROBUST_AUTH_TOKEN", account.Token.Token); - Environment.SetEnvironmentVariable("ROBUST_AUTH_USERID", account.UserId.ToString()); - Environment.SetEnvironmentVariable("ROBUST_AUTH_PUBKEY", buildInfo.BuildInfo.Auth.PublicKey); - Environment.SetEnvironmentVariable("ROBUST_AUTH_SERVER", account.AuthLoginPassword.AuthServer); - } - - var args = new List - { - // Pass username to launched client. - // We don't load username from client_config.toml when launched via launcher. - "--username", account.AuthLoginPassword.Login, - - // Tell game we are launcher - "--cvar", "launch.launcher=true" - }; - - var connectionString = url.ToString(); - if (!string.IsNullOrEmpty(buildInfo.BuildInfo.ConnectAddress)) - connectionString = buildInfo.BuildInfo.ConnectAddress; - - // We are using the launcher. Don't show main menu etc.. - // Note: --launcher also implied --connect. - // For this reason, content bundles do not set --launcher. - args.Add("--launcher"); - - args.Add("--connect-address"); - args.Add(connectionString); - - args.Add("--ss14-address"); - args.Add(url.ToString()); - debugService.Debug("Connect to " + url.ToString() + " " + account.AuthLoginPassword.AuthServer); - - await Run(args.ToArray(), buildInfo, this, cancelTokenSource.Token); - } - - public async void Redial(Uri uri, string text = "") - { - //await RunGame(uri.ToString()); - } } \ No newline at end of file