VRChat Config Warnings (#1358)

Co-authored-by: Erimel <marioluigivideo@gmail.com>
This commit is contained in:
lucas lelievre
2025-04-23 14:36:29 +02:00
committed by GitHub
parent edbaf49e7a
commit 0dc073ca48
19 changed files with 965 additions and 11 deletions

View File

@@ -7,6 +7,9 @@ import dev.slimevr.bridge.ISteamVRBridge
import dev.slimevr.config.ConfigManager
import dev.slimevr.firmware.FirmwareUpdateHandler
import dev.slimevr.firmware.SerialFlashingHandler
import dev.slimevr.games.vrchat.VRCConfigHandler
import dev.slimevr.games.vrchat.VRCConfigHandlerStub
import dev.slimevr.games.vrchat.VRChatConfigManager
import dev.slimevr.osc.OSCHandler
import dev.slimevr.osc.OSCRouter
import dev.slimevr.osc.VMCHandler
@@ -50,6 +53,7 @@ class VRServer @JvmOverloads constructor(
bridgeProvider: BridgeProvider = { _, _ -> sequence {} },
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null },
vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() },
acquireMulticastLock: () -> Any? = { null },
// configPath is used by VRWorkout, do not remove!
configPath: String,
@@ -87,6 +91,8 @@ class VRServer @JvmOverloads constructor(
val firmwareUpdateHandler: FirmwareUpdateHandler
val vrcConfigManager: VRChatConfigManager
@JvmField
val autoBoneHandler: AutoBoneHandler
@@ -124,6 +130,7 @@ class VRServer @JvmOverloads constructor(
// AutoBone requires HumanPoseManager first
autoBoneHandler = AutoBoneHandler(this)
firmwareUpdateHandler = FirmwareUpdateHandler(this)
vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this))
protocolAPI = ProtocolAPI(this)
val computedTrackers = humanPoseManager.computedTrackers

View File

