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 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SS14.Launcher/Views/ConfirmDialog.xaml.cs b/SS14.Launcher/Views/ConfirmDialog.xaml.cs
new file mode 100644
index 000000000..67e55990d
--- /dev/null
+++ b/SS14.Launcher/Views/ConfirmDialog.xaml.cs
@@ -0,0 +1,44 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using ReactiveUI;
+
+namespace SS14.Launcher.Views;
+
+public partial class ConfirmDialog : Window
+{
+ public string? DialogContent
+ {
+ get => Content.Text;
+ set => Content.Text = value;
+ }
+
+ public string? ConfirmButtonText
+ {
+ get => ConfirmButton.Content as string;
+ set => ConfirmButton.Content = value;
+ }
+
+ public string? CancelButtonText
+ {
+ get => CancelButton.Content as string;
+ set => CancelButton.Content = value;
+ }
+
+ public ConfirmDialog()
+ {
+ InitializeComponent();
+
+ ConfirmButton.Command = ReactiveCommand.Create(() => Close(true));
+ CancelButton.Command = ReactiveCommand.Create(() => Close(false));
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close(false);
+ }
+
+ base.OnKeyDown(e);
+ }
+}
diff --git a/SS14.Launcher/Views/MainWindow.xaml.cs b/SS14.Launcher/Views/MainWindow.xaml.cs
index 93a373893..534449cd0 100644
--- a/SS14.Launcher/Views/MainWindow.xaml.cs
+++ b/SS14.Launcher/Views/MainWindow.xaml.cs
@@ -27,10 +27,14 @@ public MainWindow()
AddHandler(DragDrop.DragLeaveEvent, DragLeave);
AddHandler(DragDrop.DragOverEvent, DragOver);
AddHandler(DragDrop.DropEvent, Drop);
+ AddHandler(LoadedEvent, Load);
+ }
+ private async void Load(object? sender, RoutedEventArgs e)
+ {
+ ReloadTitle();
+ await _viewModel!.OnWindowLoaded();
_content = (MainWindowContent) Content!;
-
- ReloadTitle();
}
public void ReloadContent()
diff --git a/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml b/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml
index 5427ece34..cb4bb07ad 100644
--- a/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml
+++ b/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml
@@ -25,5 +25,10 @@
+
+
+
diff --git a/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml.cs b/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml.cs
index f45d15dff..905094a7e 100644
--- a/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml.cs
+++ b/SS14.Launcher/Views/MainWindowTabs/DevelopmentTabView.xaml.cs
@@ -1,4 +1,7 @@
-using Avalonia.Controls;
+using System;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Serilog;
namespace SS14.Launcher.Views.MainWindowTabs;
@@ -9,4 +12,27 @@ public DevelopmentTabView()
InitializeComponent();
}
+ private async void RegisterProtocols(object? _1, RoutedEventArgs _2)
+ {
+ try
+ {
+ await Protocol.RegisterProtocol();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Error registering protocols");
+ }
+ }
+
+ private async void UnregisterProtocols(object? _1, RoutedEventArgs _2)
+ {
+ try
+ {
+ await Protocol.UnregisterProtocol();
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Error unregistering protocols");
+ }
+ }
}
diff --git a/SS14.Launcher/Views/MainWindowTabs/HomePageView.xaml.cs b/SS14.Launcher/Views/MainWindowTabs/HomePageView.xaml.cs
index c2427a405..b95a0b3a9 100644
--- a/SS14.Launcher/Views/MainWindowTabs/HomePageView.xaml.cs
+++ b/SS14.Launcher/Views/MainWindowTabs/HomePageView.xaml.cs
@@ -54,9 +54,11 @@ private async void OpenReplayClicked(object? sender, RoutedEventArgs e)
[
new FilePickerFileType("Replay or content bundle files")
{
- Patterns = ["*.zip"],
- MimeTypes = ["application/zip"],
- AppleUniformTypeIdentifiers = ["zip"]
+ Patterns = ["*.zip", "*.rtbundle", "*.rtreplay"],
+ MimeTypes = ["application/zip", "application/rtbundle", "application/rtreplay"],
+ // Retrived using "mdls -name kMDItemContentType file.zip/rtreplay/rtbundle"
+ // No I'm not tripping... THIS is apparently how macOS identifies our file extension. Check Avalonia docs.
+ AppleUniformTypeIdentifiers = ["public.zip-archive", "dyn.ah62d4rv4ge81e7dwqz2g22p3", "dyn.ah62d4rv4ge81e7dcsz1gk5df"]
}
]
});
diff --git a/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml b/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml
index 1d12c675a..1c094ae41 100644
--- a/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml
+++ b/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml
@@ -61,9 +61,10 @@
Text="{loc:Loc tab-options-seasonal-branding-desc}"
Margin="8" />
-
-
+
diff --git a/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml.cs b/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml.cs
index 87fdfe1f5..a7fc30c5a 100644
--- a/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml.cs
+++ b/SS14.Launcher/Views/MainWindowTabs/OptionsTabView.xaml.cs
@@ -4,6 +4,7 @@
using Avalonia.Threading;
using Avalonia.VisualTree;
using ReactiveUI;
+using Serilog;
using SS14.Launcher.Utility;
using SS14.Launcher.ViewModels.MainWindowTabs;
@@ -43,4 +44,18 @@ private async void OpenHubSettings(object? sender, RoutedEventArgs args)
{
await new HubSettingsDialog().ShowDialog((Window)this.GetVisualRoot()!);
}
+
+ public async void OSProtocol(object? sender, RoutedEventArgs args)
+ {
+ try
+ {
+ var mainWindow = (MainWindow?)this.GetVisualRoot();
+ if (mainWindow != null)
+ await Protocol.OptionsManualPopup(mainWindow);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Error while registering protocol");
+ }
+ }
}
diff --git a/SS14.Launcher/Views/OkDialog.xaml b/SS14.Launcher/Views/OkDialog.xaml
new file mode 100644
index 000000000..f2dfa07f4
--- /dev/null
+++ b/SS14.Launcher/Views/OkDialog.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/SS14.Launcher/Views/OkDialog.xaml.cs b/SS14.Launcher/Views/OkDialog.xaml.cs
new file mode 100644
index 000000000..1aac2c2a5
--- /dev/null
+++ b/SS14.Launcher/Views/OkDialog.xaml.cs
@@ -0,0 +1,37 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using ReactiveUI;
+
+namespace SS14.Launcher.Views;
+
+public partial class OkDialog : Window
+{
+ public string? DialogContent
+ {
+ get => Content.Text;
+ set => Content.Text = value;
+ }
+
+ public string? ButtonText
+ {
+ get => OkButton.Content as string;
+ set => OkButton.Content = value;
+ }
+
+ public OkDialog()
+ {
+ InitializeComponent();
+
+ OkButton.Command = ReactiveCommand.Create(Close);
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (e.Key == Key.Escape)
+ {
+ Close(false);
+ }
+
+ base.OnKeyDown(e);
+ }
+}
diff --git a/publish_linux.sh b/publish_linux.sh
index b26934f63..f797cb2a8 100755
--- a/publish_linux.sh
+++ b/publish_linux.sh
@@ -16,7 +16,7 @@ mkdir -p bin/publish/Linux/bin
mkdir -p bin/publish/Linux/bin/loader
mkdir -p bin/publish/Linux/dotnet
-cp PublishFiles/SS14.Launcher PublishFiles/SS14.desktop bin/publish/Linux/
+cp PublishFiles/SS14.Launcher PublishFiles/SS14.desktop PublishFiles/ss14-mime-type.xml bin/publish/Linux/
cp SS14.Launcher/bin/Release/net9.0/linux-x64/publish/* bin/publish/Linux/bin/
cp SS14.Loader/bin/Release/net9.0/linux-x64/publish/* bin/publish/Linux/bin/loader
cp -r Dependencies/dotnet/linux/* bin/publish/Linux/dotnet/