Improbable Icon

Installing to a different drive while still using the SpatialOS Launcher and shared links


#1

Lately, we have had multiple playtests with different third parties. Using the SpatialOS Launcher and the share link from the deployment is quite straightforward in itself, but we’ve stumbled a few times on a minor hiccup: the third parties in question, when installing the game via launcher, runs out of system disk space while downloading and/or unzipping the client.

This wouldn’t really be an issue if the Launcher supported installing clients to different paths, but unfortunately at this time it doesn’t. Thus, I turned to the SpatialOS Discord Server (you should definitely check it out, very helpful people there!) and with the help of @Jackblue and @dvanamst I’ve managed to hack together a workaround (read: a dirty, dirty hack) for installing the game to any folder on any drive - and still use the Launcher for starting the game. We use Unreal and the example is for Unreal packages, but it should work with any client assemblies that the SpatialOS Launcher works with.

Some caveats:

  1. Works only on NTFS drives (shouldn’t be a problem, but still worth mentioning)
  2. Requires a bit of manual packaging
  3. Requires serving that package somehow (we used a direct download link from Google Drive)

Now, the first and second point are basically non-issues (NTFS should be used on all modern Windows OSs since Windows XP, and the manual packaging could be automated), but at least for now, I haven’t come up with a way to use the SpatialOS -deployment’s own client download services… Maybe at some point. :smirk:

Basically, the idea is to manually unzip the client package, then use an NTFS feature called Junction Point (or Symbolic Link, they are a bit different things but in the end work the same in our case) to link the client folder from SpatialOS Launcher client location. After that we register the client to a cache json so that the Launcher knows we have the correct version installed.

Step by step:

  1. Get client package
    1a. Download client package from an uploaded deployment

    or

    1b. Copy the client package from build folders (in our case the file would be build/assembly/worker/UnrealClient@Windows.zip)

  2. Prepare an empty directory with the package

  3. Get MD5 of the file
    3a. Get it from the assembly page of the deployment for the client package file

    or

    3b. Run certutil -hashfile <client_package_file> MD5 in command prompt

  4. Create a script/C# tool/batch file/etc. that does the following:

    1. Unzips package to a chosen directory

    2. Creates a symbolic link or junction point from
      %LOCALAPPDATA%\SpatialOS\Launch\clients\<MD5>
      to the directory the client was unzipped.

    3. Adds the following data to the json array in
      %LOCALAPPDATA%\SpatialOS\Launch\cache_log.json:

    		 {
    		    "md5" : "<MD5>",
    		    "dateCached" : 0 
    		 }
  1. Package this program/script with the client package, and serve using some method (we used Google Drive direct linking)

The client can now be installed to any folder or drive, and started using the shared deployment link or from the SpatialOS deployment console in case the person installing has a SpatialOS account and access to the project.

Here is a “quick” C# that installs a client to D:\SpatialOS\Clients (the example uses Newtonsoft JSON, see https://www.newtonsoft.com/json for more info) :

using System;
using System.IO;
using System.IO.Compression;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Runtime.InteropServices;
using System.Security.Permissions;

namespace SpatialInstaller
{
	[PrincipalPermission(SecurityAction.Assert, Role = @"BUILTIN\Administrators")]
	class Program
	{
		[DllImport("kernel32.dll", SetLastError=true)]
		static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags);

		enum SymbolicLink
		{
			File = 0,
			Directory = 1
		}

		static void Main(string[] args)
		{
			string MD5 = "d29fa4dc82fc5eb44f6c5da0509a55fc";
			string clientPackage = "UnrealClient@Windows.zip";
			string launcherPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SpatialOS", "Launch");
			string clientsPath = Path.Combine(launcherPath, "clients");
			string targetPath = Path.Combine("D:", "SpatialOS", "Clients", MD5);

			Directory.CreateDirectory(targetPath);

			ZipFile.ExtractToDirectory(clientPackage, targetPath);

			if (!CreateSymbolicLink(clientPath, targetPath, SymbolicLink.Directory))
			{
				throw new Exception("Creating symlink failed. Error: "+Marshal.GetLastWin32Error());
			}
			RegisterSpatialOSClient(Path.Combine(launcherPath, "cache_log.json"), md5);
		}

		private static void RegisterSpatialOSClient(string cacheLogPath, string md5)
		{
			var cacheLogJson = new JArray();
			if (File.Exists(cacheLogPath))
			{
				using (var sr = new StreamReader(cacheLogPath))
				{
					cacheLogJson = (JArray)JToken.ReadFrom(new JsonTextReader(sr));
				}
			}

			bool cacheFound = false;

			foreach (var item in cacheLogJson)
			{
				if (item["md5"].Value<string>().Equals(md5))
				{
					item["dateCached"] = 0;
					cacheFound = true;
				}
			}

			if (!cacheFound)
			{
				var newItem = new JObject();
				newItem["md5"] = md5;
				newItem["dateCached"] = 0;
				cacheLogJson.Add(newItem);
			}

			using (var sw = new StreamWriter(cacheLogPath))
    		{
				sw.Write(cacheLogJson.ToString());
			}
 		}
 	}
}

Hope this helps! In case something doesn’t work right, I’m always open to answering questions either on the SpatialOS Discord (@zment) or in this thread.

Also, if I could get access to the REST API, I might be able to download the assemblies straight from SpatialOS and even possibly get session tokens for use in testing, to create our own launcher (which we’d be willing to share). Auto-updating launcher for testing and prototyping would help us immensely… well, a man can dream. :smiley:


#3

Hello @jani.karkkainen,

Thank you very much for this amazing write-up and nice hack. This is a prime example of the great things we love to see from the community! :wink:

Just wanted to push this up again as this is an issue that multiple developers have been running into. We are obviously aware of this internally and are scheduling some work to address this at the root level but in the meantime your approach will certainly save the day for some people!

With regards to your last point about REST APIs, etc.: this is actually also scheduled with our Platform API. Although the assembly services will not be among the first to be exposed they will be in the longer term.

Kind regards,
Duco