@@ -0,0 +1,179 @@
package dev.slimevr.games.vrchat
import dev.slimevr.VRServer
import dev.slimevr.tracking.processor.config.SkeletonConfigToggles
import dev.slimevr.tracking.trackers.TrackerPosition
import dev.slimevr.tracking.trackers.TrackerUtils
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.math.*
enum class VRCTrackerModel(val value: Int, val id: Int) {
UNKNOWN(-1, solarxr_protocol.rpc.VRCTrackerModel.UNKNOWN),
SPHERE(0, solarxr_protocol.rpc.VRCTrackerModel.SPHERE),
SYSTEM(1, solarxr_protocol.rpc.VRCTrackerModel.SYSTEM),
BOX(2, solarxr_protocol.rpc.VRCTrackerModel.BOX),
AXIS(3, solarxr_protocol.rpc.VRCTrackerModel.AXIS),
;
companion object {
private val byValue = VRCTrackerModel.entries.associateBy { it.value }
fun getByValue(value: Int): VRCTrackerModel? = byValue[value]
}
}
enum class VRCSpineMode(val value: Int, val id: Int) {
UNKNOWN(-1, solarxr_protocol.rpc.VRCSpineMode.UNKNOWN),
LOCK_HIP(0, solarxr_protocol.rpc.VRCSpineMode.LOCK_HIP),
LOCK_HEAD(1, solarxr_protocol.rpc.VRCSpineMode.LOCK_HEAD),
LOCK_BOTH(2, solarxr_protocol.rpc.VRCSpineMode.LOCK_BOTH),
;
companion object {
private val byValue = VRCSpineMode.entries.associateBy { it.value }
fun getByValue(value: Int): VRCSpineMode? = byValue[value]
}
}
enum class VRCAvatarMeasurementType(val value: Int, val id: Int) {
UNKNOWN(-1, solarxr_protocol.rpc.VRCAvatarMeasurementType.UNKNOWN),
ARM_SPAN(0, solarxr_protocol.rpc.VRCAvatarMeasurementType.ARM_SPAN),
HEIGHT(1, solarxr_protocol.rpc.VRCAvatarMeasurementType.HEIGHT),
;
companion object {
private val byValue = VRCAvatarMeasurementType.entries.associateBy { it.value }
fun getByValue(value: Int): VRCAvatarMeasurementType? = byValue[value]
}
}
data class VRCConfigValues(
val legacyMode: Boolean,
val shoulderTrackingDisabled: Boolean,
val shoulderWidthCompensation: Boolean,
val userHeight: Double,
val calibrationRange: Double,
val calibrationVisuals: Boolean,
val trackerModel: VRCTrackerModel,
val spineMode: VRCSpineMode,
val avatarMeasurementType: VRCAvatarMeasurementType,
)
data class VRCConfigRecommendedValues(
val legacyMode: Boolean,
val shoulderTrackingDisabled: Boolean,
val shoulderWidthCompensation: Boolean,
val userHeight: Double,
val calibrationRange: Double,
val calibrationVisuals: Boolean,
val trackerModel: VRCTrackerModel,
val spineMode: Array<VRCSpineMode>,
val avatarMeasurementType: VRCAvatarMeasurementType,
)
data class VRCConfigValidity(
val legacyModeOk: Boolean,
val shoulderTrackingOk: Boolean,
val shoulderWidthCompensationOk: Boolean,
val userHeightOk: Boolean,
val calibrationOk: Boolean,
val calibrationVisualsOk: Boolean,
val tackerModelOk: Boolean,
val spineModeOk: Boolean,
val avatarMeasurementOk: Boolean,
)
abstract class VRCConfigHandler {
abstract val isSupported: Boolean
abstract fun initHandler(onChange: (config: VRCConfigValues) -> Unit)
}
class VRCConfigHandlerStub : VRCConfigHandler() {
override val isSupported: Boolean
get() = false
override fun initHandler(onChange: (config: VRCConfigValues) -> Unit) {}
}
interface VRCConfigListener {
fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues)
}
class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHandler) {
private val listeners: MutableList<VRCConfigListener> = CopyOnWriteArrayList()
var currentValues: VRCConfigValues? = null
val isSupported: Boolean
get() = handler.isSupported
init {
handler.initHandler(::onChange)
}
/**
* shoulderTrackingDisabled should be true if:
* The user isn't tracking their whole arms from their controllers:
* forceArmsFromHMD is enabled || the user doesn't have hand trackers with position || the user doesn't have lower arms trackers || the user doesn't have upper arm trackers
* And the user isn't tracking their arms from their HMD or doesn't have both shoulders:
* (forceArmsFromHMD is disabled && user has hand trackers with position) || user is missing a shoulder tracker
*/
fun recommendedValues(): VRCConfigRecommendedValues {
val forceArmsFromHMD = server.humanPoseManager.getToggle(SkeletonConfigToggles.FORCE_ARMS_FROM_HMD)
val hasLeftHandWithPosition = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_HAND)?.hasPosition ?: false
val hasRightHandWithPosition = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_HAND)?.hasPosition ?: false
val isMissingAnArmTracker = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_LOWER_ARM) == null ||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_LOWER_ARM) == null ||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_UPPER_ARM) == null ||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_UPPER_ARM) == null
val isMissingAShoulderTracker = TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.LEFT_SHOULDER) == null ||
TrackerUtils.getTrackerForSkeleton(server.allTrackers, TrackerPosition.RIGHT_SHOULDER) == null
return VRCConfigRecommendedValues(
legacyMode = false,
shoulderTrackingDisabled =
((forceArmsFromHMD || !hasLeftHandWithPosition || !hasRightHandWithPosition) || isMissingAnArmTracker) && // Not tracking shoulders from hands
((!forceArmsFromHMD && hasLeftHandWithPosition && hasRightHandWithPosition) || isMissingAShoulderTracker), // Not tracking shoulders from HMD
userHeight = server.humanPoseManager.realUserHeight.toDouble(),
calibrationRange = 0.2,
trackerModel = VRCTrackerModel.AXIS,
spineMode = arrayOf(VRCSpineMode.LOCK_HIP, VRCSpineMode.LOCK_HEAD),
calibrationVisuals = true,
avatarMeasurementType = VRCAvatarMeasurementType.HEIGHT,
shoulderWidthCompensation = true,
)
}
fun addListener(listener: VRCConfigListener) {
listeners.add(listener)
}
fun removeListener(listener: VRCConfigListener) {
listeners.removeIf { l -> l === listener }
}
fun checkValidity(values: VRCConfigValues, recommended: VRCConfigRecommendedValues): VRCConfigValidity = VRCConfigValidity(
legacyModeOk = values.legacyMode == recommended.legacyMode,
shoulderTrackingOk = values.shoulderTrackingDisabled == recommended.shoulderTrackingDisabled,
spineModeOk = recommended.spineMode.contains(values.spineMode),
tackerModelOk = values.trackerModel == recommended.trackerModel,
calibrationOk = abs(values.calibrationRange - recommended.calibrationRange) < 0.1,
userHeightOk = abs(server.humanPoseManager.realUserHeight - values.userHeight) < 0.1,
calibrationVisualsOk = values.calibrationVisuals == recommended.calibrationVisuals,
avatarMeasurementOk = values.avatarMeasurementType == recommended.avatarMeasurementType,
shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation,
)
fun onChange(values: VRCConfigValues) {
val recommended = recommendedValues()
val validity = checkValidity(values, recommended)
currentValues = values
listeners.forEach {
it.onChange(validity, values, recommended)
}
}
}

