diff --git a/Docs/VRCSDKSettings.webp b/Docs/VRCSDKSettings.webp
new file mode 100644
index 0000000..86b21d2
Binary files /dev/null and b/Docs/VRCSDKSettings.webp differ
diff --git a/Docs/VRCSdkSettings b/Docs/VRCSdkSettings
deleted file mode 100644
index 5f3e320..0000000
Binary files a/Docs/VRCSdkSettings and /dev/null differ
diff --git a/NOTES.md b/NOTES.md
new file mode 100644
index 0000000..cd51447
--- /dev/null
+++ b/NOTES.md
@@ -0,0 +1,70 @@
+# VRCSDK Patching Notes
+
+## How VRCSDK is meant to work
+
+The Unity EditorPref key `VRC_installedClientPath` is meant to hold a string of a absolute path that points to VRChat.exe.
+
+This is always set after VRCSDK initializes; if it was not already set, it pulls the value from some key in the registry, which was probably set beforehand by VRChat.exe or launch.exe.
+
+In the settings page, the user has an option to change `VRC_installedClientPath`. I have no idea what the practical use-case is for doing this. Clicking "Revert to Default" resets it by reading from the registry again.
+
+The VRCSDK.World "Build and test" button will execute VRChat.exe directly, giving it many arguments that launch the game in offline mode with a `file:///` URI to the built world AssetBundle ending in `.vrcw`. It only falls back to "executing" the equivalent `vrchat://` link directly if VRChat.exe hasn't been found -- that is, `VRC_installedClientPath` is unset because the registry key was missing, or VRChat was recently moved to a different Steam library.
+
+The VRCSDK fetches avatars in the Content Manager window, by searching directly inside `Environment.SpecialFolder.LocalApplicationData`. This is `AppData/LocalLow` on Windows and `~/.local/share/VRChat` on Linux, but there's no practical reason for the latter to exist. This dir has even been known to interfere with VRCFaceTracking. The AssetBundle can technically work no matter where you save it, but it's best if it's saved to the LocalLow inside VRChat's Proton prefix.
+
+## Alternatives
+
+Prior to this doc, we've been manually launching Proton, and the user must select the Proton version by way of searching for the `proton` Python script file to use as an entrypoint. It also did not locate the VRChat game directory, so moving it to a non-default Steam library would break it.
+
+Now, we use `SteamLocator`, `VrcLocator`, and `ProtonLocator` to find everything, reconciling it with Unity preferences.
+
+There is still the option to ask Steam to launch it, with `steam -applaunch 438100` or something, but I don't know if it would work. We want it to use offline mode, allow multiple instances, and not interfere with any instance running in online mode. The URI handler below has the same effect as launching it from Steam. The `vrchat://` URIs can work on Linux, but they need a .desktop file.
+
+## How we patch VRCSDK
+
+This is an exhaustive list.
+
+- Fixes the VRCSDK initialization so it can correctly find VRChat.exe.
+ - We replace the `LoadRegistryVRCInstallPath` method to call our `VrcLocator` instead.
+- Prevents the pointless creation (lol) and use of `~/.local/share/VRChat`, instead saving test worlds and avatars to the Proton prefix.
+ - We replace the method that looks for `LocalLow` to do Not that.
+- Fixes the Content Manager tab so it can show your test avatars.
+- Fixes the Build and Test button.
+ - We replace the entire `VRCWorldAssetExporter.RunWorldTestDesktop` method to:
+ - Translate the path of the saved AssetBundle to a winepath relative to `Z:/`.
+ - Call Proton instead, with all `STEAM_COMPAT_` env vars set.
+ - We do not launch it in Steam Linux Runtime, and I hope it doesn't come to that. But it is possible, we'd just read `toolmanifest.vdf` and wrap the command further.
+- Adds some UI in VRCSDK Settings tab to select a different Proton to use instead of what's set in Steam.
+
+## Ideas/Roadmap
+
+- [ ] Warn if `~/.local/share/VRChat` exists, because the latest VRCFaceTracking.Avalonia release (currently v1.1.1.0) will break if it sees this.
+- [ ] Run `xdg-mime query` and offer to set up [the URI handler](#vrchat-uri-handler).
+- [ ] Patch UdonSharp exception watcher.
+- [ ] MIT license button in Tools menu.
+
+## Snippets
+
+### VRChat Offline Mode
+
+Example command I use to run VRChat in offline mode, supporting multiple clients all loaded into an instance of a world AssetBundle stored locally:
+
+```bash
+STEAM_COMPAT_DATA_PATH=/mnt/steam/steamapps/compatdata/438100/ STEAM_COMPAT_CLIENT_INSTALL_PATH=$HOME/.local/share/Steam/ STEAM_COMPAT_INSTALL_PATH=/mnt/steam/steamapps/common/VRChat/ ~/.local/share/Steam/compatibilitytools.d/GE-Proton10-33-rtsp24-1/proton run /mnt/steam/steamapps/common/VRChat/VRChat.exe '--url=create?roomId=8094722763&hidden=true&name=BuildAndRun&url=file:///Z:/mnt/steam/steamapps/common/VRChat/VRChat_Data/StreamingAssets/Worlds/errorworld.vrcw' --enable-debug-gui --enable-sdk-log-levels --enable-udon-debug-logging --no-vr
+```
+
+### VRChat URI Handler
+
+Save to `~/.local/share/applications/vrchat-uri-handler.desktop`, and Firefox will let you click "Launch World" buttons on the website. It'll let you `xdg-open vrchat://` too.
+
+```desktop file=vrchat-uri-handler.desktop
+[Desktop Entry]
+Name=URI-vrchat
+Comment=URI handler for vrchat://
+Exec=/usr/bin/env steam -applaunch 438100 %U
+Terminal=false
+Type=Application
+Categories=Game;
+MimeType=x-scheme-handler/vrchat;
+NoDisplay=true
+```
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.avatars/package.json b/Packages/befuddledlabs.linuxvrchatsdkpatch.avatars/package.json
index c53d083..0d67ea4 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.avatars/package.json
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.avatars/package.json
@@ -7,9 +7,9 @@
"name": "BefuddledLabs"
},
"unity": "2022.3",
- "description": "Patches the VRChat SDK to work properly on linux.",
+ "description": "Patches the VRChat Avatars SDK to work properly on Linux.",
"vpmDependencies": {
- "com.vrchat.avatars": "^3.8.2",
+ "com.vrchat.avatars": "^3.10.3",
"befuddledlabs.linuxvrchatsdkpatch-base": "0.2.2"
}
}
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Base.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Base.cs
deleted file mode 100644
index 0222e4b..0000000
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Base.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using HarmonyLib;
-using JetBrains.Annotations;
-using UnityEditor;
-using VRC.Core;
-using VRC.SDKBase.Editor;
-
-namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
-{
- [HarmonyPatch]
- public static class Base
- {
- [CanBeNull]
- public static string GetCompatDataPath()
- {
- var vrChatPath = SDKClientUtilities.GetSavedVRCInstallPath();
- if (string.IsNullOrWhiteSpace(vrChatPath))
- return null;
-
- var dir = new FileInfo(vrChatPath).Directory;
- if (dir == null)
- return null;
-
- while (!dir.Name.Contains("steamapps", StringComparison.OrdinalIgnoreCase))
- {
- dir = dir.Parent;
- if (dir == null)
- return null;
- }
-
- return dir.FullName + "/compatdata/";
- }
-
- static Base()
- {
- // Check if the user has protontricks installed
- try
- {
- var p = new Process
- {
- StartInfo = new ProcessStartInfo
- {
- FileName = "protontricks-launch",
- Arguments = "-h", // so it doesn't fail
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- UseShellExecute = false
- }
- };
-
- p.Start();
- p.WaitForExit();
-
- HasProtonTricks = p.ExitCode == 0;
- }
- catch
- {
- HasProtonTricks = false;
- }
- }
-
- public static bool HasProtonTricks;
-
- [CanBeNull]
- public static string GetVrcCompatDataPath()
- {
- return GetCompatDataPath() + "438100/";
- }
-
- public static string ProtonPath
- {
- get
- {
- var savedVrcInstallPath = "";
- if (EditorPrefs.HasKey("LinuxVRC_protonPath"))
- savedVrcInstallPath = EditorPrefs.GetString("LinuxVRC_protonPath");
- return savedVrcInstallPath;
- }
- set => EditorPrefs.SetString("LinuxVRC_protonPath", value);
- }
-
- public static bool ProtonTricksPrefs
- {
- get
- {
- var savedProtonTricksPrefs = true; // Default use proton tricks
- if (EditorPrefs.HasKey("LinuxVRC_protonTricksPrefs"))
- savedProtonTricksPrefs = EditorPrefs.GetBool("LinuxVRC_protonTricksPrefs");
- return savedProtonTricksPrefs;
- }
- set => EditorPrefs.SetBool("LinuxVRC_protonTricksPrefs", value);
- }
-
- // Thanks Bartkk <3
- [HarmonyPrefix]
- [HarmonyPatch(typeof(VRC_SdkBuilder), "GetLocalLowPath")]
- public static bool GetLocalLowPathPrefix(ref string __result)
- {
- __result = GetVrcCompatDataPath() + "pfx/drive_c/users/steamuser/AppData/LocalLow/";
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/CannyPopup.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/CannyPopup.cs
index aee8381..e77a5be 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/CannyPopup.cs
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/CannyPopup.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
@@ -7,23 +9,23 @@ namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
[InitializeOnLoad]
public static class CannyPopup
{
- [MenuItem("VRChat SDK/Utilities/Linux/Clear LinuxVRC_cannyDialog")]
- public static void ResetLinuxVRC_cannyDialog() => EditorPrefs.SetBool("LinuxVRC_cannyDialog", false);
+ [MenuItem("VRChat SDK/Utilities/Linux/Reset Canny popup preference")]
+ public static void ResetCannyDialog() => LinuxVrcEditorPrefs.CannyDialog = false;
static CannyPopup()
{
- if (EditorPrefs.GetBool("LinuxVRC_cannyDialog", false) ||
+ if (LinuxVrcEditorPrefs.CannyDialog ||
!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return;
- var result = EditorUtility.DisplayDialog("Linux VRChat Patch",
- "Please upvote this canny instead of needing this patch local tests.",
+ var result = EditorUtility.DisplayDialog("Linux VRChat SDK Patch",
+ "Please upvote this VRChat Canny, which would obviate the need for these SDK patches.",
"Open Canny", "Don't show again");
if (result)
Application.OpenURL(
"https://feedback.vrchat.com/sdk-bug-reports/p/add-proton-support-to-the-sdk-for-local-tests");
- EditorPrefs.SetBool("LinuxVRC_cannyDialog", true); //
+ LinuxVrcEditorPrefs.CannyDialog = true;
}
}
}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LaunchConfiguration.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LaunchConfiguration.cs
new file mode 100644
index 0000000..fa430fc
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LaunchConfiguration.cs
@@ -0,0 +1,118 @@
+#nullable enable
+
+using System.IO;
+using BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators;
+using UnityEngine;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
+{
+ ///
+ /// Represents all Linux-specific settings required to start VRChat using the World SDK Build and Test functionality,
+ /// determined by resolving all defaults with the user's preferences in Unity and Steam.
+ ///
+ public class LaunchConfiguration
+ {
+ private LaunchConfiguration(string steamRoot, string protonExecutable, string compatDataPath,
+ string vrcInstallRoot)
+ {
+ SteamRoot = steamRoot;
+ ProtonExecutable = protonExecutable;
+ CompatDataPath = compatDataPath;
+ VrcInstallRoot = vrcInstallRoot;
+ }
+
+ ///
+ /// An absolute path to the Steam root directory.
+ ///
+
+ public string SteamRoot { get; }
+
+ ///
+ /// An absolute path to a Proton version's proton Python script.
+ ///
+ public string ProtonExecutable { get; }
+
+ ///
+ /// An absolute path to the compatdata/ directory in a Proton prefix.
+ ///
+
+ public string CompatDataPath { get; }
+
+ ///
+ /// An absolute path to common/VRChat/
+ ///
+ public string VrcInstallRoot { get; }
+
+ ///
+ /// Determine the environment taking preferences and locations from Steam and Unity into account by which we should
+ /// launch VRChat in offline Build and Test mode.
+ ///
+ ///
+ /// The value of the Unity EditorPref for custom Proton version, or if it is unset.
+ ///
+ ///
+ public static LaunchConfiguration? Resolve(string? protonPath)
+ {
+ // SteamRoot
+ var steamRoot = SteamLocator.FindSteamRoot();
+ if (steamRoot == null || !SteamLocator.IsValidSteamRoot(steamRoot))
+ {
+ Debug.LogError($"Couldn't find Steam root: \"{steamRoot}\"");
+ return null;
+ }
+
+ // ProtonExecutable
+ if (!ProtonLocator.IsValidCompatToolPath(protonPath))
+ {
+ Debug.Log("Custom Proton install path is unset or invalid, will auto-detect.");
+
+ protonPath = ProtonLocator.GetSteamVdfCompatTool(steamRoot, VrcLocator.VrcAppId);
+ if (!ProtonLocator.IsValidCompatToolPath(protonPath))
+ {
+ Debug.LogError($"Couldn't find compat tool used for VRChat: {protonPath}");
+ return null;
+ }
+ }
+
+ if (protonPath == null)
+ return null; // satisfies compiler
+
+ var protonExecutable = Path.Combine(protonPath, "proton");
+
+ // CompatDataPath
+ var compatDataPath = VrcLocator.GetCompatDataPath();
+ if (compatDataPath == null || !VrcLocator.IsValidCompatDataPath(compatDataPath))
+ {
+ Debug.LogError($"Could not find VRChat's compatdata: \"{compatDataPath}\"");
+ return null;
+ }
+
+ // VrcGameRoot
+ var vrcExePath = VrcLocator.GetVrcInstallPath();
+ if (vrcExePath == null || !VrcLocator.IsValidVrcInstallPath(vrcExePath))
+ {
+ Debug.LogError($"Could not locate VRChat.exe: \"{vrcExePath}\"");
+ return null;
+ }
+
+ var vrcInstallRoot = Path.GetDirectoryName(vrcExePath);
+ // ReSharper disable once InvertIf
+ if (vrcInstallRoot == null || !Directory.Exists(vrcInstallRoot))
+ {
+ Debug.LogError($"Could not locate VRChat's install directory: \"{vrcInstallRoot}\"");
+ return null;
+ }
+
+ // TODON'T: read toolmanifest.vdf "commandline"? nah
+ return new LaunchConfiguration(steamRoot, protonExecutable, compatDataPath, vrcInstallRoot);
+ }
+
+ public void DebugPrint()
+ {
+ Debug.Log($"Steam root: \"{SteamRoot}\"");
+ Debug.Log($"Proton executable: \"{ProtonExecutable}\"");
+ Debug.Log($"Compat data path: \"{CompatDataPath}\"");
+ Debug.Log($"VRChat install directory: \"{VrcInstallRoot}\"");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LaunchConfiguration.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LaunchConfiguration.cs.meta
new file mode 100644
index 0000000..4fe72b1
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LaunchConfiguration.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: ecdc0952b219453fa1708d9bf0b8cd95
+timeCreated: 1777001244
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSDKPatch-Base.Editor.asmdef b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSdkPatch.Base.Editor.asmdef
similarity index 86%
rename from Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSDKPatch-Base.Editor.asmdef
rename to Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSdkPatch.Base.Editor.asmdef
index f0c71c1..dc981fd 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSDKPatch-Base.Editor.asmdef
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSdkPatch.Base.Editor.asmdef
@@ -1,6 +1,6 @@
{
"name": "LinuxVRChatSdkPatch.Base.Editor",
- "rootNamespace": "",
+ "rootNamespace": "BefuddledLabs.LinuxVRChatSdkPatch.Base",
"references": [
"VRC.SDKBase",
"VRC.SDKBase.Editor"
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSDKPatch-Base.Editor.asmdef.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSdkPatch.Base.Editor.asmdef.meta
similarity index 76%
rename from Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSDKPatch-Base.Editor.asmdef.meta
rename to Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSdkPatch.Base.Editor.asmdef.meta
index 0f3b41e..aeeba6a 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSDKPatch-Base.Editor.asmdef.meta
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVRChatSdkPatch.Base.Editor.asmdef.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: a330f0ccedb2dc0af8ef51b369f732f9
+guid: 6532898edc3e2729dab32e3ae1aa2126
AssemblyDefinitionImporter:
externalObjects: {}
userData:
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVrcEditorPrefs.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVrcEditorPrefs.cs
new file mode 100644
index 0000000..1f74ee4
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVrcEditorPrefs.cs
@@ -0,0 +1,35 @@
+#nullable enable
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
+{
+ public static class LinuxVrcEditorPrefs
+ {
+ public const string PrefsKeyCustomProtonPath = "LinuxVRC_customProtonPath";
+ public const string PrefsKeyCannyDialog = "LinuxVRC_cannyDialog";
+
+ ///
+ /// An absolute path to the install dir of the user's preferred Proton version, or if it is unset.
+ ///
+ public static string? CustomProtonPath
+ {
+ get
+ {
+ string? val = null;
+ if (UnityEditor.EditorPrefs.HasKey(PrefsKeyCustomProtonPath))
+ val = UnityEditor.EditorPrefs.GetString(PrefsKeyCustomProtonPath);
+ return val;
+ }
+ set => UnityEditor.EditorPrefs.SetString(PrefsKeyCustomProtonPath, value);
+ }
+
+ ///
+ /// Whether the user has been shown the popup asking them to vote for the VRCSDK Canny for Linux support.
+ /// If true, it won't be shown again.
+ ///
+ public static bool CannyDialog
+ {
+ get => UnityEditor.EditorPrefs.GetBool(PrefsKeyCannyDialog, false);
+ set => UnityEditor.EditorPrefs.SetBool(PrefsKeyCannyDialog, value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Base.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVrcEditorPrefs.cs.meta
similarity index 100%
rename from Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Base.cs.meta
rename to Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/LinuxVrcEditorPrefs.cs.meta
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators.meta
new file mode 100644
index 0000000..bcfb535
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: bebe6d906a3743cfb49bc76d5737a9fc
+timeCreated: 1776228928
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/OfficialCompatToolData.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/OfficialCompatToolData.cs
new file mode 100644
index 0000000..87baaed
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/OfficialCompatToolData.cs
@@ -0,0 +1,83 @@
+#nullable enable
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators
+{
+ ///
+ /// Data extracted from appcache.vdf on 2026-04-17 using a one-off script using new-vdf-parser[1]
+ /// [1]: https://crates.io/crates/new-vdf-parser
+ ///
+ public static class OfficialCompatToolData
+ {
+ private static readonly CompatTool Proton9 = new(
+ "proton_9",
+ "2805730",
+ "proton-9.0-4pin".Split(","),
+ "Proton 9.0 (Beta)"
+ );
+
+ private static readonly CompatTool Proton10 = new(
+ "proton_10",
+ "3658110",
+ "proton-9,proton-9.0-1RC,proton-stable,proton-next,proton_next,proton-7.0-1,proton-7.0-2,proton-7.0-3,proton-7.0-4,proton-7.0-5,proton-7.0-6,proton-8.0-1,proton-8.0-2,proton-8.0-3,proton-8.0-4,proton-8.0-5,proton-8.0RC,proton-9.0-2RC,proton-9.0-3RC,proton-9.0-4RC,proton-10,proton-10.0-beta,proton-10.0-3RC"
+ .Split(","),
+ "Proton 10.0"
+ );
+
+ private static readonly CompatTool Proton11 = new(
+ "proton_11",
+ "4628710",
+ "proton-11.0-beta".Split(","),
+ "Proton 11.0"
+ );
+
+ private static readonly CompatTool ProtonHotfix = new(
+ "proton_hotfix",
+ "2180100",
+ "proton-hotfix".Split(","),
+ "Proton Hotfix"
+ );
+
+ private static readonly CompatTool ProtonExperimental = new(
+ "proton_experimental",
+ "1493710",
+ "proton-experimental".Split(","),
+ "Proton - Experimental"
+ );
+
+ public static readonly CompatTool[] All = { Proton9, Proton10, Proton11, ProtonHotfix, ProtonExperimental };
+ }
+
+ ///
+ /// Stores information about official compatibility tools that get installed to Steam libraries.
+ ///
+ public class CompatTool
+ {
+ public CompatTool(string name, string appId, string[] aliases, string installDir)
+ {
+ Name = name;
+ AppId = appId;
+ Aliases = aliases;
+ InstallDir = installDir;
+ }
+
+ ///
+ /// The name of the tool as it appears in CompatToolMapping.
+ ///
+ public string Name { get; }
+
+ ///
+ /// The Steam AppId of this tool.
+ ///
+ public string AppId { get; }
+
+ ///
+ /// A list of other (possibly older) names that this tool may also appear in CompatToolMapping.
+ ///
+ public string[] Aliases { get; }
+
+ ///
+ /// The name of the directory within a Steam library folder that this tool will be installed in.
+ ///
+ public string InstallDir { get; }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/OfficialCompatToolData.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/OfficialCompatToolData.cs.meta
new file mode 100644
index 0000000..857100c
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/OfficialCompatToolData.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2b4bb09b50bb418d845db7749b40c637
+timeCreated: 1776485203
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/ProtonLocator.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/ProtonLocator.cs
new file mode 100644
index 0000000..1794244
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/ProtonLocator.cs
@@ -0,0 +1,303 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using UnityEngine;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators
+{
+ ///
+ /// Reads through Steam directories and VDF files to find custom and official compatibility tools ("compat tools"; e.g.
+ /// Proton), the AppIds they are used for, and the Steam user's preferred tool selections.
+ ///
+ ///
+ /// Much of this was taken from VRCX, MIT.
+ ///
+ /// See also official documentation:
+ ///
+ /// Steam compatibility tool interface
+ ///
+ ///
+ public static class ProtonLocator
+ {
+ ///
+ /// Determines the compatibility tool (e.g., Proton) for a game having the given , and return
+ /// an absolute filepath to the tool's install dir.
+ ///
+ /// An absolute path to the Steam root directory.
+ /// The Steam AppId of the game.
+ ///
+ /// An absolute filepath to the compat tool's install dir, or if it couldn't be determined
+ /// or found.
+ ///
+ public static string? GetSteamVdfCompatTool(string steamRoot, string appId)
+ {
+ var configVdfPath = Path.Combine(steamRoot, "config", "config.vdf");
+ if (!File.Exists(configVdfPath))
+ {
+ Debug.LogError($"Couldn't determine current Proton: config.vdf not found: {configVdfPath}");
+ return null;
+ }
+
+ var vdfContent = File.ReadAllText(configVdfPath);
+ var compatToolMapping = ExtractCompatToolMapping(vdfContent);
+
+ if (!compatToolMapping.TryGetValue(appId, out var compatToolName) || compatToolName == null)
+ {
+ Debug.Log("config.vdf doesn't have an entry for VRChat, so we'll look for the default.");
+ if (!compatToolMapping.TryGetValue("0", out compatToolName) || compatToolName == null)
+ {
+ Debug.LogError(
+ $"Couldn't determine current Proton: config.vdf couldn't be parsed, or doesn't have an entry for the appid {appId} and couldn't find the default compat tool.");
+ return null;
+ }
+ }
+
+ Debug.Log($"Using compat tool name: {compatToolName}");
+
+ var compatTool = GetCompatToolDirForName(steamRoot, compatToolName);
+ if (compatTool == null)
+ {
+ Debug.LogError(
+ $"Couldn't determine current Proton: couldn't find compat tool dir named \"{compatToolName}\"");
+ return null;
+ }
+
+ Debug.Log($"Found compat tool dir: {compatTool}");
+ return compatTool;
+ }
+
+ ///
+ /// Determines whether the given filepath refers to an existing compatibility tool install dir.
+ ///
+ /// An absolute path to a compatibility tool.
+ /// if the tool dir is valid, otherwise.
+ public static bool IsValidCompatToolPath(string? compatToolPath)
+ {
+ if (string.IsNullOrEmpty(compatToolPath))
+ {
+ return false;
+ }
+
+ // compatibilitytool.vdf is only included with compatibilitytools.d ones, not official ones
+ // thus, toolmanifest.vdf is a better indicator
+ var toolVdf = Path.Combine(compatToolPath!, "toolmanifest.vdf");
+ var fileExists = File.Exists(toolVdf);
+ Debug.Log($"File.Exists(\"{toolVdf}\") = {fileExists}");
+ return fileExists;
+ }
+
+ ///
+ /// Locates the Steam root and finds the compatibilitytools.d directory.
+ ///
+ /// An absolute path to compatibilitytools.d, or if it or the Steam root can't be found.
+ public static string? GetCompatibilityToolsDotD()
+ {
+ var steamRoot = SteamLocator.FindSteamRoot();
+ if (steamRoot == null || !SteamLocator.IsValidSteamRoot(steamRoot))
+ {
+ return null;
+ }
+
+ var dotD = Path.Combine(steamRoot, "compatibilitytools.d");
+ return Directory.Exists(dotD) ? dotD : null;
+ }
+
+ ///
+ /// Interprets as a VDF and extracts the mapping between each AppId and its selected
+ /// compat tool name (empty string if unset/default).
+ ///
+ ///
+ /// Taken from VRCX, MIT.
+ ///
+ ///
+ /// A mapping between Steam AppIds and the name of the selected compat tool.
+ private static Dictionary ExtractCompatToolMapping(string vdfContent)
+ {
+ var compatToolMapping = new Dictionary();
+ const string sectionHeader = "\"CompatToolMapping\"";
+ var sectionStart = vdfContent.IndexOf(sectionHeader, StringComparison.Ordinal);
+
+ if (sectionStart == -1)
+ {
+ Debug.LogError("CompatToolMapping not found");
+ return compatToolMapping;
+ }
+
+ var blockStart = vdfContent.IndexOf('{', sectionStart) + 1;
+ var blockEnd = FindMatchingBracket(vdfContent, blockStart - 1);
+
+ if (blockStart == -1 || blockEnd == -1)
+ {
+ Debug.LogError("CompatToolMapping block not found");
+ return compatToolMapping;
+ }
+
+ var blockContent = vdfContent.Substring(blockStart, blockEnd - blockStart);
+
+ // "123" { "crap" "blah" "name" "proton 67"
+ // captures `123` and `proton 67`
+ var keyValuePattern = new Regex("\"(\\d+)\"\\s*\\{[^}]*\"name\"\\s*\"([^\"]+)\"",
+ RegexOptions.Multiline);
+
+ var matches = keyValuePattern.Matches(blockContent);
+ foreach (Match match in matches)
+ {
+ var key = match.Groups[1].Value;
+ var name = match.Groups[2].Value;
+
+ if (key != "0")
+ {
+ compatToolMapping[key] = name;
+ }
+ }
+
+ return compatToolMapping;
+ }
+
+ ///
+ /// Scan common locations for all compat tool directories and libraries, and return an absolute path to the tool that
+ /// matches the name or alias .
+ ///
+ /// An absolute path to the Steam root directory.
+ ///
+ /// The name or alias of the compat tool, as listed in config.vdf or its compatibilitytool.vdf.
+ ///
+ ///
+ /// An absolute path to the compat tool install dir, or if it doesn't match a known
+ /// official name or can't be found.
+ ///
+ private static string? GetCompatToolDirForName(string steamRoot, string compatToolName)
+ {
+ var steamRootDotD = Path.Combine(steamRoot, "compatibilitytools.d");
+ var foundDotD = FindCompatToolInDotD(steamRootDotD, compatToolName);
+
+ var foundOfficial = FindOfficialCompatToolForName(steamRoot, compatToolName);
+
+ // also look for system-installed protons.
+ // I guess the majority of /usr paths come from folks getting protons from the AUR.
+ var systemDotD = Path.Combine("usr", "share", "steam", "compatibilitytools.d");
+ var foundSystemDotD = FindCompatToolInDotD(systemDotD, compatToolName);
+
+ Debug.Log(
+ $"Possibly matching compat tools: custom=\"{foundDotD}\", systemCustom=\"{foundSystemDotD}\", official=\"{foundOfficial}\"");
+
+ // if the same tool is installed in more than one, I have no idea which one takes precedence in Steam.
+ return foundDotD ?? foundSystemDotD ?? foundOfficial;
+ }
+
+ ///
+ /// Given the name of an official Proton version , try to locate its install dir.
+ ///
+ ///
+ /// If the given is the identifier or an alias for an official compatibility tool
+ /// that we recognize, knowing that it would be installed into a Steam library, check all known Steam libraries for a
+ /// compat tool whose directory matches the official installDir.
+ ///
+ /// An absolute path to the Steam root directory.
+ ///
+ /// The name or alias of the compat tool, as listed in config.vdf.
+ ///
+ ///
+ /// An absolute path to the compat tool install dir, or if it doesn't match a known name
+ /// or isn't installed.
+ ///
+ private static string? FindOfficialCompatToolForName(string steamRoot, string compatToolName)
+ {
+ // look for official protons like "Proton 9.0 (Beta)" and "Proton 10.0"
+ var matchingOfficialCompatTool = OfficialCompatToolData.All
+ .FirstOrDefault(tool => tool.Name == compatToolName || tool.Aliases.Contains(compatToolName));
+
+ // does it match an official tool name or an alias of one?
+ if (matchingOfficialCompatTool == null)
+ return null;
+
+ Debug.Log(
+ $"Matching \"{compatToolName}\" to official compat tool: {matchingOfficialCompatTool.Name}, checking" +
+ $"Steam libraries for appid {matchingOfficialCompatTool.AppId}");
+
+ // is it installed to one of our libraries?
+ var libraryWithOfficial = SteamLocator.GetLibraryWithAppId(steamRoot, matchingOfficialCompatTool.AppId);
+ Debug.Log($"Located in library: \"{libraryWithOfficial ?? "sorry, nothing"}\"");
+ if (libraryWithOfficial == null)
+ return null;
+
+ var foundOfficial = Path.Combine(libraryWithOfficial, "steamapps", "common",
+ matchingOfficialCompatTool.InstallDir);
+ if (Directory.Exists(foundOfficial) &&
+ // sanity check - all compat tools, custom or official, have this file in the installdir
+ File.Exists(Path.Combine(foundOfficial, "toolmanifest.vdf")))
+ {
+ return foundOfficial;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Given is an absolute filepath to a compatibilitytools.d directory, search
+ /// through its contents for a tool that matches the given name , and return an
+ /// absolute filepath to it.
+ ///
+ ///
+ /// An absolute filepath to a compatibilitytools.d directory, whether in a Steam root or system-wide.
+ ///
+ ///
+ /// The name of the compat tool, as listed in its compatibilitytool.vdf.
+ ///
+ /// An absolute filepath to the compat tool's install dir, or if it can't be found.
+ private static string? FindCompatToolInDotD(string toolsDir, string compatToolName)
+ {
+ if (!Directory.Exists(toolsDir))
+ return null;
+ if (string.IsNullOrWhiteSpace(compatToolName))
+ return null;
+
+ // Several files inside the directory could indicate the compat tool name. But not all are reliable!
+ // Q: the name of the directory itself?
+ // A: NOPE - the name is "Proton-stl" but the installdir is "SteamTinkerLaunch"
+ // Q: the contents of the "version" file?
+ // A: NOPE - the name "GE-Proton10-33-rtsp23-zerocopy-test" has a version of "GE-Proton10-33-rtsp23-4-2-g2e8f1d695"
+ // Q: the "internal name" inside "compatibilitytool.vdf"?
+ // A: I think so! This is as expected in all my Proton versions.
+ // The file "toolmanifest.vdf" has nothing relevant.
+
+ return (from toolDir in Directory.GetDirectories(toolsDir)
+ let compatToolVdfPath = Path.Combine(toolDir, "compatibilitytool.vdf")
+ where File.Exists(compatToolVdfPath)
+ let compatToolVdfContents = File.ReadAllText(compatToolVdfPath)
+ // I could parse this file, but why bother lol
+ where compatToolVdfContents.Contains(compatToolName)
+ select toolDir).FirstOrDefault();
+ }
+
+ // Taken from VRCX, MIT
+ private static int FindMatchingBracket(string content, int openBracketIndex)
+ {
+ var depth = 0;
+ for (var i = openBracketIndex; i < content.Length; i++)
+ {
+ switch (content[i])
+ {
+ case '{':
+ depth++;
+ break;
+ case '}':
+ {
+ depth--;
+ if (depth == 0)
+ return i;
+ break;
+ }
+ }
+ }
+
+ Debug.LogError($"No matching bracket found in VDF starting from position {openBracketIndex}");
+ return -1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/ProtonLocator.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/ProtonLocator.cs.meta
new file mode 100644
index 0000000..7d4c420
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/ProtonLocator.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 2b12e60b769a47c3b7a4f364364dae52
+timeCreated: 1776228706
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/SteamLocator.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/SteamLocator.cs
new file mode 100644
index 0000000..45a42cb
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/SteamLocator.cs
@@ -0,0 +1,100 @@
+#nullable enable
+
+using System.IO;
+using UnityEngine;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators
+{
+ public static class SteamLocator
+ {
+ ///
+ /// Search common paths for the Steam root directory, and if one is found, return an absolute path to it.
+ ///
+ /// An absolute path to the Steam root we found (if any), or otherwise.
+ public static string? FindSteamRoot()
+ {
+ var steamRoots = new[]
+ {
+ Path.Combine(XdgBaseDirectory.DataHome, "Steam"), // typically ~/.local/share/Steam
+ Path.Combine(XdgBaseDirectory.Home, ".steam", "steam"),
+ Path.Combine(XdgBaseDirectory.Home, ".steam", "root"),
+ Path.Combine(XdgBaseDirectory.Home, ".steam", "debian-installation")
+ };
+
+ foreach (var steamRoot in steamRoots)
+ {
+ if (IsValidSteamRoot(steamRoot))
+ return steamRoot;
+ }
+
+ Debug.LogError("Couldn't find any Steam directory containing libraryfolders.vdf");
+ return null;
+ }
+
+ ///
+ /// Given the path to Steam root, search steamapps/libraryfolders.vdf for the Steam library that a particular
+ /// is installed to.
+ ///
+ ///
+ /// Taken from VRCX, MIT.
+ ///
+ /// An absolute path to the Steam root directory.
+ /// The Steam AppId of the game.
+ ///
+ /// An absolute path to the library folder that should contain the app, or if it wasn't
+ /// found.
+ ///
+ public static string? GetLibraryWithAppId(string steamRoot, string appId)
+ {
+ var libraryFoldersVdfPath = Path.Combine(steamRoot, "steamapps", "libraryfolders.vdf");
+
+ if (!File.Exists(libraryFoldersVdfPath))
+ {
+ Debug.LogWarning(
+ $"Attempted to search for appid, but was handed something that is probably not libraryfolders.vdf: {libraryFoldersVdfPath}");
+ return null;
+ }
+
+ string? libraryPath = null;
+ foreach (var line in File.ReadLines(libraryFoldersVdfPath))
+ {
+ // Assumes line will be \t\t"path"\t\t"pathToLibrary"
+ if (line.Contains("\"path\""))
+ {
+ var parts = line.Split("\t");
+ if (parts.Length < 4)
+ continue;
+
+ libraryPath = parts[4].Replace("\"", "");
+ }
+
+ if (!line.Contains($"\"{appId}\""))
+ continue;
+ if (Directory.Exists(libraryPath))
+ return libraryPath;
+ Debug.LogWarning(
+ $"libraryfolders.vdf references library \"{libraryPath}\", but that path doesn't exist");
+ return null;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns true if the given path appears to be a Steam root. We'll say a Steam root is a directory that contains a
+ /// libraryfolders.vdf file.
+ ///
+ /// An absolute path to the Steam root directory.
+ /// if the directory looks like a Steam root, otherwise.
+ public static bool IsValidSteamRoot(string? steamRoot)
+ {
+ if (string.IsNullOrEmpty(steamRoot) || !Directory.Exists(steamRoot))
+ {
+ return false;
+ }
+
+ var vdfPath = Path.Combine(steamRoot, "steamapps", "libraryfolders.vdf");
+ return File.Exists(vdfPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/SteamLocator.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/SteamLocator.cs.meta
new file mode 100644
index 0000000..052c11c
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/SteamLocator.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b7291775f6d14bcaba0059b0f198876a
+timeCreated: 1776487344
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/VrcLocator.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/VrcLocator.cs
new file mode 100644
index 0000000..21a0bca
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/VrcLocator.cs
@@ -0,0 +1,188 @@
+#nullable enable
+
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+using VRC.Core;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators
+{
+ ///
+ /// Locates VRChat.exe using Steam, taking VRCSDK's user-set preferences into account.
+ ///
+ ///
+ /// We must know these VRChat paths:
+ ///
+ /// -
+ /// compatdata path, which contains the Proton prefix
+ ///
+ /// -
+ /// AppData/LocalLow, located within the prefix
+ ///
+ /// -
+ /// VRChat install path, which contains VRChat.exe
+ ///
+ /// -
+ ///
+ /// VRChat.exe
+ ///
+ ///
+ ///
+ /// These aren't in fixed locations on every machine, but if the path to VRChat.exe is known, we can locate the
+ /// rest.
+ ///
+ /// This class provides that will locate Steam, then the VRChat install
+ /// directory, then discover VRChat.exe.
+ ///
+ /// VRCSDK has a Unity EditorPref named VRC_installedClientPath which is a variable meant to hold an absolute
+ /// path to VRChat.exe. Initially, it is empty, but once VRCSDK first loads, it attempts to discover
+ /// VRChat.exe and set this preference. It will fail on Linux, so we patch it to call our
+ /// instead. With this, VRC_installedClientPath is initialized
+ /// correctly, and we can treat it as the source of truth. Being a preference, the user may also choose to set their
+ /// own custom path to VRChat.exe in VRCSDK Settings.
+ ///
+ /// Unless specified otherwise, all of these methods take the value of VRC_installedClientPath into account.
+ ///
+ public static class VrcLocator
+ {
+ ///
+ /// The Steam AppId for VRChat.
+ ///
+ public const string VrcAppId = "438100";
+
+ ///
+ /// Locates VRChat's AppData/LocalLow directory, and returns it as an absolute filepath.
+ ///
+ /// This takes into account VRCSDK's VRC_installedClientPath.
+ ///
+ /// An absolute path to LocalLow if found, otherwise.
+ public static string? GetLocalLowPath()
+ {
+ var compatDataPath = GetCompatDataPath();
+ if (compatDataPath == null)
+ {
+ return null;
+ }
+
+ var relativeLocalLow = Path.Combine(compatDataPath, "pfx",
+ "drive_c", "users", "steamuser", "AppData", "LocalLow");
+ relativeLocalLow = Path.GetFullPath(relativeLocalLow);
+
+ return relativeLocalLow;
+ }
+
+ ///
+ /// Gets the value for STEAM_COMPAT_DATA_PATH, e.g. ~/.local/share/Steam/steamapps/compatdata/438100/
+ ///
+ /// This takes the value of VRCSDK's VRC_installedClientPath into account.
+ ///
+ /// An absolute path to the compatdata path if found, otherwise.
+ public static string? GetCompatDataPath()
+ {
+ var savedVrcInstallPath = GetVrcInstallPath();
+ if (savedVrcInstallPath == null)
+ {
+ return null;
+ }
+
+ // = /steamapps/common/VRChat/VRChat.exe
+ // to
+ // = /
+ var libraryPath = Path.GetFullPath(Path.Combine(savedVrcInstallPath, "..", "..", "..", ".."));
+ var compatDataPath = Path.Combine(libraryPath, "steamapps", "compatdata", VrcAppId);
+ compatDataPath = Path.GetFullPath(compatDataPath);
+ return compatDataPath;
+ }
+
+ ///
+ /// Get the known path to VRChat.exe.
+ ///
+ /// If VRCSDK's editor preference key VRC_installedClientPath is set and the path exists, return that.
+ /// Otherwise, this tries to locate VRChat.exe via Steam libraryfolders.vdf shenanigans, then updates
+ /// VRC_installedClientPath if needed.
+ ///
+ /// An absolute filepath to VRChat.exe if found, otherwise.
+ public static string? GetVrcInstallPath()
+ {
+ var savedVrcExePath = SDKClientUtilities.GetSavedVRCInstallPath();
+ if (IsValidVrcInstallPath(savedVrcExePath))
+ return savedVrcExePath;
+
+ Debug.Log(
+ $"VRCSDK's own saved VRC install path doesn't point to VRChat.exe: \"{savedVrcExePath ?? ""}\". We'll try to locate and correct it.");
+
+ var ourVrcExePath = GetVrcInstallPathFromSteam();
+ if (!IsValidVrcInstallPath(ourVrcExePath))
+ return null;
+
+ Debug.Log($"Updating VRCSDK's saved VRC install path from \"{savedVrcExePath}\" to \"{ourVrcExePath}\"");
+ // We would ideally call SDKClientUtilities.SetVRCInstallPath() here, but that method refuses to set
+ // VRC_installedClientPath if is unset. That doesn't make any sense, but whatever. Sidestep it.
+ // SDKClientUtilities.SetVRCInstallPath(ourVrcExePath);
+ EditorPrefs.SetString("VRC_installedClientPath", ourVrcExePath);
+ var readback = SDKClientUtilities.GetSavedVRCInstallPath();
+ Debug.Log($"Updated; it's now \"{readback}\"");
+ return ourVrcExePath;
+ }
+
+ ///
+ /// Locates VRChat.exe as installed by Steam.
+ ///
+ /// Finds Steam's installation path, parses libraryfolders.vdf, finds the library that VRChat is installed in,
+ /// finds VRChat.exe, and returns its absolute filepath.
+ ///
+ /// This does not take into account VRCSDK's editor preference key VRC_installedClientPath.
+ ///
+ /// An absolute filepath to VRChat.exe if found, otherwise.
+ public static string? GetVrcInstallPathFromSteam()
+ {
+ var steamRoot = SteamLocator.FindSteamRoot();
+ if (steamRoot == null)
+ {
+ return null;
+ }
+
+ // e.g. ~/.local/share/Steam or /mnt/steam
+ var libraryPath = SteamLocator.GetLibraryWithAppId(steamRoot, VrcAppId);
+ if (libraryPath == null)
+ {
+ Debug.LogError("Couldn't find any Steam library with VRChat installed.");
+ return null;
+ }
+
+ var vrcExePath = Path.Combine(libraryPath, "steamapps", "common", "VRChat", "VRChat.exe");
+
+ if (IsValidVrcInstallPath(vrcExePath))
+ return vrcExePath;
+ Debug.LogError(
+ $"Couldn't locate VRChat.exe at path: {vrcExePath}\n" +
+ "Please try to set it manually in VRChat SDK > Show Control Panel > Settings > VRChat Client.");
+ return null;
+ }
+
+ ///
+ /// Determines whether the given path refers to an existing compatdata directory, that contains a Proton prefix.
+ ///
+ /// The path to test.
+ /// if the prefix is detected, otherwise.
+ public static bool IsValidCompatDataPath(string? compatDataPath)
+ {
+ return !string.IsNullOrEmpty(compatDataPath) &&
+ // Directory.Exists(compatDataPath) &&
+ Directory.Exists(Path.Combine(compatDataPath!, "pfx"));
+ }
+
+ ///
+ /// Determines whether the given filepath refers to an existing VRChat.exe file within an installed game
+ /// directory.
+ ///
+ /// The path to test.
+ /// if VRChat.exe was found, otherwise.
+ public static bool IsValidVrcInstallPath(string? vrcInstallPath)
+ {
+ return !string.IsNullOrEmpty(vrcInstallPath) &&
+ File.Exists(vrcInstallPath) &&
+ vrcInstallPath!.EndsWith("VRChat.exe");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/VrcLocator.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/VrcLocator.cs.meta
new file mode 100644
index 0000000..a7dd198
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/VrcLocator.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0674a58840974c958835b9de406f4dc6
+timeCreated: 1775851250
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/XdgBaseDirectory.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/XdgBaseDirectory.cs
new file mode 100644
index 0000000..9bce440
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/XdgBaseDirectory.cs
@@ -0,0 +1,30 @@
+#nullable enable
+
+using System;
+using System.IO;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators
+{
+ ///
+ /// Uses the XDG Base Directory Specification to find various user directories.
+ ///
+ ///
+ /// Pared down from Xdg.Directories library, MIT.
+ ///
+ public static class XdgBaseDirectory
+ {
+ ///
+ /// The user's home directory.
+ ///
+ public static string Home => Environment.GetEnvironmentVariable("HOME") ??
+ Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+ // TODO: should also read ${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs
+ ///
+ /// The user's data files directory (e.g. ~/.local/share).
+ ///
+ public static string DataHome =>
+ Environment.GetEnvironmentVariable("XDG_DATA_HOME")
+ ?? Path.Combine(Home, ".local", "share");
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/XdgBaseDirectory.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/XdgBaseDirectory.cs.meta
new file mode 100644
index 0000000..20274a9
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Locators/XdgBaseDirectory.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 5b66b60fbf7c4a9cb7430541738443af
+timeCreated: 1775853241
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Menus.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Menus.cs
new file mode 100644
index 0000000..0e049b7
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Menus.cs
@@ -0,0 +1,50 @@
+#nullable enable
+
+using UnityEditor;
+using UnityEngine;
+using VRC.Core;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
+{
+ public static class Menus
+ {
+ ///
+ /// Print out the LinuxVRC Unity EditorPrefs.
+ ///
+ [MenuItem("Tools/Linux VRChat SDK Patch/Debug Print Preferences")]
+ public static void DebugPrintPreferences()
+ {
+ Debug.Log($"{LinuxVrcEditorPrefs.PrefsKeyCannyDialog}: \"{LinuxVrcEditorPrefs.CannyDialog}\"");
+ Debug.Log($"{LinuxVrcEditorPrefs.PrefsKeyCustomProtonPath}: \"{LinuxVrcEditorPrefs.CustomProtonPath}\"");
+ }
+
+ ///
+ /// Print out the VRCSDK Unity EditorPrefs that we care about.
+ ///
+ [MenuItem("Tools/Linux VRChat SDK Patch/Debug Print VRCSDK Preferences")]
+ public static void DebugPrintVrcSdkPreferences()
+ {
+ Debug.Log($"{nameof(SDKClientUtilities.GetSavedVRCInstallPath)}: \"{SDKClientUtilities.GetSavedVRCInstallPath()}\"");
+ const string key = "VRC_installedClientPath";
+ Debug.Log($"EditorPrefs {key}: \"{EditorPrefs.GetString(key)}\"");
+ }
+
+ ///
+ /// Print out the current launch configuration, as if we were launching Build and Test right now.
+ ///
+ ///
+ [MenuItem("Tools/Linux VRChat SDK Patch/Debug Print Launch Configuration")]
+ public static void DebugPrintLaunchConfiguration()
+ {
+ var launchConfiguration = LaunchConfiguration.Resolve(protonPath: null);
+ if (launchConfiguration == null)
+ {
+ Debug.Log("Couldn't determine launch configuration.");
+ }
+ else
+ {
+ launchConfiguration.DebugPrint();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Menus.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Menus.cs.meta
new file mode 100644
index 0000000..59c0d59
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Menus.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b1418bd26caa4da0a0f7eb21295dc844
+timeCreated: 1777022296
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patch.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patch.cs
index d88aeff..ee2583c 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patch.cs
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patch.cs
@@ -1,22 +1,27 @@
+#nullable enable
+
+using System.Linq;
using System.Runtime.InteropServices;
using HarmonyLib;
using UnityEditor;
+using UnityEngine;
namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
{
[InitializeOnLoad]
public static class Patch
{
- internal static Harmony _harmony;
-
+ private const string HarmonyID = "BefuddledLabs.LinuxVRChatSdkPatch.Base";
+ private static readonly Harmony BaseHarmony = new(HarmonyID);
static Patch()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return;
- _harmony = new Harmony("BefuddledLabs.LinuxVRChatSdkPatch-World");
- _harmony.PatchAll();
+ BaseHarmony.PatchAll();
+ var count = BaseHarmony.GetPatchedMethods().Count();
+ Debug.Log($"{HarmonyID}: Patched {count} methods");
}
}
}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches.meta
new file mode 100644
index 0000000..ce6b6df
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 86964aac0554435c9ecb49c448e844b6
+timeCreated: 1776105494
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchGetLocalLowPath.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchGetLocalLowPath.cs
new file mode 100644
index 0000000..4051c43
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchGetLocalLowPath.cs
@@ -0,0 +1,36 @@
+#nullable enable
+
+using System.Diagnostics.CodeAnalysis;
+using BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators;
+using HarmonyLib;
+using UnityEngine;
+using VRC.SDK3.Editor.Builder;
+using VRC.SDKBase.Editor;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Patches
+{
+ ///
+ /// Patch for ,
+ /// .
+ ///
+ [HarmonyPatch]
+ // ReSharper disable once UnusedType.Global
+ public class PatchGetLocalLowPath
+ {
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(VRC_SdkBuilder), nameof(VRC_SdkBuilder.GetLocalLowPath))]
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ // ReSharper disable once UnusedMember.Global
+ public static bool GetLocalLowPathPrefix(ref string __result)
+ {
+ var ourPath = VrcLocator.GetLocalLowPath();
+ if (!string.IsNullOrEmpty(ourPath))
+ {
+ __result = ourPath!;
+ }
+
+ Debug.Log($"LocalLow Path: \"{ourPath}\"");
+ return false; // skip original method
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchGetLocalLowPath.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchGetLocalLowPath.cs.meta
new file mode 100644
index 0000000..3562f61
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchGetLocalLowPath.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 935804d9f66d4b04a935fe405d8e3d90
+timeCreated: 1776105549
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchLoadRegistryVRCInstallPath.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchLoadRegistryVRCInstallPath.cs
new file mode 100644
index 0000000..b4beed8
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchLoadRegistryVRCInstallPath.cs
@@ -0,0 +1,36 @@
+#nullable enable
+
+using System.Diagnostics.CodeAnalysis;
+using BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators;
+using HarmonyLib;
+using UnityEngine;
+using VRC.Core;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Patches
+{
+ ///
+ /// Patch for VRCSDK's "Revert to Default" button in , and
+ /// the initialization in .
+ ///
+ [HarmonyPatch]
+ // ReSharper disable once InconsistentNaming
+ // ReSharper disable once UnusedType.Global
+ public static class PatchLoadRegistryVRCInstallPath
+ {
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(SDKClientUtilities), nameof(SDKClientUtilities.LoadRegistryVRCInstallPath))]
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ // ReSharper disable once UnusedMember.Global
+ public static bool LoadRegistryVRCInstallPathPrefix(ref string __result)
+ {
+ var ourPath = VrcLocator.GetVrcInstallPathFromSteam();
+ if (!string.IsNullOrEmpty(ourPath))
+ {
+ __result = ourPath!;
+ }
+
+ Debug.Log($"Found VRChat.exe from Steam: {ourPath!}");
+ return false; // skip original method
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchLoadRegistryVRCInstallPath.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchLoadRegistryVRCInstallPath.cs.meta
new file mode 100644
index 0000000..f7e2171
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchLoadRegistryVRCInstallPath.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 0a75db8084de47d8ba2f9771c0b3feff
+timeCreated: 1776193198
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchVRCSdkControlPanel.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchVRCSdkControlPanel.cs
new file mode 100644
index 0000000..5eda3cd
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchVRCSdkControlPanel.cs
@@ -0,0 +1,82 @@
+#nullable enable
+
+using BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators;
+using HarmonyLib;
+using UnityEditor;
+using UnityEngine;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Patches
+{
+ ///
+ /// Patch to add our GUI to the VRCSDK control panel.
+ ///
+ [HarmonyPatch]
+ // ReSharper disable once InconsistentNaming
+ // ReSharper disable once UnusedType.Global
+ public static class PatchVRCSdkControlPanel
+ {
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(VRCSdkControlPanel), "OnVRCInstallPathGUI")]
+ // ReSharper disable once UnusedMember.Global
+ public static bool OnVRCInstallPathGUIPrefix()
+ {
+ // show our section
+ OnLinuxVRChatSdkPatchGUI();
+
+ // show "VRChat Client - Installed Client Path"
+ return true; // run the original
+ }
+
+ private static void OnLinuxVRChatSdkPatchGUI()
+ {
+ EditorGUILayout.LabelField("Linux VRChat SDK Patch", EditorStyles.boldLabel);
+ OnGUIRowCustomProton();
+ EditorGUILayout.Separator();
+ }
+
+ private static void OnGUIRowCustomProton()
+ {
+ var customProtonPath = LinuxVrcEditorPrefs.CustomProtonPath;
+ EditorGUILayout.LabelField("Custom Proton: ", customProtonPath ?? "");
+ EditorGUILayout.BeginHorizontal();
+ GUILayout.Label("");
+
+ if (GUILayout.Button("Edit"))
+ {
+ var initPath = GetInitPath();
+ if (!string.IsNullOrEmpty(customProtonPath))
+ initPath = customProtonPath;
+
+ customProtonPath = EditorUtility.OpenFolderPanel("Choose Proton directory", initPath, "");
+ if (!ProtonLocator.IsValidCompatToolPath(customProtonPath))
+ {
+ const string message =
+ "This does not look like a Proton path.\n" +
+ "Please choose a directory that contains a Proton version that you have installed." +
+ "It should contain files named \"proton\" and \"compatibilitytool.vdf\".";
+ EditorUtility.DisplayDialog("Couldn't set custom Proton path", message, "OK");
+ }
+ else
+ {
+ LinuxVrcEditorPrefs.CustomProtonPath = customProtonPath;
+ }
+ }
+
+ if (GUILayout.Button("Revert to Default"))
+ {
+ LinuxVrcEditorPrefs.CustomProtonPath = null;
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private static string GetInitPath()
+ {
+ var initPath = ProtonLocator.GetCompatibilityToolsDotD();
+ if (initPath != null)
+ return initPath;
+ Debug.LogWarning("Could not locate compatibilitytools.d directory");
+ return string.Empty;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/UI.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchVRCSdkControlPanel.cs.meta
similarity index 100%
rename from Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/UI.cs.meta
rename to Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Patches/PatchVRCSdkControlPanel.cs.meta
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/UI.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/UI.cs
deleted file mode 100644
index f6e85f1..0000000
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/UI.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Reflection.Emit;
-using HarmonyLib;
-using UnityEditor;
-using UnityEngine;
-
-namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
-{
- [HarmonyPatch]
- public static class UI
- {
- private static string[] _options = { "Manual", "ProtonTricks" };
-
- private static void OnProtonInstallPathGUI()
- {
- var protonPath = Base.ProtonPath;
- EditorGUILayout.LabelField("Proton", EditorStyles.boldLabel);
- EditorGUILayout.LabelField("Proton Python File: ", protonPath);
- EditorGUILayout.BeginHorizontal();
- GUILayout.Label("");
-
- if (GUILayout.Button("Edit"))
- {
- var initPath = "";
- if (!string.IsNullOrEmpty(protonPath))
- initPath = protonPath;
-
- protonPath = EditorUtility.OpenFilePanel("Choose Proton Python File (not wine in the proton folder)",
- initPath, "");
- Base.ProtonPath = protonPath;
- }
-
- EditorGUILayout.EndHorizontal();
- EditorGUILayout.Separator();
- }
-
- private static void OnUseProtonTricksGUI()
- {
- var selectedIndex = Base.ProtonTricksPrefs ? 1 : 0;
- if (EditorGUILayout.Popup("VRC/Proton Path Selection", selectedIndex, _options) != selectedIndex)
- Base.ProtonTricksPrefs = !Base.ProtonTricksPrefs;
- }
-
- private static CodeInstruction _originalCall;
-
- private static void OnGUI()
- {
- if (Base.HasProtonTricks)
- OnUseProtonTricksGUI();
- else
- {
- EditorGUILayout.LabelField(
- "If proton tricks is installed, the patch will optionally use that to find your proton install");
- EditorGUILayout.Space();
- }
-
- if (!Base.ProtonTricksPrefs)
- OnProtonInstallPathGUI();
-
- if (_originalCall.operand is MethodInfo method)
- method.Invoke(null, null);
- }
-
- [HarmonyTranspiler]
- [HarmonyPatch(typeof(VRCSdkControlPanel), "ShowSettings")]
- public static IEnumerable ShowSettingsTranspiler(IEnumerable instructions)
- {
- var codes = instructions.ToList();
- for (var i = 0; i < codes.Count; i++)
- {
- if (codes[i].opcode != OpCodes.Call) continue;
- if (!codes[i].operand.ToString().Contains("OnVRCInstallPathGUI")) continue;
-
- _originalCall = codes[i];
- codes[i] = CodeInstruction.Call(typeof(UI), nameof(OnGUI));
-
- break;
- }
-
- return codes.AsEnumerable();
- }
- }
-}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/package.json b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/package.json
index b2b1ade..0107154 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.base/package.json
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.base/package.json
@@ -7,8 +7,8 @@
"name": "BefuddledLabs"
},
"unity": "2022.3",
- "description": "Patches the VRChat SDK to work properly on linux.",
+ "description": "Patches the VRChat Base SDK to work properly on Linux.",
"vpmDependencies": {
- "com.vrchat.base": "^3.8.2"
+ "com.vrchat.base": "^3.10.3"
}
}
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSDKPatch-Worlds.Editor.asmdef b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSdkPatch.Worlds.Editor.asmdef
similarity index 87%
rename from Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSDKPatch-Worlds.Editor.asmdef
rename to Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSdkPatch.Worlds.Editor.asmdef
index 688c22c..b90ce19 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSDKPatch-Worlds.Editor.asmdef
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSdkPatch.Worlds.Editor.asmdef
@@ -1,6 +1,6 @@
{
"name": "LinuxVRChatSdkPatch.Worlds.Editor",
- "rootNamespace": "",
+ "rootNamespace": "BefuddledLabs.LinuxVRChatSdkPatch.Worlds",
"references": [
"VRC.SDKBase",
"VRC.SDKBase.Editor",
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSDKPatch-Worlds.Editor.asmdef.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSdkPatch.Worlds.Editor.asmdef.meta
similarity index 100%
rename from Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSDKPatch-Worlds.Editor.asmdef.meta
rename to Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/LinuxVRChatSdkPatch.Worlds.Editor.asmdef.meta
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patch.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patch.cs
index 2d890ab..f858f92 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patch.cs
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patch.cs
@@ -1,22 +1,27 @@
+#nullable enable
+
+using System.Linq;
using System.Runtime.InteropServices;
using HarmonyLib;
using UnityEditor;
+using UnityEngine;
namespace BefuddledLabs.LinuxVRChatSdkPatch.Worlds.Editor
{
[InitializeOnLoad]
public static class Patch
{
- internal static Harmony _harmony;
-
+ private const string HarmonyID = "BefuddledLabs.LinuxVRChatSdkPatch.Worlds";
+ private static readonly Harmony WorldsHarmony = new(HarmonyID);
static Patch()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return;
- _harmony = new Harmony("BefuddledLabs.LinuxVRChatSdkPatch-World");
- _harmony.PatchAll();
+ WorldsHarmony.PatchAll();
+ var count = WorldsHarmony.GetPatchedMethods().Count();
+ Debug.Log($"{HarmonyID}: Patched {count} methods");
}
}
}
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches.meta
new file mode 100644
index 0000000..56ec26d
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: b43cb29797044d5b94af0eab40a60483
+timeCreated: 1777017016
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches/PatchBuildAndTestWorld.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches/PatchBuildAndTestWorld.cs
new file mode 100644
index 0000000..5f7d6a9
--- /dev/null
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches/PatchBuildAndTestWorld.cs
@@ -0,0 +1,102 @@
+#nullable enable
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor;
+using HarmonyLib;
+using UnityEngine.Networking;
+using VRC.Core;
+using VRC.SDK3.Editor.Builder;
+using VRC.SDKBase.Editor;
+using Debug = UnityEngine.Debug;
+
+namespace BefuddledLabs.LinuxVRChatSdkPatch.Worlds.Editor.Patches
+{
+ ///
+ /// Patch for the VRChat Worlds SDK Build and Test functionality.
+ ///
+ [HarmonyPatch]
+ // ReSharper disable once UnusedType.Global
+ public static class PatchBuildAndTestWorld
+ {
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(VRCWorldAssetExporter), "RunWorldTestDesktop", typeof(string))]
+ [SuppressMessage("ReSharper", "InconsistentNaming")]
+ // ReSharper disable once UnusedMember.Global
+ public static bool RunWorldTestDesktopPrefix(object[] __args)
+ {
+ var bundleFilePath = (string)__args[0];
+
+ // ORIGINAL METHOD, with patches. Patches are marked clearly.
+
+ // - gather proton, prefix, steam root, and compatdata
+ var launchConfig = LaunchConfiguration.Resolve(LinuxVrcEditorPrefs.CustomProtonPath);
+ if (launchConfig == null)
+ {
+ Debug.LogError("Couldn't find everything needed; aborting build-and-test.");
+ return false;
+ }
+ //
+
+ // - logging
+ launchConfig.DebugPrint();
+ Debug.Log($"Bundle path: \"{bundleFilePath}\"");
+ //
+
+ // translate path with winepath, relative to Z:/
+ bundleFilePath = ToWinePath(bundleFilePath);
+ //
+ var bundleUri = UnityWebRequest.EscapeURL(bundleFilePath).Replace("+", "%20");
+ var randomDigits = VRC.Tools.GetRandomDigits(10);
+ var executable = SDKClientUtilities.GetSavedVRCInstallPath();
+ // - remove URL path, this is almost never taken in Windows either
+ // if (string.IsNullOrEmpty(executable) || !File.Exists(executable))
+ // executable = $"vrchat://create?roomId={randomDigits}&hidden=true&name=BuildAndRun&url=file:///{bundleUri}";
+ //
+ var argUrl = $"--url=create?roomId={randomDigits}&hidden=true&name=BuildAndRun&url=file:///{bundleUri}";
+ var argsRest =
+ $"--enable-debug-gui --enable-sdk-log-levels --enable-udon-debug-logging {(VRCSettings.ForceNoVR ? " --no-vr" : "")}{(VRCSettings.WatchWorlds ? " --watch-worlds" : "")}";
+ // - set proton as the executable, prefix args with "run" (as in the Steam compatibility tool interface Verb) and path/to/VRChat.exe
+ var startInfo = new ProcessStartInfo(launchConfig.ProtonExecutable, $"run {executable} {argUrl} {argsRest}")
+ //
+ {
+ WorkingDirectory = Path.GetDirectoryName(executable) ?? "",
+ // - add these environment variables and skip parsing with system shell
+ Environment =
+ {
+ { "STEAM_COMPAT_DATA_PATH", launchConfig.CompatDataPath },
+ { "STEAM_COMPAT_CLIENT_INSTALL_PATH", launchConfig.SteamRoot },
+ { "STEAM_COMPAT_INSTALL_PATH", launchConfig.VrcInstallRoot },
+ },
+ UseShellExecute = false
+ //
+ };
+ for (var index = 0; index < VRCSettings.NumClients; ++index)
+ {
+ Process.Start(startInfo);
+ Thread.Sleep(3000);
+ }
+
+ AnalyticsSDK.BuildAndTestLaunched(RuntimeInformation.OSDescription, "Desktop", "world");
+ // END ORIGINAL METHOD
+
+ return false; // skips the original
+ }
+
+ ///
+ /// Turn /home/you/some/filepath/file.txt into Z:\home\you\some\filepath\file.txt
+ ///
+ /// The path to translate.
+ /// An absolute Windows-style path.
+ private static string ToWinePath(string path)
+ {
+ var fullPath = Path.GetFullPath(path);
+ var winePath = "Z:" + fullPath.Replace("/", @"\");
+ Debug.Log($"Translated path: \"{winePath}\"");
+ return winePath;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/World.cs.meta b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches/PatchBuildAndTestWorld.cs.meta
similarity index 100%
rename from Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/World.cs.meta
rename to Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/Patches/PatchBuildAndTestWorld.cs.meta
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/World.cs b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/World.cs
deleted file mode 100644
index 5473b7e..0000000
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/Editor/World.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using HarmonyLib;
-using UnityEngine.Networking;
-using VRC.Core;
-using VRC.SDK3.Editor.Builder;
-using VRC.SDKBase.Editor;
-using Debug = UnityEngine.Debug;
-
-namespace BefuddledLabs.LinuxVRChatSdkPatch.Worlds.Editor
-{
- [HarmonyPatch]
- public static class World
- {
- public static void InQuotes(this StringBuilder sb, object contents)
- {
- sb.Append('"');
- sb.Append(contents);
- sb.Append('"');
- }
-
- [HarmonyPrefix]
- [HarmonyPatch(typeof(VRCWorldAssetExporter), "RunWorldTestDesktop", typeof(string))]
- public static bool RunWorldTestDesktopPrefix(object[] __args)
- {
- var vrcInstallPath = SDKClientUtilities.GetSavedVRCInstallPath();
- if (string.IsNullOrEmpty(vrcInstallPath) || !File.Exists(vrcInstallPath))
- {
- Debug.LogError("couldn't get VRChat path.. You probobly forgot to set it at: " +
- "VRChat control panel > Settings > VRChat Client");
- return false;
- }
-
- var useProtonTricks = Base.Editor.Base.HasProtonTricks && Base.Editor.Base.ProtonTricksPrefs;
-
- var protonInstallPath = Base.Editor.Base.ProtonPath;
- var compatDataPath = Base.Editor.Base.GetVrcCompatDataPath();
-
- if (!useProtonTricks) // if we are using protontricks we shouldn't check that these paths are valid
- {
- if (string.IsNullOrEmpty(protonInstallPath) || !File.Exists(protonInstallPath))
- {
- Debug.LogError("couldn't get Proton path.. You probobly forgot to set it at: " +
- "VRChat control panel > Settings > Proton Python File");
- return false;
- }
-
- if (compatDataPath == null) // Check if we could find the compatdata directory
- {
- Debug.LogError("Could not find compatdata Path");
- return false;
- }
- }
-
- // Making sure that the paths are using forward slashes
- var bundleFilePath = ((string)__args[0]).Replace('\\', '/');
-
- bundleFilePath = "file:///" + UnityWebRequest.EscapeURL(bundleFilePath).Replace("+", "%20");
-
- var args = new StringBuilder();
- if (useProtonTricks)
- args.Append("--appid 438100 ");
- else
- args.Append("run ");
-
- args.InQuotes(vrcInstallPath);
- args.Append(' ');
-
- // @formatter:off
- args.Append("--url=create?roomId=");
- args.Append(VRC.Tools.GetRandomDigits(10)); // Random roomId
- args.Append("&hidden=true");
- args.Append("&name=BuildAndRun");
- args.Append("&url=");
- args.InQuotes(bundleFilePath);
- // @formatter:on
-
- args.Append(" --enable-debug-gui");
- args.Append(" --enable-sdk-log-levels");
- args.Append(" --enable-udon-debug-logging");
- if (VRCSettings.ForceNoVR)
- args.Append(" --no-vr");
- if (VRCSettings.WatchWorlds)
- args.Append(" --watch-worlds");
-
-
- var argsPathFixed =
- Regex.Replace(args.ToString(), @"file:[/\\]*",
- "file:///Z:/"); // The file we have is relative to / and not the "c drive" Z:/ is /
-
- var launchCommand = Base.Editor.Base.HasProtonTricks && Base.Editor.Base.ProtonTricksPrefs ? "protontricks-launch" : protonInstallPath;
-
- Debug.Log(launchCommand + " " + argsPathFixed);
-
- var processStartInfo =
- new ProcessStartInfo(launchCommand, argsPathFixed)
- {
- EnvironmentVariables =
- {
- { "STEAM_COMPAT_DATA_PATH", compatDataPath },
- { "STEAM_COMPAT_CLIENT_INSTALL_PATH", Environment.GetEnvironmentVariable("HOME") + "/.steam/" }
- },
- WorkingDirectory = Path.GetDirectoryName(vrcInstallPath) ?? "",
- UseShellExecute = false
- };
- for (var index = 0; index < VRCSettings.NumClients; ++index)
- {
- Process.Start(processStartInfo);
- Thread.Sleep(3000);
- }
-
- return false;
- }
- }
-}
diff --git a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/package.json b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/package.json
index e87cca2..4d3b210 100644
--- a/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/package.json
+++ b/Packages/befuddledlabs.linuxvrchatsdkpatch.worlds/package.json
@@ -7,9 +7,9 @@
"name": "BefuddledLabs"
},
"unity": "2022.3",
- "description": "Patches the VRChat SDK to work properly on linux.",
+ "description": "Patches the VRChat Worlds SDK to work properly on Linux.",
"vpmDependencies": {
- "com.vrchat.worlds": "^3.8.2",
+ "com.vrchat.worlds": "^3.10.3",
"befuddledlabs.linuxvrchatsdkpatch-base": "0.2.2"
}
}
diff --git a/README.md b/README.md
index 8525396..037058e 100644
--- a/README.md
+++ b/README.md
@@ -4,32 +4,54 @@
> This modifies the VRChat SDK using [Harmony](https://github.com/pardeike/Harmony) to properly work on Linux. \
> This is directly against the VRChat Terms of Service.
+
+
> [!IMPORTANT]
-> Please support this [canny issue](https://feedback.vrchat.com/sdk-bug-reports/p/add-proton-support-to-the-sdk-for-local-tests) ([Add Proton support to the SDK for local tests](https://feedback.vrchat.com/sdk-bug-reports/p/add-proton-support-to-the-sdk-for-local-tests)) so that these patches wouldn't be required in the future.
+> Please support this [Canny issue - Add Proton support to the SDK for local tests](https://feedback.vrchat.com/sdk-bug-reports/p/add-proton-support-to-the-sdk-for-local-tests) so that these patches wouldn't be required in the future.
+
+## How to Install
+
+1. [ALCOM][alcom] \[Recommended\]
+ 1. Add the **Linux VRChat SDK Patch** package to [ALCOM][alcom] via the listing at [`befuddledlabs.github.io/LinuxVRChatSDKPatch`](https://befuddledlabs.github.io/LinuxVRChatSDKPatch/).
+ 2. Install the appropriate **Linux VRChat SDK Patch** package for your project: **Worlds** or **Avatars**.
+2. Manually
+ 1. Download the **Base** and either **Worlds** or **Avatars** UnityPackage(s) from [Releases](https://github.com/BefuddledLabs/LinuxVRChatSDKPatch/releases).
+
+[alcom]: https://github.com/vrc-get/vrc-get
+
+## How to Use
+
+
-## How to install
-1. [ALCOM](https://github.com/vrc-get/vrc-get) [Recommended]
- 1. Add the `Linux VRChat SDK Patch` package to [ALCOM](https://github.com/vrc-get/vrc-get) via the listing at [`befuddledlabs.github.io/LinuxVRChatSDKPatch`](https://befuddledlabs.github.io/LinuxVRChatSDKPatch/).
- 2. Install the appropriate package `Linux VRChat SDK Patch` Worlds or Avatars
-2. Manual
- 1. Download the Base and either Worlds or Avatars UnityPackage(s) from the [Releases](https://github.com/BefuddledLabs/LinuxVRChatSDKPatch/releases).
+Everything should work out of the box.
-## How to use
-
+This package detects VRChat's game directory and Proton prefix, your preferred Proton version for VRChat as set in Steam, whether custom or system-wide or official, in any Steam library, and patches VRCSDK functionality to take this into account.
-Select the VRChat binary the settings of the VRChat SDK's settings panel. \
-if you have `protontricks` installed the patch will auto detect the proton install for VRChat, \
-if you don't have protontricks you'll also need to select the proton python file in the VRChat SDK's settings panel.
+If you want, you can choose a different VRChat.exe or different Proton version to launch by clicking **Edit** in the VRCSDK Settings tab.
+
+If it isn't working, use the buttons in the **Tools → Linux VRChat SDK Patch** menu to print some logs, then please open a [GitHub issue](https://github.com/BefuddledLabs/LinuxVRChatSDKPatch/issues)!
+
+## Features
+
+- Fixes the VRCSDK initialization so it can correctly find VRChat.exe.
+- Prevents the pointless creation (lol) and use of `~/.local/share/VRChat`, instead saving test worlds and avatars to the Proton prefix.
+- Fixes the Content Manager tab so it can show your test avatars.
+- Fixes the Build and Test button, allowing for multiple clients to test a world in offline mode.
+- Adds some UI in VRCSDK Settings tab to select a different Proton to use instead of what's set in Steam.
+- Show a one-time dialog to ask for votes on the Canny for Linux support.
+- Add debugging helpers to the **Tools → Linux VRChat SDK Patch** menu.
## Development
+For a technical overview on what these packages do, see [NOTES.md](NOTES.md).
+
To make modifications to this package:
-1. Clone this repository to a non-unity project folder.
-2. Create a symbolic link from the package(s) into a Unity project's package folder.
-3. The package should be editable via Unity and any external editor.
+1. Uninstall the **Linux VRChat SDK Patch** packages from your project, if necessary.
+2. Clone this repository to a directory not inside your Unity project.
+3. Create a symbolic link from the package(s) into your Unity project's package folder.
+4. The package should be editable via Unity and any external editor.
## Acknowledgements
-- [*Bartkk*](https://github.com/Bartkk0)
- - For making the original [VRCSDKonLinux](https://github.com/Bartkk0/VRCSDKonLinux).
- - And sharing their latest patches they hadn't gotten around to releasing.
+
+[**Bartkk**](https://github.com/Bartkk0) for making the original [VRCSDKonLinux](https://github.com/Bartkk0/VRCSDKonLinux) and sharing their then-unreleased patches.
diff --git a/source.json b/source.json
index bda080d..c500557 100644
--- a/source.json
+++ b/source.json
@@ -1,17 +1,17 @@
{
- "name":"Linux VRChat SDK Patch Listing",
- "id":"befuddledlabs.linuxvrchatsdkpatch",
- "url":"https://befuddledlabs.github.io/LinuxVRChatSDKPatch/index.json",
- "author":{
- "name":"BefuddledLabs"
+ "name": "Linux VRChat SDK Patch Listing",
+ "id": "befuddledlabs.linuxvrchatsdkpatch",
+ "url": "https://befuddledlabs.github.io/LinuxVRChatSDKPatch/index.json",
+ "author": {
+ "name": "BefuddledLabs"
},
- "description":"Listing for some Linux patchs to the VRChat SDK",
- "infoLink":{
- "url":"https://befuddledlabs.github.io/LinuxVRChatSDKPatch/",
- "text":"View on GitHub"
+ "description": "Listing for some Linux patches to the VRChat SDK",
+ "infoLink": {
+ "url": "https://befuddledlabs.github.io/LinuxVRChatSDKPatch/",
+ "text": "View on GitHub"
},
- "bannerUrl":"banner.png",
- "githubRepos":[
+ "bannerUrl": "banner.png",
+ "githubRepos": [
"BefuddledLabs/LinuxVRChatSDKPatch"
]
}