Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Docs/VRCSDKSettings.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed Docs/VRCSdkSettings
Binary file not shown.
70 changes: 70 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Bumping the VRC SDK version required here?
If not, I'd prefer it not be bumped

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I bumped VRCSDK in response to this, but I misremembered the details:

https://ask.vrchat.com/t/developer-update-23-october-2025/46983#p-85382-a-reminder-to-upgrade-to-the-latest-sdk-3

VRC now requires SDK 3.9.0+ on newly-uploaded avatars, but I forgot the conditional words on that: newly, uploaded, and avatars. But beyond that, I have not tested old SDKs so the patches may or may not apply. If you'd like, we can un-bump, and cross that bridge when we come to it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to bump if the patches work for older versions of the SDK, but bumping to 3.9.0 would be okay

"befuddledlabs.linuxvrchatsdkpatch-base": "0.2.2"
}
}
105 changes: 0 additions & 105 deletions Packages/befuddledlabs.linuxvrchatsdkpatch.base/Editor/Base.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable

using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
Expand All @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#nullable enable

using System.IO;
using BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor.Locators;
using UnityEngine;

namespace BefuddledLabs.LinuxVRChatSdkPatch.Base.Editor
{
/// <summary>
/// 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.
/// </summary>
public class LaunchConfiguration
{
private LaunchConfiguration(string steamRoot, string protonExecutable, string compatDataPath,
string vrcInstallRoot)
{
SteamRoot = steamRoot;
ProtonExecutable = protonExecutable;
CompatDataPath = compatDataPath;
VrcInstallRoot = vrcInstallRoot;
}

/// <summary>
/// An absolute path to the Steam root directory.
/// </summary>

public string SteamRoot { get; }

/// <summary>
/// An absolute path to a Proton version's <c>proton</c> Python script.
/// </summary>
public string ProtonExecutable { get; }

/// <summary>
/// An absolute path to the <c>compatdata/</c> directory in a Proton prefix.
/// </summary>

public string CompatDataPath { get; }

/// <summary>
/// An absolute path to <c>common/VRChat/</c>
/// </summary>
public string VrcInstallRoot { get; }

/// <summary>
/// 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.
/// </summary>
/// <param name="protonPath">
/// The value of the Unity EditorPref for custom Proton version, or <see langword="null" /> if it is unset.
/// </param>
/// <returns></returns>
public static LaunchConfiguration? Resolve(string? protonPath)
{
// SteamRoot
var steamRoot = SteamLocator.FindSteamRoot();
if (steamRoot == null || !SteamLocator.IsValidSteamRoot(steamRoot))
Comment on lines +57 to +58
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var steamRoot = SteamLocator.FindSteamRoot();
if (steamRoot == null || !SteamLocator.IsValidSteamRoot(steamRoot))
var steamRoot = SteamLocator.FindSteamRoot();
if (steamRoot == null)

Why check twice here?
SteamLocator.FindSteamRoot already runs SteamLocator.IsValidSteamRoot

{
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}\"");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "LinuxVRChatSdkPatch.Base.Editor",
"rootNamespace": "",
"rootNamespace": "BefuddledLabs.LinuxVRChatSdkPatch.Base",
"references": [
"VRC.SDKBase",
"VRC.SDKBase.Editor"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading