- {column({
- id: DisplayColumn.NAME,
- label: l10n.getString('tracker-table-column-name'),
- row: ({ tracker }) => (
-
- ),
- })}
-
- {column({
- id: DisplayColumn.TYPE,
- label: l10n.getString('tracker-table-column-type'),
- row: ({ device }) => (
-
- {device?.hardwareInfo?.manufacturer || '--'}
-
- ),
- })}
-
- {column({
- id: DisplayColumn.BATTERY,
- label: l10n.getString('tracker-table-column-battery'),
- row: ({ device, tracker }) =>
- device?.hardwareStatus?.batteryPctEstimate != null && (
-
- ),
- })}
-
- {column({
- id: DisplayColumn.PING,
- label: l10n.getString('tracker-table-column-ping'),
- row: ({ device, tracker }) =>
- (device?.hardwareStatus?.rssi != null ||
- device?.hardwareStatus?.ping != null) && (
-
- ),
- })}
-
- {column({
- id: DisplayColumn.TPS,
- label: l10n.getString('tracker-table-column-tps'),
- row: ({ tracker }) => (
-
- {tracker?.tps != null ? <>{tracker.tps}> : <>>}
-
- ),
- })}
-
- {column({
- id: DisplayColumn.ROTATION,
- label: l10n.getString('tracker-table-column-rotation'),
- labelClassName: classNames({
- 'w-44': config?.devSettings?.preciseRotation,
- 'w-32': !config?.devSettings?.preciseRotation,
- }),
- row: ({ tracker }) => (
-
- ),
- })}
-
- {column({
- id: DisplayColumn.TEMPERATURE,
- label: l10n.getString('tracker-table-column-temperature'),
- row: ({ tracker }) =>
- tracker?.temp &&
- tracker?.temp?.temp != 0 && (
-
- {`${tracker.temp.temp.toFixed(2)}`}
-
- ),
- })}
-
- {column({
- id: DisplayColumn.LINEAR_ACCELERATION,
- label: l10n.getString('tracker-table-column-linear-acceleration'),
- labelClassName: 'w-36',
- row: ({ tracker }) =>
- tracker.linearAcceleration && (
-
- {formatVector3(tracker.linearAcceleration, 1)}
-
- ),
- })}
-
- {column({
- id: DisplayColumn.POSITION,
- label: l10n.getString('tracker-table-column-position'),
- labelClassName: 'w-36',
- row: ({ tracker }) =>
- tracker.position && (
-
- {formatVector3(tracker.position, 2)}
-
- ),
- })}
-
- {column({
- id: DisplayColumn.STAY_ALIGNED,
- label: l10n.getString('tracker-table-column-stay_aligned'),
- labelClassName: 'w-36',
- row: ({ tracker }) => (
-
- ),
- })}
-
- {column({
- id: DisplayColumn.URL,
- label: l10n.getString('tracker-table-column-url'),
- row: ({ device }) => (
-
- udp://
- {IPv4.fromNumber(
- device?.hardwareInfo?.ipAddress?.addr || 0
- ).toString()}
-
- ),
- })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {filteredSortedTrackers.map((data) => (
+
+ ))}
+
);
}
diff --git a/gui/src/components/tracking-checklist/TrackingChecklist.tsx b/gui/src/components/tracking-checklist/TrackingChecklist.tsx
new file mode 100644
index 0000000000..7245369f25
--- /dev/null
+++ b/gui/src/components/tracking-checklist/TrackingChecklist.tsx
@@ -0,0 +1,549 @@
+import {
+ TrackingChecklistStep,
+ TrackingChecklistContext,
+ useTrackingChecklist,
+ trackingchecklistIdtoLabel,
+} from '@/hooks/tracking-checklist';
+import classNames from 'classnames';
+import {
+ ResetType,
+ TrackingChecklistPublicNetworksT,
+ TrackingChecklistStepId,
+} from 'solarxr-protocol';
+import { ReactNode, useEffect, useMemo, useState } from 'react';
+import { openUrl } from '@tauri-apps/plugin-opener';
+import { CheckIcon } from '@/components/commons/icon/CheckIcon';
+import { Typography } from '@/components/commons/Typography';
+import { Button } from '@/components/commons/Button';
+import { ResetButton } from '@/components/home/ResetButton';
+import { A } from '@/components/commons/A';
+import { LoaderIcon, SlimeState } from '@/components/commons/icon/LoaderIcon';
+import { ProgressBar } from '@/components/commons/ProgressBar';
+import { CrossIcon } from '@/components/commons/icon/CrossIcon';
+import {
+ ArrowDownIcon,
+ ArrowRightIcon,
+} from '@/components/commons/icon/ArrowIcons';
+import { Localized } from '@fluent/react';
+import { WrenchIcon } from '@/components/commons/icon/WrenchIcons';
+import { TrackingChecklistModal } from './TrackingChecklistModal';
+import { NavLink, useNavigate } from 'react-router-dom';
+import { useBreakpoint } from '@/hooks/breakpoint';
+
+function Step({
+ step: { status, id, optional, firstRequired },
+ children,
+}: {
+ step: TrackingChecklistStep;
+ index: number;
+ children: ReactNode;
+}) {
+ const [open, setOpen] = useState(firstRequired);
+
+ const canBeOpened =
+ (status === 'skipped' || status === 'invalid') && !firstRequired;
+
+ useEffect(() => {
+ if (!canBeOpened) setOpen(false);
+ }, [open]);
+
+ return (
+
+
{
+ if (canBeOpened) setOpen((open) => !open);
+ }}
+ >
+
+ {status === 'skipped' &&
}
+ {status === 'complete' &&
}
+ {(status === 'invalid' || status === 'blocked') && (
+
+ )}
+
+
+
+ {canBeOpened && (
+
+ )}
+
+
+ {(firstRequired || open) && children && (
+
{children}
+ )}
+
+ );
+}
+
+const stepContentLookup: Record<
+ number,
+ (
+ step: TrackingChecklistStep,
+ context: TrackingChecklistContext
+ ) => JSX.Element
+> = {
+ [TrackingChecklistStepId.TRACKERS_REST_CALIBRATION]: (step, { toggle }) => {
+ return (
+
+
+
+ {step.ignorable && (
+ toggle(step.id)}
+ >
+ )}
+
+
+ );
+ },
+ [TrackingChecklistStepId.FULL_RESET]: () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ },
+ [TrackingChecklistStepId.STEAMVR_DISCONNECTED]: (step, { toggle }) => {
+ return (
+ <>
+
+
+
+ openUrl('steam://run/250820')}
+ >
+ {step.ignorable && (
+ toggle(step.id)}
+ >
+ )}
+
+
+ >
+ );
+ },
+ [TrackingChecklistStepId.TRACKER_ERROR]: () => {
+ return
;
+ },
+ [TrackingChecklistStepId.UNASSIGNED_HMD]: () => {
+ return (
+
+ );
+ },
+ [TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]: (step, { toggle }) => {
+ const data = step.extraData as TrackingChecklistPublicNetworksT | null;
+ return (
+ <>
+
+
+ ),
+ }}
+ whitespace="whitespace-pre-wrap"
+ >
+
+ openUrl('ms-settings:network')}
+ >
+ {step.ignorable && (
+ toggle(step.id)}
+ >
+ )}
+
+
+ >
+ );
+ },
+ [TrackingChecklistStepId.VRCHAT_SETTINGS]: (step, { toggle }) => {
+ return (
+ <>
+
+
+
+
+ {step.ignorable && (
+ toggle(step.id)}
+ >
+ )}
+
+
+ >
+ );
+ },
+ [TrackingChecklistStepId.MOUNTING_CALIBRATION]: (step, { toggle }) => {
+ return (
+
+
+
+
+
+
+
+
+ {step.ignorable && (
+ toggle(step.id)}
+ >
+ )}
+
+
+ );
+ },
+ [TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]: (step, { toggle }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {step.ignorable && (
+ toggle(step.id)}
+ >
+ )}
+
+
+ );
+ },
+ [TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]: (step, { toggle }) => {
+ return (
+ <>
+
+
+
+
+ {step.ignorable && (
+ toggle(step.id)}
+ >
+ )}
+
+
+ >
+ );
+ },
+};
+
+export function TrackingChecklistMobile() {
+ const context = useTrackingChecklist();
+ const { completion, firstRequired, warnings } = context;
+
+ return (
+
+
+
+ {completion === 'incomplete' ? 'Required:' : 'Warning:'}{' '}
+
+
+
+
+
+ );
+}
+
+export function TrackingChecklist({
+ closable = true,
+ closed,
+ closing,
+ toggleClosed,
+}: {
+ closable?: boolean;
+ closed: boolean;
+ closing: boolean;
+ toggleClosed: () => void;
+}) {
+ const context = useTrackingChecklist();
+ const { visibleSteps, progress, completion, warnings } = context;
+
+ const slimeState = useMemo(() => {
+ if (completion === 'complete') return SlimeState.HAPPY;
+ if (completion === 'incomplete') return SlimeState.CURIOUS;
+ if (completion === 'partial') return SlimeState.SAD;
+ return SlimeState.HAPPY;
+ }, [completion]);
+
+ const settingsOpenState = useState(false);
+ const [, setSettingsOpen] = settingsOpenState;
+
+ return (
+ <>
+
+
+
+
+
+
+
setSettingsOpen(true)}
+ >
+
+
+ {closable && (
+
toggleClosed()}
+ >
+ {closed &&
}
+ {!closed &&
}
+
+ )}
+
+
+
+ {visibleSteps.map((step, index) => (
+
+ {stepContentLookup[step.id]?.(step, context) || undefined}
+
+ ))}
+
+
+
toggleClosed()}
+ >
+
+
+ {completion === 'incomplete' && (
+
+ )}
+ {completion === 'partial' && (
+
+ )}
+ {completion == 'complete' && (
+
+ )}
+
+
+
+
+ {!closed && (
+
+ )}
+
+
+
+
+
+ >
+ );
+}
+
+export function ChecklistPage() {
+ const nav = useNavigate();
+ const { isMobile } = useBreakpoint('mobile');
+
+ useEffect(() => {
+ if (!isMobile) nav('/');
+ }, [isMobile]);
+
+ return (
+
+ {}}
+ >
+
+ );
+}
diff --git a/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx b/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx
new file mode 100644
index 0000000000..9478e260e6
--- /dev/null
+++ b/gui/src/components/tracking-checklist/TrackingChecklistModal.tsx
@@ -0,0 +1,36 @@
+import { Dispatch, SetStateAction } from 'react';
+import { BaseModal } from '@/components/commons/BaseModal';
+import { Typography } from '@/components/commons/Typography';
+import { Button } from '@/components/commons/Button';
+import { TrackingChecklistSettings } from '@/components/settings/pages/HomeScreenSettings';
+
+export function TrackingChecklistModal({
+ open,
+}: {
+ open: [boolean, Dispatch
>];
+}) {
+ return (
+ {
+ open[1](false);
+ }}
+ >
+
+
+
+
+ open[1](false)}
+ id="tracking_checklist-settings-close"
+ />
+
+
+
+ );
+}
diff --git a/gui/src/components/tracking-checklist/TrackingChecklistProvider.tsx b/gui/src/components/tracking-checklist/TrackingChecklistProvider.tsx
new file mode 100644
index 0000000000..0f65226dc4
--- /dev/null
+++ b/gui/src/components/tracking-checklist/TrackingChecklistProvider.tsx
@@ -0,0 +1,19 @@
+import {
+ TrackingChecklistContectC,
+ provideTrackingChecklist,
+} from '@/hooks/tracking-checklist';
+import { ReactNode } from 'react';
+
+export function TrackingChecklistProvider({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ const context = provideTrackingChecklist();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/gui/src/components/vr-mode/VRModePage.tsx b/gui/src/components/vr-mode/VRModePage.tsx
index 093384cd94..96ff6ed5a8 100644
--- a/gui/src/components/vr-mode/VRModePage.tsx
+++ b/gui/src/components/vr-mode/VRModePage.tsx
@@ -1,7 +1,10 @@
import { useEffect } from 'react';
import { useBreakpoint } from '@/hooks/breakpoint';
-import { WidgetsComponent } from '@/components/WidgetsComponent';
-import { useNavigate } from 'react-router-dom';
+import { NavLink, useNavigate } from 'react-router-dom';
+import { SkeletonVisualizerWidget } from '@/components/widgets/SkeletonVisualizerWidget';
+import { Checklist } from '@/components/commons/icon/ChecklistIcon';
+import { PreviewControls } from '@/components/Sidebar';
+import { Vector3 } from 'three';
export function VRModePage() {
const nav = useNavigate();
@@ -12,8 +15,30 @@ export function VRModePage() {
}, [isMobile]);
return (
-
-
+
+
{
+ context.addView({
+ left: 0,
+ bottom: 0,
+ width: 1,
+ height: 1,
+ position: new Vector3(3, 2.5, -3),
+ onHeightChange(v, newHeight) {
+ v.controls.target.set(0, newHeight / 2.4, 0.1);
+ const scale = Math.max(1, newHeight) / 1;
+ v.camera.zoom = 1 / scale;
+ },
+ });
+ }}
+ >
+
+
+
+
);
}
diff --git a/gui/src/components/vrc/VRCWarningsPage.tsx b/gui/src/components/vrc/VRCWarningsPage.tsx
index 7c048b0cff..22214b285f 100644
--- a/gui/src/components/vrc/VRCWarningsPage.tsx
+++ b/gui/src/components/vrc/VRCWarningsPage.tsx
@@ -104,7 +104,7 @@ const onOffKey = (value: boolean) =>
export function VRCWarningsPage() {
const { l10n } = useLocalization();
- const { state, toggleMutedSettings, mutedSettings } = useVRCConfig();
+ const { state, toggleMutedSettings } = useVRCConfig();
const { currentLocales } = useLocaleConfig();
const meterFormat = Intl.NumberFormat(currentLocales, {
@@ -119,7 +119,7 @@ export function VRCWarningsPage() {
const settingRowProps = (key: keyof VRCConfigStateSupported['validity']) => ({
mute: () => toggleMutedSettings(key),
- muted: mutedSettings.includes(key),
+ muted: state.muted.includes(key),
valid: state.validity[key] == true,
});
diff --git a/gui/src/components/widgets/DeveloperModeWidget.tsx b/gui/src/components/widgets/DeveloperModeWidget.tsx
index 860c5fec2b..67f3d14fd7 100644
--- a/gui/src/components/widgets/DeveloperModeWidget.tsx
+++ b/gui/src/components/widgets/DeveloperModeWidget.tsx
@@ -4,7 +4,6 @@ import { useConfig } from '@/hooks/config';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { CheckBox } from '@/components/commons/Checkbox';
import { useLocalization } from '@fluent/react';
-import { Typography } from '@/components/commons/Typography';
export interface DeveloperModeWidgetForm {
highContrast: boolean;
@@ -57,6 +56,7 @@ export function DeveloperModeWidget() {
key={index}
control={control}
variant="toggle"
+ outlined
name={name}
label={l10n.getString(`widget-developer_mode-${label}`)}
>
@@ -73,10 +73,7 @@ export function DeveloperModeWidget() {
};
return (
-
);
diff --git a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx
index 1a8cef4bd3..e81a62a232 100644
--- a/gui/src/components/widgets/SkeletonVisualizerWidget.tsx
+++ b/gui/src/components/widgets/SkeletonVisualizerWidget.tsx
@@ -10,7 +10,6 @@ import * as THREE from 'three';
import { BodyPart, BoneT } from 'solarxr-protocol';
import { QuaternionFromQuatT, isIdentity } from '@/maths/quaternion';
import classNames from 'classnames';
-import { Button } from '@/components/commons/Button';
import { useLocalization } from '@fluent/react';
import { ErrorBoundary } from 'react-error-boundary';
import { Typography } from '@/components/commons/Typography';
@@ -18,8 +17,9 @@ import { useAtomValue } from 'jotai';
import { bonesAtom } from '@/store/app-store';
import { useConfig } from '@/hooks/config';
import { Tween } from '@tweenjs/tween.js';
+import { EyeIcon } from '@/components/commons/icon/EyeIcon';
-const GROUND_COLOR = '#4444aa';
+const GROUND_COLOR = '#2c2c6b';
// Just need to know the length of the total body, so don't need right legs
const Y_PARTS = [
@@ -32,61 +32,6 @@ const Y_PARTS = [
BodyPart.LEFT_LOWER_LEG,
];
-interface SkeletonVisualizerWidgetProps {
- height?: number | string;
- maxHeight?: number | string;
-}
-
-export function ToggleableSkeletonVisualizerWidget({
- height,
- maxHeight,
-}: SkeletonVisualizerWidgetProps) {
- const { l10n } = useLocalization();
- const [enabled, setEnabled] = useState(false);
-
- useEffect(() => {
- const state = localStorage.getItem('skeletonModelPreview');
- if (state) setEnabled(state === 'true');
- }, []);
-
- return (
- <>
- {!enabled && (
-
{
- setEnabled(true);
- localStorage.setItem('skeletonModelPreview', 'true');
- }}
- >
- {l10n.getString('widget-skeleton_visualizer-preview')}
-
- )}
- {enabled && (
-
-
{
- setEnabled(false);
- localStorage.setItem('skeletonModelPreview', 'false');
- }}
- >
- {l10n.getString('widget-skeleton_visualizer-hide')}
-
-
-
-
-
- )}
- >
- );
-}
-
export type SkeletonPreviewView = {
left: number;
bottom: number;
@@ -110,7 +55,7 @@ function initializePreview(
const resolution = new THREE.Vector2(canvas.clientWidth, canvas.clientHeight);
const scene = new THREE.Scene();
- const renderer = new THREE.WebGLRenderer({
+ let renderer: THREE.WebGLRenderer | null = new THREE.WebGLRenderer({
canvas,
alpha: true,
antialias: true,
@@ -195,7 +140,7 @@ function initializePreview(
const render = (delta: number) => {
views.forEach((v) => {
- if (v.hidden) return;
+ if (v.hidden || !renderer) return;
v.controls.update(delta);
const left = Math.floor(resolution.x * v.left);
@@ -251,6 +196,7 @@ function initializePreview(
resize: (width: number, height: number) => {
resolution.set(width, height);
skeletonHelper.resolution.copy(resolution);
+ if (!renderer) return;
renderer.setSize(width, height);
},
setFrameInterval: (interval: number) => {
@@ -276,9 +222,11 @@ function initializePreview(
}
},
destroy: () => {
+ cancelAnimationFrame(animationFrameId);
skeletonHelper.dispose();
+ if (!renderer) return;
renderer.dispose();
- cancelAnimationFrame(animationFrameId);
+ renderer = null; // Very important for js to free the WebGL context. dispose does not to it alone
},
addView: ({
left,
@@ -297,6 +245,8 @@ function initializePreview(
hidden?: boolean;
onHeightChange: (view: SkeletonPreviewView, newHeight: number) => void;
}) => {
+ if (!renderer) return;
+
const camera = new THREE.PerspectiveCamera(
20,
resolution.width / resolution.height,
@@ -344,8 +294,10 @@ type PreviewContext = ReturnType
;
function SkeletonVisualizer({
onInit,
+ disabled = false,
}: {
onInit: (context: PreviewContext) => void;
+ disabled?: boolean;
}) {
const { config } = useConfig();
@@ -362,15 +314,15 @@ function SkeletonVisualizer({
useEffect(() => {
if (bones.size === 0) return;
const context = previewContext.current;
- if (!context) return;
+ if (!context || disabled) return;
context.rebuildSkeleton(createChildren(bones, BoneKind.root), bones);
- }, [bones.size]);
+ }, [bones.size, disabled]);
useEffect(() => {
const context = previewContext.current;
- if (!context) return;
+ if (!context || disabled) return;
context.updatesBones(bones);
- }, [bones]);
+ }, [bones, disabled]);
const onResize = (e: ResizeObserverEntry) => {
const context = previewContext.current;
@@ -393,6 +345,7 @@ function SkeletonVisualizer({
};
useLayoutEffect(() => {
+ if (disabled) return;
if (!canvasRef.current || !containerRef.current)
throw 'invalid state - no canvas or container';
resizeObserver.current.observe(containerRef.current);
@@ -416,11 +369,12 @@ function SkeletonVisualizer({
if (!previewContext.current || !containerRef.current) return;
resizeObserver.current.unobserve(containerRef.current);
previewContext.current.destroy();
+ previewContext.current = null;
containerRef.current.removeEventListener('mouseenter', onEnter);
containerRef.current.removeEventListener('mouseleave', onLeave);
};
- }, []);
+ }, [disabled]);
return (
@@ -444,20 +398,56 @@ export function SkeletonVisualizerWidget({
},
});
},
+ disabled = false,
+ toggleDisabled,
}: {
onInit?: (context: PreviewContext) => void;
+ disabled?: boolean;
+ toggleDisabled?: () => void;
}) {
const { l10n } = useLocalization();
+ const [error, setError] = useState(false);
return (
-
- {l10n.getString('tips-failed_webgl')}
-
- }
- >
-
-
+
+
+ setError(true)} fallback={<>>}>
+
+
+
+
+
toggleDisabled?.()}
+ >
+
+
+
+
+
+
+
+ {l10n.getString('tips-failed_webgl')}
+
+
+
+
);
}
diff --git a/gui/src/hooks/body-parts.ts b/gui/src/hooks/body-parts.ts
new file mode 100644
index 0000000000..a2ac2766f0
--- /dev/null
+++ b/gui/src/hooks/body-parts.ts
@@ -0,0 +1,90 @@
+import { BodyPart } from 'solarxr-protocol';
+
+export const ALL_BODY_PARTS = [
+ BodyPart.NONE,
+ BodyPart.HEAD,
+ BodyPart.NECK,
+ BodyPart.CHEST,
+ BodyPart.WAIST,
+ BodyPart.HIP,
+ BodyPart.LEFT_UPPER_LEG,
+ BodyPart.RIGHT_UPPER_LEG,
+ BodyPart.LEFT_LOWER_LEG,
+ BodyPart.RIGHT_LOWER_LEG,
+ BodyPart.LEFT_FOOT,
+ BodyPart.RIGHT_FOOT,
+ BodyPart.LEFT_LOWER_ARM,
+ BodyPart.RIGHT_LOWER_ARM,
+ BodyPart.LEFT_UPPER_ARM,
+ BodyPart.RIGHT_UPPER_ARM,
+ BodyPart.LEFT_HAND,
+ BodyPart.RIGHT_HAND,
+ BodyPart.LEFT_SHOULDER,
+ BodyPart.RIGHT_SHOULDER,
+ BodyPart.UPPER_CHEST,
+ BodyPart.LEFT_HIP,
+ BodyPart.RIGHT_HIP,
+ BodyPart.LEFT_THUMB_METACARPAL,
+ BodyPart.LEFT_THUMB_PROXIMAL,
+ BodyPart.LEFT_THUMB_DISTAL,
+ BodyPart.LEFT_INDEX_PROXIMAL,
+ BodyPart.LEFT_INDEX_INTERMEDIATE,
+ BodyPart.LEFT_INDEX_DISTAL,
+ BodyPart.LEFT_MIDDLE_PROXIMAL,
+ BodyPart.LEFT_MIDDLE_INTERMEDIATE,
+ BodyPart.LEFT_MIDDLE_DISTAL,
+ BodyPart.LEFT_RING_PROXIMAL,
+ BodyPart.LEFT_RING_INTERMEDIATE,
+ BodyPart.LEFT_RING_DISTAL,
+ BodyPart.LEFT_LITTLE_PROXIMAL,
+ BodyPart.LEFT_LITTLE_INTERMEDIATE,
+ BodyPart.LEFT_LITTLE_DISTAL,
+ BodyPart.RIGHT_THUMB_METACARPAL,
+ BodyPart.RIGHT_THUMB_PROXIMAL,
+ BodyPart.RIGHT_THUMB_DISTAL,
+ BodyPart.RIGHT_INDEX_PROXIMAL,
+ BodyPart.RIGHT_INDEX_INTERMEDIATE,
+ BodyPart.RIGHT_INDEX_DISTAL,
+ BodyPart.RIGHT_MIDDLE_PROXIMAL,
+ BodyPart.RIGHT_MIDDLE_INTERMEDIATE,
+ BodyPart.RIGHT_MIDDLE_DISTAL,
+ BodyPart.RIGHT_RING_PROXIMAL,
+ BodyPart.RIGHT_RING_INTERMEDIATE,
+ BodyPart.RIGHT_RING_DISTAL,
+ BodyPart.RIGHT_LITTLE_PROXIMAL,
+ BodyPart.RIGHT_LITTLE_INTERMEDIATE,
+ BodyPart.RIGHT_LITTLE_DISTAL,
+];
+export const FEET_BODY_PARTS = [BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT];
+export const FINGER_BODY_PARTS = [
+ BodyPart.LEFT_THUMB_METACARPAL,
+ BodyPart.LEFT_THUMB_PROXIMAL,
+ BodyPart.LEFT_THUMB_DISTAL,
+ BodyPart.LEFT_INDEX_PROXIMAL,
+ BodyPart.LEFT_INDEX_INTERMEDIATE,
+ BodyPart.LEFT_INDEX_DISTAL,
+ BodyPart.LEFT_MIDDLE_PROXIMAL,
+ BodyPart.LEFT_MIDDLE_INTERMEDIATE,
+ BodyPart.LEFT_MIDDLE_DISTAL,
+ BodyPart.LEFT_RING_PROXIMAL,
+ BodyPart.LEFT_RING_INTERMEDIATE,
+ BodyPart.LEFT_RING_DISTAL,
+ BodyPart.LEFT_LITTLE_PROXIMAL,
+ BodyPart.LEFT_LITTLE_INTERMEDIATE,
+ BodyPart.LEFT_LITTLE_DISTAL,
+ BodyPart.RIGHT_THUMB_METACARPAL,
+ BodyPart.RIGHT_THUMB_PROXIMAL,
+ BodyPart.RIGHT_THUMB_DISTAL,
+ BodyPart.RIGHT_INDEX_PROXIMAL,
+ BodyPart.RIGHT_INDEX_INTERMEDIATE,
+ BodyPart.RIGHT_INDEX_DISTAL,
+ BodyPart.RIGHT_MIDDLE_PROXIMAL,
+ BodyPart.RIGHT_MIDDLE_INTERMEDIATE,
+ BodyPart.RIGHT_MIDDLE_DISTAL,
+ BodyPart.RIGHT_RING_PROXIMAL,
+ BodyPart.RIGHT_RING_INTERMEDIATE,
+ BodyPart.RIGHT_RING_DISTAL,
+ BodyPart.RIGHT_LITTLE_PROXIMAL,
+ BodyPart.RIGHT_LITTLE_INTERMEDIATE,
+ BodyPart.RIGHT_LITTLE_DISTAL,
+];
diff --git a/gui/src/hooks/bvh.ts b/gui/src/hooks/bvh.ts
new file mode 100644
index 0000000000..e9202d4321
--- /dev/null
+++ b/gui/src/hooks/bvh.ts
@@ -0,0 +1,54 @@
+import { useLocalization } from '@fluent/react';
+import { isTauri } from '@tauri-apps/api/core';
+import { useEffect, useState } from 'react';
+import { RecordBVHRequestT, RecordBVHStatusT, RpcMessage } from 'solarxr-protocol';
+import { useWebsocketAPI } from './websocket-api';
+import { useConfig } from './config';
+import { save } from '@tauri-apps/plugin-dialog';
+
+export function useBHV() {
+ const { config } = useConfig();
+ const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
+ const [state, setState] = useState<'idle' | 'recording' | 'saving'>('idle');
+ const { l10n } = useLocalization();
+
+ useEffect(() => {
+ sendRPCPacket(RpcMessage.RecordBVHStatusRequest, new RecordBVHRequestT());
+ }, []);
+
+ const toggle = async () => {
+ const record = new RecordBVHRequestT(state === 'recording');
+
+ if (isTauri() && state === 'idle') {
+ if (config?.bvhDirectory) {
+ record.path = config.bvhDirectory;
+ } else {
+ setState('saving');
+ record.path = await save({
+ title: l10n.getString('bvh-save_title'),
+ filters: [
+ {
+ name: 'BVH',
+ extensions: ['bvh'],
+ },
+ ],
+ defaultPath: 'bvh-recording.bvh',
+ });
+ setState('idle');
+ }
+ }
+
+ sendRPCPacket(RpcMessage.RecordBVHRequest, record);
+ };
+
+ useRPCPacket(RpcMessage.RecordBVHStatus, (data: RecordBVHStatusT) => {
+ setState(data.recording ? 'recording' : 'idle');
+ });
+
+ return {
+ available:
+ typeof window.__ANDROID__ === 'undefined' || !window.__ANDROID__?.isThere(),
+ state,
+ toggle,
+ };
+}
diff --git a/gui/src/hooks/config.ts b/gui/src/hooks/config.ts
index e5693b9d0e..4b2ad485a3 100644
--- a/gui/src/hooks/config.ts
+++ b/gui/src/hooks/config.ts
@@ -46,6 +46,8 @@ export interface Config {
showNavbarOnboarding: boolean;
vrcMutedWarnings: string[];
bvhDirectory: string | null;
+ homeLayout: 'default' | 'table';
+ skeletonPreview: boolean;
}
export interface ConfigContext {
@@ -75,6 +77,8 @@ export const defaultConfig: Config = {
vrcMutedWarnings: [],
devSettings: defaultDevSettings,
bvhDirectory: null,
+ homeLayout: 'default',
+ skeletonPreview: true,
};
interface CrossStorage {
diff --git a/gui/src/hooks/pause-tracking.ts b/gui/src/hooks/pause-tracking.ts
new file mode 100644
index 0000000000..981929552a
--- /dev/null
+++ b/gui/src/hooks/pause-tracking.ts
@@ -0,0 +1,37 @@
+import { useEffect, useState } from 'react';
+import { useWebsocketAPI } from './websocket-api';
+import {
+ RpcMessage,
+ SetPauseTrackingRequestT,
+ TrackingPauseStateRequestT,
+ TrackingPauseStateResponseT,
+} from 'solarxr-protocol';
+
+export function usePauseTracking() {
+ const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
+ const [paused, setPaused] = useState(false);
+
+ const toggle = () => {
+ const pause = new SetPauseTrackingRequestT(!paused);
+ sendRPCPacket(RpcMessage.SetPauseTrackingRequest, pause);
+ };
+
+ useRPCPacket(
+ RpcMessage.TrackingPauseStateResponse,
+ (data: TrackingPauseStateResponseT) => {
+ setPaused(data.trackingPaused);
+ }
+ );
+
+ useEffect(() => {
+ sendRPCPacket(
+ RpcMessage.TrackingPauseStateRequest,
+ new TrackingPauseStateRequestT()
+ );
+ }, []);
+
+ return {
+ toggle,
+ paused,
+ };
+}
diff --git a/gui/src/hooks/reset-settings.ts b/gui/src/hooks/reset-settings.ts
new file mode 100644
index 0000000000..8b94e20fad
--- /dev/null
+++ b/gui/src/hooks/reset-settings.ts
@@ -0,0 +1,58 @@
+import {
+ ChangeSettingsRequestT,
+ ResetsSettingsT,
+ RpcMessage,
+ SettingsResetRequestT,
+ SettingsResponseT,
+} from 'solarxr-protocol';
+import { useWebsocketAPI } from './websocket-api';
+import { useEffect, useState } from 'react';
+
+export interface ResetSettingsForm {
+ resetMountingFeet: boolean;
+ armsMountingResetMode: number;
+ yawResetSmoothTime: number;
+ saveMountingReset: boolean;
+ resetHmdPitch: boolean;
+}
+
+export const defaultResetSettings = {
+ resetMountingFeet: false,
+ armsMountingResetMode: 0,
+ yawResetSmoothTime: 0.0,
+ saveMountingReset: false,
+ resetHmdPitch: false,
+};
+
+export function loadResetSettings(resetSettingsForm: ResetSettingsForm) {
+ const resetsSettings = new ResetsSettingsT();
+ resetsSettings.resetMountingFeet = resetSettingsForm.resetMountingFeet;
+ resetsSettings.armsMountingResetMode = resetSettingsForm.armsMountingResetMode;
+ resetsSettings.yawResetSmoothTime = resetSettingsForm.yawResetSmoothTime;
+ resetsSettings.saveMountingReset = resetSettingsForm.saveMountingReset;
+ resetsSettings.resetHmdPitch = resetSettingsForm.resetHmdPitch;
+
+ return resetsSettings;
+}
+
+export function useResetSettings() {
+ const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
+ const [settings, setSettings] = useState
(defaultResetSettings);
+
+ useEffect(() =>
+ sendRPCPacket(RpcMessage.SettingsRequest, new SettingsResetRequestT())
+ );
+
+ useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
+ if (settings.resetsSettings) setSettings(settings.resetsSettings);
+ });
+
+ return {
+ update: (resetSettingsForm: Partial) => {
+ const req = new ChangeSettingsRequestT();
+ const res = loadResetSettings({ ...settings, ...resetSettingsForm });
+ req.resetsSettings = res;
+ sendRPCPacket(RpcMessage.ChangeSettingsRequest, req);
+ },
+ };
+}
diff --git a/gui/src/hooks/reset.ts b/gui/src/hooks/reset.ts
new file mode 100644
index 0000000000..c62938b91b
--- /dev/null
+++ b/gui/src/hooks/reset.ts
@@ -0,0 +1,144 @@
+import { playSoundOnResetEnded, playSoundOnResetStarted } from '@/sounds/sounds';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import {
+ BodyPart,
+ ResetRequestT,
+ ResetResponseT,
+ ResetStatus,
+ ResetType,
+ RpcMessage,
+} from 'solarxr-protocol';
+import { useConfig } from './config';
+import { useWebsocketAPI } from './websocket-api';
+import { useCountdown } from './countdown';
+import { useAtomValue } from 'jotai';
+import { assignedTrackersAtom } from '@/store/app-store';
+import { FEET_BODY_PARTS, FINGER_BODY_PARTS } from './body-parts';
+
+export type ResetBtnStatus = 'idle' | 'counting' | 'finished';
+
+export type MountingResetGroup = 'default' | 'feet' | 'fingers';
+export type UseResetOptions =
+ | { type: ResetType.Full | ResetType.Yaw }
+ | { type: ResetType.Mounting; group: MountingResetGroup };
+
+export const BODY_PARTS_GROUPS: Record = {
+ default: [],
+ feet: FEET_BODY_PARTS,
+ fingers: FINGER_BODY_PARTS,
+};
+
+export function useReset(options: UseResetOptions, onReseted?: () => void) {
+ if (options.type === ResetType.Mounting && !options.group) options.group = 'default';
+
+ const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
+
+ const { config } = useConfig();
+ const finishedTimeoutRef = useRef(-1);
+ const [status, setStatus] = useState('idle');
+
+ const reset = () => {
+ const req = new ResetRequestT();
+ req.resetType = options.type;
+ req.bodyParts = BODY_PARTS_GROUPS['group' in options ? options.group : 'default'];
+ sendRPCPacket(RpcMessage.ResetRequest, req);
+ };
+
+ const duration = 3;
+ const { startCountdown, timer, abortCountdown } = useCountdown({
+ duration: options.type === ResetType.Yaw ? 0 : duration,
+ onCountdownEnd: () => {
+ maybePlaySoundOnResetEnd(options.type);
+ reset();
+ onResetFinished();
+ },
+ });
+
+ const onResetFinished = () => {
+ setStatus('finished');
+
+ // If a timer was already running / clear it
+ abortCountdown();
+ if (finishedTimeoutRef.current !== -1) clearTimeout(finishedTimeoutRef.current);
+
+ // After 2s go back to idle state
+ finishedTimeoutRef.current = setTimeout(() => {
+ setStatus('idle');
+ finishedTimeoutRef.current = -1;
+ }, 2000);
+
+ if (onReseted) onReseted();
+ };
+
+ const maybePlaySoundOnResetEnd = (type: ResetType) => {
+ if (!config?.feedbackSound) return;
+ playSoundOnResetEnded(type, config?.feedbackSoundVolume);
+ };
+
+ const maybePlaySoundOnResetStart = () => {
+ if (!config?.feedbackSound) return;
+ if (options.type !== ResetType.Yaw)
+ playSoundOnResetStarted(config?.feedbackSoundVolume);
+ };
+
+ const triggerReset = () => {
+ setStatus('counting');
+ startCountdown();
+ maybePlaySoundOnResetStart();
+ };
+
+ useEffect(() => {
+ return () => {
+ if (finishedTimeoutRef.current !== -1) clearTimeout(finishedTimeoutRef.current);
+ };
+ }, []);
+
+ useRPCPacket(RpcMessage.ResetResponse, ({ status, resetType }: ResetResponseT) => {
+ if (resetType !== options.type) return;
+ switch (status) {
+ case ResetStatus.FINISHED: {
+ onResetFinished();
+ break;
+ }
+ }
+ });
+
+ const name = useMemo(() => {
+ switch (options.type) {
+ case ResetType.Yaw:
+ return 'reset-yaw';
+ case ResetType.Full:
+ return 'reset-full';
+ case ResetType.Mounting:
+ if (options.group !== 'default') return `reset-mounting-${options.group}`;
+ return 'reset-mounting';
+ default:
+ return 'unhandled';
+ }
+ }, [options.type]);
+
+ let disabled = status === 'counting';
+ if (options.type === ResetType.Mounting && options.group !== 'default') {
+ const assignedTrackers = useAtomValue(assignedTrackersAtom);
+
+ if (
+ !assignedTrackers.some(
+ ({ tracker }) =>
+ tracker.info?.bodyPart &&
+ BODY_PARTS_GROUPS[options.group].includes(tracker.info?.bodyPart)
+ )
+ )
+ disabled = true;
+ }
+
+ return {
+ triggerReset,
+ timer,
+ status,
+ disabled,
+ name,
+ duration,
+ };
+}
+
+export function useMountingReset() {}
diff --git a/gui/src/hooks/status-system.ts b/gui/src/hooks/status-system.ts
deleted file mode 100644
index 38226301e6..0000000000
--- a/gui/src/hooks/status-system.ts
+++ /dev/null
@@ -1,207 +0,0 @@
-import { createContext, useEffect, useReducer, useContext } from 'react';
-import {
- BodyPart,
- RpcMessage,
- StatusData,
- StatusMessageT,
- StatusPublicNetworkT,
- StatusSteamVRDisconnectedT,
- StatusSystemFixedT,
- StatusSystemRequestT,
- StatusSystemResponseT,
- StatusSystemUpdateT,
- StatusTrackerErrorT,
- StatusTrackerResetT,
- StatusUnassignedHMDT,
- TrackerDataT,
-} from 'solarxr-protocol';
-import { useWebsocketAPI } from './websocket-api';
-import { FluentVariable } from '@fluent/bundle';
-import { ReactLocalization } from '@fluent/react';
-import { FlatDeviceTracker } from '@/store/app-store';
-
-type StatusSystemStateAction =
- | StatusSystemStateFixedAction
- | StatusSystemStateNewAction
- | StatusSystemStateUpdateAction;
-
-interface StatusSystemStateFixedAction {
- type: RpcMessage.StatusSystemFixed;
- data: number;
-}
-
-interface StatusSystemStateUpdateAction {
- type: RpcMessage.StatusSystemUpdate;
- data: StatusMessageT;
-}
-
-interface StatusSystemStateNewAction {
- type: RpcMessage.StatusSystemResponse;
- data: StatusMessageT[];
-}
-
-interface StatusSystemState {
- statuses: {
- [id: number]: StatusMessageT;
- };
-}
-
-export interface StatusSystemContext {
- statuses: {
- [id: number]: StatusMessageT;
- };
-}
-
-function reducer(
- state: StatusSystemState,
- action: StatusSystemStateAction
-): StatusSystemState {
- switch (action.type) {
- case RpcMessage.StatusSystemFixed: {
- const newState = {
- statuses: { ...state.statuses },
- };
- delete newState.statuses[action.data];
- return newState;
- }
- case RpcMessage.StatusSystemUpdate: {
- return {
- statuses: { ...state.statuses, [action.data.id]: action.data },
- };
- }
- case RpcMessage.StatusSystemResponse: {
- return {
- // Convert the array into an object, we dont want to have an array on our map!
- statuses: action.data.reduce((prev, cur) => ({ ...prev, [cur.id]: cur }), {}),
- };
- }
- }
-}
-
-export function useProvideStatusContext(): StatusSystemContext {
- const { useRPCPacket, sendRPCPacket, isConnected } = useWebsocketAPI();
- const [state, dispatch] = useReducer(reducer, { statuses: {} });
-
- useRPCPacket(
- RpcMessage.StatusSystemResponse,
- ({ currentStatuses }: StatusSystemResponseT) =>
- dispatch({ type: RpcMessage.StatusSystemResponse, data: currentStatuses })
- );
-
- useRPCPacket(RpcMessage.StatusSystemFixed, ({ fixedStatusId }: StatusSystemFixedT) =>
- dispatch({ type: RpcMessage.StatusSystemFixed, data: fixedStatusId })
- );
-
- useRPCPacket(
- RpcMessage.StatusSystemUpdate,
- ({ newStatus }: StatusSystemUpdateT) =>
- newStatus && dispatch({ type: RpcMessage.StatusSystemUpdate, data: newStatus })
- );
-
- useEffect(() => {
- if (!isConnected) return;
- sendRPCPacket(RpcMessage.StatusSystemRequest, new StatusSystemRequestT());
- }, [isConnected]);
-
- return state;
-}
-
-export const StatusSystemC = createContext(undefined as never);
-
-export function useStatusContext() {
- const context = useContext(StatusSystemC);
- if (!context) {
- throw new Error('useStatusContext must be within a StatusSystemContext Provider');
- }
- return context;
-}
-
-export function parseStatusToLocale(
- status: StatusMessageT,
- trackers: FlatDeviceTracker[] | null,
- l10n: ReactLocalization
-): Record {
- switch (status.dataType) {
- case StatusData.NONE:
- case StatusData.StatusTrackerReset:
- case StatusData.StatusUnassignedHMD:
- return {};
- case StatusData.StatusPublicNetwork: {
- const data = status.data as StatusPublicNetworkT;
- return {
- adapters: data.adapters.join(', '),
- count: data.adapters.length,
- };
- }
- case StatusData.StatusSteamVRDisconnected: {
- const data = status.data as StatusSteamVRDisconnectedT;
- if (typeof data.bridgeSettingsName === 'string') {
- return { type: data.bridgeSettingsName };
- }
- return {};
- }
- case StatusData.StatusTrackerError: {
- const data = status.data as StatusTrackerErrorT;
- if (data.trackerId?.trackerNum === undefined || !trackers) {
- return {};
- }
-
- const tracker = trackers.find(
- ({ tracker }) =>
- tracker?.trackerId?.trackerNum == data.trackerId?.trackerNum &&
- tracker?.trackerId?.deviceId?.id == data.trackerId?.deviceId?.id
- );
- if (!tracker)
- return {
- trackerName: 'unknown',
- };
- const name = tracker.tracker.info?.customName
- ? tracker.tracker.info?.customName
- : tracker.tracker.info?.bodyPart
- ? l10n.getString('body_part-' + BodyPart[tracker.tracker.info?.bodyPart])
- : tracker.tracker.info?.displayName || 'unknown';
- if (typeof name !== 'string') {
- return {
- trackerName: new TextDecoder().decode(name),
- };
- }
- return { trackerName: name };
- }
- }
-}
-
-export const doesntContainTrackerInfo: readonly StatusData[] = [StatusData.NONE];
-export function trackerStatusRelated(
- tracker: TrackerDataT,
- status: StatusMessageT
-): boolean {
- if (doesntContainTrackerInfo.includes(status.dataType)) {
- return false;
- }
-
- switch (status.dataType) {
- case StatusData.StatusTrackerReset: {
- const data = status.data as StatusTrackerResetT;
- return (
- data.trackerId?.trackerNum == tracker.trackerId?.trackerNum &&
- data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id
- );
- }
- case StatusData.StatusTrackerError: {
- const data = status.data as StatusTrackerErrorT;
- return (
- data.trackerId?.trackerNum == tracker.trackerId?.trackerNum &&
- data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id
- );
- }
- case StatusData.StatusUnassignedHMD: {
- const data = status.data as StatusUnassignedHMDT;
- return (
- data.trackerId?.trackerNum == tracker.trackerId?.trackerNum &&
- data.trackerId?.deviceId?.id === tracker.trackerId?.deviceId?.id
- );
- }
- default:
- return false;
- }
-}
diff --git a/gui/src/hooks/tracking-checklist.ts b/gui/src/hooks/tracking-checklist.ts
new file mode 100644
index 0000000000..b63cae9c97
--- /dev/null
+++ b/gui/src/hooks/tracking-checklist.ts
@@ -0,0 +1,193 @@
+import {
+ TrackingChecklistRequestT,
+ TrackingChecklistResponseT,
+ TrackingChecklistStepId,
+ TrackingChecklistStepT,
+ TrackingChecklistStepVisibility,
+ IgnoreTrackingChecklistStepRequestT,
+ RpcMessage,
+ TrackerIdT,
+} from 'solarxr-protocol';
+import { useWebsocketAPI } from './websocket-api';
+import { createContext, useContext, useEffect, useMemo, useState } from 'react';
+
+export const trackingchecklistIdtoLabel: Record = {
+ [TrackingChecklistStepId.UNKNOWN]: '',
+ [TrackingChecklistStepId.TRACKERS_REST_CALIBRATION]:
+ 'tracking_checklist-TRACKERS_REST_CALIBRATION',
+ [TrackingChecklistStepId.FULL_RESET]: 'tracking_checklist-FULL_RESET',
+ [TrackingChecklistStepId.VRCHAT_SETTINGS]: 'tracking_checklist-VRCHAT_SETTINGS',
+ [TrackingChecklistStepId.STEAMVR_DISCONNECTED]:
+ 'tracking_checklist-STEAMVR_DISCONNECTED',
+ [TrackingChecklistStepId.UNASSIGNED_HMD]: 'tracking_checklist-UNASSIGNED_HMD',
+ [TrackingChecklistStepId.TRACKER_ERROR]: 'tracking_checklist-TRACKER_ERROR',
+ [TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC]:
+ 'tracking_checklist-NETWORK_PROFILE_PUBLIC',
+ [TrackingChecklistStepId.MOUNTING_CALIBRATION]:
+ 'tracking_checklist-MOUNTING_CALIBRATION',
+ [TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION]:
+ 'tracking_checklist-FEET_MOUNTING_CALIBRATION',
+ [TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED]:
+ 'tracking_checklist-STAY_ALIGNED_CONFIGURED',
+};
+
+export type TrackingChecklistStepStatus =
+ | 'complete'
+ | 'skipped'
+ | 'blocked'
+ | 'invalid';
+export type TrackingChecklistStep = TrackingChecklistStepT & {
+ status: TrackingChecklistStepStatus;
+ firstRequired: boolean;
+};
+export type highlightedTrackers = {
+ step: TrackingChecklistStep;
+ trackers: Array;
+};
+
+const stepVisibility = ({ visibility, status, firstRequired }: TrackingChecklistStep) =>
+ firstRequired ||
+ visibility === TrackingChecklistStepVisibility.ALWAYS ||
+ (visibility === TrackingChecklistStepVisibility.WHEN_INVALID && status != 'complete');
+
+const createStep = (
+ steps: TrackingChecklistStepT[],
+ step: TrackingChecklistStepT,
+ index: number
+): TrackingChecklistStep => {
+ const previousSteps = steps.slice(0, index);
+ const previousBlocked = previousSteps.some(
+ ({ valid, optional }) => !valid && !optional
+ );
+
+ let status: TrackingChecklistStepStatus = 'complete';
+ if (previousBlocked && !step.valid) status = 'blocked';
+ if (!previousBlocked && !step.valid) status = 'invalid';
+
+ const firstRequiredIndex = steps.findIndex(
+ (s, index) => !s.valid || (index === steps.length - 1 && !s.valid)
+ );
+
+ const skipped =
+ steps.find(
+ (s, sIndex) =>
+ (sIndex > index && s.valid && !s.optional) || sIndex === steps.length - 1
+ ) || index === steps.length - 1;
+ if (!step.valid && step.optional && skipped) status = 'skipped';
+
+ return {
+ ...step,
+ status,
+ firstRequired:
+ firstRequiredIndex === index ||
+ (firstRequiredIndex === -1 && index === steps.length - 1 && !step.valid),
+ pack: () => 0,
+ };
+};
+
+export type TrackingChecklistContext = ReturnType;
+export type Steps = {
+ steps: TrackingChecklistStepT[];
+ visibleSteps: TrackingChecklistStep[];
+ ignoredSteps: TrackingChecklistStepId[];
+};
+export function provideTrackingChecklist() {
+ const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
+ const [steps, setSteps] = useState({
+ steps: [],
+ visibleSteps: [],
+ ignoredSteps: [],
+ });
+
+ useRPCPacket(
+ RpcMessage.TrackingChecklistResponse,
+ (data: TrackingChecklistResponseT) => {
+ const activeSteps = data.steps.filter(
+ (step) => !data.ignoredSteps.includes(step.id) && step.enabled
+ );
+ setSteps({
+ steps: data.steps,
+ visibleSteps: activeSteps
+ .map((step: TrackingChecklistStepT, index) =>
+ createStep(activeSteps, step, index)
+ )
+ .filter(stepVisibility),
+ ignoredSteps: data.ignoredSteps,
+ });
+ }
+ );
+
+ useEffect(() => {
+ sendRPCPacket(RpcMessage.TrackingChecklistRequest, new TrackingChecklistRequestT());
+ }, []);
+
+ const firstRequired = useMemo(
+ () =>
+ steps.visibleSteps.find(
+ (step) => !step.valid && step.status != 'blocked' && !step.optional
+ ),
+ [steps]
+ );
+
+ const highlightedTrackers: highlightedTrackers | undefined = useMemo(() => {
+ if (!firstRequired || !firstRequired.extraData) return undefined;
+ if ('trackersId' in firstRequired.extraData) {
+ return { step: firstRequired, trackers: firstRequired.extraData.trackersId };
+ }
+ if ('trackerId' in firstRequired.extraData && firstRequired.extraData.trackerId) {
+ return { step: firstRequired, trackers: [firstRequired.extraData.trackerId] };
+ }
+ return { step: firstRequired, trackers: [] };
+ }, [firstRequired]);
+
+ const progress = useMemo(() => {
+ const completeSteps = steps.visibleSteps.filter(
+ (step) => step.status === 'complete' || step.status === 'skipped'
+ );
+ return Math.min(1, completeSteps.length / steps.visibleSteps.length);
+ }, [steps]);
+
+ const completion: 'complete' | 'partial' | 'incomplete' = useMemo(() => {
+ if (progress === 1 && steps.visibleSteps.find((step) => step.status === 'skipped'))
+ return 'partial';
+ return progress === 1 || steps.visibleSteps.length === 0
+ ? 'complete'
+ : 'incomplete';
+ }, [progress, steps]);
+
+ const warnings = useMemo(
+ () => steps.visibleSteps.filter((step) => !step.valid),
+ [steps]
+ );
+
+ const ignoreStep = (step: TrackingChecklistStepId, ignore: boolean) => {
+ const res = new IgnoreTrackingChecklistStepRequestT();
+ res.stepId = step;
+ res.ignore = ignore;
+ sendRPCPacket(RpcMessage.IgnoreTrackingChecklistStepRequest, res);
+ };
+
+ return {
+ ...steps,
+ firstRequired,
+ highlightedTrackers,
+ progress,
+ completion,
+ warnings,
+ ignoreStep,
+ toggle: (step: TrackingChecklistStepId) =>
+ ignoreStep(step, !steps.ignoredSteps.includes(step)),
+ };
+}
+
+export const TrackingChecklistContectC = createContext(
+ undefined as never
+);
+
+export function useTrackingChecklist() {
+ const context = useContext(TrackingChecklistContectC);
+ if (!context) {
+ throw new Error('useTrackingChecklist must be within a TrackingChecklistProvider');
+ }
+ return context;
+}
diff --git a/gui/src/hooks/vrc-config.ts b/gui/src/hooks/vrc-config.ts
index 6251807b5e..e38305df4d 100644
--- a/gui/src/hooks/vrc-config.ts
+++ b/gui/src/hooks/vrc-config.ts
@@ -3,19 +3,19 @@ import { useWebsocketAPI } from './websocket-api';
import {
RpcMessage,
VRCAvatarMeasurementType,
+ VRCConfigSettingToggleMuteT,
VRCConfigStateChangeResponseT,
VRCConfigStateRequestT,
VRCSpineMode,
VRCTrackerModel,
} from 'solarxr-protocol';
-import { useConfig } from './config';
type NonNull = {
[P in keyof T]: NonNullable;
};
export type VRCConfigStateSupported = { isSupported: true } & NonNull<
- Pick
+ Pick
>;
export type VRCConfigState = { isSupported: false } | VRCConfigStateSupported;
@@ -45,7 +45,6 @@ export const avatarMeasurementTypeTranslationMap: Record<
export function useVRCConfig() {
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
- const { config, setConfig } = useConfig();
const [state, setState] = useState(null);
useLayoutEffect(() => {
@@ -59,31 +58,20 @@ export function useVRCConfig() {
}
);
- const mutedSettings = useMemo(() => {
- if (!state?.isSupported) return [];
- return Object.keys(state.validity).filter((k) =>
- config?.vrcMutedWarnings.includes(k)
- );
- }, [state, config]);
-
const invalidConfig = useMemo(() => {
if (!state?.isSupported) return false;
return Object.entries(state.validity)
- .filter(([k]) => !config?.vrcMutedWarnings.includes(k))
+ .filter(([k]) => !state.muted.includes(k))
.some(([, v]) => !v);
- }, [state, config]);
+ }, [state]);
return {
state,
invalidConfig,
- mutedSettings,
toggleMutedSettings: async (key: keyof VRCConfigStateSupported['validity']) => {
- if (!config) return;
- const index = config.vrcMutedWarnings.findIndex((v) => v === key);
- if (index === -1) config.vrcMutedWarnings.push(key);
- else config?.vrcMutedWarnings.splice(index, 1);
- await setConfig(config);
- console.log(config.vrcMutedWarnings);
+ const req = new VRCConfigSettingToggleMuteT();
+ req.key = key;
+ sendRPCPacket(RpcMessage.VRCConfigSettingToggleMute, req);
},
};
}
diff --git a/gui/src/index.scss b/gui/src/index.scss
index 1cadf718da..28c5673d6f 100644
--- a/gui/src/index.scss
+++ b/gui/src/index.scss
@@ -63,12 +63,11 @@ body {
background: theme('colors.background.20');
--navbar-w: 101px;
- --widget-w: 274px;
--topbar-h: 38px;
@screen mobile {
--topbar-h: 44px;
- --navbar-h: 90px;
+ --navbar-h: 73px;
}
}
@@ -91,7 +90,7 @@ body {
--accent-background-50: 46, 33, 69;
--success: 80, 232, 151;
- --warning: 216, 205, 55;
+ --warning: 255, 225, 53;
--critical: 223, 109, 140;
--special: 164, 79, 237;
--window-icon-stroke: 192, 161, 216;
@@ -153,7 +152,7 @@ body {
--accent-background-50: 19, 57, 19;
--success: 80, 232, 151;
- --warning: 216, 205, 55;
+ --warning: 255, 225, 53;
--critical: 223, 109, 140;
--special: 54, 161, 54;
--window-icon-stroke: 129, 213, 130;
@@ -179,7 +178,7 @@ body {
--accent-background-50: 57, 57, 19;
--success: 80, 232, 151;
- --warning: 216, 205, 55;
+ --warning: 255, 225, 53;
--critical: 223, 109, 140;
--special: 161, 160, 54;
--window-icon-stroke: 213, 212, 129;
@@ -205,7 +204,7 @@ body {
--accent-background-50: 57, 34, 19;
--success: 80, 232, 151;
- --warning: 216, 205, 55;
+ --warning: 255, 225, 53;
--critical: 223, 109, 140;
--special: 161, 95, 54;
--window-icon-stroke: 213, 162, 129;
@@ -231,7 +230,7 @@ body {
--accent-background-50: 57, 19, 19;
--success: 80, 232, 151;
- --warning: 216, 205, 55;
+ --warning: 255, 225, 53;
--critical: 223, 109, 140;
--special: 161, 54, 54;
--window-icon-stroke: 213, 129, 129;
@@ -257,7 +256,7 @@ body {
--accent-background-50: 39, 39, 39;
--success: 80, 232, 151;
- --warning: 216, 205, 55;
+ --warning: 255, 225, 53;
--critical: 223, 109, 140;
--special: 121, 121, 121;
--window-icon-stroke: 172, 172, 172;
diff --git a/gui/src/store/app-store.ts b/gui/src/store/app-store.ts
index 3b48ab44a2..5786b8cc89 100644
--- a/gui/src/store/app-store.ts
+++ b/gui/src/store/app-store.ts
@@ -9,6 +9,7 @@ import {
} from 'solarxr-protocol';
import { selectAtom } from 'jotai/utils';
import { isEqual } from '@react-hookz/deep-equal';
+import { FEET_BODY_PARTS, FINGER_BODY_PARTS } from '@/hooks/body-parts';
export interface FlatDeviceTracker {
device?: DeviceDataT;
@@ -45,14 +46,18 @@ export const unassignedTrackersAtom = atom((get) => {
return trackers.filter(({ tracker }) => tracker.info?.bodyPart === BodyPart.NONE);
});
-export const connectedIMUTrackersAtom = atom((get) => {
+export const connectedTrackersAtom = atom((get) => {
const trackers = get(flatTrackersAtom);
return trackers.filter(
- ({ tracker }) =>
- tracker.status !== TrackerStatus.DISCONNECTED && tracker.info?.isImu
+ ({ tracker }) => tracker.status !== TrackerStatus.DISCONNECTED
);
});
+export const connectedIMUTrackersAtom = atom((get) => {
+ const trackers = get(connectedTrackersAtom);
+ return trackers.filter(({ tracker }) => tracker.info?.isImu);
+});
+
export const computedTrackersAtom = selectAtom(
datafeedAtom,
(datafeed) => datafeed.syntheticTrackers.map((tracker) => ({ tracker })),
@@ -95,3 +100,16 @@ export const trackerFromIdAtom = ({
(a) => a,
isEqual
);
+
+export const feetAssignedTrackers = atom((get) =>
+ get(assignedTrackersAtom).some(
+ (t) => t.tracker.info?.bodyPart && FEET_BODY_PARTS.includes(t.tracker.info.bodyPart)
+ )
+);
+
+export const fingerAssignedTrackers = atom((get) =>
+ get(assignedTrackersAtom).some(
+ (t) =>
+ t.tracker.info?.bodyPart && FINGER_BODY_PARTS.includes(t.tracker.info.bodyPart)
+ )
+);
diff --git a/gui/src/utils/skeletonHelper.ts b/gui/src/utils/skeletonHelper.ts
index 0ff8f8e23b..eb77d95a48 100644
--- a/gui/src/utils/skeletonHelper.ts
+++ b/gui/src/utils/skeletonHelper.ts
@@ -188,11 +188,11 @@ export class BoneKind extends Bone {
case BodyPart.NONE:
throw 'Unexpected body part';
case BodyPart.HEAD:
- return new Color('black');
+ return new Color('gold');
case BodyPart.NECK:
return new Color('silver');
case BodyPart.UPPER_CHEST:
- return new Color('blue');
+ return new Color('chartreuse');
case BodyPart.CHEST:
return new Color('purple');
case BodyPart.WAIST:
@@ -201,13 +201,13 @@ export class BoneKind extends Bone {
return new Color('orange');
case BodyPart.LEFT_UPPER_LEG:
case BodyPart.RIGHT_UPPER_LEG:
- return new Color('blue');
+ return new Color('chartreuse');
case BodyPart.LEFT_LOWER_LEG:
case BodyPart.RIGHT_LOWER_LEG:
return new Color('teal');
case BodyPart.LEFT_FOOT:
case BodyPart.RIGHT_FOOT:
- return new Color('#00ffcc');
+ return new Color('gold');
case BodyPart.LEFT_LOWER_ARM:
case BodyPart.RIGHT_LOWER_ARM:
return new Color('red');
diff --git a/gui/tailwind.config.ts b/gui/tailwind.config.ts
index 34ab01a0bb..55c1c02437 100644
--- a/gui/tailwind.config.ts
+++ b/gui/tailwind.config.ts
@@ -172,6 +172,7 @@ const config = {
nsm: { raw: 'not (min-width: 900px)' },
sm: '900px',
md: '1100px',
+ nmd: { raw: 'not (min-width: 1100px)' },
'md-max': { raw: 'not (min-width: 1100px)' },
lg: '1300px',
xl: '1600px',
@@ -228,6 +229,46 @@ const config = {
'animation-timing-function': 'cubic-bezier(0.8, 0, 1, 1)',
},
},
+ 'spin-ccw': {
+ '0%': {
+ transform: 'rotate(0deg)',
+ },
+ '100%': {
+ transform: 'rotate(-360deg)',
+ },
+ },
+ skiing: {
+ '0%, 100%': {
+ transform: 'rotate(0deg) translateX(0%) translateY(0%)',
+ },
+ '10%': {
+ transform: 'rotate(12deg) translateX(-5%) translateY(5%)',
+ },
+ '20%': {
+ transform: 'rotate(10deg) translateX(0%) translateY(0%)',
+ },
+ '30%': {
+ transform: 'rotate(12deg) translateX(5%) translateY(-5%)',
+ },
+ '40%': {
+ transform: 'rotate(10deg) translateX(0%) translateY(0%)',
+ },
+ '50%': {
+ transform: 'rotate(12deg) translateX(-5%) translateY(5%)',
+ },
+ '60%': {
+ transform: 'rotate(10deg) translateX(0%) translateY(0%)',
+ },
+ '70%': {
+ transform: 'rotate(12deg) translateX(5%) translateY(-5%)',
+ },
+ '80%': {
+ transform: 'rotate(10deg) translateX(0%) translateY(0%)',
+ },
+ '90%': {
+ transform: 'rotate(10deg) translateX(-5%) translateY(5%)',
+ },
+ },
},
backgroundImage: {
slime: `linear-gradient(135deg, ${colors.purple[100]} 50%, ${colors['blue-gray'][700]} 50% 100%)`,
@@ -240,6 +281,10 @@ const config = {
'trans-flag': `linear-gradient(135deg, ${colors['trans-blue'][800]} 40%, ${colors['trans-blue'][700]} 40% 70%, ${colors['trans-blue'][600]} 70% 100%)`,
'asexual-flag': `linear-gradient(135deg, ${colors['asexual'][100]} 30%, ${colors['asexual'][200]} 30% 50%, ${colors['asexual'][300]} 50% 70%, ${colors['asexual'][400]} 70% 100%)`,
},
+ animation: {
+ 'spin-ccw': 'spin-ccw 1s linear infinite',
+ skiing: 'skiing 1s linear infinite',
+ },
},
data: {
checked: 'checked=true',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3ffeb45faa..59f79ffca5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -65,12 +65,15 @@ importers:
'@tauri-apps/plugin-http':
specifier: ^2.5.0
version: 2.5.0
+ '@tauri-apps/plugin-opener':
+ specifier: ^2.4.0
+ version: 2.4.0
'@tauri-apps/plugin-os':
specifier: ^2.0.0
version: 2.0.0
'@tauri-apps/plugin-shell':
- specifier: ^2.0.0
- version: 2.0.0
+ specifier: ^2.3.0
+ version: 2.3.0
'@tauri-apps/plugin-store':
specifier: ^2.0.0
version: 2.0.0
@@ -1260,11 +1263,14 @@ packages:
'@tauri-apps/plugin-http@2.5.0':
resolution: {integrity: sha512-l4M2DUIsOBIMrbj4dJZwrB4mJiB7OA/2Tj3gEbX2fjq5MOpETklJPKfDvzUTDwuq4lIKCKKykz8E8tpOgvi0EQ==}
+ '@tauri-apps/plugin-opener@2.4.0':
+ resolution: {integrity: sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ==}
+
'@tauri-apps/plugin-os@2.0.0':
resolution: {integrity: sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==}
- '@tauri-apps/plugin-shell@2.0.0':
- resolution: {integrity: sha512-OpW2+ycgJLrEoZityWeWYk+6ZWP9VyiAfbO+N/O8VfLkqyOym8kXh7odKDfINx9RAotkSGBtQM4abyKfJDkcUg==}
+ '@tauri-apps/plugin-shell@2.3.0':
+ resolution: {integrity: sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA==}
'@tauri-apps/plugin-store@2.0.0':
resolution: {integrity: sha512-l4xsbxAXrKGdBdYNNswrLfcRv3v1kOatdycOcVPYW+jKwkznCr1HEOrPXkPhXsZLSLyYmNXpgfOmdSZNmcykDg==}
@@ -5472,13 +5478,17 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.6.0
+ '@tauri-apps/plugin-opener@2.4.0':
+ dependencies:
+ '@tauri-apps/api': 2.6.0
+
'@tauri-apps/plugin-os@2.0.0':
dependencies:
'@tauri-apps/api': 2.0.2
- '@tauri-apps/plugin-shell@2.0.0':
+ '@tauri-apps/plugin-shell@2.3.0':
dependencies:
- '@tauri-apps/api': 2.0.2
+ '@tauri-apps/api': 2.6.0
'@tauri-apps/plugin-store@2.0.0':
dependencies:
diff --git a/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt b/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt
new file mode 100644
index 0000000000..787e634acb
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/NetworkProfileChecker.kt
@@ -0,0 +1,60 @@
+package dev.slimevr
+
+data class NetworkInfo(
+ val name: String?,
+ val description: String?,
+ val category: NetworkCategory?,
+ val connectivity: Set?,
+ val connected: Boolean?,
+)
+
+/**
+ * @see NLM_NETWORK_CATEGORY enumeration (netlistmgr.h)
+ */
+enum class NetworkCategory(val value: Int) {
+ PUBLIC(0),
+ PRIVATE(1),
+ DOMAIN_AUTHENTICATED(2),
+ ;
+
+ companion object {
+ fun fromInt(value: Int) = values().find { it.value == value }
+ }
+}
+
+/**
+ * @see NLM_CONNECTIVITY enumeration (netlistmgr.h)
+ */
+enum class ConnectivityFlags(val value: Int) {
+ DISCONNECTED(0),
+ IPV4_NOTRAFFIC(0x1),
+ IPV6_NOTRAFFIC(0x2),
+ IPV4_SUBNET(0x10),
+ IPV4_LOCALNETWORK(0x20),
+ IPV4_INTERNET(0x40),
+ IPV6_SUBNET(0x100),
+ IPV6_LOCALNETWORK(0x200),
+ IPV6_INTERNET(0x400),
+ ;
+
+ companion object {
+ fun fromInt(value: Int): Set =
+ if (value == 0) {
+ setOf(DISCONNECTED)
+ } else {
+ values().filter { it != DISCONNECTED && (value and it.value) != 0 }.toSet()
+ }
+ }
+}
+
+abstract class NetworkProfileChecker {
+ abstract val isSupported: Boolean
+ abstract val publicNetworks: List
+}
+
+class NetworkProfileCheckerStub : NetworkProfileChecker() {
+ override val isSupported: Boolean
+ get() = false
+ override val publicNetworks: List
+ get() = listOf()
+}
diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt
index e675dc633a..283c4c792a 100644
--- a/server/core/src/main/java/dev/slimevr/VRServer.kt
+++ b/server/core/src/main/java/dev/slimevr/VRServer.kt
@@ -28,6 +28,7 @@ import dev.slimevr.tracking.processor.HumanPoseManager
import dev.slimevr.tracking.processor.skeleton.HumanSkeleton
import dev.slimevr.tracking.trackers.*
import dev.slimevr.tracking.trackers.udp.TrackersUDPServer
+import dev.slimevr.trackingchecklist.TrackingChecklistManager
import dev.slimevr.util.ann.VRServerThread
import dev.slimevr.websocketapi.WebSocketVRBridge
import io.eiren.util.ann.ThreadSafe
@@ -55,6 +56,7 @@ class VRServer @JvmOverloads constructor(
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null },
vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() },
+ networkProfileProvider: (VRServer) -> NetworkProfileChecker = { _ -> NetworkProfileCheckerStub() },
acquireMulticastLock: () -> Any? = { null },
// configPath is used by VRWorkout, do not remove!
configPath: String,
@@ -117,6 +119,10 @@ class VRServer @JvmOverloads constructor(
@JvmField
val handshakeHandler = HandshakeHandler()
+ val trackingChecklistManager: TrackingChecklistManager
+
+ val networkProfileChecker: NetworkProfileChecker
+
init {
// UwU
configManager = ConfigManager(configPath)
@@ -132,6 +138,8 @@ class VRServer @JvmOverloads constructor(
autoBoneHandler = AutoBoneHandler(this)
firmwareUpdateHandler = FirmwareUpdateHandler(this)
vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this))
+ networkProfileChecker = networkProfileProvider(this)
+ trackingChecklistManager = TrackingChecklistManager(this)
protocolAPI = ProtocolAPI(this)
val computedTrackers = humanPoseManager.computedTrackers
@@ -170,6 +178,7 @@ class VRServer @JvmOverloads constructor(
for (tracker in computedTrackers) {
registerTracker(tracker)
}
+
instance = this
}
@@ -306,7 +315,7 @@ class VRServer @JvmOverloads constructor(
queueTask { humanPoseManager.resetTrackersYaw(resetSourceName, bodyParts) }
}
- fun resetTrackersMounting(resetSourceName: String?, bodyParts: List = TrackerUtils.allBodyPartsButFingers) {
+ fun resetTrackersMounting(resetSourceName: String?, bodyParts: List? = null) {
queueTask { humanPoseManager.resetTrackersMounting(resetSourceName, bodyParts) }
}
diff --git a/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt b/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt
index ca869e3670..5750b53b3b 100644
--- a/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt
+++ b/server/core/src/main/java/dev/slimevr/bridge/Bridge.kt
@@ -53,4 +53,6 @@ interface ISteamVRBridge : Bridge {
fun getAutomaticSharedTrackers(): Boolean
fun setAutomaticSharedTrackers(value: Boolean)
+
+ fun getBridgeConfigKey(): String
}
diff --git a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt b/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt
index ce33bddd81..025cc5ece0 100644
--- a/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt
+++ b/server/core/src/main/java/dev/slimevr/config/ResetsConfig.kt
@@ -29,6 +29,24 @@ enum class ArmsResetModes(val id: Int) {
}
}
+enum class MountingMethods(val id: Int) {
+ MANUAL(0),
+ AUTOMATIC(1),
+ ;
+
+ companion object {
+ val values = MountingMethods.entries.toTypedArray()
+
+ @JvmStatic
+ fun fromId(id: Int): MountingMethods? {
+ for (filter in values) {
+ if (filter.id == id) return filter
+ }
+ return null
+ }
+ }
+}
+
class ResetsConfig {
// Always reset mounting for feet
@@ -46,6 +64,8 @@ class ResetsConfig {
// Reset the HMD's pitch upon full reset
var resetHmdPitch = false
+ var preferedMountingMethod = MountingMethods.AUTOMATIC
+
fun updateTrackersResetsSettings() {
for (t in VRServer.instance.allTrackers) {
t.resetsHandler.readResetConfig(this)
diff --git a/server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt b/server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt
new file mode 100644
index 0000000000..182a97bfff
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/config/TrackingChecklistConfig.kt
@@ -0,0 +1,5 @@
+package dev.slimevr.config
+
+class TrackingChecklistConfig {
+ val ignoredStepsIds: MutableList = mutableListOf()
+}
diff --git a/server/core/src/main/java/dev/slimevr/config/VRCConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRCConfig.kt
new file mode 100644
index 0000000000..fd40b13dda
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/config/VRCConfig.kt
@@ -0,0 +1,6 @@
+package dev.slimevr.config
+
+class VRCConfig {
+ // List of fields ignored in vrc warnings - @see VRCConfigValidity
+ val mutedWarnings: MutableList = mutableListOf()
+}
diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt
index de6bc93192..0a01a4d42b 100644
--- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt
+++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt
@@ -54,6 +54,10 @@ class VRConfig {
val overlay: OverlayConfig = OverlayConfig()
+ val trackingChecklist: TrackingChecklistConfig = TrackingChecklistConfig()
+
+ val vrcConfig: VRCConfig = VRCConfig()
+
init {
// Initialize default settings for OSC Router
oscRouter.portIn = 9002
@@ -104,7 +108,7 @@ class VRConfig {
tracker.readConfig(config)
if (tracker.isImu()) tracker.resetsHandler.readDriftCompensationConfig(driftCompensation)
tracker.resetsHandler.readResetConfig(resetsConfig)
- if (tracker.needsReset) {
+ if (tracker.allowReset) {
tracker.saveMountingResetOrientation(config)
}
if (tracker.allowFiltering) {
diff --git a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt
index 8e54d5de8a..fc2829e803 100644
--- a/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/games/vrchat/VRCConfigHandler.kt
@@ -78,11 +78,11 @@ data class VRCConfigValidity(
val shoulderTrackingOk: Boolean,
val shoulderWidthCompensationOk: Boolean,
val userHeightOk: Boolean,
- val calibrationOk: Boolean,
+ val calibrationRangeOk: Boolean,
val calibrationVisualsOk: Boolean,
- val tackerModelOk: Boolean,
+ val trackerModelOk: Boolean,
val spineModeOk: Boolean,
- val avatarMeasurementOk: Boolean,
+ val avatarMeasurementTypeOk: Boolean,
)
abstract class VRCConfigHandler {
@@ -98,13 +98,14 @@ class VRCConfigHandlerStub : VRCConfigHandler() {
}
interface VRCConfigListener {
- fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues)
+ fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List)
}
class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHandler) {
private val listeners: MutableList = CopyOnWriteArrayList()
var currentValues: VRCConfigValues? = null
+ var currentValidity: VRCConfigValidity? = null
val isSupported: Boolean
get() = handler.isSupported
@@ -113,6 +114,31 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa
handler.initHandler(::onChange)
}
+ fun toggleMuteWarning(key: String) {
+ val keys = VRCConfigValidity::class.java.declaredFields.asSequence().map { p -> p.name }
+ if (!keys.contains(key)) return
+
+ if (!server.configManager.vrConfig.vrcConfig.mutedWarnings.contains(key)) {
+ server.configManager.vrConfig.vrcConfig.mutedWarnings.add(key)
+ } else {
+ server.configManager.vrConfig.vrcConfig.mutedWarnings.remove(key)
+ }
+
+ server.configManager.saveConfig()
+
+ val recommended = recommendedValues()
+ val validity = currentValidity ?: return
+ val values = currentValues ?: return
+ listeners.forEach {
+ it.onChange(
+ validity,
+ values,
+ recommended,
+ server.configManager.vrConfig.vrcConfig.mutedWarnings,
+ )
+ }
+ }
+
/**
* shoulderTrackingDisabled should be true if:
* The user isn't tracking their whole arms from their controllers:
@@ -160,20 +186,28 @@ class VRChatConfigManager(val server: VRServer, private val handler: VRCConfigHa
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,
+ trackerModelOk = values.trackerModel == recommended.trackerModel,
+ calibrationRangeOk = 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,
+ avatarMeasurementTypeOk = values.avatarMeasurementType == recommended.avatarMeasurementType,
shoulderWidthCompensationOk = values.shoulderWidthCompensation == recommended.shoulderWidthCompensation,
)
+ fun forceUpdate() {
+ val values = currentValues
+ if (values != null) {
+ this.onChange(values)
+ }
+ }
+
fun onChange(values: VRCConfigValues) {
val recommended = recommendedValues()
val validity = checkValidity(values, recommended)
+ currentValidity = validity
currentValues = values
listeners.forEach {
- it.onChange(validity, values, recommended)
+ it.onChange(validity, values, recommended, server.configManager.vrConfig.vrcConfig.mutedWarnings)
}
}
}
diff --git a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt
index e3833ec954..2e4f146658 100644
--- a/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/osc/VMCHandler.kt
@@ -290,7 +290,7 @@ class VMCHandler(
userEditable = true,
isComputed = position != null,
usesTimeout = true,
- needsReset = position != null,
+ allowReset = position != null,
)
trackerDevice!!.trackers[trackerDevice!!.trackers.size] = tracker
byTrackerNameTracker[name] = tracker
diff --git a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
index 7199082c08..8723f96f76 100644
--- a/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/osc/VRCOSCHandler.kt
@@ -275,7 +275,7 @@ class VRCOSCHandler(
hasPosition = true,
userEditable = true,
isComputed = true,
- needsReset = trackerPosition != TrackerPosition.HEAD,
+ allowReset = trackerPosition != TrackerPosition.HEAD,
usesTimeout = true,
)
vrsystemTrackersDevice!!.trackers[trackerPosition.ordinal] = tracker
@@ -368,7 +368,7 @@ class VRCOSCHandler(
hasPosition = true,
userEditable = true,
isComputed = true,
- needsReset = true,
+ allowReset = true,
usesTimeout = true,
)
oscTrackersDevice!!.trackers[trackerId] = tracker
diff --git a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java
index b2efbd0ab6..3c256ff2fd 100644
--- a/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java
+++ b/server/core/src/main/java/dev/slimevr/protocol/datafeed/DataFeedBuilder.java
@@ -141,7 +141,7 @@ public static int createTrackerInfos(
TrackerInfo.addAllowDriftCompensation(fbb, false);
}
- if (tracker.getNeedsMounting()) {
+ if (tracker.getAllowMounting()) {
Quaternion quaternion = tracker.getResetsHandler().getMountingOrientation();
Quaternion mountResetFix = tracker.getResetsHandler().getMountRotFix();
TrackerInfo.addMountingOrientation(fbb, createQuat(fbb, quaternion));
@@ -215,7 +215,7 @@ public static int createTrackerData(
if (trackerTemperatureOffset != 0)
TrackerData.addTemp(fbb, trackerTemperatureOffset);
}
- if (tracker.getNeedsMounting() && tracker.getHasRotation()) {
+ if (tracker.getAllowMounting() && tracker.getHasRotation()) {
if (mask.getRotationReferenceAdjusted()) {
TrackerData
.addRotationReferenceAdjusted(fbb, createQuat(fbb, tracker.getRotation()));
@@ -227,7 +227,7 @@ public static int createTrackerData(
createQuat(fbb, tracker.getIdentityAdjustedRotation())
);
}
- } else if (tracker.getNeedsReset() && tracker.getHasRotation()) {
+ } else if (tracker.getAllowReset() && tracker.getHasRotation()) {
if (mask.getRotationReferenceAdjusted()) {
TrackerData
.addRotationReferenceAdjusted(fbb, createQuat(fbb, tracker.getRotation()));
diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt
index 79a82265a6..def48d864c 100644
--- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt
@@ -1,6 +1,7 @@
package dev.slimevr.protocol.rpc
import com.google.flatbuffers.FlatBufferBuilder
+import dev.slimevr.config.MountingMethods
import dev.slimevr.config.config
import dev.slimevr.protocol.GenericConnection
import dev.slimevr.protocol.ProtocolAPI
@@ -18,6 +19,7 @@ import dev.slimevr.protocol.rpc.setup.RPCHandshakeHandler
import dev.slimevr.protocol.rpc.setup.RPCTapSetupHandler
import dev.slimevr.protocol.rpc.setup.RPCUtil.getLocalIp
import dev.slimevr.protocol.rpc.status.RPCStatusHandler
+import dev.slimevr.protocol.rpc.trackingchecklist.RPCTrackingChecklistHandler
import dev.slimevr.protocol.rpc.trackingpause.RPCTrackingPause
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
import dev.slimevr.tracking.processor.stayaligned.poses.RelaxedPose
@@ -48,6 +50,7 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler,
): Int {
if (!isSupported) {
VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb)
@@ -71,11 +72,13 @@ fun buildVRCConfigStateResponse(
val validityOffset = buildVRCConfigValidity(fbb, validity)
val valuesOffset = buildVRCConfigValues(fbb, values)
val recommendedOffset = buildVRCConfigRecommendedValues(fbb, recommended)
+ val mutedOffset = VRCConfigStateChangeResponse.createMutedVector(fbb, muted.map { fbb.createString(it) }.toIntArray())
VRCConfigStateChangeResponse.startVRCConfigStateChangeResponse(fbb)
VRCConfigStateChangeResponse.addIsSupported(fbb, true)
VRCConfigStateChangeResponse.addValidity(fbb, validityOffset)
VRCConfigStateChangeResponse.addState(fbb, valuesOffset)
VRCConfigStateChangeResponse.addRecommended(fbb, recommendedOffset)
+ VRCConfigStateChangeResponse.addMuted(fbb, mutedOffset)
return VRCConfigStateChangeResponse.endVRCConfigStateChangeResponse(fbb)
}
diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt
index 2d4bd92776..481e918d35 100644
--- a/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/games/vrchat/RPCVRChatHandler.kt
@@ -18,12 +18,8 @@ class RPCVRChatHandler(
init {
api.server.vrcConfigManager.addListener(this)
- rpcHandler.registerPacketListener(RpcMessage.VRCConfigStateRequest) { conn: GenericConnection, messageHeader: RpcMessageHeader ->
- this.onConfigStateRequest(
- conn,
- messageHeader,
- )
- }
+ rpcHandler.registerPacketListener(RpcMessage.VRCConfigStateRequest, ::onConfigStateRequest)
+ rpcHandler.registerPacketListener(RpcMessage.VRCConfigSettingToggleMute, ::onToggleMuteRequest)
}
private fun onConfigStateRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
@@ -41,6 +37,7 @@ class RPCVRChatHandler(
validity = validity,
values = values,
recommended = api.server.vrcConfigManager.recommendedValues(),
+ muted = api.server.configManager.vrConfig.vrcConfig.mutedWarnings,
)
val outbound = rpcHandler.createRPCMessage(
@@ -52,7 +49,13 @@ class RPCVRChatHandler(
conn.send(fbb.dataBuffer())
}
- override fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues) {
+ private fun onToggleMuteRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
+ val req = messageHeader.message(VRCConfigSettingToggleMute()) as VRCConfigSettingToggleMute?
+ ?: return
+ api.server.vrcConfigManager.toggleMuteWarning(req.key())
+ }
+
+ override fun onChange(validity: VRCConfigValidity, values: VRCConfigValues, recommended: VRCConfigRecommendedValues, muted: List) {
val fbb = FlatBufferBuilder(32)
val response = buildVRCConfigStateResponse(
@@ -61,6 +64,7 @@ class RPCVRChatHandler(
validity = validity,
values = values,
recommended = recommended,
+ muted,
)
val outbound = rpcHandler.createRPCMessage(
diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt
new file mode 100644
index 0000000000..ca6f6b34e7
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/trackingchecklist/RPCTrackingChecklistHandler.kt
@@ -0,0 +1,73 @@
+package dev.slimevr.protocol.rpc.trackingchecklist
+
+import com.google.flatbuffers.FlatBufferBuilder
+import dev.slimevr.protocol.GenericConnection
+import dev.slimevr.protocol.ProtocolAPI
+import dev.slimevr.protocol.rpc.RPCHandler
+import dev.slimevr.trackingchecklist.TrackingChecklistListener
+import solarxr_protocol.rpc.*
+
+class RPCTrackingChecklistHandler(
+ private val rpcHandler: RPCHandler,
+ var api: ProtocolAPI,
+) : TrackingChecklistListener {
+
+ init {
+ api.server.trackingChecklistManager.addListener(this)
+
+ rpcHandler.registerPacketListener(RpcMessage.TrackingChecklistRequest, ::onTrackingChecklistRequest)
+ rpcHandler.registerPacketListener(RpcMessage.IgnoreTrackingChecklistStepRequest, ::onToggleTrackingChecklistRequest)
+ }
+
+ fun buildTrackingChecklistResponse(fbb: FlatBufferBuilder): Int = TrackingChecklistResponse.pack(
+ fbb,
+ TrackingChecklistResponseT().apply {
+ steps = api.server.trackingChecklistManager.steps.toTypedArray()
+ ignoredSteps = api.server.configManager.vrConfig.trackingChecklist.ignoredStepsIds.toIntArray()
+ },
+ )
+
+ private fun onTrackingChecklistRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
+ val fbb = FlatBufferBuilder(32)
+ val response = buildTrackingChecklistResponse(fbb)
+ val outbound = rpcHandler.createRPCMessage(
+ fbb,
+ RpcMessage.TrackingChecklistResponse,
+ response,
+ )
+ fbb.finish(outbound)
+ conn.send(fbb.dataBuffer())
+ }
+
+ private fun onToggleTrackingChecklistRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
+ val req = messageHeader.message(IgnoreTrackingChecklistStepRequest()) as IgnoreTrackingChecklistStepRequest?
+ ?: return
+ val step = api.server.trackingChecklistManager.steps.find { it.id == req.stepId() } ?: error("invalid step id requested")
+
+ api.server.trackingChecklistManager.ignoreStep(step, req.ignore())
+
+ val fbb = FlatBufferBuilder(32)
+ val response = buildTrackingChecklistResponse(fbb)
+ val outbound = rpcHandler.createRPCMessage(
+ fbb,
+ RpcMessage.TrackingChecklistResponse,
+ response,
+ )
+ fbb.finish(outbound)
+ conn.send(fbb.dataBuffer())
+ }
+
+ override fun onStepsUpdate() {
+ val fbb = FlatBufferBuilder(32)
+ val response = buildTrackingChecklistResponse(fbb)
+ val outbound = rpcHandler.createRPCMessage(
+ fbb,
+ RpcMessage.TrackingChecklistResponse,
+ response,
+ )
+ fbb.finish(outbound)
+ this.api.apiServers.forEach { apiServer ->
+ apiServer.apiConnections.forEach { it.send(fbb.dataBuffer()) }
+ }
+ }
+}
diff --git a/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt b/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt
index fe590b6be8..bd521f9a81 100644
--- a/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt
+++ b/server/core/src/main/java/dev/slimevr/status/StatusSystem.kt
@@ -28,36 +28,6 @@ class StatusSystem {
status
}.toTypedArray()
- /**
- * @return the ID of the status, 0 is not a valid ID, can be used as replacement of null
- */
- @JvmName("addStatusInt")
- fun addStatus(statusData: StatusDataUnion, prioritized: Boolean = false): UInt {
- val id = idCounter.getAndUpdate {
- (it.toUInt() + 1u).toInt() // the simple way of making unsigned math
- }
- statuses[id] = statusData
- if (prioritized) {
- prioritizedStatuses.add(id)
- }
-
- listeners.forEach {
- it.onStatusChanged(id.toUInt(), statusData, prioritized)
- }
-
- return id.toUInt()
- }
-
- @JvmName("removeStatusInt")
- fun removeStatus(id: UInt) {
- statuses.remove(id.toInt())
- prioritizedStatuses.remove(id.toInt())
-
- listeners.forEach {
- it.onStatusRemoved(id)
- }
- }
-
fun hasStatusType(dataType: Byte): Boolean = statuses.any {
it.value.type == dataType
}
diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt
index e2935f4dfa..e261d4e48c 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/processor/HumanPoseManager.kt
@@ -20,11 +20,6 @@ import io.github.axisangles.ktmath.Quaternion.Companion.IDENTITY
import io.github.axisangles.ktmath.Vector3
import io.github.axisangles.ktmath.Vector3.Companion.POS_Y
import org.apache.commons.math3.util.Precision
-import solarxr_protocol.datatypes.DeviceIdT
-import solarxr_protocol.datatypes.TrackerIdT
-import solarxr_protocol.rpc.StatusData
-import solarxr_protocol.rpc.StatusDataUnion
-import solarxr_protocol.rpc.StatusUnassignedHMDT
import java.util.function.Consumer
import kotlin.math.*
@@ -525,7 +520,7 @@ class HumanPoseManager(val server: VRServer?) {
for (tracker in server!!.allTrackers) {
if ((
tracker.isImu() &&
- tracker.needsReset
+ tracker.allowReset
) && tracker.resetsHandler.lastResetQuaternion != null
) {
if (trackersDriftText.isNotEmpty()) {
@@ -571,8 +566,15 @@ class HumanPoseManager(val server: VRServer?) {
}
@JvmOverloads
- fun resetTrackersMounting(resetSourceName: String?, bodyParts: List = TrackerUtils.allBodyPartsButFingers) {
- skeleton.resetTrackersMounting(resetSourceName, bodyParts)
+ fun resetTrackersMounting(resetSourceName: String?, bodyParts: List? = null) {
+ val finalBodyParts = bodyParts
+ ?: if (server?.configManager?.vrConfig?.resetsConfig?.resetMountingFeet == true) {
+ TrackerUtils.allBodyPartsButFingers
+ } else {
+ TrackerUtils.allBodyPartsButFingersAndFeets
+ }
+
+ skeleton.resetTrackersMounting(resetSourceName, finalBodyParts)
}
fun clearTrackersMounting(resetSourceName: String?) {
@@ -669,51 +671,12 @@ class HumanPoseManager(val server: VRServer?) {
return
}
server.allTrackers
- .filter { !it.isInternal && it.trackerPosition != null }
+ .filter { it.trackerPosition != null }
.forEach {
- it.checkReportRequireReset()
- }
- }
-
- private var lastMissingHmdStatus = 0u
- fun checkReportMissingHmd() {
- // Check if this is main skeleton, there is no head tracker currently,
- // and there is an available HMD one
- if (server == null) return
- val tracker = VRServer.instance.allTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData }
- if (skeleton.headTracker == null &&
- lastMissingHmdStatus == 0u &&
- tracker != null
- ) {
- reportMissingHmd(tracker)
- } else if (lastMissingHmdStatus != 0u &&
- (skeleton.headTracker != null || tracker == null)
- ) {
- server.statusSystem.removeStatus(lastMissingHmdStatus)
- lastMissingHmdStatus = 0u
- }
- }
-
- private fun reportMissingHmd(tracker: Tracker) {
- require(lastMissingHmdStatus == 0u) {
- "${::lastMissingHmdStatus.name} must be 0u, but was $lastMissingHmdStatus"
- }
- require(server != null) {
- "${::server.name} must not be null"
- }
-
- val status = StatusDataUnion().apply {
- type = StatusData.StatusUnassignedHMD
- value = StatusUnassignedHMDT().apply {
- trackerId = TrackerIdT().apply {
- if (tracker.device != null) {
- deviceId = DeviceIdT().apply { id = tracker.device.id }
- }
- trackerNum = tracker.trackerNum
+ if (it.allowReset && !it.needReset) {
+ it.needReset = true
}
}
- }
- lastMissingHmdStatus = server.statusSystem.addStatus(status, true)
}
// #endregion
diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt
index 62adbaec45..7cdec98b7a 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt
@@ -91,6 +91,9 @@ class SkeletonConfigManager(
// Re-calculate user height
userHeightFromOffsets = calculateUserHeight()
userNeckHeightFromOffsets = userHeightFromOffsets - getOffset(SkeletonConfigOffsets.NECK)
+
+ // Update vrc config checker if user height change
+ humanPoseManager?.server?.vrcConfigManager?.forceUpdate()
}
fun setOffset(config: SkeletonConfigOffsets, newValue: Float?) {
diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt
index 1678d6db65..5dbb57f4d2 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt
@@ -1,6 +1,7 @@
package dev.slimevr.tracking.processor.skeleton
import dev.slimevr.VRServer
+import dev.slimevr.config.MountingMethods
import dev.slimevr.config.StayAlignedConfig
import dev.slimevr.tracking.processor.Bone
import dev.slimevr.tracking.processor.BoneType
@@ -124,7 +125,6 @@ class HumanSkeleton(
var headTracker: Tracker? by Delegates.observable(null) { _, old, new ->
if (old == new) return@observable
- humanPoseManager.checkReportMissingHmd()
humanPoseManager.checkTrackersRequiringReset()
}
var neckTracker: Tracker? = null
@@ -1531,7 +1531,7 @@ class HumanSkeleton(
// Resets all axes of the trackers with the HMD as reference.
for (tracker in trackersToReset) {
// Only reset if tracker needsReset
- if (tracker != null && (tracker.needsReset || tracker.isHmd) && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) {
+ if (tracker != null && (tracker.allowReset || tracker.isHmd) && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) {
tracker.resetsHandler.resetFull(referenceRotation)
}
}
@@ -1553,16 +1553,16 @@ class HumanSkeleton(
var referenceRotation = IDENTITY
headTracker?.let {
if (bodyParts.isEmpty() || bodyParts.contains(BodyPart.HEAD)) {
- // Only reset if head needsReset and isn't computed
- if (it.needsReset && !it.isComputed) {
+ // Only reset if head allowReset and isn't computed
+ if (it.allowReset && !it.isComputed) {
it.resetsHandler.resetYaw(referenceRotation)
}
}
referenceRotation = it.getRotation()
}
for (tracker in trackersToReset) {
- // Only reset if tracker needsReset
- if (tracker != null && tracker.needsReset && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) {
+ // Only reset if tracker allowReset
+ if (tracker != null && tracker.allowReset && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) {
tracker.resetsHandler.resetYaw(referenceRotation)
}
}
@@ -1584,7 +1584,7 @@ class HumanSkeleton(
// If there's a server present (required for status) and any tracker reports a
// non-zero reset status (indicates reset required), then block mounting reset,
// as it requires a full reset first
- if (humanPoseManager.server != null && trackersToReset.any { it != null && it.lastResetStatus != 0u }) {
+ if (humanPoseManager.server != null && trackersToReset.any { it != null && it.needReset }) {
LogManager.info("[HumanSkeleton] Reset: mounting ($resetSourceName) failed, reset required")
return
}
@@ -1593,41 +1593,66 @@ class HumanSkeleton(
var referenceRotation = IDENTITY
headTracker?.let {
if (bodyParts.isEmpty() || bodyParts.contains(BodyPart.HEAD)) {
- // Only reset if head needsMounting or is computed but not HMD
- if (it.needsMounting || (it.isComputed && !it.isHmd)) {
+ // Only reset if head allowMounting or is computed but not HMD
+ if (it.allowMounting || (it.isComputed && !it.isHmd)) {
it.resetsHandler.resetMounting(referenceRotation)
}
}
referenceRotation = it.getRotation()
}
- // If onlyFeet is true, feet will be forced to be mounting reset in their reset handlers.
- val onlyFeet = bodyParts.isNotEmpty() && bodyParts.all { it == BodyPart.LEFT_FOOT || it == BodyPart.RIGHT_FOOT }
for (tracker in trackersToReset) {
// Only reset if tracker needsMounting
- if (tracker != null && tracker.needsMounting && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) {
- tracker.resetsHandler.resetMounting(referenceRotation, onlyFeet)
+ if (tracker != null && tracker.allowMounting && (bodyParts.isEmpty() || bodyParts.contains(tracker.trackerPosition?.bodyPart))) {
+ tracker.resetsHandler.resetMounting(referenceRotation)
}
}
legTweaks.resetBuffer()
localizer.reset()
+
+ if (humanPoseManager.server != null) {
+ humanPoseManager.server.configManager.vrConfig.resetsConfig.preferedMountingMethod =
+ MountingMethods.AUTOMATIC
+ if (!humanPoseManager.server.trackingChecklistManager.resetMountingCompleted) {
+ humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = bodyParts.any { it ->
+ val defaultParts = if (humanPoseManager.server.configManager.vrConfig.resetsConfig.resetMountingFeet) {
+ TrackerUtils.allBodyPartsButFingers
+ } else {
+ TrackerUtils.allBodyPartsButFingersAndFeets
+ }
+
+ return@any defaultParts.contains(it)
+ }
+ }
+ if (!humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted) {
+ humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = bodyParts.any { TrackerUtils.feetsBodyParts.contains(it) }
+ }
+ humanPoseManager.server.configManager.saveConfig()
+ }
+
LogManager.info("[HumanSkeleton] Reset: mounting ($resetSourceName)")
}
@VRServerThread
fun clearTrackersMounting(resetSourceName: String?) {
headTracker?.let {
- if (it.needsMounting) it.resetsHandler.clearMounting()
+ if (it.allowMounting) it.resetsHandler.clearMounting()
}
for (tracker in trackersToReset) {
if (tracker != null &&
- tracker.needsMounting
+ tracker.allowMounting
) {
tracker.resetsHandler.clearMounting()
}
}
legTweaks.resetBuffer()
LogManager.info("[HumanSkeleton] Clear: mounting ($resetSourceName)")
+
+ if (humanPoseManager.server != null) {
+ humanPoseManager.server.trackingChecklistManager.resetMountingCompleted = false
+ humanPoseManager.server.trackingChecklistManager.feetResetMountingCompleted = false
+ humanPoseManager.server.configManager.saveConfig()
+ }
}
fun updateTapDetectionConfig() {
diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java
index 404f38c6a5..2d345fea16 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java
+++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/TapDetectionManager.java
@@ -7,7 +7,6 @@
import dev.slimevr.setup.TapSetupHandler;
import dev.slimevr.tracking.processor.HumanPoseManager;
import dev.slimevr.tracking.trackers.Tracker;
-import dev.slimevr.tracking.trackers.TrackerUtils;
import solarxr_protocol.rpc.ResetType;
import solarxr_protocol.rpc.StatusData;
@@ -221,11 +220,7 @@ private void checkMountingReset() {
// However, feet being reset or not will end up being decided on a
// per-tracker basis
// due to the setting being in ResetsConfig.kt
- skeleton
- .resetTrackersMounting(
- resetSourceName,
- TrackerUtils.INSTANCE.getAllBodyPartsButFingers()
- );
+ humanPoseManager.resetTrackersMounting(resetSourceName);
mountingResetDetector.resetDetector();
mountingResetAllowPlaySound = true;
diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt
index 8f639c4407..3d4a8616b6 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt
@@ -11,12 +11,6 @@ import dev.slimevr.util.InterpolationHandler
import io.eiren.util.BufferedTimer
import io.github.axisangles.ktmath.Quaternion
import io.github.axisangles.ktmath.Vector3
-import solarxr_protocol.datatypes.DeviceIdT
-import solarxr_protocol.datatypes.TrackerIdT
-import solarxr_protocol.rpc.StatusData
-import solarxr_protocol.rpc.StatusDataUnion
-import solarxr_protocol.rpc.StatusTrackerErrorT
-import solarxr_protocol.rpc.StatusTrackerResetT
import kotlin.properties.Delegates
const val TIMEOUT_MS = 2_000L
@@ -71,9 +65,23 @@ class Tracker @JvmOverloads constructor(
* [trackRotDirection] is set to false.
*/
val allowFiltering: Boolean = false,
- val needsReset: Boolean = false,
- val needsMounting: Boolean = false,
+
+ /**
+ * If true, the tracker can be reset
+ */
+ val allowReset: Boolean = false,
+ /**
+ * If true, the tracker can do mounting calibration
+ */
+ val allowMounting: Boolean = false,
+
val isHmd: Boolean = false,
+
+ /**
+ * If true, the tracker need the user to perform a reset
+ */
+ var needReset: Boolean = false,
+
/**
* Whether to track the direction of the tracker's rotation
* (positive vs negative rotation). This needs to be disabled for AutoBone and
@@ -108,33 +116,27 @@ class Tracker @JvmOverloads constructor(
var magStatus: MagnetometerStatus = magStatus
private set
+ /**
+ * Watch the rest calibration status
+ */
+ var hasCompletedRestCalibration: Boolean? = null
+
/**
* If the tracker has gotten disconnected after it was initialized first time
*/
- var statusResetRecently = false
- private var alreadyInitialized = false
var status: TrackerStatus by Delegates.observable(TrackerStatus.DISCONNECTED) { _, old, new ->
if (old == new) return@observable
- if (!new.reset) {
- if (alreadyInitialized) {
- statusResetRecently = true
- }
- alreadyInitialized = true
+ if (allowReset && !old.reset && new.reset && !needReset) {
+ needReset = true
}
+
if (!isInternal && VRServer.instanceInitialized) {
// If the status of a non-internal tracker has changed, inform
// the VRServer to recreate the skeleton, as it may need to
// assign or un-assign the tracker to a body part
VRServer.instance.updateSkeletonModel()
VRServer.instance.refreshTrackersDriftCompensationEnabled()
-
- if (isHmd) {
- VRServer.instance.humanPoseManager.checkReportMissingHmd()
- }
- checkReportErrorStatus()
- checkReportRequireReset()
-
VRServer.instance.trackerStatusChanged(this, old, new)
}
}
@@ -142,16 +144,18 @@ class Tracker @JvmOverloads constructor(
var trackerPosition: TrackerPosition? by Delegates.observable(trackerPosition) { _, old, new ->
if (old == new) return@observable
+ if (allowReset && !needReset) {
+ needReset = true
+ }
+
if (!isInternal) {
// Set default mounting orientation for that body part
new?.let { resetsHandler.mountingOrientation = it.defaultMounting() }
-
- checkReportRequireReset()
}
}
// Computed value to simplify availability checks
- val hasAdjustedRotation = hasRotation && (allowFiltering || needsReset)
+ val hasAdjustedRotation = hasRotation && (allowFiltering || allowReset)
/**
* It's like the ID, but it should be local to the device if it has one
@@ -163,11 +167,11 @@ class Tracker @JvmOverloads constructor(
init {
// IMPORTANT: Look here for the required states of inputs
- require(!needsReset || (hasRotation && needsReset)) {
- "If ${::needsReset.name} is true, then ${::hasRotation.name} must also be true"
+ require(!allowReset || (hasRotation && allowReset)) {
+ "If ${::allowReset.name} is true, then ${::hasRotation.name} must also be true"
}
- require(!needsMounting || (needsReset && needsMounting)) {
- "If ${::needsMounting.name} is true, then ${::needsReset.name} must also be true"
+ require(!allowMounting || (allowReset && allowMounting)) {
+ "If ${::allowMounting.name} is true, then ${::allowReset.name} must also be true"
}
require(!isHmd || (hasPosition && isHmd)) {
"If ${::isHmd.name} is true, then ${::hasPosition.name} must also be true"
@@ -177,73 +181,6 @@ class Tracker @JvmOverloads constructor(
// }
}
- fun checkReportRequireReset() {
- if (needsReset && trackerPosition != null && lastResetStatus == 0u &&
- !status.reset && (isImu() || !statusResetRecently && trackerDataType != TrackerDataType.FLEX_ANGLE)
- ) {
- reportRequireReset()
- } else if (lastResetStatus != 0u && (trackerPosition == null || status.reset)) {
- VRServer.instance.statusSystem.removeStatus(lastResetStatus)
- lastResetStatus = 0u
- }
- }
-
- /**
- * If 0 then it's null
- */
- var lastResetStatus = 0u
- private fun reportRequireReset() {
- require(lastResetStatus == 0u) {
- "lastResetStatus must be 0u, but was $lastResetStatus"
- }
-
- val tempTrackerNum = this.trackerNum
- val statusMsg = StatusTrackerResetT().apply {
- trackerId = TrackerIdT().apply {
- if (device != null) {
- deviceId = DeviceIdT().apply { id = device.id }
- }
- trackerNum = tempTrackerNum
- }
- }
- val status = StatusDataUnion().apply {
- type = StatusData.StatusTrackerReset
- value = statusMsg
- }
- lastResetStatus = VRServer.instance.statusSystem.addStatus(status, true)
- }
-
- private fun checkReportErrorStatus() {
- if (status == TrackerStatus.ERROR && lastErrorStatus == 0u) {
- reportErrorStatus()
- } else if (lastErrorStatus != 0u && status != TrackerStatus.ERROR) {
- VRServer.instance.statusSystem.removeStatus(lastErrorStatus)
- lastErrorStatus = 0u
- }
- }
-
- var lastErrorStatus = 0u
- private fun reportErrorStatus() {
- require(lastErrorStatus == 0u) {
- "lastResetStatus must be 0u, but was $lastErrorStatus"
- }
-
- val tempTrackerNum = this.trackerNum
- val statusMsg = StatusTrackerErrorT().apply {
- trackerId = TrackerIdT().apply {
- if (device != null) {
- deviceId = DeviceIdT().apply { id = device.id }
- }
- trackerNum = tempTrackerNum
- }
- }
- val status = StatusDataUnion().apply {
- type = StatusData.StatusTrackerError
- value = statusMsg
- }
- lastErrorStatus = VRServer.instance.statusSystem.addStatus(status, true)
- }
-
/**
* Reads/loads from the given config
*/
@@ -254,7 +191,7 @@ class Tracker @JvmOverloads constructor(
config.designation?.let { designation ->
getByDesignation(designation)?.let { trackerPosition = it }
} ?: run { trackerPosition = null }
- if (needsMounting) {
+ if (allowMounting) {
// Load manual mounting
config.mountingOrientation?.let { resetsHandler.mountingOrientation = it.toValue() }
}
@@ -268,11 +205,6 @@ class Tracker @JvmOverloads constructor(
resetsHandler.allowDriftCompensation = it
}
}
- if (!isInternal &&
- !(!isImu() && (trackerPosition == TrackerPosition.LEFT_HAND || trackerPosition == TrackerPosition.RIGHT_HAND))
- ) {
- checkReportRequireReset()
- }
}
/**
@@ -281,7 +213,7 @@ class Tracker @JvmOverloads constructor(
fun writeConfig(config: TrackerConfig) {
trackerPosition?.let { config.designation = it.designation } ?: run { config.designation = null }
customName?.let { config.customName = it }
- if (needsMounting) {
+ if (allowMounting) {
// Save manual mounting
config.mountingOrientation = resetsHandler.mountingOrientation.toObject()
}
@@ -360,7 +292,7 @@ class Tracker @JvmOverloads constructor(
}
// Reset if needed and is not computed and internal
- return if (needsReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) {
+ return if (allowReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) {
// Adjust to reset, mounting and drift compensation
resetsHandler.getReferenceAdjustedDriftRotationFrom(rot)
} else {
@@ -380,7 +312,7 @@ class Tracker @JvmOverloads constructor(
rot = Quaternion.rotationAroundYAxis(stayAligned.yawCorrection.toRad()) * rot
// Reset if needed and is not computed and internal
- return if (needsReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) {
+ return if (needReset && !(isComputed && isInternal) && trackerDataType == TrackerDataType.ROTATION) {
// Adjust to reset, mounting and drift compensation
resetsHandler.getReferenceAdjustedDriftRotationFrom(rot)
} else {
@@ -402,7 +334,7 @@ class Tracker @JvmOverloads constructor(
}
// Reset if needed or is a computed tracker besides head
- return if (needsReset && !(isComputed && trackerPosition != TrackerPosition.HEAD) && trackerDataType == TrackerDataType.ROTATION) {
+ return if (allowReset && !(isComputed && trackerPosition != TrackerPosition.HEAD) && trackerDataType == TrackerDataType.ROTATION) {
// Adjust to reset and mounting
resetsHandler.getIdentityAdjustedDriftRotationFrom(rot)
} else {
@@ -431,7 +363,7 @@ class Tracker @JvmOverloads constructor(
/**
* Gets the world-adjusted acceleration
*/
- fun getAcceleration(): Vector3 = if (needsReset) {
+ fun getAcceleration(): Vector3 = if (allowReset) {
resetsHandler.getReferenceAdjustedAccel(_rotation, _acceleration)
} else {
_acceleration
@@ -477,7 +409,7 @@ class Tracker @JvmOverloads constructor(
/**
* Gets the magnetic field vector, in mGauss.
*/
- fun getMagVector() = if (needsReset) {
+ fun getMagVector() = if (allowReset) {
resetsHandler.getReferenceAdjustedAccel(_rotation, _magVector)
} else {
_magVector
diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt
index 0cb5d94f7e..751d57eaeb 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt
@@ -15,10 +15,7 @@ import kotlin.math.*
private const val DRIFT_COOLDOWN_MS = 50000L
-/**
- * Class taking care of full reset, yaw reset, mounting reset,
- * and drift compensation logic.
- */
+/** Class taking care of full reset, yaw reset, mounting reset, and drift compensation logic. */
class TrackerResetsHandler(val tracker: Tracker) {
private val HalfHorizontal = EulerAngles(
@@ -38,7 +35,6 @@ class TrackerResetsHandler(val tracker: Tracker) {
private var compensateDrift = false
private var driftPrediction = false
private var driftCompensationEnabled = false
- private var resetMountingFeet = false
private var armsResetMode = ArmsResetModes.BACK
private var yawResetSmoothTime = 0.0f
var saveMountingReset = false
@@ -164,7 +160,6 @@ class TrackerResetsHandler(val tracker: Tracker) {
* Reads/loads reset settings from the given config
*/
fun readResetConfig(config: ResetsConfig) {
- resetMountingFeet = config.resetMountingFeet
armsResetMode = config.mode
yawResetSmoothTime = config.yawResetSmoothTime
saveMountingReset = config.saveMountingReset
@@ -274,7 +269,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
val mountingAdjustedRotation = tracker.getRawRotation() * mountingOrientation
// Gyrofix
- if (tracker.needsMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) {
+ if (tracker.allowMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) {
gyroFix = if (tracker.isComputed) {
fixGyroscope(tracker.getRawRotation())
} else {
@@ -306,7 +301,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
}
// Rotate attachmentFix by 180 degrees as a workaround for t-pose (down)
- if (tposeDownFix != Quaternion.IDENTITY && tracker.needsMounting) {
+ if (tposeDownFix != Quaternion.IDENTITY && tracker.allowMounting) {
attachmentFix *= HalfHorizontal
}
@@ -328,9 +323,8 @@ class TrackerResetsHandler(val tracker: Tracker) {
}
private fun postProcessResetFull(reference: Quaternion) {
- if (this.tracker.lastResetStatus != 0u) {
- VRServer.instance.statusSystem.removeStatus(this.tracker.lastResetStatus)
- this.tracker.lastResetStatus = 0u
+ if (this.tracker.needReset) {
+ this.tracker.needReset = false
}
tracker.resetFilteringQuats(reference)
@@ -375,13 +369,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
)
}
- // Remove the status if yaw reset was performed after the tracker
- // was disconnected and connected.
- if (this.tracker.lastResetStatus != 0u && this.tracker.statusResetRecently) {
- VRServer.instance.statusSystem.removeStatus(this.tracker.lastResetStatus)
- this.tracker.statusResetRecently = false
- this.tracker.lastResetStatus = 0u
- }
+ this.tracker.needReset = false
// Reset Stay Aligned (before resetting filtering, which depends on the
// tracker's rotation)
@@ -393,17 +381,14 @@ class TrackerResetsHandler(val tracker: Tracker) {
/**
* Perform the math to align the tracker to go forward
* and stores it in mountRotFix, and adjusts yawFix
- * If forceFeet is true, always reset feet regardless of resetMountingFeet's value.
*/
- fun resetMounting(reference: Quaternion, forceFeet: Boolean = false) {
+ fun resetMounting(reference: Quaternion) {
if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE) {
tracker.trackerFlexHandler.resetMax()
tracker.resetFilteringQuats(reference)
return
} else if (tracker.trackerDataType == TrackerDataType.FLEX_ANGLE) {
return
- } else if (!resetMountingFeet && tracker.trackerPosition.isFoot() && !forceFeet) {
- return
}
constraintFix = Quaternion.IDENTITY
diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt
index 8aa27052df..f3e73b3eee 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerUtils.kt
@@ -102,4 +102,18 @@ object TrackerUtils {
BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER,
BodyPart.LEFT_FOOT, BodyPart.RIGHT_FOOT,
)
+
+ val allBodyPartsButFingersAndFeets = listOf(
+ BodyPart.HEAD, BodyPart.NECK, BodyPart.UPPER_CHEST,
+ BodyPart.CHEST, BodyPart.WAIST, BodyPart.HIP,
+ BodyPart.LEFT_UPPER_LEG, BodyPart.RIGHT_UPPER_LEG, BodyPart.LEFT_LOWER_LEG,
+ BodyPart.RIGHT_LOWER_LEG, BodyPart.LEFT_LOWER_ARM, BodyPart.RIGHT_LOWER_ARM,
+ BodyPart.LEFT_UPPER_ARM, BodyPart.RIGHT_UPPER_ARM, BodyPart.LEFT_HAND,
+ BodyPart.RIGHT_HAND, BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER,
+ )
+
+ val feetsBodyParts = listOf(
+ BodyPart.LEFT_FOOT,
+ BodyPart.RIGHT_FOOT,
+ )
}
diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDCommon.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDCommon.kt
index 6e18e05ec6..79879fa41a 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDCommon.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/hid/HIDCommon.kt
@@ -90,8 +90,8 @@ class HIDCommon {
userEditable = true,
imuType = sensorType,
allowFiltering = true,
- needsReset = true,
- needsMounting = true,
+ allowReset = true,
+ allowMounting = true,
usesTimeout = false,
magStatus = magStatus,
)
diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt
index ac9f15d8d0..79a91f97ef 100644
--- a/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt
+++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/udp/TrackersUDPServer.kt
@@ -174,7 +174,16 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
// Set up new sensor for older firmware.
// Firmware after 7 should send sensor status packet and sensor
// will be created when it's received
- setUpSensor(connection, 0, handshake.imuType, 1, MagnetometerStatus.NOT_SUPPORTED, null, TrackerDataType.ROTATION)
+ setUpSensor(
+ connection,
+ 0,
+ handshake.imuType,
+ 1,
+ MagnetometerStatus.NOT_SUPPORTED,
+ null,
+ TrackerDataType.ROTATION,
+ null,
+ )
}
connection
}
@@ -186,7 +195,16 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
}
private val mainScope = CoroutineScope(SupervisorJob())
- private fun setUpSensor(connection: UDPDevice, trackerId: Int, sensorType: IMUType, sensorStatus: Int, magStatus: MagnetometerStatus, trackerPosition: TrackerPosition?, trackerDataType: TrackerDataType) {
+ private fun setUpSensor(
+ connection: UDPDevice,
+ trackerId: Int,
+ sensorType: IMUType,
+ sensorStatus: Int,
+ magStatus: MagnetometerStatus,
+ trackerPosition: TrackerPosition?,
+ trackerDataType: TrackerDataType,
+ hasCompletedRestCalibration: Boolean?,
+ ) {
LogManager.info("[TrackerServer] Sensor $trackerId for ${connection.name} status: $sensorStatus")
var imuTracker = connection.getTracker(trackerId)
if (imuTracker == null) {
@@ -210,8 +228,8 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
userEditable = true,
imuType = if (trackerDataType == TrackerDataType.ROTATION) sensorType else null,
allowFiltering = true,
- needsReset = true,
- needsMounting = true,
+ allowReset = true,
+ allowMounting = true,
usesTimeout = true,
magStatus = magStatus,
trackerDataType = trackerDataType,
@@ -223,6 +241,8 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
val status = UDPPacket15SensorInfo.getStatus(sensorStatus)
if (status != null) imuTracker.status = status
+ imuTracker.hasCompletedRestCalibration = hasCompletedRestCalibration
+
if (magStatus == MagnetometerStatus.NOT_SUPPORTED) return
if (magStatus == MagnetometerStatus.ENABLED &&
(!VRServer.instance.configManager.vrConfig.server.useMagnetometerOnAllTrackers || imuTracker.config.shouldHaveMagEnabled == false)
@@ -483,6 +503,7 @@ class TrackersUDPServer(private val port: Int, name: String, private val tracker
magStatus,
packet.trackerPosition,
packet.trackerDataType,
+ packet.hasCompletedRestCalibration,
)
// Send ack
bb.limit(bb.capacity())
diff --git a/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt
new file mode 100644
index 0000000000..a1181f42b7
--- /dev/null
+++ b/server/core/src/main/java/dev/slimevr/trackingchecklist/TrackingChecklistManager.kt
@@ -0,0 +1,344 @@
+package dev.slimevr.trackingchecklist
+
+import dev.slimevr.VRServer
+import dev.slimevr.bridge.ISteamVRBridge
+import dev.slimevr.config.MountingMethods
+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.tracking.trackers.Tracker
+import dev.slimevr.tracking.trackers.TrackerStatus
+import dev.slimevr.tracking.trackers.TrackerUtils
+import dev.slimevr.tracking.trackers.udp.TrackerDataType
+import solarxr_protocol.datatypes.DeviceIdT
+import solarxr_protocol.datatypes.TrackerIdT
+import solarxr_protocol.rpc.*
+import java.util.*
+import java.util.concurrent.CopyOnWriteArrayList
+import kotlin.concurrent.timerTask
+
+interface TrackingChecklistListener {
+ fun onStepsUpdate()
+}
+
+class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListener {
+
+ private val listeners: MutableList = CopyOnWriteArrayList()
+ val steps: MutableList = mutableListOf()
+
+ private val updateTrackingChecklistTimer = Timer("TrackingChecklistTimer")
+
+ // Simple flag set to true if reset mounting was performed at least once.
+ // This value is only runtime and never saved
+ var resetMountingCompleted = false
+ var feetResetMountingCompleted = false
+
+ init {
+ vrServer.vrcConfigManager.addListener(this)
+
+ createSteps()
+ updateTrackingChecklistTimer.scheduleAtFixedRate(
+ timerTask {
+ updateChecklist()
+ },
+ 0,
+ 1000,
+ )
+ }
+
+ fun addListener(channel: TrackingChecklistListener) {
+ listeners.add(channel)
+ }
+
+ fun removeListener(channel: TrackingChecklistListener) {
+ listeners.removeIf { channel == it }
+ }
+
+ fun buildTrackersIds(trackers: List): Array = trackers.map { tracker ->
+ TrackerIdT().apply {
+ if (tracker.device != null) {
+ deviceId = DeviceIdT().apply { id = tracker.device.id }
+ }
+ trackerNum = tracker.trackerNum
+ }
+ }.toTypedArray()
+
+ private fun createSteps() {
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC
+ enabled = vrServer.networkProfileChecker.isSupported
+ optional = false
+ ignorable = true
+ visibility = TrackingChecklistStepVisibility.WHEN_INVALID
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.STEAMVR_DISCONNECTED
+ enabled = true
+ optional = false
+ ignorable = true
+ visibility = TrackingChecklistStepVisibility.WHEN_INVALID
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.TRACKER_ERROR
+ valid = true // Default to valid
+ enabled = true
+ optional = false
+ ignorable = false
+ visibility = TrackingChecklistStepVisibility.WHEN_INVALID
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.TRACKERS_REST_CALIBRATION
+ enabled = true
+ optional = false
+ ignorable = true
+ visibility = TrackingChecklistStepVisibility.ALWAYS
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.FULL_RESET
+ enabled = true
+ optional = false
+ ignorable = false
+ visibility = TrackingChecklistStepVisibility.ALWAYS
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.MOUNTING_CALIBRATION
+ valid = false
+ enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC
+ optional = false
+ ignorable = true
+ visibility = TrackingChecklistStepVisibility.ALWAYS
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION
+ valid = false
+ enabled = false
+ optional = false
+ ignorable = true
+ visibility = TrackingChecklistStepVisibility.ALWAYS
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.UNASSIGNED_HMD
+ enabled = true
+ optional = false
+ ignorable = false
+ visibility = TrackingChecklistStepVisibility.WHEN_INVALID
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED
+ enabled = true
+ optional = true
+ ignorable = true
+ visibility = TrackingChecklistStepVisibility.WHEN_INVALID
+ },
+ )
+
+ steps.add(
+ TrackingChecklistStepT().apply {
+ id = TrackingChecklistStepId.VRCHAT_SETTINGS
+ enabled = vrServer.vrcConfigManager.isSupported
+ optional = true
+ ignorable = true
+ visibility = TrackingChecklistStepVisibility.WHEN_INVALID
+ },
+ )
+ }
+
+ fun updateChecklist() {
+ val assignedTrackers =
+ vrServer.allTrackers.filter { it.trackerPosition != null && it.status != TrackerStatus.DISCONNECTED }
+ val imuTrackers =
+ assignedTrackers.filter { it.isImu() && it.trackerDataType != TrackerDataType.FLEX_ANGLE }
+
+ val trackersWithError =
+ imuTrackers.filter { it.status == TrackerStatus.ERROR }
+ updateValidity(
+ TrackingChecklistStepId.TRACKER_ERROR,
+ trackersWithError.isEmpty(),
+ ) {
+ if (trackersWithError.isNotEmpty()) {
+ it.extraData = TrackingChecklistExtraDataUnion().apply {
+ type = TrackingChecklistExtraData.TrackingChecklistTrackerError
+ value = TrackingChecklistTrackerErrorT().apply {
+ trackersId = buildTrackersIds(trackersWithError)
+ }
+ }
+ } else {
+ it.extraData = null
+ }
+ }
+
+ val trackerRequireReset = imuTrackers.filter {
+ it.status !== TrackerStatus.ERROR && !it.isInternal && it.allowReset && it.needReset
+ }
+ updateValidity(TrackingChecklistStepId.FULL_RESET, trackerRequireReset.isEmpty()) {
+ if (trackerRequireReset.isNotEmpty()) {
+ it.extraData = TrackingChecklistExtraDataUnion().apply {
+ type = TrackingChecklistExtraData.TrackingChecklistTrackerReset
+ value = TrackingChecklistTrackerResetT().apply {
+ trackersId = buildTrackersIds(trackerRequireReset)
+ }
+ }
+ } else {
+ it.extraData = null
+ }
+ }
+
+ val hmd =
+ assignedTrackers.firstOrNull { it.isHmd && !it.isInternal && it.status.sendData }
+ val assignedHmd = hmd == null || vrServer.humanPoseManager.skeleton.headTracker != null
+ updateValidity(TrackingChecklistStepId.UNASSIGNED_HMD, assignedHmd) {
+ if (!assignedHmd) {
+ it.extraData = TrackingChecklistExtraDataUnion().apply {
+ type = TrackingChecklistExtraData.TrackingChecklistUnassignedHMD
+ value = TrackingChecklistUnassignedHMDT().apply {
+ trackerId = TrackerIdT().apply {
+ if (hmd.device != null) {
+ deviceId = DeviceIdT().apply { id = hmd.device.id }
+ }
+ trackerNum = hmd.trackerNum
+ }
+ }
+ }
+ } else {
+ it.extraData = null
+ }
+ }
+
+ val trackersNeedCalibration = imuTrackers.filter {
+ it.hasCompletedRestCalibration == false
+ }
+ updateValidity(
+ TrackingChecklistStepId.TRACKERS_REST_CALIBRATION,
+ trackersNeedCalibration.isEmpty(),
+ ) {
+ // Don't show the step if none of the trackers connected support IMU calibration
+ it.enabled = imuTrackers.any { t ->
+ t.hasCompletedRestCalibration != null
+ }
+ if (trackersNeedCalibration.isNotEmpty()) {
+ it.extraData = TrackingChecklistExtraDataUnion().apply {
+ type = TrackingChecklistExtraData.TrackingChecklistNeedCalibration
+ value = TrackingChecklistNeedCalibrationT().apply {
+ trackersId = buildTrackersIds(trackersNeedCalibration)
+ }
+ }
+ } else {
+ it.extraData = null
+ }
+ }
+
+ val steamVRBridge = vrServer.getVRBridge(ISteamVRBridge::class.java)
+ if (steamVRBridge != null) {
+ val steamvrConnected = steamVRBridge.isConnected()
+ updateValidity(
+ TrackingChecklistStepId.STEAMVR_DISCONNECTED,
+ steamvrConnected,
+ ) {
+ if (!steamvrConnected) {
+ it.extraData = TrackingChecklistExtraDataUnion().apply {
+ type = TrackingChecklistExtraData.TrackingChecklistSteamVRDisconnected
+ value = TrackingChecklistSteamVRDisconnectedT().apply {
+ bridgeSettingsName = steamVRBridge.getBridgeConfigKey()
+ }
+ }
+ } else {
+ it.extraData = null
+ }
+ }
+ }
+
+ if (vrServer.networkProfileChecker.isSupported) {
+ updateValidity(TrackingChecklistStepId.NETWORK_PROFILE_PUBLIC, vrServer.networkProfileChecker.publicNetworks.isEmpty()) {
+ if (vrServer.networkProfileChecker.publicNetworks.isNotEmpty()) {
+ it.extraData = TrackingChecklistExtraDataUnion().apply {
+ type = TrackingChecklistExtraData.TrackingChecklistPublicNetworks
+ value = TrackingChecklistPublicNetworksT().apply {
+ adapters = vrServer.networkProfileChecker.publicNetworks.map { it.name }.toTypedArray()
+ }
+ }
+ } else {
+ it.extraData = null
+ }
+ }
+ }
+
+ updateValidity(TrackingChecklistStepId.MOUNTING_CALIBRATION, resetMountingCompleted) {
+ it.enabled = vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC
+ }
+
+ updateValidity(TrackingChecklistStepId.FEET_MOUNTING_CALIBRATION, feetResetMountingCompleted) {
+ it.enabled =
+ vrServer.configManager.vrConfig.resetsConfig.preferedMountingMethod == MountingMethods.AUTOMATIC &&
+ !vrServer.configManager.vrConfig.resetsConfig.resetMountingFeet &&
+ imuTrackers.any { t -> TrackerUtils.feetsBodyParts.contains(t.trackerPosition?.bodyPart) }
+ }
+
+ updateValidity(TrackingChecklistStepId.STAY_ALIGNED_CONFIGURED, vrServer.configManager.vrConfig.stayAlignedConfig.enabled)
+
+ listeners.forEach { it.onStepsUpdate() }
+ }
+
+ private fun updateValidity(id: Int, valid: Boolean, beforeUpdate: ((step: TrackingChecklistStepT) -> Unit)? = null) {
+ require(id != TrackingChecklistStepId.UNKNOWN) {
+ "id is unknown"
+ }
+ val step = steps.find { it.id == id } ?: return
+ step.valid = valid
+ if (beforeUpdate != null) {
+ beforeUpdate(step)
+ }
+ }
+
+ override fun onChange(
+ validity: VRCConfigValidity,
+ values: VRCConfigValues,
+ recommended: VRCConfigRecommendedValues,
+ muted: List,
+ ) {
+ updateValidity(
+ TrackingChecklistStepId.VRCHAT_SETTINGS,
+ VRCConfigValidity::class.java.declaredFields.asSequence().all { p ->
+ p.isAccessible = true
+ return@all p.get(validity) == true || muted.contains(p.name)
+ },
+ )
+ listeners.forEach { it.onStepsUpdate() }
+ }
+
+ fun ignoreStep(step: TrackingChecklistStepT, ignore: Boolean) {
+ if (!step.ignorable) return
+ val ignoredSteps = vrServer.configManager.vrConfig.trackingChecklist.ignoredStepsIds
+ if (ignore && !ignoredSteps.contains(step.id)) {
+ ignoredSteps.add(step.id)
+ } else if (!ignore) {
+ ignoredSteps.remove(step.id)
+ }
+ vrServer.configManager.saveConfig()
+ }
+}
diff --git a/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt b/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt
index 4676adc5fd..830a60f9db 100644
--- a/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt
+++ b/server/core/src/test/java/dev/slimevr/unit/LegTweaksTests.kt
@@ -26,8 +26,8 @@ class LegTweaksTests {
hasRotation = true,
isComputed = true,
imuType = null,
- needsReset = false,
- needsMounting = false,
+ allowReset = false,
+ allowMounting = false,
isHmd = true,
trackRotDirection = false,
)
diff --git a/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt b/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt
index c046ab9144..c7de28c0d3 100644
--- a/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt
+++ b/server/core/src/test/java/dev/slimevr/unit/MountingResetTests.kt
@@ -45,8 +45,8 @@ class MountingResetTests {
null,
hasRotation = true,
imuType = IMUType.UNKNOWN,
- needsReset = true,
- needsMounting = true,
+ allowReset = true,
+ allowMounting = true,
trackRotDirection = false,
)
@@ -130,8 +130,8 @@ class MountingResetTests {
null,
hasRotation = true,
imuType = IMUType.UNKNOWN,
- needsReset = true,
- needsMounting = true,
+ allowReset = true,
+ allowMounting = true,
trackRotDirection = false,
)
diff --git a/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt b/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt
index a28d8c85e8..5a90b1ddb9 100644
--- a/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt
+++ b/server/core/src/test/java/dev/slimevr/unit/ReferenceAdjustmentsTests.kt
@@ -93,7 +93,7 @@ class ReferenceAdjustmentsTests {
null,
hasRotation = true,
imuType = IMUType.UNKNOWN,
- needsReset = true,
+ allowReset = true,
)
tracker.setRotation(trackerQuat)
tracker.resetsHandler.resetFull(referenceQuat)
@@ -125,7 +125,7 @@ class ReferenceAdjustmentsTests {
null,
hasRotation = true,
imuType = IMUType.UNKNOWN,
- needsReset = true,
+ allowReset = true,
)
tracker.setRotation(trackerQuat)
tracker.resetsHandler.resetYaw(referenceQuat)
@@ -153,7 +153,7 @@ class ReferenceAdjustmentsTests {
null,
hasRotation = true,
imuType = IMUType.UNKNOWN,
- needsReset = true,
+ allowReset = true,
)
tracker.setRotation(trackerQuat)
tracker.resetsHandler.resetFull(referenceQuat)
diff --git a/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt b/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt
index 92ec22b602..cac3f38514 100644
--- a/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt
+++ b/server/core/src/test/java/dev/slimevr/unit/TestTrackerSet.kt
@@ -58,8 +58,8 @@ class TestTrackerSet(
hasPosition = positional || isHmd,
hasRotation = true,
isComputed = computed || isHmd,
- needsReset = resetHead || !isHmd,
- needsMounting = resetHead || !isHmd,
+ allowReset = resetHead || !isHmd,
+ allowMounting = resetHead || !isHmd,
isHmd = isHmd,
trackRotDirection = false,
)
diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/NetworkProfileChecker.kt b/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt
similarity index 77%
rename from server/desktop/src/main/java/dev/slimevr/desktop/NetworkProfileChecker.kt
rename to server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt
index 09a7b48810..3fbf796673 100644
--- a/server/desktop/src/main/java/dev/slimevr/desktop/NetworkProfileChecker.kt
+++ b/server/desktop/src/main/java/dev/slimevr/desktop/DesktopNetworkProfileChecker.kt
@@ -13,61 +13,15 @@ import com.sun.jna.platform.win32.WTypes
import com.sun.jna.platform.win32.WinNT.HRESULT
import com.sun.jna.ptr.IntByReference
import com.sun.jna.ptr.PointerByReference
+import dev.slimevr.ConnectivityFlags
+import dev.slimevr.NetworkCategory
+import dev.slimevr.NetworkInfo
+import dev.slimevr.NetworkProfileChecker
import dev.slimevr.VRServer
import io.eiren.util.OperatingSystem
-import solarxr_protocol.rpc.StatusData
-import solarxr_protocol.rpc.StatusDataUnion
-import solarxr_protocol.rpc.StatusPublicNetworkT
import java.util.*
import kotlin.concurrent.scheduleAtFixedRate
-data class NetworkInfo(
- val name: String?,
- val description: String?,
- val category: NetworkCategory?,
- val connectivity: Set?,
- val connected: Boolean?,
-)
-
-/**
- * @see NLM_NETWORK_CATEGORY enumeration (netlistmgr.h)
- */
-enum class NetworkCategory(val value: Int) {
- PUBLIC(0),
- PRIVATE(1),
- DOMAIN_AUTHENTICATED(2),
- ;
-
- companion object {
- fun fromInt(value: Int) = values().find { it.value == value }
- }
-}
-
-/**
- * @see NLM_CONNECTIVITY enumeration (netlistmgr.h)
- */
-enum class ConnectivityFlags(val value: Int) {
- DISCONNECTED(0),
- IPV4_NOTRAFFIC(0x1),
- IPV6_NOTRAFFIC(0x2),
- IPV4_SUBNET(0x10),
- IPV4_LOCALNETWORK(0x20),
- IPV4_INTERNET(0x40),
- IPV6_SUBNET(0x100),
- IPV6_LOCALNETWORK(0x200),
- IPV6_INTERNET(0x400),
- ;
-
- companion object {
- fun fromInt(value: Int): Set =
- if (value == 0) {
- setOf(DISCONNECTED)
- } else {
- values().filter { it != DISCONNECTED && (value and it.value) != 0 }.toSet()
- }
- }
-}
-
/**
* @see INetworkConnection interface (netlistmgr.h)
*/
@@ -309,38 +263,22 @@ fun enumerateNetworks(): List? {
return null
}
-class NetworkProfileChecker(private val server: VRServer) {
+class DesktopNetworkProfileChecker(private val server: VRServer) : NetworkProfileChecker() {
private val updateTickTimer = Timer("NetworkProfileCheck")
- private var lastPublicNetworkStatus: UInt = 0u
- private var numPublicNetworks = 0
+ private var publicNetworksLocal: List = listOf()
+
+ override val isSupported: Boolean
+ get() = OperatingSystem.currentPlatform == OperatingSystem.WINDOWS
+
+ override val publicNetworks: List
+ get() = publicNetworksLocal
init {
if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS) {
this.updateTickTimer.scheduleAtFixedRate(0, 3000) {
- val currentNumPublicNetworks = enumerateNetworks()?.filter { net ->
+ publicNetworksLocal = enumerateNetworks()?.filter { net ->
net.connected == true && net.category == NetworkCategory.PUBLIC
} ?: listOf()
- val currentNumPublicNetworksCount = currentNumPublicNetworks.count()
-
- if (numPublicNetworks != currentNumPublicNetworksCount) {
- numPublicNetworks = currentNumPublicNetworksCount
- if (lastPublicNetworkStatus != 0u) {
- server.statusSystem.removeStatus(lastPublicNetworkStatus)
- lastPublicNetworkStatus = 0u
- }
-
- if (lastPublicNetworkStatus == 0u && numPublicNetworks > 0) {
- lastPublicNetworkStatus = server.statusSystem.addStatus(
- StatusDataUnion().apply {
- type = StatusData.StatusPublicNetwork
- value = StatusPublicNetworkT().apply {
- adapters = currentNumPublicNetworks.map { it.name }.toTypedArray()
- }
- },
- false,
- )
- }
- }
}
}
}
diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt
index 20d1a8170c..088c670b0f 100644
--- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt
+++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt
@@ -125,12 +125,11 @@ fun main(args: Array) {
{ _ -> DesktopSerialHandler() },
{ _ -> DesktopSerialFlashingHandler() },
{ _ -> DesktopVRCConfigHandler() },
+ { server -> DesktopNetworkProfileChecker(server) },
configPath = configDir,
)
vrServer.start()
- NetworkProfileChecker(vrServer)
-
// Start service for USB HID trackers
DesktopHIDManager(
"Sensors HID service",
diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt b/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt
index 52fc936115..d5de321b92 100644
--- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt
+++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/SteamVRBridge.kt
@@ -15,20 +15,18 @@ import dev.slimevr.tracking.trackers.TrackerRole.Companion.getById
import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton
import dev.slimevr.util.ann.VRServerThread
import io.eiren.util.collections.FastList
-import solarxr_protocol.rpc.StatusData
-import solarxr_protocol.rpc.StatusDataUnion
-import solarxr_protocol.rpc.StatusSteamVRDisconnectedT
abstract class SteamVRBridge(
protected val server: VRServer,
threadName: String,
bridgeName: String,
- protected val bridgeSettingsKey: String,
+ val bridgeSettingsKey: String,
protected val shareableTrackers: List,
) : ProtobufBridge(bridgeName),
Runnable {
protected val runnerThread: Thread = Thread(this, threadName)
protected val config: BridgeConfig = server.configManager.vrConfig.getBridge(bridgeSettingsKey)
+ var connected: Boolean = false
@VRServerThread
override fun startBridge() {
@@ -187,7 +185,7 @@ abstract class SteamVRBridge(
hasRotation = true,
userEditable = true,
isComputed = true,
- needsReset = true,
+ allowReset = true,
isHmd = isHmd,
)
@@ -427,33 +425,13 @@ abstract class SteamVRBridge(
}
}
- /**
- * When 0, then it means null
- */
- protected var lastSteamVRStatus: Int = 0
-
@BridgeThread
protected fun reportDisconnected() {
- if (lastSteamVRStatus != 0) {
- return
- }
- val statusData = StatusSteamVRDisconnectedT()
- statusData.bridgeSettingsName = bridgeSettingsKey
-
- val status = StatusDataUnion()
- status.type = StatusData.StatusSteamVRDisconnected
- status.value = statusData
- lastSteamVRStatus = instance.statusSystem
- .addStatus(status, false).toInt()
+ connected = false
}
@BridgeThread
protected fun reportConnected() {
- if (lastSteamVRStatus == 0) {
- return
- }
- instance.statusSystem
- .removeStatus(lastSteamVRStatus.toUInt())
- lastSteamVRStatus = 0
+ connected = true
}
}
diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java
index 3b3580c73e..50436a8895 100644
--- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java
+++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketBridge.java
@@ -8,6 +8,7 @@
import dev.slimevr.tracking.trackers.Tracker;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.logging.LogManager;
+import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
@@ -25,6 +26,7 @@
public class UnixSocketBridge extends SteamVRBridge implements AutoCloseable {
public final String socketPath;
public final UnixDomainSocketAddress socketAddress;
+ private final String bridgeSettingsKey;
private final ByteBuffer dst = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN);
private final ByteBuffer src = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN);
@@ -41,6 +43,7 @@ public UnixSocketBridge(
List shareableTrackers
) {
super(server, "Named socket thread", bridgeName, bridgeSettingsKey, shareableTrackers);
+ this.bridgeSettingsKey = bridgeSettingsKey;
this.socketPath = socketPath;
this.socketAddress = UnixDomainSocketAddress.of(socketPath);
@@ -241,5 +244,11 @@ public void close() throws Exception {
public boolean isConnected() {
return channel != null && channel.isConnected();
}
+
+ @NotNull
+ @Override
+ public String getBridgeConfigKey() {
+ return bridgeSettingsKey;
+ }
}
diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java
index ab577eb9af..5099fccd8d 100644
--- a/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java
+++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/windows/WindowsNamedPipeBridge.java
@@ -14,6 +14,7 @@
import dev.slimevr.tracking.trackers.Tracker;
import io.eiren.util.ann.ThreadSafe;
import io.eiren.util.logging.LogManager;
+import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.List;
@@ -37,6 +38,7 @@ public class WindowsNamedPipeBridge extends SteamVRBridge {
private static final Advapi32 adv32 = Advapi32.INSTANCE;
protected final String pipeName;
+ protected final String bridgeSettingsKey;
private final byte[] buffArray = new byte[2048];
protected WindowsPipe pipe;
protected WinNT.HANDLE openEvent = k32.CreateEvent(null, false, false, null);
@@ -63,6 +65,7 @@ public WindowsNamedPipeBridge(
) {
super(server, "Named pipe thread", bridgeName, bridgeSettingsKey, shareableTrackers);
this.pipeName = pipeName;
+ this.bridgeSettingsKey = bridgeSettingsKey;
overlappedWait.hEvent = rxEvent;
}
@@ -321,4 +324,10 @@ private boolean tryOpeningPipe(WindowsPipe pipe) {
public boolean isConnected() {
return pipe != null && pipe.state == PipeState.OPEN;
}
+
+ @NotNull
+ @Override
+ public String getBridgeConfigKey() {
+ return this.bridgeSettingsKey;
+ }
}
diff --git a/solarxr-protocol b/solarxr-protocol
index df26226d10..7e6cc3b425 160000
--- a/solarxr-protocol
+++ b/solarxr-protocol
@@ -1 +1 @@
-Subproject commit df26226d104f75527a669e03879be675777885e3
+Subproject commit 7e6cc3b42593fdd7022af11ad3eb2e8a29f3c42f