using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Web; using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using HanumanInstitute.MvvmDialogs; using Nitrox.Launcher.Models.Design; using Nitrox.Launcher.Models.Services; using Nitrox.Launcher.Models.Utils; using Nitrox.Launcher.ViewModels.Abstract; using NitroxModel.Discovery.Models; using NitroxModel.Helper; using NitroxModel.Logger; using NitroxModel.Platforms.OS.Shared; using NitroxModel.Platforms.Store; using NitroxModel.Platforms.Store.Interfaces; namespace Nitrox.Launcher.ViewModels; public partial class LaunchGameViewModel : RoutableViewModelBase { public static Task LastFindSubnauticaTask; private static bool hasInstantLaunched; private readonly OptionsViewModel optionsViewModel; private readonly ServerService serverService; private readonly IKeyValueStore keyValueStore; private readonly IDialogService dialogService; [ObservableProperty] private Platform gamePlatform; [ObservableProperty] private string platformToolTip; public Bitmap[] GalleryImageSources { get; } = [ AssetHelper.GetAssetFromStream("/Assets/Images/gallery/image-1.png", static stream => new Bitmap(stream)), AssetHelper.GetAssetFromStream("/Assets/Images/gallery/image-2.png", static stream => new Bitmap(stream)), AssetHelper.GetAssetFromStream("/Assets/Images/gallery/image-3.png", static stream => new Bitmap(stream)), AssetHelper.GetAssetFromStream("/Assets/Images/gallery/image-4.png", static stream => new Bitmap(stream)) ]; public string Version => $"{NitroxEnvironment.ReleasePhase} {NitroxEnvironment.Version}"; public string SubnauticaLaunchArguments => keyValueStore.GetSubnauticaLaunchArguments(); public LaunchGameViewModel() { } public LaunchGameViewModel(IDialogService dialogService, ServerService serverService, OptionsViewModel optionsViewModel, IKeyValueStore keyValueStore) { this.dialogService = dialogService; this.serverService = serverService; this.optionsViewModel = optionsViewModel; this.keyValueStore = keyValueStore; } internal override async Task ViewContentLoadAsync() { await Task.Run(() => { NitroxUser.GamePlatformChanged += UpdateGamePlatform; UpdateGamePlatform(); HandleInstantLaunchForDevelopment(); }); } internal override Task ViewContentUnloadAsync() { NitroxUser.GamePlatformChanged -= UpdateGamePlatform; return Task.CompletedTask; } [RelayCommand] private async Task StartSingleplayerAsync() { if (GameInspect.WarnIfGameProcessExists(GameInfo.Subnautica)) { return; } Log.Info("Launching Subnautica in singleplayer mode"); try { if (string.IsNullOrWhiteSpace(NitroxUser.GamePath) || !Directory.Exists(NitroxUser.GamePath)) { await HostScreen.ShowAsync(optionsViewModel); LauncherNotifier.Warning("Location of Subnautica is unknown. Set the path to it in settings"); return; } NitroxEntryPatch.Remove(NitroxUser.GamePath); await StartSubnauticaAsync(); } catch (Exception ex) { Log.Error(ex, "Error while starting game in singleplayer mode:"); await dialogService.ShowErrorAsync(ex, "Error while starting game in singleplayer mode"); } } [RelayCommand] private async Task StartMultiplayerAsync(string[] args = null) { Log.Info("Launching Subnautica in multiplayer mode"); try { bool setupResult = await Task.Run(async () => { if (string.IsNullOrWhiteSpace(NitroxUser.GamePath) || !Directory.Exists(NitroxUser.GamePath)) { await Dispatcher.UIThread.InvokeAsync(async () => await HostScreen.ShowAsync(optionsViewModel)); LauncherNotifier.Warning("Location of Subnautica is unknown. Set the path to it in settings"); return false; } if (PirateDetection.HasTriggered) { LauncherNotifier.Error("Aarrr! Nitrox has walked the plank :("); return false; } if (GameInspect.WarnIfGameProcessExists(GameInfo.Subnautica)) { return false; } if (await GameInspect.IsOutdatedGameAndNotify(NitroxUser.GamePath, dialogService)) { return false; } // TODO: The launcher should override FileRead win32 API for the Subnautica process to give it the modified Assembly-CSharp from memory try { const string PATCHER_DLL_NAME = "NitroxPatcher.dll"; string patcherDllPath = Path.Combine(NitroxUser.ExecutableRootPath ?? "", "lib", "net472", PATCHER_DLL_NAME); if (!File.Exists(patcherDllPath)) { LauncherNotifier.Error("Launcher files seems corrupted, please contact us"); return false; } File.Copy( patcherDllPath, Path.Combine(NitroxUser.GamePath, GameInfo.Subnautica.DataFolder, "Managed", PATCHER_DLL_NAME), true ); } catch (IOException ex) { Log.Error(ex, "Unable to move initialization dll to Managed folder. Still attempting to launch because it might exist from previous runs"); } // Try inject Nitrox into Subnautica code. if (LastFindSubnauticaTask != null) { await LastFindSubnauticaTask; } NitroxEntryPatch.Remove(NitroxUser.GamePath); NitroxEntryPatch.Apply(NitroxUser.GamePath); if (QModHelper.IsQModInstalled(NitroxUser.GamePath)) { Log.Warn("Seems like QModManager is installed"); LauncherNotifier.Warning("QModManager Detected in the game folder"); } return true; }); if (!setupResult) { return; } await StartSubnauticaAsync(args); } catch (Exception ex) { Log.Error(ex, "Error while starting game in multiplayer mode:"); await Dispatcher.UIThread.InvokeAsync(async () => await dialogService.ShowErrorAsync(ex, "Error while starting game in multiplayer mode")); } } [RelayCommand] private void OpenContributionsOfYear() { Process.Start(new ProcessStartInfo($"https://github.com/SubnauticaNitrox/Nitrox/graphs/contributors?from={HttpUtility.UrlEncode($"{DateTime.UtcNow.AddYears(-1):yyyy/M/d}")}") { UseShellExecute = true, Verb = "open" })?.Dispose(); } /// /// Launches the server and Subnautica immediately if instant launch is active. /// [Conditional("DEBUG")] private void HandleInstantLaunchForDevelopment() { if (hasInstantLaunched) { return; } hasInstantLaunched = true; if (App.InstantLaunch == null) { return; } Task.Run(async () => { // Start the server ServerEntry server = await serverService.GetOrCreateServerAsync(App.InstantLaunch.SaveName); server.Name = App.InstantLaunch.SaveName; Task serverStartTask = Dispatcher.UIThread.InvokeAsync(async () => await serverService.StartServerAsync(server)).ContinueWithHandleError(); // Start a game in multiplayer for each player foreach (string playerName in App.InstantLaunch.PlayerNames) { await StartMultiplayerAsync(["--instantlaunch", playerName]).ContinueWithHandleError(); } await serverStartTask; }).ContinueWithHandleError(); } private async Task StartSubnauticaAsync(string[] args = null) { LauncherNotifier.Info("Starting game"); string subnauticaPath = NitroxUser.GamePath; string subnauticaLaunchArguments = $"{SubnauticaLaunchArguments} {string.Join(" ", args ?? Environment.GetCommandLineArgs())}"; string subnauticaExe; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { subnauticaExe = Path.Combine(subnauticaPath, "MacOS", GameInfo.Subnautica.ExeName); } else { subnauticaExe = Path.Combine(subnauticaPath, GameInfo.Subnautica.ExeName); } if (!File.Exists(subnauticaExe)) { throw new FileNotFoundException("Unable to find Subnautica executable"); } IGamePlatform platform = GamePlatforms.GetPlatformByGameDir(subnauticaPath); // Start game & gaming platform if needed. using ProcessEx game = platform switch { Steam s => await s.StartGameAsync(subnauticaExe, subnauticaLaunchArguments, GameInfo.Subnautica.SteamAppId), EpicGames e => await e.StartGameAsync(subnauticaExe, subnauticaLaunchArguments), MSStore m => await m.StartGameAsync(subnauticaExe, subnauticaLaunchArguments), Discord d => await d.StartGameAsync(subnauticaExe, subnauticaLaunchArguments), _ => throw new Exception($"Directory '{subnauticaPath}' is not a valid {GameInfo.Subnautica.Name} game installation or the game platform is unsupported by Nitrox.") }; if (game is null) { throw new Exception($"Game failed to start through {platform.Name}"); } } private void UpdateGamePlatform() { GamePlatform = NitroxUser.GamePlatform?.Platform ?? Platform.NONE; PlatformToolTip = GamePlatform.GetAttribute()?.Description ?? "Unknown"; } }