<|start_filename|>LegacyInstaller/SteamDownloader.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace LegacyInstaller
{
    class SteamDownloader
    {
        public readonly string SteamExecutable;
        public readonly string ContentDir;
        public readonly string ContentAppDepotDir;

        private const string SteamAppId = "620980";
        private const string SteamDepotId = "620981";

        private FileSystemWatcher _contentDirWatcher;

        public SteamDownloader(string installDir)
        {
            SteamExecutable = Path.Combine(installDir, "steam.exe");
            ContentDir = Path.Combine(installDir, "steamapps", "content");
            ContentAppDepotDir = Path.Combine(ContentDir, $"app_{SteamAppId}", $"depot_{SteamDepotId}");
            Directory.CreateDirectory(ContentDir);
            _contentDirWatcher = new FileSystemWatcher(ContentDir);
            _contentDirWatcher.NotifyFilter = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size;
            _contentDirWatcher.IncludeSubdirectories = true;
            _contentDirWatcher.EnableRaisingEvents = true;
            _contentDirWatcher.Changed += ContentDirChanged;
        }

        private async Task PatchDownloadDepot() => await Task.Run(() =>
        {
            try { SteamPatcher.ApplyPatch(); }
            catch (SteamPatcher.PatchAlreadyAppliedException) { }
            catch (SteamPatcher.StringNotFoundException) { }
        });

        private TaskCompletionSource<bool> _downloadDepotTcs;

        public async Task DownloadDepot(string manifestId, FileSystemEventHandler eventHandler = null)
        {
            await PatchDownloadDepot();

            if (_downloadDepotTcs != null)
                throw new DownloadInProgressException();

            if (eventHandler != null)
                _contentDirWatcher.Changed += eventHandler;

            _downloadDepotTcs = new TaskCompletionSource<bool>();

            Process.Start(SteamExecutable, $"-console +download_depot {SteamAppId} {SteamDepotId} {manifestId}");

            await _downloadDepotTcs.Task;
            await Task.Delay(5000);

            _contentDirWatcher.Changed -= eventHandler;
        }

        private void ContentDirChanged(object sender, FileSystemEventArgs e)
        {
            if (!e.FullPath.Contains(ContentAppDepotDir))
                return;

            if (e.FullPath.Contains("UnityPlayer.dll") && e.ChangeType == WatcherChangeTypes.Changed && _downloadDepotTcs != null)
                _downloadDepotTcs.SetResult(true);
        }

        public class DownloadInProgressException : Exception
        {
            public DownloadInProgressException() : base() { }
            public DownloadInProgressException(string message) : base(message) { }
            public DownloadInProgressException(string message, Exception inner) : base(message, inner) { }
        }
    }
}


<|start_filename|>LegacyInstaller/SteamProcess.cs<|end_filename|>
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace LegacyInstaller
{
    class SteamProcess
    {
        private Process _process = null;
        public Process Process 
        {
            get
            {
                if (_process == null || _process.HasExited)
                    _process = Process.GetProcessesByName("steam").FirstOrDefault();
                return _process;
            }
        }

        private SteamShortcuts _shortcuts = null;
        public SteamShortcuts Shortcuts
        {
            get
            {
                if (_shortcuts == null || _shortcuts.UserId != CurrentUserId || _shortcuts.ProcessId != Process.Id)
                    _shortcuts = new SteamShortcuts(InstallDir, CurrentUserId, Process.Id);
                return _shortcuts;
            }
        }

        public readonly SteamDownloader Downloader;

        public int CurrentUserId => Utilities.GetCurrentSteamUser();
        public bool HasExited => Process == null || Process.HasExited;

        public readonly string InstallDir;
        public readonly string SteamExecutable;

        private readonly string[] MainWindowTitles = new[] { "Steam", "Servers", "Friends", "Default IME" };

        public SteamProcess(string installDir)
        {
            InstallDir = installDir;
            SteamExecutable = Path.Combine(InstallDir, "steam.exe");
            Downloader = new SteamDownloader(InstallDir);
        }

        public async Task Restart(string openTo = null)
        {
            await Task.Run(() =>
            {
                Process.Kill();
                Process.WaitForExit();
                var args = $"-silent +open steam://open/library" + (openTo != null ? $" +open {openTo}" : "");
                Debug.WriteLine(args);
                var steamProcess = new Process();
                steamProcess.StartInfo.FileName = SteamExecutable;
                steamProcess.StartInfo.Arguments = args;
                steamProcess.StartInfo.UseShellExecute = true;
                steamProcess.Start();
                steamProcess.WaitForInputIdle();
            });
            await WaitForMainWindow();
        }



        private const int MainWindowPollTime = 500;
        private const int WM_GETTEXT = 0x000D;

        [DllImport("user32.dll")]
        static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);
        delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, int wParam, StringBuilder lParam);

        public async Task WaitForMainWindow()
        {
            List<string> windowTitles = new List<string>();
            while (!MainWindowTitles.All(title => windowTitles.Contains(title)))
            {
                if (Process == null)
                    continue;

                await Task.Run(() =>
                {
                    Process.Refresh();
                    var handles = new List<IntPtr>();
                    foreach (ProcessThread thread in Process.Threads)
                        EnumThreadWindows(thread.Id,
                            (hWnd, lParam) => { handles.Add(hWnd); return true; }, IntPtr.Zero);
                    foreach (var handle in handles)
                    {
                        StringBuilder message = new StringBuilder(1000);
                        SendMessage(handle, WM_GETTEXT, message.Capacity, message);
                        if (!string.IsNullOrEmpty(message.ToString()))
                            windowTitles.Add(message.ToString());
                    }
                });
                await Task.Delay(MainWindowPollTime);
            }
        }
    }
}