View File

@@ -0,0 +1,28 @@
package dev.slimevr.protocol.rpc;
import com.google.flatbuffers.FlatBufferBuilder;
import dev.slimevr.tracking.processor.HumanPoseManager;
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets;
import solarxr_protocol.rpc.SkeletonConfigResponse;
import solarxr_protocol.rpc.SkeletonPart;
public class RPCBuilder {
public static int createSkeletonConfig(
FlatBufferBuilder fbb,
HumanPoseManager humanPoseManager
) {
int[] partsOffsets = new int[SkeletonConfigOffsets.values().length];
for (int index = 0; index < SkeletonConfigOffsets.values().length; index++) {
SkeletonConfigOffsets val = SkeletonConfigOffsets.values[index];
int part = SkeletonPart
.createSkeletonPart(fbb, val.id, humanPoseManager.getOffset(val));
partsOffsets[index] = part;
}
int parts = SkeletonConfigResponse.createSkeletonPartsVector(fbb, partsOffsets);
return SkeletonConfigResponse.createSkeletonConfigResponse(fbb, parts, 0);
}
}

View File

@@ -8,6 +8,7 @@ import dev.slimevr.protocol.ProtocolHandler
import dev.slimevr.protocol.datafeed.DataFeedBuilder
import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler
import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler
import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler
import dev.slimevr.protocol.rpc.reset.RPCResetHandler
import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler
import dev.slimevr.protocol.rpc.serial.RPCSerialHandler
@@ -44,6 +45,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
RPCHandshakeHandler(this, api)
RPCTrackingPause(this, api)
RPCFirmwareUpdateHandler(this, api)
RPCVRChatHandler(this, api)
registerPacketListener(
RpcMessage.ResetRequest,

View File

@@ -0,0 +1,81 @@
package dev.slimevr.protocol.rpc.games.vrchat
import com.google.flatbuffers.FlatBufferBuilder
import solarxr_protocol.rpc.*
fun buildVRCConfigValues(fbb: FlatBufferBuilder, values: dev.slimevr.games.vrchat.VRCConfigValues): Int {
VRCConfigValues.startVRCConfigValues(fbb)
VRCConfigValues.addCalibrationRange(fbb, values.calibrationRange.toFloat())
VRCConfigValues.addCalibrationVisuals(fbb, values.calibrationVisuals)
VRCConfigValues.addSpineMode(fbb, values.spineMode.id)
VRCConfigValues.addLegacyMode(fbb, values.legacyMode)
VRCConfigValues.addShoulderTrackingDisabled(fbb, values.shoulderTrackingDisabled)
VRCConfigValues.addTrackerModel(fbb, values.trackerModel.id)
VRCConfigValues.addAvatarMeasurementType(fbb, values.avatarMeasurementType.id)
VRCConfigValues.addUserHeight(fbb, values.userHeight.toFloat())
VRCConfigValues.addShoulderWidthCompensation(fbb, values.shoulderWidthCompensation)
return VRCConfigValues.endVRCConfigValues(fbb)
}
fun buildVRCConfigValidity(fbb: FlatBufferBuilder, validity: dev.slimevr.games.vrchat.VRCConfigValidity): Int {
VRCConfigValidity.startVRCConfigValidity(fbb)
VRCConfigValidity.addCalibrationRangeOk(fbb, validity.calibrationOk)
VRCConfigValidity.addCalibrationVisualsOk(fbb, validity.calibrationVisualsOk)
VRCConfigValidity.addSpineModeOk(fbb, validity.spineModeOk)
VRCConfigValidity.addLegacyModeOk(fbb, validity.legacyModeOk)
VRCConfigValidity.addShoulderTrackingOk(fbb, validity.shoulderTrackingOk)
VRCConfigValidity.addTrackerModelOk(fbb, validity.tackerModelOk)
VRCConfigValidity.addUserHeightOk(fbb, validity.userHeightOk)
VRCConfigValidity.addAvatarMeasurementTypeOk(fbb, validity.avatarMeasurementOk)
VRCConfigValidity.addShoulderWidthCompensationOk(fbb, validity.shoulderWidthCompensationOk)
return VRCConfigValidity.endVRCConfigValidity(fbb)
}
fun buildVRCConfigRecommendedValues(fbb: FlatBufferBuilder, values: dev.slimevr.games.vrchat.VRCConfigRecommendedValues): Int {
val spineModeOffset = VRCConfigRecommendedValues
.createSpineModeVector(
fbb,
values.spineMode.map { it.id.toByte() }.toByteArray(),
)
VRCConfigRecommendedValues.startVRCConfigRecommendedValues(fbb)
VRCConfigRecommendedValues.addCalibrationRange(fbb, values.calibrationRange.toFloat())
VRCConfigRecommendedValues.addCalibrationVisuals(fbb, values.calibrationVisuals)
VRCConfigRecommendedValues.addSpineMode(fbb, spineModeOffset)
VRCConfigRecommendedValues.addLegacyMode(fbb, values.legacyMode)
VRCConfigRecommendedValues.addShoulderTrackingDisabled(fbb, values.shoulderTrackingDisabled)
VRCConfigRecommendedValues.addTrackerModel(fbb, values.trackerModel.id)
VRCConfigRecommendedValues.addAvatarMeasurementType(fbb, values.avatarMeasurementType.id)
VRCConfigRecommendedValues.addUserHeight(fbb, values.userHeight.toFloat())
VRCConfigRecommendedValues.addShoulderWidthCompensation(fbb, values.shoulderWidthCompensation)
return VRCConfigRecommendedValues.endVRCConfigRecommendedValues(fbb)
}
fun buildVRCConfigStateResponse(
fbb: FlatBufferBuilder,
isSupported: Boolean,
validity: dev.slimevr.games.vrchat.VRCConfigValidity?,
values: dev.slimevr.games.vrchat.VRCConfigValues?,
recommended: dev.slimevr.games.vrchat.VRCConfigRecommendedValues?,
): Int {
if (!isSupported) {
VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb)
VRCConfigStateChangeResponse.addIsSupported(fbb, false)
return VRCConfigStateChangeResponse.endVRCConfigStateChangeResponse(fbb)
}
if (validity == null || values == null || recommended == null) {
error("invalid state - all should be set")
}
val validityOffset = buildVRCConfigValidity(fbb, validity)
val valuesOffset = buildVRCConfigValues(fbb, values)
val recommendedOffset = buildVRCConfigRecommendedValues(fbb, recommended)
VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb)
VRCConfigStateChangeResponse.addIsSupported(fbb, true)
VRCConfigStateChangeResponse.addValidity(fbb, validityOffset)
VRCConfigStateChangeResponse.addState(fbb, valuesOffset)
VRCConfigStateChangeResponse.addRecommended(fbb, recommendedOffset)
return VRCConfigStateChangeResponse.endVRCConfigStateChangeResponse(fbb)
}

