diff --git a/PublishFiles/SS14.desktop b/PublishFiles/SS14.desktop index a6dc97476..95d749061 100755 --- a/PublishFiles/SS14.desktop +++ b/PublishFiles/SS14.desktop @@ -5,3 +5,7 @@ Version=1.0 Type=Application Exec=env ./SS14.Launcher %u Name=Space Station 14 +Comment=Open source Multiplayer Disaster Simulator +Categories=Game; +Keywords=ss14; +MimeType=x-scheme-handler/ss14s;x-scheme-handler/ss14;application/rtbundle;application/rtreplay;application/zip diff --git a/PublishFiles/Space Station 14 Launcher.app/Contents/Info.plist b/PublishFiles/Space Station 14 Launcher.app/Contents/Info.plist index d5e34b078..71480e9eb 100644 --- a/PublishFiles/Space Station 14 Launcher.app/Contents/Info.plist +++ b/PublishFiles/Space Station 14 Launcher.app/Contents/Info.plist @@ -16,5 +16,37 @@ --> CFBundleIconFile ss14 + CFBundleIdentifier + com.spacestation14.launcher + CFBundleURLTypes + + + CFBundleURLName + com.spacestation14.launcher + CFBundleURLSchemes + + ss14 + ss14s + + + + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + rtreplay + rtbundle + + CFBundleTypeIconFile + ss14 + LSHandlerRank + Owner + CFBundleTypeName + Robust Toolbox Bundle File + CFBundleTypeRole + Viewer + + diff --git a/PublishFiles/ss14-mime-type.xml b/PublishFiles/ss14-mime-type.xml new file mode 100644 index 000000000..d20186b3c --- /dev/null +++ b/PublishFiles/ss14-mime-type.xml @@ -0,0 +1,15 @@ + + + + Robust Toolbox Game Bundle + + + + Robust Toolbox Replay + + + + diff --git a/SS14.Launcher.Bootstrap/Program.cs b/SS14.Launcher.Bootstrap/Program.cs index 7d745d594..22c254993 100644 --- a/SS14.Launcher.Bootstrap/Program.cs +++ b/SS14.Launcher.Bootstrap/Program.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.IO; using Microsoft.Win32; +using System.Linq; namespace SS14.Launcher.Bootstrap { @@ -11,6 +12,53 @@ public static void Main(string[] args) { UnfuckDotnetRoot(); + if (args.Contains("--register-protocol")) + { + // ss14s:// + var key = Registry.ClassesRoot.CreateSubKey("ss14s"); + key!.SetValue("URL Protocol", "Space Station 14 Secure protocol"); + key = key.CreateSubKey("Shell\\Open\\Command"); + key!.SetValue("", $"\"{AppDomain.CurrentDomain.BaseDirectory}Space Station 14 Launcher.exe\" \"%1\""); + key.Close(); + + // ss14:// + key = Registry.ClassesRoot.CreateSubKey("ss14"); + key!.SetValue("URL Protocol", "Space Station 14 protocol"); + key = key.CreateSubKey("Shell\\Open\\Command"); + key!.SetValue("", $"\"{AppDomain.CurrentDomain.BaseDirectory}Space Station 14 Launcher.exe\" \"%1\""); + key.Close(); + + // RobustToolbox (required for the file extensions) + key = Registry.ClassesRoot.CreateSubKey("RobustToolbox"); + key!.SetValue("", "Robust Toolbox Bundle File"); + var icon = key.CreateSubKey("DefaultIcon"); + icon!.SetValue("", $"{AppDomain.CurrentDomain.BaseDirectory}Space Station 14 Launcher.exe"); + key = key.CreateSubKey("Shell\\Open\\Command"); + key!.SetValue("", $"\"{AppDomain.CurrentDomain.BaseDirectory}Space Station 14 Launcher.exe\" \"%1\""); + key.Close(); + + // .rtbundle + key = Registry.ClassesRoot.CreateSubKey(".rtbundle"); + key!.SetValue("", "RobustToolbox"); + key.Close(); + + // .rtreplay + key = Registry.ClassesRoot.CreateSubKey(".rtreplay"); + key!.SetValue("", "RobustToolbox"); + key.Close(); + + Environment.Exit(0); + } + if (args.Contains("--unregister-protocol")) + { + Registry.ClassesRoot.DeleteSubKeyTree("ss14s"); + Registry.ClassesRoot.DeleteSubKeyTree("ss14"); + Registry.ClassesRoot.DeleteSubKeyTree("RobustToolbox"); + Registry.ClassesRoot.DeleteSubKeyTree(".rtbundle"); + Registry.ClassesRoot.DeleteSubKeyTree(".rtreplay"); + Environment.Exit(0); + } + var path = typeof(Program).Assembly.Location; var ourDir = Path.GetDirectoryName(path); Debug.Assert(ourDir != null); @@ -19,7 +67,16 @@ public static void Main(string[] args) var exeDir = Path.Combine(ourDir, "bin", "SS14.Launcher.exe"); Environment.SetEnvironmentVariable("DOTNET_ROOT", dotnetDir); - Process.Start(new ProcessStartInfo(exeDir)); + if (args.Length > 0) + { + // blursed + // thanks anonymous for how to make args pass in properly + Process.Start(new ProcessStartInfo(exeDir, string.Join("", args.Select((str) => $"\"{str}\" ")))); + } + else + { + Process.Start(new ProcessStartInfo(exeDir)); + } } private static void UnfuckDotnetRoot() diff --git a/SS14.Launcher/App.xaml.cs b/SS14.Launcher/App.xaml.cs index 321a9c309..3e199fb8e 100644 --- a/SS14.Launcher/App.xaml.cs +++ b/SS14.Launcher/App.xaml.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; @@ -43,6 +42,24 @@ public App() public App(OverrideAssetsManager overrideAssets) { _overrideAssets = overrideAssets; + + if (Current?.TryGetFeature(out var lifetime) == true) + { + lifetime.Activated += OnOSXUrlsOpened; + } + } + + private void OnOSXUrlsOpened(object? sender, ActivatedEventArgs e) + { + // I think this only works on macOS anyway? Well I will leave this here just so I don't surprise myself later. + if (!OperatingSystem.IsMacOS()) + return; + + var args = Environment.GetCommandLineArgs(); + if (args.Length > 1) + { + Program.ParseCommandLineArgs(args[1..], new LauncherMessaging()); + } } public override void Initialize() @@ -148,7 +165,7 @@ private void OnStartup(object? s, ControlledApplicationLifetimeStartupEventArgs GC.Collect(); }; - var lc = new LauncherCommands(viewModel); + var lc = new LauncherCommands(viewModel, window.StorageProvider); lc.RunCommandTask(); Locator.CurrentMutable.RegisterConstant(lc); msgr.StartServerTask(lc); diff --git a/SS14.Launcher/Assets/Locale/en-US/text.ftl b/SS14.Launcher/Assets/Locale/en-US/text.ftl index 10e1d0283..86ed32e78 100644 --- a/SS14.Launcher/Assets/Locale/en-US/text.ftl +++ b/SS14.Launcher/Assets/Locale/en-US/text.ftl @@ -297,6 +297,8 @@ tab-development-disable-signing = Disable Engine Signature Checks tab-development-disable-signing-desc = { "[" }DEV ONLY] Disables verification of engine signatures. DO NOT ENABLE UNLESS YOU KNOW EXACTLY WHAT YOU'RE DOING. tab-development-enable-engine-override = Enable engine override tab-development-enable-engine-override-desc = Override path to load engine zips from (release/ in RobustToolbox) +tab-development-force-register-os-protocols = Force Register OS Protocols +tab-development-force-unregister-os-protocols = Force Unegister OS Protocols ## Strings for the "home" tab @@ -338,6 +340,9 @@ tab-options-disable-signing = Disable Engine Signature Checks tab-options-disable-signing-desc = { "[" }DEV ONLY] Disables verification of engine signatures. DO NOT ENABLE UNLESS YOU KNOW EXACTLY WHAT YOU'RE DOING. tab-options-hub-settings = Hub Settings tab-options-hub-settings-desc = Change what hub server or servers you would like to use to fetch the server list. +tab-options-os-protocol = Register/Unregister File extensions & URL protocol +tab-options-os-protocol-desc = Register the launcher to your operating system, this will allow you to use ss14(s):// links on web browsers and file extensions like .rtreplay. + tab-options-desc-incompatible = This option is incompatible with your platform and has been disabled. ## For the language selection menu. @@ -352,3 +357,52 @@ language-selector-help-translate = Want to help translate? You can! language-selector-system-language = System language ({ $languageName }) # Used for contents of each language button. language-selector-language = { $languageName } ({ $englishName }) + +# Strings for the dialog box for protocol registration +protocols-dialog-title = OS protocol registration +protocols-dialog-content = + Would you like to allow the launcher to be able to open special links and files? + + This will allow you to launch replay files by just double clicking them on your + computer, or allow you to join a server directly from your web browser. + + You can disable this feature later on in options. +protocols-dialog-content-success = + The launcher has been successfully registered to your operating system! + + You can now use ss14(s):// links on web browsers and file extensions like .rtreplay +protocols-dialog-content-update = + The launcher's special links/file extension registration seems to be out of date. + + Want to update it? +protocols-dialog-content-action-question = + You are about to { $action } the operating system protocols for the Launcher + + Do you want to continue? +protocols-dialog-action-register = Register +protocols-dialog-action-unregister = Unregister +protocols-dialog-confirm = For sure! +protocols-dialog-deny = No Thanks +protocols-dialog-continue = Yes, please continue +protocols-dialog-back = Keep it as is +protocols-dialog-ok = Yay +protocols-dialog-error-title = OS protocol error +protocols-dialog-error-windows-uac = + It appears we were unable to register the launcher to Windows + Do you have administrator rights to this computer? Or deny the admin prompt? + + Want to try again? +protocols-dialog-error-macos-translocation = + It appears we were unable to register the launcher to MacOS + + MacOS Gatekeeper sandboxing is currently active on the launcher, please move + the Space Station 14 Launcher into your Applications folder. +protocols-dialog-error-generic = + It appears we were unable to register the launcher to your operating system. + + If this continues, please contact our Discord, Github or forum to report + this Bug. And provide a copy of your launcher log file. + + Want to try again? +protocols-dialog-error-again = Try again +protocols-dialog-error-ok = Ok diff --git a/SS14.Launcher/Helpers.cs b/SS14.Launcher/Helpers.cs index e61ca229c..2bc7a27af 100644 --- a/SS14.Launcher/Helpers.cs +++ b/SS14.Launcher/Helpers.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Mono.Unix; using Serilog; +using SS14.Launcher.Localization; +using SS14.Launcher.Views; using TerraFX.Interop.Windows; using Win = TerraFX.Interop.Windows.Windows; @@ -202,4 +204,33 @@ public static unsafe int MessageBoxHelper(string text, string caption, uint type return Win.MessageBoxW(HWND.NULL, (ushort*)pText, (ushort*)pCaption, type); } } + + // Has two buttons, Confirm/Deny that returns true or false respectively + public static async Task ConfirmDialogBuilder(MainWindow control, string title, string dialogContent, string confirmButtonText, string cancelButtonText) + { + var dialog = new ConfirmDialog + { + Title = LocalizationManager.Instance.GetString(title), + DialogContent = LocalizationManager.Instance.GetString(dialogContent), + ConfirmButtonText = LocalizationManager.Instance.GetString(confirmButtonText), + CancelButtonText = LocalizationManager.Instance.GetString(cancelButtonText), + }; + + var answer = await dialog.ShowDialog(control); + + return answer; + } + + // Only one button and no bool is returned. + public static async Task OkDialogBuilder(MainWindow control, string title, string dialogContent, string confirmButtonText) + { + var dialog = new OkDialog() + { + Title = LocalizationManager.Instance.GetString(title), + DialogContent = LocalizationManager.Instance.GetString(dialogContent), + ButtonText = LocalizationManager.Instance.GetString(confirmButtonText), + }; + + await dialog.ShowDialog(control); + } } diff --git a/SS14.Launcher/LauncherCommands.cs b/SS14.Launcher/LauncherCommands.cs index 2f7938ac5..7900f8461 100644 --- a/SS14.Launcher/LauncherCommands.cs +++ b/SS14.Launcher/LauncherCommands.cs @@ -2,6 +2,7 @@ using System.Text; using System.Threading.Channels; using System.Threading.Tasks; +using Avalonia.Platform.Storage; using Avalonia.Threading; using Serilog; using Splat; @@ -18,13 +19,15 @@ public class LauncherCommands private MainWindowViewModel _windowVm; private LoginManager _loginMgr; private LauncherMessaging _msgr; + private readonly IStorageProvider _storageProvider; public readonly Channel CommandChannel; - public LauncherCommands(MainWindowViewModel windowVm) + public LauncherCommands(MainWindowViewModel windowVm, IStorageProvider provider) { _windowVm = windowVm; _loginMgr = Locator.Current.GetRequiredService(); _msgr = Locator.Current.GetRequiredService(); + _storageProvider = provider; CommandChannel = Channel.CreateUnbounded(); } @@ -157,6 +160,16 @@ private async Task RunSingleCommand(string cmd) // Used by the "pass URI as argument" logic, doesn't need to bother with safety measures await Connect(cmd.Substring(1)); } + else if (cmd.StartsWith("b")) + { + // Content bundle file + var uri = new Uri(cmd.Substring(1)); + var thingy = await _storageProvider.TryGetFileFromPathAsync(uri); + if (thingy != null) + await Task.Run(() => ConnectingViewModel.StartContentBundle(_windowVm, thingy)); + else + Log.Error("File does not exist. Aborting"); + } else { Log.Error($"Unhandled launcher command: {cmd}"); @@ -169,5 +182,6 @@ private async Task RunSingleCommand(string cmd) public const string RedialWaitCommand = ":RedialWait"; public const string BlankReasonCommand = "r"; public static string ConstructConnectCommand(Uri uri) => "c" + uri.ToString(); + public static string ConstructContentBundleCommand(string fileName) => "b" + fileName; } diff --git a/SS14.Launcher/LauncherMessaging.cs b/SS14.Launcher/LauncherMessaging.cs index 8b355aace..046aaeaf7 100644 --- a/SS14.Launcher/LauncherMessaging.cs +++ b/SS14.Launcher/LauncherMessaging.cs @@ -81,7 +81,7 @@ public bool SendCommandsOrClaim(string[] commands, bool sendAnyway = true) catch (Exception) { // Ok, so we're server (we hope) - Console.WriteLine("We are primary launcher (or primary launcher is out for lunch)"); + Console.WriteLine("We are the primary launcher (or primary launcher is out for lunch)"); } // Try to create server diff --git a/SS14.Launcher/Models/Data/CVars.cs b/SS14.Launcher/Models/Data/CVars.cs index 331889b74..b39ccc266 100644 --- a/SS14.Launcher/Models/Data/CVars.cs +++ b/SS14.Launcher/Models/Data/CVars.cs @@ -110,6 +110,11 @@ public static readonly CVarDef HasDismissedEarlyAccessWarning /// public static readonly CVarDef WineWarningShown = CVarDef.Create("WineWarningShown", false); + /// + /// Has the user been shown the protocols alert? + /// + public static readonly CVarDef HasSeenProtocolsDialog = CVarDef.Create("HasSeenProtocolsDialog", false); + /// /// Language the user selected. Null means it should be automatically selected based on system language. /// diff --git a/SS14.Launcher/Program.cs b/SS14.Launcher/Program.cs index e27bec068..c1dadd1e5 100644 --- a/SS14.Launcher/Program.cs +++ b/SS14.Launcher/Program.cs @@ -45,40 +45,7 @@ public static void Main(string[] args) var msgr = new LauncherMessaging(); Locator.CurrentMutable.RegisterConstant(msgr); - // Parse arguments as early as possible for launcher messaging reasons. - string[] commands = { LauncherCommands.PingCommand }; - var commandSendAnyway = false; - if (args.Length == 1) - { - // Check if this is a valid Uri, since that indicates re-invocation. - if (Uri.TryCreate(args[0], UriKind.Absolute, out var result)) - { - commands = new string[] - { LauncherCommands.BlankReasonCommand, LauncherCommands.ConstructConnectCommand(result) }; - // This ensures we queue up the connection even if we're starting the launcher now. - commandSendAnyway = true; - } - } - else if (args.Length >= 2) - { - if (args[0] == "--commands") - { - // Trying to send an arbitrary series of commands. - // This is how the Loader is expected to communicate (and start the launcher if necessary). - // Note that there are special "untrusted text" versions of the commands that should be used. - commands = new string[args.Length - 1]; - for (var i = 0; i < commands.Length; i++) - commands[i] = args[i + 1]; - commandSendAnyway = true; - } - } - - // Note: This MUST occur before we do certain actions like: - // + Open the launcher log file (and therefore wipe a user's existing launcher log) - // + Initialize Avalonia (and therefore waste whatever time it takes to do that) - // Therefore any messages you receive at this point will be Console.WriteLine-only! - if (msgr.SendCommandsOrClaim(commands, commandSendAnyway)) - return; + ParseCommandLineArgs(args, msgr); var logCfg = new LoggerConfiguration() .MinimumLevel.Debug() @@ -128,7 +95,52 @@ public static void Main(string[] args) } } - private static unsafe void CheckWindowsVersion() + public static void ParseCommandLineArgs(string[] args, LauncherMessaging msgr) + { + // Parse arguments as early as possible for launcher messaging reasons. + string[] commands = { LauncherCommands.PingCommand }; + var commandSendAnyway = false; + if (args.Length == 1) + { + // Handle files being opened with the launcher. + if (args.Any(arg => arg.StartsWith("file://") || arg.EndsWith(".rtbundle") || arg.EndsWith(".rtreplay"))) + { + commands = [LauncherCommands.BlankReasonCommand, LauncherCommands.ConstructContentBundleCommand(args[0]) + ]; + commandSendAnyway = true; + } + + // Check if this is a valid Uri, since that indicates re-invocation. + else if (Uri.TryCreate(args[0], UriKind.Absolute, out var result)) + { + commands = [LauncherCommands.BlankReasonCommand, LauncherCommands.ConstructConnectCommand(result)]; + // This ensures we queue up the connection even if we're starting the launcher now. + commandSendAnyway = true; + } + } + else if (args.Length >= 2) + { + if (args[0] == "--commands") + { + // Trying to send an arbitrary series of commands. + // This is how the Loader is expected to communicate (and start the launcher if necessary). + // Note that there are special "untrusted text" versions of the commands that should be used. + commands = new string[args.Length - 1]; + for (var i = 0; i < commands.Length; i++) + commands[i] = args[i + 1]; + commandSendAnyway = true; + } + } + + // Note: This MUST occur before we do certain actions like: + // + Open the launcher log file (and therefore wipe a user's existing launcher log) + // + Initialize Avalonia (and therefore waste whatever time it takes to do that) + // Therefore any messages you receive at this point will be Console.WriteLine-only! + if (msgr.SendCommandsOrClaim(commands, commandSendAnyway)) + return; + } + + private static void CheckWindowsVersion() { // 14393 is Windows 10 version 1607, minimum we currently support. if (!OperatingSystem.IsWindows() || Environment.OSVersion.Version.Build >= 14393) @@ -153,7 +165,7 @@ private static unsafe void CheckWindowsVersion() Helpers.MessageBoxHelper(text, caption, type); } - private static unsafe void CheckBadAntivirus() + private static void CheckBadAntivirus() { // Avast Free Antivirus breaks the game due to their AMSI integration crashing the process. Awesome! // Oh hey back here again, turns out AVG is just the same product as Avast with different paint. diff --git a/SS14.Launcher/Protocol.cs b/SS14.Launcher/Protocol.cs new file mode 100644 index 000000000..1b5c07f5d --- /dev/null +++ b/SS14.Launcher/Protocol.cs @@ -0,0 +1,345 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Win32; +using Serilog; +using SS14.Launcher.Localization; +using SS14.Launcher.Models.Data; +using SS14.Launcher.Views; + +namespace SS14.Launcher; + +public abstract class Protocol +{ + private static ProtocolsCheckResultCode CheckExisting() + { + if (OperatingSystem.IsWindows()) + { + using var key1 = Registry.ClassesRoot.OpenSubKey("ss14s", false); + using var key2 = Registry.ClassesRoot.OpenSubKey("ss14", false); + using var key3 = Registry.ClassesRoot.OpenSubKey("RobustToolbox", false); + using var key4 = Registry.ClassesRoot.OpenSubKey(".rtreplay", false); + using var key5 = Registry.ClassesRoot.OpenSubKey(".rtbundle", false); + + if (key1 != null && key2 != null && key3 != null && key4 != null && key5 != null) + { + return ProtocolsCheckResultCode.Exists; + } + + if (key1 == null && key2 == null && key3 == null && key4 == null && key5 == null) + { + return ProtocolsCheckResultCode.NonExistent; + } + + return ProtocolsCheckResultCode.NeedsUpdate; + } + + if (OperatingSystem.IsMacOS()) + { + // todo macos check existing protocol setup + // I got no idea how to do this, lsregister does not report anything. + // Lets just assume theres no record + return ProtocolsCheckResultCode.NonExistent; + } + + if (OperatingSystem.IsLinux()) + { + // todo steam makes its own .desktop and idk if its possible to add mime types to it via steam, so this is a bit of a problem + // this will assume you have downloaded the zip launcher + // todo how do i get data to see the output of this + var proc = new Process(); + proc.StartInfo.FileName = "xdg-mime"; + proc.StartInfo.Arguments = "default x-scheme-handler/ss14;xdg-mime default x-scheme-handler/ss14"; + proc.Start(); + // https://stackoverflow.com/questions/206323/how-to-execute-command-line-in-c-get-std-out-results + var output = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + } + + return ProtocolsCheckResultCode.NonExistent; + } + public static async Task RegisterProtocol() + { + try + { + // Windows registration + if (OperatingSystem.IsWindows()) + { + try + { + var proc = new Process(); + proc.StartInfo.FileName = "Space Station 14 Launcher.exe"; + proc.StartInfo.Arguments = "--register-protocol"; + proc.StartInfo.UseShellExecute = true; + proc.StartInfo.Verb = "runas"; + proc.Start(); + await proc.WaitForExitAsync(); + } + catch (System.ComponentModel.Win32Exception) + { + // Do nothing, the user either declined UAC, they don't have administrator rights or something else went wrong. + Log.Warning("User declined UAC or doesn't have admin rights."); + return ProtocolsResultCode.ErrorWindowsUac; + } + } + + // macOS registration + if (OperatingSystem.IsMacOS()) + { + var path = $"{AppDomain.CurrentDomain.BaseDirectory}"; + + // User needs to move the app manually to get this sandbox restriction lifted. This can be done "automated" by making one of those installer dmg stuff + if (path.Contains("AppTranslocation")) + { + Log.Error( + "I have been put in apple jail (Gatekeeper path randomisation)... move me to your application folder"); + return ProtocolsResultCode.ErrorMacOSTranslocation; + } + + var newPath = string.Empty; + var appIndex = path.IndexOf(".app", StringComparison.Ordinal); + if (appIndex >= 0) + { + newPath = path.Substring(0, appIndex + 4); + } + + var proc = new Process(); + // Yes you have to manually go to this + proc.StartInfo.FileName = + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"; + proc.StartInfo.Arguments = $"-R -f {newPath}"; + proc.Start(); + await proc.WaitForExitAsync(); + } + + // Linux registration + if (OperatingSystem.IsLinux()) + { + var desktopfile = ""; + + // todo ditto (2) + var proc = new Process(); + proc.StartInfo.FileName = "xdg-mime"; + proc.StartInfo.Arguments = + $"default {desktopfile} x-scheme-handler/ss14;xdg-mime default SS14.desktop x-scheme-handler/ss14s"; + proc.Start(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to register protocol, and we did not catch it"); + return ProtocolsResultCode.ErrorUnknown; + } + + Log.Information("Successfully registered protocol"); + return ProtocolsResultCode.Success; + } + public static async Task UnregisterProtocol() + { + try + { + // Windows unregistration + if (OperatingSystem.IsWindows()) + { + try + { + var proc = new Process(); + proc.StartInfo.FileName = "Space Station 14 Launcher.exe"; + proc.StartInfo.Arguments = "--unregister-protocol"; + proc.StartInfo.UseShellExecute = true; + proc.StartInfo.Verb = "runas"; + proc.Start(); + await proc.WaitForExitAsync(); + } + catch (System.ComponentModel.Win32Exception) + { + // Do nothing, the user either declined UAC, they don't have administrator rights or something else went wrong. + Log.Warning("User declined UAC or doesn't have admin rights."); + return ProtocolsResultCode.ErrorWindowsUac; + } + } + + // macOS unregistration + if (OperatingSystem.IsMacOS()) + { + // This just... seems to do nothing. Its correct to my documentation... + var path = $"{AppDomain.CurrentDomain.BaseDirectory}"; + + var newPath = string.Empty; + var appIndex = path.IndexOf(".app", StringComparison.Ordinal); + if (appIndex >= 0) + { + newPath = path.Substring(0, appIndex + 4); + } + + var proc = new Process(); + proc.StartInfo.FileName = + "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"; + proc.StartInfo.Arguments = $"-R -f -u {newPath}"; + proc.Start(); + await proc.WaitForExitAsync(); + } + + // Linux unregistration + if (OperatingSystem.IsLinux()) + { + // todo ditto (2) + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to unregister protocol, and we did not catch it"); + return ProtocolsResultCode.ErrorUnknown; + } + + Log.Information("Successfully unregistered protocol"); + return ProtocolsResultCode.Success; + } + + // UI popup stuff + public static async Task OptionsManualPopup(MainWindow control) + { + var existing = CheckExisting() != ProtocolsCheckResultCode.Exists; + + // Not using ConfirmDialogBuilder because I am not sure how to make it support variables or whatever they are named + var dialog = new ConfirmDialog + { + Title = LocalizationManager.Instance.GetString("protocols-dialog-title"), + DialogContent = LocalizationManager.Instance.GetString("protocols-dialog-content-action-question", + ("action", existing ? LocalizationManager.Instance.GetString("protocols-dialog-action-register") + : LocalizationManager.Instance.GetString("protocols-dialog-action-unregister"))), + ConfirmButtonText = LocalizationManager.Instance.GetString("protocols-dialog-continue"), + CancelButtonText = LocalizationManager.Instance.GetString("protocols-dialog-back"), + }; + + var question = await dialog.ShowDialog(control); + + if (question) + { + await HandleResult(control); + } + } + + public static async Task ProtocolSignupPopup(MainWindow control, DataManager cfg) + { + if (CheckExisting() == ProtocolsCheckResultCode.NeedsUpdate) + { + await ProtocolUpdatePopup(control); + return; + } + + if (!IsCandidateForProtocols(cfg)) + return; + + var answer = await Helpers.ConfirmDialogBuilder(control, + "protocols-dialog-title", + "protocols-dialog-content", + "protocols-dialog-confirm", + "protocols-dialog-deny"); + + if (answer) + { + await HandleResult(control); + } + + cfg.SetCVar(CVars.HasSeenProtocolsDialog, true); + } + + private static async Task ProtocolUpdatePopup(MainWindow control) + { + var answer = await Helpers.ConfirmDialogBuilder(control, + "protocols-dialog-title", + "protocols-dialog-content-update", + "protocols-dialog-confirm", + "protocols-dialog-deny"); + + if (answer) + { + await HandleResult(control); + } + } + + private static async Task HandleResult(MainWindow control) + { + // Lord, spare me for I have sinned. + // The goto is evil, yet the alternative is worse (in my opinion). + // Judge me not for the sin, but for the necessity. amen. + retryPoint: + + var action = CheckExisting() == ProtocolsCheckResultCode.Exists ? await UnregisterProtocol() : await RegisterProtocol(); + + switch (action) + { + case ProtocolsResultCode.Success: + await Helpers.OkDialogBuilder(control, + "protocols-dialog-title", + "protocols-dialog-content-success", + "protocols-dialog-ok"); + break; + case ProtocolsResultCode.ErrorWindowsUac: + var retryUac = await Helpers.ConfirmDialogBuilder(control, + "protocols-dialog-error-title", + "protocols-dialog-error-windows-uac", + "protocols-dialog-error-again", + "protocols-dialog-deny"); + if (retryUac) + goto retryPoint; + break; + case ProtocolsResultCode.ErrorMacOSTranslocation: + await Helpers.OkDialogBuilder(control, + "protocols-dialog-error-title", + "protocols-dialog-error-macos-translocation", + "protocols-dialog-error-ok"); + break; + case ProtocolsResultCode.ErrorUnknown: + var retryUnknown = await Helpers.ConfirmDialogBuilder(control, + "protocols-dialog-error-title", + "protocols-dialog-error-generic", + "protocols-dialog-error-again", + "protocols-dialog-deny"); + if (retryUnknown) + goto retryPoint; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + private static bool IsCandidateForProtocols(DataManager cfg) + { + // They have been shown this dialog before, don't bother. + if (cfg.GetCVar(CVars.HasSeenProtocolsDialog)) + return false; + + // It already exists. Either cause of a reset config file or already installed by steam. + // Let's also set the cvar. + if (CheckExisting() == ProtocolsCheckResultCode.Exists) + { + cfg.SetCVar(CVars.HasSeenProtocolsDialog, true); + + return false; + } + + // Check if the OS is compatible... im sorry freebsd users + if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux()) + return false; + + // We (hopefully) are ready! + return true; + } + + public enum ProtocolsResultCode : byte + { + Success = 0, + ErrorWindowsUac, + ErrorMacOSTranslocation, + ErrorUnknown + } + + public enum ProtocolsCheckResultCode : byte + { + Exists = 0, + NeedsUpdate, + NonExistent + } +} diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs index 2dec1f94c..435a2795f 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/OptionsTabViewModel.cs @@ -1,5 +1,7 @@ using System; +using System.ComponentModel; using System.Diagnostics; +using System.Threading.Tasks; using Splat; using SS14.Launcher.Localization; using SS14.Launcher.Models.ContentManagement; @@ -26,12 +28,7 @@ public OptionsTabViewModel() DisableIncompatibleMacOS = OperatingSystem.IsMacOS(); } public bool DisableIncompatibleMacOS { get; } - -#if RELEASE - public bool HideDisableSigning => true; -#else - public bool HideDisableSigning => false; -#endif + public bool OsProtocolBool; public override string Name => LocalizationManager.Instance.GetString("tab-options-title"); @@ -75,16 +72,6 @@ public bool LogLauncherVerbose } } - public bool DisableSigning - { - get => Cfg.GetCVar(CVars.DisableSigning); - set - { - Cfg.SetCVar(CVars.DisableSigning, value); - Cfg.CommitConfig(); - } - } - public bool OverrideAssets { get => Cfg.GetCVar(CVars.OverrideAssets); diff --git a/SS14.Launcher/ViewModels/MainWindowViewModel.cs b/SS14.Launcher/ViewModels/MainWindowViewModel.cs index 82f9cb612..0bacc1e27 100644 --- a/SS14.Launcher/ViewModels/MainWindowViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowViewModel.cs @@ -270,7 +270,7 @@ public bool IsContentBundleDropValid(IStorageFile file) if (ConnectingVM != null) return false; - return Path.GetExtension(file.Name) == ".zip"; + return new List { ".zip", ".rtbundle", ".rtreplay" }.Contains(Path.GetExtension(file.Name)); } public void Dropped(IStorageFile file) @@ -280,4 +280,11 @@ public void Dropped(IStorageFile file) ConnectingViewModel.StartContentBundle(this, file); } + + public async Task OnWindowLoaded() + { + #if !DEBUG + await Protocol.ProtocolSignupPopup(Control!, _cfg); + #endif + } } diff --git a/SS14.Launcher/Views/ConfirmDialog.xaml b/SS14.Launcher/Views/ConfirmDialog.xaml new file mode 100644 index 000000000..3485603b8 --- /dev/null +++ b/SS14.Launcher/Views/ConfirmDialog.xaml @@ -0,0 +1,22 @@ + + + + + + +