<|start_filename|>LegacyInstaller/Utilities.cs<|end_filename|>
﻿using Microsoft.Win32;
using System;
using System.Data.HashFunction.CRC;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace LegacyInstaller
{
    public static class Utilities
    {
        public static async Task CopyDirectory(string sourceDir, string targetDir)
        {
            try
            {
                Directory.CreateDirectory(targetDir);

                foreach (var file in Directory.GetFiles(sourceDir))
                {
                    var fileSource = File.OpenRead(file);
                    var fileTarget = File.Create(Path.Combine(targetDir, Path.GetFileName(file)));
                    await fileSource.CopyToAsync(fileTarget);
                }

                foreach (var directory in Directory.GetDirectories(sourceDir))
                    await CopyDirectory(directory, Path.Combine(targetDir, Path.GetFileName(directory)));
            }
            catch { }
        }

        public static int Search(byte[] src, byte[] pattern)
        {
            int maxFirstCharSlot = src.Length - pattern.Length + 1;
            for (int i = 0; i < maxFirstCharSlot; i++)
            {
                if (src[i] != pattern[0]) // compare only first byte
                    continue;

                // found a match on first byte, now try to match rest of the pattern
                for (int j = pattern.Length - 1; j >= 1; j--)
                {
                    if (src[i + j] != pattern[j]) break;
                    if (j == 1) return i;
                }
            }
            return -1;
        }

        public static string GenerateChecksum(string targetDir)
        {
            var sha = SHA1.Create();

            var files = Directory.GetFiles(targetDir, "*.*", SearchOption.AllDirectories);
            var bytes = new byte[0];
            foreach (var file in files)
            {
                if (file.Contains("LaunchBS"))
                    continue;
                if (!File.Exists(file))
                    continue;
                bytes = bytes.Concat(sha.ComputeHash(File.ReadAllBytes(file))).ToArray();
            }
            return BitConverter.ToString(sha.ComputeHash(bytes));
        }

        public static string GenerateStringChecksum(string input)
        {
            var sha = SHA1.Create();
            return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(input)));
        }

        public static string DetectSteamInstallPath()
        {
            try
            {
                var detectedSteamPath = Registry.GetValue(
                    @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Valve\Steam",
                    "InstallPath",
                    null
                );

                if (detectedSteamPath != null && Directory.Exists((string)detectedSteamPath))
                    return (string)detectedSteamPath;
            }
            catch { }
            return null;
        }

        public static string DetectBeatSaberInstallPath()
        {
            try
            {
                var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
                var bsRegistryKey = hklm.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 620980");
                var detectedBSPath = bsRegistryKey.GetValue("InstallLocation");
                if (detectedBSPath != null && Directory.Exists((string)detectedBSPath))
                    return (string)detectedBSPath;
            }
            catch { }
            return null;
        }

        public static int GetCurrentSteamUser()
            => (int)RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)
                .OpenSubKey(@"SOFTWARE\Valve\Steam\ActiveProcess").GetValue("ActiveUser");

        public static UInt64 GetSteamAppId(string appName, string path, string exe)
        {
            var crc = CRCFactory.Instance.Create(new CRCConfig
            {
                HashSizeInBits = 32,
                Polynomial = 0x04C11DB7,
                ReflectIn = true,
                InitialValue = 0xffffffff,
                ReflectOut = true,
                XOrOut = 0xffffffff
            });

            byte[] inputBytes = Encoding.UTF8.GetBytes("\"" + Path.Combine(path, exe) + "\"" + appName);
            UInt64 top32 = BitConverter.ToUInt32(crc.ComputeHash(inputBytes).Hash, 0) | 0x80000000;
            UInt64 gameId = (top32 << 32) | 0x02000000;

            return gameId;
        }
    }
}


