Add support for Linux registry checking for VRChat (#1459)

This commit is contained in:
Uriel
2025-07-01 23:55:09 +02:00
committed by GitHub
parent b2817cb3ba
commit b978eaf3f1
2 changed files with 178 additions and 65 deletions

View File

@@ -1,11 +1,5 @@
package dev.slimevr.desktop.games.vrchat
import com.sun.jna.Memory
import com.sun.jna.platform.win32.Advapi32
import com.sun.jna.platform.win32.Advapi32Util
import com.sun.jna.platform.win32.WinNT
import com.sun.jna.platform.win32.WinReg
import com.sun.jna.ptr.IntByReference
import dev.slimevr.games.vrchat.VRCAvatarMeasurementType
import dev.slimevr.games.vrchat.VRCConfigHandler
import dev.slimevr.games.vrchat.VRCConfigValues
@@ -15,64 +9,17 @@ import io.eiren.util.OperatingSystem
import java.util.Timer
import kotlin.concurrent.timerTask
// Vrchat is dumb and write 64 bit doubles in the registry as DWORD instead of QWORD.
// so we have to be creative
fun getQwordValue(path: String, key: String): Double? {
val hKey = WinReg.HKEY_CURRENT_USER
val phkResult = WinReg.HKEYByReference()
// Open the registry key
if (Advapi32.INSTANCE.RegOpenKeyEx(hKey, path, 0, WinNT.KEY_READ, phkResult) != 0) {
println("Error: Cannot open registry key")
return null
}
val lpData = Memory(8)
val lpcbData = IntByReference(8)
val result = Advapi32.INSTANCE.RegQueryValueEx(
phkResult.value,
key,
0,
null,
lpData,
lpcbData,
)
Advapi32.INSTANCE.RegCloseKey(phkResult.value)
if (result != 0) {
println("Error: Cannot read registry key")
return null
}
return lpData.getDouble(0)
}
fun getDwordValue(path: String, key: String): Int? = try {
val data = Advapi32Util.registryGetIntValue(WinReg.HKEY_CURRENT_USER, path, key)
data
} catch (e: Exception) {
println("Error reading DWORD: ${e.message}")
null
}
fun getVRChatKeys(path: String): Map<String, String> {
val keysMap = mutableMapOf<String, String>()
try {
Advapi32Util.registryGetValues(WinReg.HKEY_CURRENT_USER, path).forEach {
keysMap[it.key.replace("""_h\d+$""".toRegex(), "")] = it.key
}
} catch (e: Exception) {
println("Error reading Values from VRC registry: ${e.message}")
}
return keysMap
}
const val VRC_REG_PATH = "Software\\VRChat\\VRChat"
class DesktopVRCConfigHandler : VRCConfigHandler() {
private val getDevicesTimer = Timer("FetchVRCConfigTimer")
private val regEdit: AbstractRegEdit =
if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS) {
RegEditWindows()
} else {
RegEditLinux()
}
private var configState: VRCConfigValues? = null
private var vrcConfigKeys: Map<String, String>
@@ -80,24 +27,26 @@ class DesktopVRCConfigHandler : VRCConfigHandler() {
private fun intValue(key: String): Int? {
val realKey = vrcConfigKeys[key] ?: return null
return getDwordValue(VRC_REG_PATH, realKey)
return regEdit.getDwordValue(VRC_REG_PATH, realKey)
}
private fun doubleValue(key: String): Double? {
val realKey = vrcConfigKeys[key] ?: return null
return getQwordValue(VRC_REG_PATH, realKey)
return regEdit.getQwordValue(VRC_REG_PATH, realKey)
}
init {
vrcConfigKeys = if (OperatingSystem.currentPlatform === OperatingSystem.WINDOWS) {
getVRChatKeys(VRC_REG_PATH)
vrcConfigKeys = if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS ||
OperatingSystem.currentPlatform == OperatingSystem.LINUX
) {
regEdit.getVRChatKeys(VRC_REG_PATH)
} else {
mapOf()
}
}
private fun updateCurrentState() {
vrcConfigKeys = getVRChatKeys(VRC_REG_PATH)
vrcConfigKeys = regEdit.getVRChatKeys(VRC_REG_PATH)
val newConfig = VRCConfigValues(
legacyMode = intValue("VRC_IK_LEGACY") == 1,
shoulderTrackingDisabled = intValue("VRC_IK_DISABLE_SHOULDER_TRACKING") == 1,
@@ -116,7 +65,10 @@ class DesktopVRCConfigHandler : VRCConfigHandler() {
}
override val isSupported: Boolean
get() = OperatingSystem.currentPlatform === OperatingSystem.WINDOWS && vrcConfigKeys.isNotEmpty()
get() = (
OperatingSystem.currentPlatform == OperatingSystem.WINDOWS ||
OperatingSystem.currentPlatform == OperatingSystem.LINUX
) && vrcConfigKeys.isNotEmpty()
override fun initHandler(onChange: (config: VRCConfigValues) -> Unit) {
this.onChange = onChange

View File

@@ -0,0 +1,161 @@
package dev.slimevr.desktop.games.vrchat
import com.sun.jna.Memory
import com.sun.jna.platform.win32.Advapi32
import com.sun.jna.platform.win32.Advapi32Util
import com.sun.jna.platform.win32.WinNT
import com.sun.jna.platform.win32.WinReg
import com.sun.jna.ptr.IntByReference
import io.eiren.util.logging.LogManager
import java.io.BufferedReader
import java.io.FileReader
import java.io.InvalidObjectException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.io.path.Path
import kotlin.io.path.exists
abstract class AbstractRegEdit {
abstract fun getQwordValue(path: String, key: String): Double?
abstract fun getDwordValue(path: String, key: String): Int?
abstract fun getVRChatKeys(path: String): Map<String, String>
}
class RegEditWindows : AbstractRegEdit() {
// Vrchat is dumb and write 64 bit doubles in the registry as DWORD instead of QWORD.
// so we have to be creative
override fun getQwordValue(path: String, key: String): Double? {
val hKey = WinReg.HKEY_CURRENT_USER
val phkResult = WinReg.HKEYByReference()
// Open the registry key
if (Advapi32.INSTANCE.RegOpenKeyEx(hKey, path, 0, WinNT.KEY_READ, phkResult) != 0) {
LogManager.severe("[VRChatRegEdit] Error: Cannot open registry key")
return null
}
val lpData = Memory(8)
val lpcbData = IntByReference(8)
val result = Advapi32.INSTANCE.RegQueryValueEx(
phkResult.value,
key,
0,
null,
lpData,
lpcbData,
)
Advapi32.INSTANCE.RegCloseKey(phkResult.value)
if (result != 0) {
LogManager.severe("[VRChatRegEdit] Error: Cannot read registry key")
return null
}
return lpData.getDouble(0)
}
override fun getDwordValue(path: String, key: String): Int? = try {
val data = Advapi32Util.registryGetIntValue(WinReg.HKEY_CURRENT_USER, path, key)
data
} catch (e: Exception) {
LogManager.severe("[VRChatRegEdit] Error reading DWORD: ${e.message}")
null
}
override fun getVRChatKeys(path: String): Map<String, String> {
val keysMap = mutableMapOf<String, String>()
try {
Advapi32Util.registryGetValues(WinReg.HKEY_CURRENT_USER, path).forEach {
keysMap[it.key.replace("""_h\d+$""".toRegex(), "")] = it.key
}
} catch (e: Exception) {
LogManager.severe("[VRChatRegEdit] Error reading Values from VRC registry: ${e.message}")
}
return keysMap
}
}
class RegEditLinux : AbstractRegEdit() {
init {
if (USER_REG_PATH == null) {
LogManager.info("[VRChatRegEdit] Couldn't find any VRChat registry file")
} else {
LogManager.info("[VRChatRegEdit] Using VRChat registry file: $USER_REG_PATH")
}
}
lateinit var registry: Map<String, String>
@OptIn(ExperimentalStdlibApi::class)
override fun getQwordValue(path: String, key: String): Double? {
val value = registry[key] ?: return null
if (!value.startsWith("hex(4):")) {
LogManager.severe("[VRChatRegEdit] Couldn't find value with the expected type")
return null
}
return ByteBuffer.wrap(value.substring(7).hexToByteArray(HEX_FORMAT))
.order(ByteOrder.LITTLE_ENDIAN)
.double
}
override fun getDwordValue(path: String, key: String): Int? = try {
val value = registry[key] ?: return null
if (value.startsWith("dword:")) {
value.substring(6).toInt()
} else {
throw InvalidObjectException("The requested key is not a DWORD but it is instead a $value")
}
} catch (e: Exception) {
LogManager.severe("[VRChatRegEdit] Error reading DWORD: ${e.message}")
null
}
override fun getVRChatKeys(path: String): Map<String, String> {
val keysMap = mutableMapOf<String, String>()
val map = mutableMapOf<String, String>()
try {
BufferedReader(FileReader(USER_REG_PATH?.toFile() ?: return keysMap)).use { reader ->
// The reg file uses double backward-slash for paths
val actualPath = "[${path.replace("\\", """\\""")}]"
while (reader.ready()) {
val line = reader.readLine()
if (!line.startsWith(actualPath)) continue
// Skip the `#time` line
reader.readLine()
while (reader.ready()) {
val keyValue = reader.readLine()
if (keyValue == "") break
KEY_VALUE_PATTERN.matchEntire(keyValue)?.let {
map[it.groupValues[1]] = it.groupValues[2]
keysMap[it.groupValues[1].replace("""_h\d+$""".toRegex(), "")] = it.groupValues[1]
}
}
break
}
}
} catch (e: Exception) {
LogManager.severe("[VRChatRegEdit] Error reading Values from VRC registry: ${e.message}")
}
registry = map
return keysMap
}
companion object {
const val USER_REG_SUBPATH = "steamapps/compatdata/438100/pfx/user.reg"
val USER_REG_PATH =
System.getenv("HOME")?.let {
Path(it, ".steam", "root", USER_REG_SUBPATH).let { if (it.exists()) it else null }
?: Path(it, ".steam", "debian-installation", USER_REG_SUBPATH).let { if (it.exists()) it else null }
?: Path(it, ".var", "app", "com.valvesoftware.Steam", "data", "Steam", USER_REG_SUBPATH).let { if (it.exists()) it else null }
}
val KEY_VALUE_PATTERN = Regex(""""(.+)"=(.+)""")
@OptIn(ExperimentalStdlibApi::class)
val HEX_FORMAT = HexFormat {
upperCase = false
bytes.byteSeparator = ","
}
}
}