View File

@@ -0,0 +1,77 @@
package dev.slimevr.protocol.rpc.games.vrchat
import com.google.flatbuffers.FlatBufferBuilder
import dev.slimevr.games.vrchat.VRCConfigListener
import dev.slimevr.games.vrchat.VRCConfigRecommendedValues
import dev.slimevr.games.vrchat.VRCConfigValidity
import dev.slimevr.games.vrchat.VRCConfigValues
import dev.slimevr.protocol.GenericConnection
import dev.slimevr.protocol.ProtocolAPI
import dev.slimevr.protocol.rpc.RPCHandler
import solarxr_protocol.rpc.*
class RPCVRChatHandler(
private val rpcHandler: RPCHandler,
var api: ProtocolAPI,
) : VRCConfigListener {
init {
api.server.vrcConfigManager.addListener(this)
rpcHandler.registerPacketListener(RpcMessage.VRCConfigStateRequest) { conn: GenericConnection, messageHeader: RpcMessageHeader ->
this.onConfigStateRequest(
conn,
messageHeader,
)
}
}
private fun onConfigStateRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
val fbb = FlatBufferBuilder(32)
val configManager = api.server.vrcConfigManager
val values = configManager.currentValues
val recommended = configManager.recommendedValues()
// FUCKING KOTLIN BRING ME BACK MY FUCKING TERNARY OPERATORS!!!!!!!!!!!!!!!!! - With love <3 Futura
val validity = if (values !== null) configManager.checkValidity(values, recommended) else null
val response = buildVRCConfigStateResponse(
fbb,
isSupported = api.server.vrcConfigManager.isSupported,
validity = validity,
values = values,
recommended = api.server.vrcConfigManager.recommendedValues(),
)
val outbound = rpcHandler.createRPCMessage(
fbb,
RpcMessage.VRCConfigStateChangeResponse,
response,
)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
override fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues) {
val fbb = FlatBufferBuilder(32)
val response = buildVRCConfigStateResponse(
fbb,
isSupported = api.server.vrcConfigManager.isSupported,
validity = validity,
values = values,
recommended = recommended,
)
val outbound = rpcHandler.createRPCMessage(
fbb,
RpcMessage.VRCConfigStateChangeResponse,
response,
)
fbb.finish(outbound)
this.api.apiServers.forEach { apiServer ->
apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) }
}
}
}

View File

@@ -3,6 +3,7 @@ package dev.slimevr.tracking.processor
import com.jme3.math.FastMath
import dev.slimevr.VRServer
import dev.slimevr.VRServer.Companion.getNextLocalTrackerId
import dev.slimevr.autobone.errors.BodyProportionError
import dev.slimevr.config.ConfigManager
import dev.slimevr.tracking.processor.config.SkeletonConfigManager
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
@@ -641,6 +642,10 @@ class HumanPoseManager(val server: VRServer?) {
val userHeightFromConfig: Float
get() = skeletonConfigManager.userHeightFromOffsets
@get:ThreadSafe
val realUserHeight: Float
get() = skeletonConfigManager.userHeightFromOffsets / BodyProportionError.eyeHeightToHeightRatio
// #endregion
fun getPauseTracking(): Boolean = skeleton.getPauseTracking()

View File

@@ -7,6 +7,7 @@ import dev.slimevr.SLIMEVR_IDENTIFIER
import dev.slimevr.VRServer
import dev.slimevr.bridge.Bridge
import dev.slimevr.desktop.firmware.DesktopSerialFlashingHandler
import dev.slimevr.desktop.games.vrchat.DesktopVRCConfigHandler
import dev.slimevr.desktop.platform.SteamVRBridge
import dev.slimevr.desktop.platform.linux.UnixSocketBridge
import dev.slimevr.desktop.platform.linux.UnixSocketRpcBridge
@@ -123,6 +124,7 @@ fun main(args: Array<String>) {
::provideBridges,
{ _ -> DesktopSerialHandler() },
{ _ -> DesktopSerialFlashingHandler() },
{ _ -> DesktopVRCConfigHandler() },
configPath = configDir,
)
vrServer.start()

View File

@@ -0,0 +1,126 @@
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
import dev.slimevr.games.vrchat.VRCSpineMode
import dev.slimevr.games.vrchat.VRCTrackerModel
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 var configState: VRCConfigValues? = null
private var vrcConfigKeys = getVRChatKeys(VRC_REG_PATH)
lateinit var onChange: (config: VRCConfigValues) -> Unit
private fun intValue(key: String): Int? {
val realKey = vrcConfigKeys[key] ?: return null
return getDwordValue(VRC_REG_PATH, realKey)
}
private fun doubleValue(key: String): Double? {
val realKey = vrcConfigKeys[key] ?: return null
return getQwordValue(VRC_REG_PATH, realKey)
}
private fun updateCurrentState() {
vrcConfigKeys = getVRChatKeys(VRC_REG_PATH)
val newConfig = VRCConfigValues(
legacyMode = intValue("VRC_IK_LEGACY") == 1,
shoulderTrackingDisabled = intValue("VRC_IK_DISABLE_SHOULDER_TRACKING") == 1,
userHeight = doubleValue("PlayerHeight") ?: -1.0,
calibrationRange = doubleValue("VRC_IK_CALIBRATION_RANGE") ?: -1.0,
trackerModel = VRCTrackerModel.getByValue(intValue("VRC_IK_TRACKER_MODEL") ?: -1) ?: VRCTrackerModel.UNKNOWN,
spineMode = VRCSpineMode.getByValue(intValue("VRC_IK_FBT_SPINE_MODE") ?: -1) ?: VRCSpineMode.UNKNOWN,
calibrationVisuals = intValue("VRC_IK_CALIBRATION_VIS") == 1,
avatarMeasurementType = VRCAvatarMeasurementType.getByValue(intValue("VRC_IK_AVATAR_MEASUREMENT_TYPE") ?: -1) ?: VRCAvatarMeasurementType.UNKNOWN,
shoulderWidthCompensation = intValue("VRC_IK_SHOULDER_WIDTH_COMPENSATION") == 1,
)
if (newConfig != configState) {
configState = newConfig
onChange(newConfig)
}
}
override val isSupported: Boolean
get() = OperatingSystem.currentPlatform === OperatingSystem.WINDOWS && vrcConfigKeys.isNotEmpty()
override fun initHandler(onChange: (config: VRCConfigValues) -> Unit) {
this.onChange = onChange
if (isSupported) {
updateCurrentState()
getDevicesTimer.scheduleAtFixedRate(
timerTask {
updateCurrentState()
},
0,
3000,
)
}
}
}