<|start_filename|>LegacyInstaller/SteamShortcut.cs<|end_filename|>
﻿using System.Text;

namespace LegacyInstaller
{
    class SteamShortcut
    {
        public string AppName { get; set; }
        public string Path { get; set; }
        public string Executable { get; set; }
        public bool IsHidden { get; set; } = false;
        public bool AllowDesktopConfig { get; set; } = true;
        public bool AllowOverlay { get; set; } = true;
        public bool OpenVR { get; set; } = true;

        public SteamShortcut(string appName, string path, string executable)
        {
            AppName = appName;
            Path = path;
            Executable = executable;
        }

        public byte[] GetBytes(int shortcutIndex)
        {
            string strShortcut =
                "\x00" + shortcutIndex          + "\x00" +
                "\x02" + "appid"                + "\x00" + "\x00\x00\x00\x00"                       + "" +
                "\x01" + "AppName"              + "\x00" + AppName                                  + "\x00" +
                "\x01" + "Exe"                  + "\x00" + $"\"{Path}\\{Executable}\""              + "\x00" +
                "\x01" + "StartDir"             + "\x00" + $"\"{Path}\\\""                          + "\x00" +
                "\x01" + "icon"                 + "\x00" + $"\"{Path}\\Beat Saber.exe\""            + "\x00" +
                "\x01" + "ShortcutPath"         + "\x00" + ""                                       + "\x00" +
                "\x01" + "LaunchOptions"        + "\x00" + ""                                       + "\x00" +
                "\x02" + "IsHidden"             + "\x00" + (IsHidden ? "\x01" : "\x00")             + "\x00\x00\x00" +
                "\x02" + "AllowDesktopConfig"   + "\x00" + (AllowDesktopConfig ? "\x01" : "\x00")   + "\x00\x00\x00" +
                "\x02" + "AllowOverlay"         + "\x00" + (AllowOverlay ? "\x01" : "\x00")         + "\x00\x00\x00" +
                "\x02" + "OpenVR"               + "\x00" + (OpenVR ? "\x01" : "\x00")               + "\x00\x00\x00" +
                "\x02" + "Devkit"               + "\x00" + "\x00"                                   + "\x00\x00\x00" +
                "\x01" + "DevkitGameID"         + "\x00" + ""                                       + "\x00" +
                "\x02" + "DevkitOverrideAppID"  + "\x00" + "\x00\x00\x00"                           + "\x00" +
                "\x02" + "LastPlayTime"         + "\x00" + "\x00\x00\x00\x00"                       + "" +
                "\x00" + "tags"                 + "\x00" + ""                                       + "\x08\x08";

            return Encoding.UTF8.GetBytes(strShortcut);
        }
    }
}


