mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
VRChat Config Warnings (#1358)
Co-authored-by: Erimel <marioluigivideo@gmail.com>
This commit is contained in:
@@ -629,6 +629,9 @@ settings-general-gesture_control-numberTrackersOverThreshold-description = Incre
|
||||
|
||||
## Appearance settings
|
||||
settings-interface-appearance = Appearance
|
||||
settings-general-interface-dev_mode = Developer Mode
|
||||
settings-general-interface-dev_mode-description = This mode can be useful if you need in-depth data or to interact with connected trackers on a more advanced level.
|
||||
settings-general-interface-dev_mode-label = Developer Mode
|
||||
settings-general-interface-theme = Color theme
|
||||
settings-general-interface-show-navbar-onboarding = Show "{ navbar-onboarding }" on navigation bar
|
||||
settings-general-interface-show-navbar-onboarding-description = This changes if the "{ navbar-onboarding }" button shows on the navigation bar.
|
||||
@@ -1411,6 +1414,48 @@ unknown_device-modal-description = There is a new tracker with MAC address <b>{$
|
||||
unknown_device-modal-confirm = Sure!
|
||||
unknown_device-modal-forget = Ignore it
|
||||
|
||||
|
||||
# VRChat config warnings
|
||||
vrc_config-page-title = VRChat configuration warnings
|
||||
vrc_config-page-desc = This page shows the state of your VRChat settings and shows what settings are incompatible with SlimeVR. It is highly recommended that you fix any warnings showing up here for the best user experience with SlimeVR.
|
||||
vrc_config-page-help = Can't find the settings?
|
||||
vrc_config-page-help-desc = Check out our <a>documentation on this topic!</a>
|
||||
vrc_config-page-big_menu = Tracking & IK (Big Menu)
|
||||
vrc_config-page-big_menu-desc = Settings related to IK in the big settings menu
|
||||
vrc_config-page-wrist_menu = Tracking & IK (Wrist Menu)
|
||||
vrc_config-page-wrist_menu-desc = Settings related to IK in small settings menu (wrist menu)
|
||||
vrc_config-on = On
|
||||
vrc_config-off = Off
|
||||
vrc_config-invalid = You have misconfigured VRChat settings!
|
||||
vrc_config-show_more = Show more
|
||||
vrc_config-setting_name = VRChat Setting name
|
||||
vrc_config-recommended_value = Recommended Value
|
||||
vrc_config-current_value = Current Value
|
||||
vrc_config-legacy_mode = Use Legacy IK Solving
|
||||
vrc_config-disable_shoulder_tracking = Disable Shoulder Tracking
|
||||
vrc_config-shoulder_width_compensation = Shoulder Width Compensation
|
||||
vrc_config-spine_mode = FBT Spine Mode
|
||||
vrc_config-tracker_model = FBT Tracker Model
|
||||
vrc_config-avatar_measurement_type = Avatar Measurement
|
||||
vrc_config-calibration_range = Calibration Range
|
||||
vrc_config-calibration_visuals = Display Calibration Visuals
|
||||
vrc_config-user_height = User Real Height
|
||||
|
||||
vrc_config-spine_mode-UNKNOWN = Unknown
|
||||
vrc_config-spine_mode-LOCK_BOTH = Lock Both
|
||||
vrc_config-spine_mode-LOCK_HEAD = Lock Head
|
||||
vrc_config-spine_mode-LOCK_HIP = Lock Hip
|
||||
|
||||
vrc_config-tracker_model-UNKNOWN = Unkown
|
||||
vrc_config-tracker_model-AXIS = Axis
|
||||
vrc_config-tracker_model-BOX = Box
|
||||
vrc_config-tracker_model-SPHERE = Sphere
|
||||
vrc_config-tracker_model-SYSTEM = System
|
||||
|
||||
vrc_config-avatar_measurement_type-UNKNOWN = Unknown
|
||||
vrc_config-avatar_measurement_type-HEIGHT = Height
|
||||
vrc_config-avatar_measurement_type-ARM_SPAN = Arm Span
|
||||
|
||||
## Error collection consent modal
|
||||
error_collection_modal-title = Can we collect errors?
|
||||
error_collection_modal-description_v2 = { settings-interface-behavior-error_tracking-description_v2 }
|
||||
|
||||
@@ -59,6 +59,7 @@ import { ScaledProportionsPage } from './components/onboarding/pages/body-propor
|
||||
import { AdvancedSettings } from './components/settings/pages/AdvancedSettings';
|
||||
import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate';
|
||||
import { ConnectionLost } from './components/onboarding/pages/ConnectionLost';
|
||||
import { VRCWarningsPage } from './components/vrc/VRCWarningsPage';
|
||||
|
||||
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
|
||||
export const VersionContext = createContext('');
|
||||
@@ -110,6 +111,14 @@ function Layout() {
|
||||
</MainLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/vrc-warnings"
|
||||
element={
|
||||
<MainLayout isMobile={isMobile} widgets={false}>
|
||||
<VRCWarningsPage />
|
||||
</MainLayout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
|
||||
@@ -64,7 +64,7 @@ export function WarningBox({
|
||||
>
|
||||
<WarningIcon></WarningIcon>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<div className="flex flex-col justify-center w-full">
|
||||
<Typography
|
||||
color="text-background-60"
|
||||
whitespace={whitespace ? 'whitespace-pre-line' : undefined}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export function CheckIcon(_props: any) {
|
||||
export function CheckIcon({ size = 9 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
width="9"
|
||||
height="7"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 9 7"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
export function CrossIcon() {
|
||||
export function CrossIcon({ size = 20 }: { size: number }) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
width={size}
|
||||
height={20}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { StatusData, TrackerDataT } from 'solarxr-protocol';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { useTrackers } from '@/hooks/tracker';
|
||||
@@ -15,6 +15,7 @@ import { useMemo } from 'react';
|
||||
import { WarningBox } from '@/components/commons/TipBox';
|
||||
import { HeadsetIcon } from '@/components/commons/icon/HeadsetIcon';
|
||||
import classNames from 'classnames';
|
||||
import { useVRCConfig } from '@/hooks/vrc-config';
|
||||
|
||||
const DONT_REPEAT_STATUSES = [StatusData.StatusTrackerReset];
|
||||
|
||||
@@ -23,6 +24,7 @@ export function Home() {
|
||||
const { config } = useConfig();
|
||||
const { trackers } = useTrackers();
|
||||
const { statuses } = useStatusContext();
|
||||
const { invalidConfig } = useVRCConfig();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const sendToSettings = (tracker: TrackerDataT) => {
|
||||
@@ -51,9 +53,7 @@ export function Home() {
|
||||
<div className="h-full overflow-y-auto">
|
||||
<div
|
||||
className={classNames(
|
||||
'px-3 pt-3 gap-3 w-full grid md:grid-cols-2 mobile:grid-cols-1',
|
||||
filteredStatuses.filter(([, status]) => status.prioritized)
|
||||
.length === 0 && 'hidden'
|
||||
'px-3 pt-3 gap-3 w-full grid md:grid-cols-2 mobile:grid-cols-1'
|
||||
)}
|
||||
>
|
||||
{filteredStatuses
|
||||
@@ -69,6 +69,22 @@ export function Home() {
|
||||
</WarningBox>
|
||||
</Localized>
|
||||
))}
|
||||
{invalidConfig && (
|
||||
<WarningBox whitespace={false}>
|
||||
<div className="flex gap-2 justify-between items-center w-full">
|
||||
<div className="flex">
|
||||
<Localized id={'vrc_config-invalid'}></Localized>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Link to="/vrc-warnings">
|
||||
<div className="rounded-md p-2 bg-background-90 bg-opacity-15 hover:bg-background-10 hover:bg-opacity-25 text-nowrap">
|
||||
<Localized id={'vrc_config-show_more'}></Localized>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</WarningBox>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-y-auto flex flex-col gap-3">
|
||||
{trackers.length === 0 && (
|
||||
|
||||
301
gui/src/components/vrc/VRCWarningsPage.tsx
Normal file
301
gui/src/components/vrc/VRCWarningsPage.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { ReactNode } from 'react';
|
||||
import {} from 'solarxr-protocol';
|
||||
import { CheckIcon } from '@/components/commons/icon/CheckIcon';
|
||||
import { WarningIcon } from '@/components/commons/icon/WarningIcon';
|
||||
import {
|
||||
avatarMeasurementTypeTranslationMap,
|
||||
spineModeTranslationMap,
|
||||
trackerModelTranslationMap,
|
||||
useVRCConfig,
|
||||
} from '@/hooks/vrc-config';
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import classNames from 'classnames';
|
||||
import { useLocaleConfig } from '@/i18n/config';
|
||||
import { A } from '@/components/commons/A';
|
||||
|
||||
export function SettingRow({
|
||||
name,
|
||||
valid,
|
||||
value,
|
||||
recommendedValue,
|
||||
}: {
|
||||
valid: boolean;
|
||||
name: string;
|
||||
recommendedValue: ReactNode;
|
||||
value: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<tr className="group border-b border-background-60">
|
||||
<td className="px-6 py-4 flex gap-2 fill-status-success items-center">
|
||||
{valid ? (
|
||||
<CheckIcon size={20} />
|
||||
) : (
|
||||
<WarningIcon width={20} className="text-status-warning" />
|
||||
)}
|
||||
<Localized id={name}>
|
||||
<Typography>{name}</Typography>
|
||||
</Localized>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-end items-center">{recommendedValue}</td>
|
||||
<td
|
||||
className={classNames(
|
||||
'px-6 py-4 text-end items-center',
|
||||
!valid && 'text-status-warning'
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const onOffKey = (value: boolean) =>
|
||||
value ? 'vrc_config-on' : 'vrc_config-off';
|
||||
|
||||
export function VRCWarningsPage() {
|
||||
const { l10n } = useLocalization();
|
||||
const { state } = useVRCConfig();
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
|
||||
const meterFormat = Intl.NumberFormat(currentLocales, {
|
||||
style: 'unit',
|
||||
unit: 'meter',
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
if (!state || !state.isSupported) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4 w-full">
|
||||
<div className="flex flex-col max-w-lg mobile:w-full gap-3">
|
||||
<Localized id={'vrc_config-page-title'}>
|
||||
<Typography variant="main-title" />
|
||||
</Localized>
|
||||
<Localized id={'vrc_config-page-desc'}>
|
||||
<Typography variant="standard" color="secondary" />
|
||||
</Localized>
|
||||
</div>
|
||||
<div className="w-full mt-4 gap-2 flex flex-col">
|
||||
<div className="-m-2 overflow-x-auto">
|
||||
<div className="p-2 min-w-full inline-block align-middle">
|
||||
<div className="overflow-hidden flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Localized id="vrc_config-page-big_menu">
|
||||
<Typography variant="section-title" />
|
||||
</Localized>
|
||||
<Localized id="vrc_config-page-big_menu-desc">
|
||||
<Typography color="secondary" />
|
||||
</Localized>
|
||||
<table className="min-w-full divide-y divide-background-50">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-start">
|
||||
<Localized id={'vrc_config-setting_name'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
</th>
|
||||
|
||||
<th scope="col" className="px-6 py-3 text-end">
|
||||
<Localized id={'vrc_config-recommended_value'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-end">
|
||||
<Localized id={'vrc_config-current_value'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<SettingRow
|
||||
name="vrc_config-user_height"
|
||||
recommendedValue={meterFormat.format(
|
||||
state.recommended.userHeight
|
||||
)}
|
||||
value={meterFormat.format(state.state.userHeight)}
|
||||
valid={state.validity.userHeightOk}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name="vrc_config-legacy_mode"
|
||||
recommendedValue={
|
||||
<Localized
|
||||
id={onOffKey(state.recommended.legacyMode)}
|
||||
></Localized>
|
||||
}
|
||||
value={
|
||||
<Localized
|
||||
id={onOffKey(state.state.legacyMode)}
|
||||
></Localized>
|
||||
}
|
||||
valid={state.validity.legacyModeOk}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name="vrc_config-disable_shoulder_tracking"
|
||||
recommendedValue={
|
||||
<Localized
|
||||
id={onOffKey(
|
||||
state.recommended.shoulderTrackingDisabled
|
||||
)}
|
||||
></Localized>
|
||||
}
|
||||
value={
|
||||
<Localized
|
||||
id={onOffKey(state.state.shoulderTrackingDisabled)}
|
||||
></Localized>
|
||||
}
|
||||
valid={state.validity.shoulderTrackingOk}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name="vrc_config-shoulder_width_compensation"
|
||||
recommendedValue={
|
||||
<Localized
|
||||
id={onOffKey(
|
||||
state.recommended.shoulderWidthCompensation
|
||||
)}
|
||||
></Localized>
|
||||
}
|
||||
value={
|
||||
<Localized
|
||||
id={onOffKey(state.state.shoulderWidthCompensation)}
|
||||
></Localized>
|
||||
}
|
||||
valid={state.validity.shoulderWidthCompensationOk}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name="vrc_config-calibration_visuals"
|
||||
recommendedValue={
|
||||
<Localized
|
||||
id={onOffKey(state.recommended.calibrationVisuals)}
|
||||
></Localized>
|
||||
}
|
||||
value={
|
||||
<Localized
|
||||
id={onOffKey(state.state.calibrationVisuals)}
|
||||
></Localized>
|
||||
}
|
||||
valid={state.validity.calibrationVisualsOk}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name="vrc_config-calibration_range"
|
||||
recommendedValue={meterFormat.format(
|
||||
state.recommended.calibrationRange
|
||||
)}
|
||||
value={meterFormat.format(state.state.calibrationRange)}
|
||||
valid={state.validity.calibrationRangeOk}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name="vrc_config-tracker_model"
|
||||
recommendedValue={
|
||||
<Localized
|
||||
id={
|
||||
trackerModelTranslationMap[
|
||||
state.recommended.trackerModel
|
||||
]
|
||||
}
|
||||
></Localized>
|
||||
}
|
||||
value={
|
||||
<Localized
|
||||
id={
|
||||
trackerModelTranslationMap[state.state.trackerModel]
|
||||
}
|
||||
></Localized>
|
||||
}
|
||||
valid={state.validity.trackerModelOk}
|
||||
></SettingRow>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Localized id="vrc_config-page-wrist_menu">
|
||||
<Typography variant="section-title" />
|
||||
</Localized>
|
||||
<Localized id="vrc_config-page-wrist_menu-desc">
|
||||
<Typography color="secondary" />
|
||||
</Localized>
|
||||
<table className="min-w-full divide-y divide-background-50">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-start">
|
||||
<Localized id={'vrc_config-setting_name'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
</th>
|
||||
|
||||
<th scope="col" className="px-6 py-3 text-end">
|
||||
<Localized id={'vrc_config-recommended_value'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-end">
|
||||
<Localized id={'vrc_config-current_value'}>
|
||||
<Typography />
|
||||
</Localized>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<SettingRow
|
||||
name="vrc_config-spine_mode"
|
||||
recommendedValue={state.recommended.spineMode
|
||||
.map((mode) =>
|
||||
l10n.getString(spineModeTranslationMap[mode])
|
||||
)
|
||||
.join(', ')}
|
||||
value={
|
||||
<Localized
|
||||
id={spineModeTranslationMap[state.state.spineMode]}
|
||||
></Localized>
|
||||
}
|
||||
valid={state.validity.spineModeOk}
|
||||
></SettingRow>
|
||||
|
||||
<SettingRow
|
||||
name="vrc_config-avatar_measurement_type"
|
||||
recommendedValue={
|
||||
<Localized
|
||||
id={
|
||||
avatarMeasurementTypeTranslationMap[
|
||||
state.recommended.avatarMeasurementType
|
||||
]
|
||||
}
|
||||
></Localized>
|
||||
}
|
||||
value={
|
||||
<Localized
|
||||
id={
|
||||
avatarMeasurementTypeTranslationMap[
|
||||
state.state.avatarMeasurementType
|
||||
]
|
||||
}
|
||||
></Localized>
|
||||
}
|
||||
valid={state.validity.avatarMeasurementTypeOk}
|
||||
></SettingRow>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col max-w-lg mobile:w-full gap-2 mt-4">
|
||||
<Localized id={'vrc_config-page-help'}>
|
||||
<Typography variant="section-title" />
|
||||
</Localized>
|
||||
<Localized
|
||||
id={'vrc_config-page-help-desc'}
|
||||
elems={{
|
||||
a: <A href="https://docs.slimevr.dev/tools/vrchat-config.html"></A>,
|
||||
}}
|
||||
>
|
||||
<Typography color="secondary" />
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -64,6 +64,7 @@ export const boardTypeToFirmwareToolBoardType: Record<
|
||||
[BoardType.XIAO_ESP32C3]: null,
|
||||
[BoardType.ESP32C6DEVKITC1]: null,
|
||||
[BoardType.GLOVE_IMU_SLIMEVR_DEV]: null,
|
||||
[BoardType.GESTURES]: null,
|
||||
};
|
||||
|
||||
export const firmwareToolToBoardType: Record<CreateBoardConfigDTO['type'], BoardType> =
|
||||
|
||||
70
gui/src/hooks/vrc-config.ts
Normal file
70
gui/src/hooks/vrc-config.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import {
|
||||
RpcMessage,
|
||||
VRCAvatarMeasurementType,
|
||||
VRCConfigStateChangeResponseT,
|
||||
VRCConfigStateRequestT,
|
||||
VRCSpineMode,
|
||||
VRCTrackerModel,
|
||||
} from 'solarxr-protocol';
|
||||
|
||||
type NonNull<T> = {
|
||||
[P in keyof T]: NonNullable<T[P]>;
|
||||
};
|
||||
|
||||
export type VRCConfigState =
|
||||
| { isSupported: false }
|
||||
| ({ isSupported: true } & NonNull<
|
||||
Pick<VRCConfigStateChangeResponseT, 'recommended' | 'state' | 'validity'>
|
||||
>);
|
||||
|
||||
export const spineModeTranslationMap: Record<VRCSpineMode, string> = {
|
||||
[VRCSpineMode.UNKNOWN]: 'vrc_config-spine_mode-UNKNOWN',
|
||||
[VRCSpineMode.LOCK_BOTH]: 'vrc_config-spine_mode-LOCK_BOTH',
|
||||
[VRCSpineMode.LOCK_HEAD]: 'vrc_config-spine_mode-LOCK_HEAD',
|
||||
[VRCSpineMode.LOCK_HIP]: 'vrc_config-spine_mode-LOCK_HIP',
|
||||
};
|
||||
|
||||
export const trackerModelTranslationMap: Record<VRCTrackerModel, string> = {
|
||||
[VRCTrackerModel.UNKNOWN]: 'vrc_config-tracker_model-UNKNOWN',
|
||||
[VRCTrackerModel.AXIS]: 'vrc_config-tracker_model-AXIS',
|
||||
[VRCTrackerModel.BOX]: 'vrc_config-tracker_model-BOX',
|
||||
[VRCTrackerModel.SPHERE]: 'vrc_config-tracker_model-SPHERE',
|
||||
[VRCTrackerModel.SYSTEM]: 'vrc_config-tracker_model-SYSTEM',
|
||||
};
|
||||
|
||||
export const avatarMeasurementTypeTranslationMap: Record<
|
||||
VRCAvatarMeasurementType,
|
||||
string
|
||||
> = {
|
||||
[VRCAvatarMeasurementType.UNKNOWN]: 'vrc_config-avatar_measurement_type-UNKNOWN',
|
||||
[VRCAvatarMeasurementType.HEIGHT]: 'vrc_config-avatar_measurement_type-HEIGHT',
|
||||
[VRCAvatarMeasurementType.ARM_SPAN]: 'vrc_config-avatar_measurement_type-ARM_SPAN',
|
||||
};
|
||||
|
||||
export function useVRCConfig() {
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [state, setState] = useState<VRCConfigState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.VRCConfigStateRequest, new VRCConfigStateRequestT());
|
||||
}, []);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.VRCConfigStateChangeResponse,
|
||||
(data: VRCConfigStateChangeResponseT) => {
|
||||
setState(data as VRCConfigState);
|
||||
}
|
||||
);
|
||||
|
||||
const invalidConfig = useMemo(() => {
|
||||
if (!state?.isSupported) return false;
|
||||
return Object.values(state.validity).some((v) => !v);
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
state,
|
||||
invalidConfig,
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule solarxr-protocol updated: b0b5d6c9f5...74cdc8b7be
Reference in New Issue
Block a user