<|start_filename|>LegacyInstaller/SteamPatcher.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace LegacyInstaller
{
    class SteamPatcher
    {
        private const string ProcessName = "steam";
        private const string ModuleName = "steamclient.dll";
        private const string TargetString = "Depot download failed : Manifest not available";

        private const int PROCESS_VM_READ = 0x0010;
        private const int PROCESS_VM_WRITE = 0x0020;
        private const int PROCESS_VM_OPERATION = 0x0008;
        private const int PROCESS_QUERY_INFORMATION = 0x0400;
        private const int PROCESS_ALL_ACCESS = 0x1F0FFF;

        [DllImport("kernel32.dll")]
        public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

        [DllImport("kernel32.dll")]
        public static extern bool ReadProcessMemory(int hProcess, int lpBaseAddress, byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesRead);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool WriteProcessMemory(int hProcess, int lpBaseAddress, byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesWritten);

        public static void ApplyPatch()
        {
            // Find steam process
            var steamProcess = Process.GetProcessesByName(ProcessName).FirstOrDefault();
            if (steamProcess == null)
                throw new ProcessNotFoundException();

            var steamClientModule = steamProcess.Modules.Cast<ProcessModule>().FirstOrDefault(module => module.ModuleName == ModuleName);
            if (steamClientModule == null)
                throw new ProcessNotFoundException();

            const int accessFlags = PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE;
            IntPtr steamProcessHandle = OpenProcess(accessFlags, false, steamProcess.Id);
            if (steamProcessHandle == IntPtr.Zero)
                throw new UnableToOpenProcessException();

            IntPtr steamClientHandle = steamClientModule.BaseAddress;

            Debug.WriteLine($"Process handle: {steamProcessHandle.ToString("X")}");
            Debug.WriteLine($"Module handle: {steamClientHandle.ToString("X")}");

            int bytesRead = 0;
            byte[] buffer = new byte[steamClientModule.ModuleMemorySize];
            if (ReadProcessMemory((int)steamProcessHandle, (int)steamClientHandle, buffer, buffer.Length, ref bytesRead) == false)
                throw new MemoryUnreadableException();

            var strBytes = Encoding.UTF8.GetBytes(TargetString);
            var strIndex = Utilities.Search(buffer, strBytes);
            if (strIndex == -1)
                throw new StringNotFoundException();

            var strAddress = (int)steamClientHandle + strIndex;
            var pushBytes = BitConverter.GetBytes(strAddress).Prepend<byte>(0x68).ToArray();

            Debug.WriteLine($"String address: {strAddress.ToString("X")}");
            Debug.WriteLine(BitConverter.ToString(pushBytes));

            var pushIndex = Utilities.Search(buffer, pushBytes);
            if (pushIndex == -1)
                throw new PatternNotFoundException();

            Debug.WriteLine($"Pattern index: {pushIndex.ToString("X")}");

            // 0x0F 0x85 is JNZ instruction (jump if true)
            var index = pushIndex;
            while (buffer[index] != 0x0F && buffer[index + 1] != 0x85)
                index -= 1;

            // Either the patch has already been applied or the JNZ isn't there
            // We want to prevent writing over code which isn't actually part
            // of what we want to patch.
            if (index < pushIndex - 10)
                throw new PatchAlreadyAppliedException();

            // Replace 2-byte jnz with nop, jmp
            buffer[index] = 0x90;
            buffer[index + 1] = 0xE9;

            int bytesWritten = 0;
            var patchAddress = (int)steamClientHandle + index;
            if (WriteProcessMemory((int)steamProcessHandle, patchAddress, buffer.Skip(index).Take(2).ToArray(), 2, ref bytesWritten) == false)
                throw new UnableToWriteMemoryException();

            Debug.WriteLine("Wrote patch to memory.");
        }

        public class ProcessNotFoundException : Exception
        {
            public ProcessNotFoundException() : base() { }
            public ProcessNotFoundException(string message) : base(message) { }
            public ProcessNotFoundException(string message, Exception inner) : base(message, inner) { }
        }

        public class UnableToOpenProcessException : Exception
        {
            public UnableToOpenProcessException() : base() { }
            public UnableToOpenProcessException(string message) : base(message) { }
            public UnableToOpenProcessException(string message, Exception inner) : base(message, inner) { }
        }

        public class StringNotFoundException : Exception
        {
            public StringNotFoundException() : base() { }
            public StringNotFoundException(string message) : base(message) { }
            public StringNotFoundException(string message, Exception inner) : base(message, inner) { }
        }

        public class PatternNotFoundException : Exception
        {
            public PatternNotFoundException() : base() { }
            public PatternNotFoundException(string message) : base(message) { }
            public PatternNotFoundException(string message, Exception inner) : base(message, inner) { }
        }

        public class PatchAlreadyAppliedException : Exception
        {
            public PatchAlreadyAppliedException() : base() { }
            public PatchAlreadyAppliedException(string message) : base(message) { }
            public PatchAlreadyAppliedException(string message, Exception inner) : base(message, inner) { }
        }

        public class PatchAppliedIncorrectlyException : Exception
        {
            public PatchAppliedIncorrectlyException() : base() { }
            public PatchAppliedIncorrectlyException(string message) : base(message) { }
            public PatchAppliedIncorrectlyException(string message, Exception inner) : base(message, inner) { }
        }

        public class MemoryUnreadableException : Exception
        {
            public MemoryUnreadableException() : base() { }
            public MemoryUnreadableException(string message) : base(message) { }
            public MemoryUnreadableException(string message, Exception inner) : base(message, inner) { }
        }

        public class UnableToWriteMemoryException : Exception
        {
            public UnableToWriteMemoryException() : base() { }
            public UnableToWriteMemoryException(string message) : base(message) { }
            public UnableToWriteMemoryException(string message, Exception inner) : base(message, inner) { }
        }
    }
}


<|start_filename|>LegacyInstaller/Version.cs<|end_filename|>
﻿namespace LegacyInstaller
{
    public class Version
    {
        public string BSVersion;
        public string ManifestId;
        public string ReleaseURL;

        public override string ToString()
        {
            return BSVersion;
        }
    }
}


<|start_filename|>LegacyInstaller/SteamShortcuts.cs<|end_filename|>
﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace LegacyInstaller
{
    class SteamShortcuts
    {
        public readonly string ShortcutsPath;
        public readonly int UserId;
        public readonly int ProcessId;

        public bool HasChanged { get; private set; }

        public SteamShortcuts(string installDir, int userId, int processId)
        {
            ShortcutsPath = Path.Combine(installDir, "userdata", userId.ToString(), "config", "shortcuts.vdf");
            UserId = userId;
            ProcessId = processId;
        }

        public bool CheckForSteamShortcut(string shortcut)
        {
            if (UserId == 0)
                throw new NotLoggedInException();

            if (!File.Exists(ShortcutsPath))
                return false;
            var shortcuts = File.ReadAllText(ShortcutsPath);
            return shortcuts.Contains($"\x00{shortcut}\x00");
        }

        public void AddSteamShortcut(SteamShortcut shortcut)
        {
            if (UserId == 0)
                throw new NotLoggedInException();

            if (!File.Exists(ShortcutsPath))
                File.WriteAllText(ShortcutsPath, "\x00shortcuts\x00\x08\x08");

            if (File.ReadAllText(ShortcutsPath).Contains($"\x00{shortcut.AppName}\x00"))
                return;

            var shortcuts = File.ReadAllBytes(ShortcutsPath);

            var newShortcuts = shortcuts.Take(shortcuts.Length - 2); // remove file end
            newShortcuts = newShortcuts.Concat(shortcut.GetBytes(GetSteamShortcutCount())); // append new shortcut
            newShortcuts = newShortcuts.Append((byte)0x08).Append((byte)0x08); // add file end

            File.WriteAllBytes(ShortcutsPath, newShortcuts.ToArray());

            HasChanged = true;
        }

        public void DeleteSteamShortcut(string appName)
        {
            if (UserId == 0)
                throw new NotLoggedInException();

            if (!File.Exists(ShortcutsPath))
                return;

            var shortcuts = File.ReadAllText(ShortcutsPath, Encoding.ASCII);
            if (!shortcuts.Contains($"\x00{appName}\x00"))
                return;

            var newShortcuts = Regex.Replace(shortcuts, @"\x00[\d]\x00(?!.*?\x00[\d]\x00.*?\x01AppName\x00" + appName + @"\x00).*?\x08\x08", "");
            File.WriteAllText(ShortcutsPath, newShortcuts, Encoding.ASCII);

            HasChanged = true;
        }

        public int GetSteamShortcutCount()
        {
            var shortcuts = File.ReadAllText(ShortcutsPath);

            int highestIndex = -1;
            var matches = Regex.Matches(shortcuts, @"\x00([\d])\x00");
            foreach (Match match in matches)
            {
                int index = Int32.Parse(match.Groups[1].Value);
                if (highestIndex < index)
                    highestIndex = index;
            }

            return highestIndex + 1;
        }

        public class NotLoggedInException : Exception
        {
            public NotLoggedInException() : base() { }
            public NotLoggedInException(string message) : base(message) { }
            public NotLoggedInException(string message, Exception inner) : base(message, inner) { }
        }
    }
}


<|start_filename|>LegacyInstaller/LaunchBS.bat<|end_filename|>
@echo off
setlocal
	set SteamAppId=620980 && start "" "Beat Saber.exe" %*
endlocal