mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Compare commits
133 Commits
v19.0.0-rc
...
hannah/key
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb0423457f | ||
|
|
13f2abb7ab | ||
|
|
f938d7e49c | ||
|
|
cbf59e0ec7 | ||
|
|
04eb4ce33d | ||
|
|
9d26032460 | ||
|
|
cde42aa071 | ||
|
|
13e1265a51 | ||
|
|
2ef6a3172d | ||
|
|
934dd3dee2 | ||
|
|
9c8cd8517e | ||
|
|
ef7e4b1550 | ||
|
|
4f84ccb399 | ||
|
|
6c5c358805 | ||
|
|
810c7e5327 | ||
|
|
faa3da362e | ||
|
|
22319a5c7e | ||
|
|
a83c7ff31e | ||
|
|
be722624f1 | ||
|
|
3fbe64b0c3 | ||
|
|
1b299c499d | ||
|
|
3c945946e7 | ||
|
|
6c9341f333 | ||
|
|
4409050359 | ||
|
|
8580e9a18b | ||
|
|
77e0dab795 | ||
|
|
6bbf325b47 | ||
|
|
db0b3d72b2 | ||
|
|
b55448c421 | ||
|
|
9bf559ab97 | ||
|
|
e79dfd514c | ||
|
|
8e98a2bab6 | ||
|
|
b2aa3ab394 | ||
|
|
6da2691f2b | ||
|
|
6227c682ee | ||
|
|
c1a864176f | ||
|
|
f5911f01b7 | ||
|
|
daeeac2baa | ||
|
|
13930a402e | ||
|
|
dbc7f57274 | ||
|
|
d4002114ef | ||
|
|
f9653e941a | ||
|
|
d2f4721b8a | ||
|
|
764b034abe | ||
|
|
a13e5e9650 | ||
|
|
a57c7fc1c3 | ||
|
|
d4c53086dc | ||
|
|
25a8bd144d | ||
|
|
43544f0c86 | ||
|
|
bfd5f02ff2 | ||
|
|
3172271ddd | ||
|
|
a554b46263 | ||
|
|
d52ea42d01 | ||
|
|
903b59201d | ||
|
|
c181d83245 | ||
|
|
8ebaf51750 | ||
|
|
76814e041c | ||
|
|
4e09bd26fb | ||
|
|
5084f27611 | ||
|
|
b3932e3b0f | ||
|
|
ffb4ca6f61 | ||
|
|
28c241d6c5 | ||
|
|
d7735db8bf | ||
|
|
2d3f168b88 | ||
|
|
ca79d16420 | ||
|
|
3fe4f8f94d | ||
|
|
0b9b2394c0 | ||
|
|
d84cb7f0bf | ||
|
|
2c32fde7d1 | ||
|
|
838fadf523 | ||
|
|
01a63a2e28 | ||
|
|
8f73937a52 | ||
|
|
c46def9e31 | ||
|
|
3e0297a355 | ||
|
|
e087d76781 | ||
|
|
7b5f526fe6 | ||
|
|
51247f23bf | ||
|
|
c87e3eaccb | ||
|
|
e477258d67 | ||
|
|
7132268d58 | ||
|
|
1ee4e79b4e | ||
|
|
bbfa45a5c9 | ||
|
|
81f50f39c5 | ||
|
|
3b40c9ec06 | ||
|
|
bbdf16ee42 | ||
|
|
2a4a72eaf7 | ||
|
|
6b227f8324 | ||
|
|
2247725ba5 | ||
|
|
be3494cdf8 | ||
|
|
9cf0a637d3 | ||
|
|
fd29f41276 | ||
|
|
72d193e5e9 | ||
|
|
c2f2fc219b | ||
|
|
7e7e67631e | ||
|
|
a0e8e356bb | ||
|
|
35955f77df | ||
|
|
cd7a58501c | ||
|
|
9353504b39 | ||
|
|
daf818b2cd | ||
|
|
34d35fa6fd | ||
|
|
ca1c453451 | ||
|
|
84806c44db | ||
|
|
24b2c8722b | ||
|
|
215228551d | ||
|
|
f765ef0e4a | ||
|
|
c956e5d1cb | ||
|
|
865ffc5543 | ||
|
|
25e771f4f5 | ||
|
|
74628d215a | ||
|
|
f19f5cbe51 | ||
|
|
711d0cae63 | ||
|
|
60321891b8 | ||
|
|
b89a290ac9 | ||
|
|
c4d0fe59fd | ||
|
|
58a9d3b6eb | ||
|
|
a3314fa4f9 | ||
|
|
778e0bde56 | ||
|
|
5a6ef6dee5 | ||
|
|
ba5adca358 | ||
|
|
67f1ac7684 | ||
|
|
636514361d | ||
|
|
63d36db15e | ||
|
|
9b8073153a | ||
|
|
7b40b8d315 | ||
|
|
0f1a19e04b | ||
|
|
d1387508e6 | ||
|
|
50a2be81f7 | ||
|
|
29598583a9 | ||
|
|
e83adadf1f | ||
|
|
87699438b4 | ||
|
|
d1cf5dee24 | ||
|
|
b90f52ad7b | ||
|
|
af917d7158 |
@@ -114,9 +114,3 @@ licensed under `GPL-v3`.
|
||||
## Discord
|
||||
We use discord *a lot* to coordinate and discuss development. Come join us at
|
||||
https://discord.gg/SlimeVR!
|
||||
|
||||
## Use of AI
|
||||
We DO NOT accept contributions that are generated with AI (for example, "vibe-coding").
|
||||
|
||||
If you do use AI, and you believe your usage of AI is reasonable, you must clearly disclose
|
||||
how you used AI in your submission.
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { program } from "commander";
|
||||
import { Option, program } from "commander";
|
||||
|
||||
program
|
||||
.option('-p --path <path>', 'set launch path')
|
||||
.option('-p, --path <path>', 'set launch path')
|
||||
.option('-s, --steam', 'steam mode')
|
||||
.option('-i, --install', 'run the driver installer')
|
||||
.option(
|
||||
'--skip-server-if-running',
|
||||
'gui will not launch the server if it is already running'
|
||||
)
|
||||
.allowUnknownOption();
|
||||
|
||||
if (process.platform === "linux") {
|
||||
const noUdevOption = new Option('--no-udev', 'disable udev warning');
|
||||
noUdevOption.negate = false;
|
||||
program.addOption(noUdevOption)
|
||||
}
|
||||
|
||||
program.parse(process.argv);
|
||||
export const options = program.opts();
|
||||
|
||||
@@ -20,6 +20,7 @@ import { getPlatform, handleIpc, isPortAvailable } from './utils';
|
||||
import {
|
||||
findServerJar,
|
||||
findSystemJRE,
|
||||
getExeFolder,
|
||||
getGuiDataFolder,
|
||||
getLogsFolder,
|
||||
getServerDataFolder,
|
||||
@@ -177,6 +178,8 @@ handleIpc(IPC_CHANNELS.GET_FOLDER, (e, folder) => {
|
||||
return getGuiDataFolder();
|
||||
case 'logs':
|
||||
return getLogsFolder();
|
||||
case 'exe':
|
||||
return getExeFolder();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -385,8 +388,17 @@ const spawnServer = async () => {
|
||||
}
|
||||
|
||||
logger.info({ javaBin, serverJar }, 'Found Java and server jar');
|
||||
const serverArgs = ['-Xmx128M', '-jar', serverJar]
|
||||
|
||||
const process = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run']);
|
||||
if (options.steam) serverArgs.push(`--steam`)
|
||||
if (options.install) serverArgs.push(`--install`)
|
||||
if (options.noUdev) serverArgs.push(`--no-udev`)
|
||||
|
||||
serverArgs.push('run')
|
||||
|
||||
const process = spawn(javaBin, serverArgs)
|
||||
|
||||
logger.info(`Java start command: ${serverArgs.join(' ')})`)
|
||||
|
||||
process.stdout?.on('data', (message) => {
|
||||
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {
|
||||
|
||||
@@ -48,6 +48,10 @@ export const getLogsFolder = () => {
|
||||
return join(getGuiDataFolder(), 'logs');
|
||||
};
|
||||
|
||||
export const getExeFolder = () => {
|
||||
return path.dirname(app.getPath('exe'));
|
||||
}
|
||||
|
||||
export const getWindowStateFile = () =>
|
||||
join(getServerDataFolder(), '.window-state.json');
|
||||
|
||||
|
||||
@@ -35,5 +35,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openLogsFolder: async () => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'logs')),
|
||||
openFile: (path) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, path),
|
||||
ghGet: (req) => ipcRenderer.invoke(IPC_CHANNELS.GH_FETCH, req),
|
||||
setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options)
|
||||
setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options),
|
||||
getInstallDir: () => ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'exe')
|
||||
} satisfies IElectronAPI);
|
||||
|
||||
1
gui/electron/preload/interface.d.ts
vendored
1
gui/electron/preload/interface.d.ts
vendored
@@ -55,6 +55,7 @@ export interface IElectronAPI {
|
||||
openFile: (path: string) => void;
|
||||
ghGet: <T extends GHGet>(options: T) => Promise<GHReturn[T['type']]>;
|
||||
setPresence: (options: DiscordPresence) => void;
|
||||
getInstallDir: () => Promise<string>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface IpcInvokeMap {
|
||||
value?: unknown;
|
||||
}) => Promise<unknown>;
|
||||
[IPC_CHANNELS.OPEN_FILE]: (path: string) => void;
|
||||
[IPC_CHANNELS.GET_FOLDER]: (folder: 'config' | 'logs') => string;
|
||||
[IPC_CHANNELS.GET_FOLDER]: (folder: 'config' | 'logs' | 'exe') => string;
|
||||
[IPC_CHANNELS.GH_FETCH]: <T extends GHGet>(
|
||||
options: T
|
||||
) => Promise<GHReturn[T['type']]>;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite --force",
|
||||
"dev:clean": "rm -rf node_modules/.vite && pnpm run gui",
|
||||
"gui": "electron-vite dev --config electron.vite.config.ts --watch",
|
||||
"build": "electron-vite build --config electron.vite.config.ts",
|
||||
"package": "electron-builder",
|
||||
|
||||
@@ -603,6 +603,29 @@ settings-stay_aligned-debug-label = Debugging
|
||||
settings-stay_aligned-debug-description = Please include your settings when reporting problems about Stay Aligned.
|
||||
settings-stay_aligned-debug-copy-label = Copy settings to clipboard
|
||||
|
||||
settings-keybinds = Keybind settings
|
||||
settings-keybinds_ = ''
|
||||
settings-keybinds-description = Change keybinds for various shortcuts
|
||||
keybind_config-keybind_name = Keybind
|
||||
keybind_config-keybind_value = Combination
|
||||
keybind_config-keybind_delay = Delay before trigger (s)
|
||||
settings-keybinds_full-reset = Full Reset
|
||||
settings-keybinds_yaw-reset = Yaw Reset
|
||||
settings-keybinds_mounting-reset = Mounting Reset
|
||||
settings-keybinds_feet-mounting-reset = Feet Mounting Reset
|
||||
settings-keybinds_pause-tracking = Pause Tracking
|
||||
settings-keybinds_record-keybind = Click to record
|
||||
settings-keybinds_now-recording = Recording…
|
||||
settings-keybinds_reset-button = Reset
|
||||
settings-keybinds_reset-all-button = Reset all
|
||||
settings-keybinds-wayland-description = You appear to be using wayland, Please change your shortcuts in your system settings.
|
||||
settings-keybinds-wayland-open-system-settings-button = Open system settings
|
||||
settings-sidebar-keybinds = Keybinds
|
||||
settings-keybinds-recorder-modal-title = Assign keybind for
|
||||
settings-keybinds-recorder-modal-reset-button = Reset
|
||||
settings-keybinds-recorder-modal-unbind-button = Unbind
|
||||
settings-keybinds-recorder-modal-done-button = Done
|
||||
|
||||
## FK/Tracking settings
|
||||
settings-general-fk_settings = Tracking settings
|
||||
|
||||
@@ -980,6 +1003,11 @@ onboarding-reset_tutorial-2 = Tap the highlighted tracker { $taps } times to tri
|
||||
|
||||
You need to be in a pose like you are skiing as shown in the Automatic Mounting wizard, and you have a 3 second delay (configurable) before it gets triggered.
|
||||
|
||||
## Install info
|
||||
install-info_udev-rules_modal_title = Hardware udev access rules not found
|
||||
install-info_udev-rules_warning = Access rules via udev are required for serial console access & dongle connection. Paste the following command into your terminal to add the udev rules.
|
||||
install-info_udev-rules_modal_button = Close
|
||||
install-info_udev-rules_modal-dont-show-again_checkbox = Don't show again
|
||||
## Setup start
|
||||
onboarding-home = Welcome to SlimeVR
|
||||
onboarding-home-start = Let's get set up!
|
||||
|
||||
@@ -17,6 +17,7 @@ import { AutomaticProportionsPage } from './components/onboarding/pages/body-pro
|
||||
import { ManualProportionsPage } from './components/onboarding/pages/body-proportions/ManualProportions';
|
||||
import { ConnectTrackersPage } from './components/onboarding/pages/ConnectTracker';
|
||||
import { HomePage } from './components/onboarding/pages/Home';
|
||||
import { ErrorCollectingConsentPage } from './components/onboarding/pages/ErrorCollectingConsent';
|
||||
import { AutomaticMountingPage } from './components/onboarding/pages/mounting/AutomaticMounting';
|
||||
import { ManualMountingPage } from './components/onboarding/pages/mounting/ManualMounting';
|
||||
import { TrackersAssignPage } from './components/onboarding/pages/trackers-assign/TrackerAssignment';
|
||||
@@ -50,9 +51,11 @@ import { StayAlignedSetup } from './components/onboarding/pages/stay-aligned/Sta
|
||||
import { TrackingChecklistProvider } from './components/tracking-checklist/TrackingChecklistProvider';
|
||||
import { HomeScreenSettings } from './components/settings/pages/HomeScreenSettings';
|
||||
import { ChecklistPage } from './components/tracking-checklist/TrackingChecklist';
|
||||
import { KeybindSettings } from './components/settings/pages/KeybindSettings';
|
||||
import { ElectronContextC, provideElectron } from './hooks/electron';
|
||||
import { AppLocalizationProvider } from './i18n/config';
|
||||
import { openUrl } from './hooks/crossplatform';
|
||||
import { UdevRulesModal } from './components/onboarding/UdevRulesModal';
|
||||
|
||||
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
|
||||
export const VersionContext = createContext('');
|
||||
@@ -70,6 +73,7 @@ function Layout() {
|
||||
<SerialDetectionModal />
|
||||
<VersionUpdateModal />
|
||||
<UnknownDeviceModal />
|
||||
<UdevRulesModal />
|
||||
<SentryRoutes>
|
||||
<Route element={<AppLayout />}>
|
||||
<Route
|
||||
@@ -137,6 +141,7 @@ function Layout() {
|
||||
<Route path="interface" element={<InterfaceSettings />} />
|
||||
<Route path="interface/home" element={<HomeScreenSettings />} />
|
||||
<Route path="advanced" element={<AdvancedSettings />} />
|
||||
<Route path="keybinds" element={<KeybindSettings />} />
|
||||
</Route>
|
||||
<Route
|
||||
path="/onboarding"
|
||||
@@ -147,6 +152,10 @@ function Layout() {
|
||||
}
|
||||
>
|
||||
<Route path="home" element={<HomePage />} />
|
||||
<Route
|
||||
path="error-collecting-consent"
|
||||
element={<ErrorCollectingConsentPage />}
|
||||
/>
|
||||
<Route path="wifi-creds" element={<WifiCredsPage />} />
|
||||
<Route path="connect-trackers" element={<ConnectTrackersPage />} />
|
||||
<Route path="trackers-assign" element={<TrackersAssignPage />} />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { useConfig } from './hooks/config';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export function AppLayout() {
|
||||
const { config } = useConfig();
|
||||
const { pathname } = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -28,10 +29,14 @@ export function AppLayout() {
|
||||
}, [config]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (config && !config.doneOnboarding) {
|
||||
if (
|
||||
config &&
|
||||
!config.doneOnboarding &&
|
||||
!pathname.startsWith('/onboarding/')
|
||||
) {
|
||||
navigate('/onboarding/home');
|
||||
}
|
||||
}, [config?.doneOnboarding]);
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Localized, useLocalization } from '@fluent/react';
|
||||
import { BaseModal } from './commons/BaseModal';
|
||||
import { Button } from './commons/Button';
|
||||
import { Typography } from './commons/Typography';
|
||||
|
||||
export function ErrorConsentModal({
|
||||
isOpen = true,
|
||||
cancel,
|
||||
accept,
|
||||
}: {
|
||||
/**
|
||||
* Is the parent/sibling component opened?
|
||||
*/
|
||||
isOpen: boolean;
|
||||
/**
|
||||
* Function to trigger when you still want to close the app
|
||||
*/
|
||||
accept: () => void;
|
||||
/**
|
||||
* Function to trigger when cancelling app close
|
||||
*/
|
||||
cancel?: () => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
return (
|
||||
<BaseModal isOpen={isOpen} onRequestClose={cancel} closeable={false}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<>
|
||||
<div className="flex flex-col items-center gap-3 fill-accent-background-20">
|
||||
<div className="flex flex-col items-center gap-2 max-w-[512px]">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('error_collection_modal-title')}
|
||||
</Typography>
|
||||
<Localized
|
||||
id={'error_collection_modal-description_v2'}
|
||||
elems={{
|
||||
b: <b />,
|
||||
h1: <span className="text-lg font-bold" />,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="standard"
|
||||
whitespace="whitespace-pre-line"
|
||||
/>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" onClick={accept}>
|
||||
{l10n.getString('error_collection_modal-confirm')}
|
||||
</Button>
|
||||
<Button variant="tertiary" onClick={cancel}>
|
||||
{l10n.getString('error_collection_modal-cancel')}
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import { GearIcon } from './commons/icon/GearIcon';
|
||||
import { TrackersStillOnModal } from './TrackersStillOnModal';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { TrayOrExitModal } from './TrayOrExitModal';
|
||||
import { ErrorConsentModal } from './ErrorConsentModal';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { connectedIMUTrackersAtom } from '@/store/app-store';
|
||||
import { useElectron } from '@/hooks/electron';
|
||||
@@ -72,6 +71,7 @@ export function TopBar({
|
||||
await saveConfig();
|
||||
electron.api.close();
|
||||
};
|
||||
|
||||
const tryCloseApp = async (dontTray = false) => {
|
||||
if (!electron.isElectron) throw 'no electron';
|
||||
|
||||
@@ -286,11 +286,6 @@ export function TopBar({
|
||||
setConnectedTrackerWarning(false);
|
||||
}}
|
||||
/>
|
||||
<ErrorConsentModal
|
||||
isOpen={config?.errorTracking === null}
|
||||
accept={() => setConfig({ errorTracking: true })}
|
||||
cancel={() => setConfig({ errorTracking: false })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
119
gui/src/components/commons/KeybindRecorder.tsx
Normal file
119
gui/src/components/commons/KeybindRecorder.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { useState, forwardRef, useRef } from 'react';
|
||||
import { Typography } from './Typography';
|
||||
import classNames from 'classnames';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
const excludedKeys = [' ', 'SPACE', 'META'];
|
||||
const maxKeybindLength = 4;
|
||||
|
||||
export const KeybindRecorder = forwardRef<
|
||||
HTMLInputElement,
|
||||
{
|
||||
keys: string[];
|
||||
onKeysChange: (v: string[]) => void;
|
||||
error?: string;
|
||||
}
|
||||
>(function KeybindRecorder({ keys, onKeysChange, error }) {
|
||||
const [localKeys, setLocalKeys] = useState<string[]>(keys);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [oldKeys, setOldKeys] = useState<string[]>([]);
|
||||
const [invalidSlot, setInvalidSlot] = useState<number | null>(null);
|
||||
const [errorText, setErrorText] = useState<string>('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const displayKeys = isRecording ? localKeys : keys;
|
||||
const activeIndex = isRecording ? displayKeys.length : -1;
|
||||
const displayError = errorText || error;
|
||||
|
||||
const { clearErrors } = useFormContext();
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
const key = e.key.toUpperCase();
|
||||
const errorMsg = excludedKeys.includes(key)
|
||||
? `Cannot use ${key}!`
|
||||
: displayKeys.includes(key)
|
||||
? `${key} is a Duplicate Key!`
|
||||
: null;
|
||||
if (errorMsg) {
|
||||
setErrorText(errorMsg);
|
||||
setInvalidSlot(activeIndex);
|
||||
setTimeout(() => {
|
||||
setInvalidSlot(null);
|
||||
}, 350);
|
||||
return;
|
||||
}
|
||||
|
||||
if (displayKeys.length < maxKeybindLength) {
|
||||
const updatedKeys = [...displayKeys, key];
|
||||
setLocalKeys(updatedKeys);
|
||||
onKeysChange(updatedKeys);
|
||||
if (updatedKeys.length == maxKeybindLength) {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = () => {
|
||||
setIsRecording(false);
|
||||
if (displayKeys.length < maxKeybindLength - 2 || error) {
|
||||
onKeysChange(oldKeys);
|
||||
setLocalKeys(oldKeys);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnFocus = () => {
|
||||
clearErrors('keybinds');
|
||||
const initialKeys: string[] = [];
|
||||
setOldKeys(keys);
|
||||
setLocalKeys(initialKeys);
|
||||
onKeysChange(initialKeys);
|
||||
setIsRecording(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full justify-center items-center flex flex-col gap-2">
|
||||
<div className="flex gap-2 p-2 items-center rounded-lg relative">
|
||||
<input
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
className="opacity-0 absolute cursor-pointer w-full"
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<div className="flex flex-grow gap-2 justify-center h-full">
|
||||
{Array.from({ length: maxKeybindLength }).map((_, i) => {
|
||||
const key = displayKeys[i];
|
||||
const isActive = isRecording && i === activeIndex;
|
||||
const isInvalid = invalidSlot === i;
|
||||
return (
|
||||
<div key={i} className="flex flex-row">
|
||||
<div
|
||||
className={classNames(
|
||||
'flex p-2 rounded-lg min-w-[50px] min-h-[50px] text-main-title justify-center items-center bg-background-80 mobile:text-sm',
|
||||
{
|
||||
'keyslot-invalid ring-2 ring-status-critical': isInvalid,
|
||||
'keyslot-animate ring-2 ring-accent':
|
||||
isActive && !isInvalid,
|
||||
'ring-accent': !isInvalid && !isInvalid,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{key ?? ''}
|
||||
</div>
|
||||
<div className="flex pl-2 text-main-title justify-center items-center mobile:text-sm">
|
||||
{i < maxKeybindLength - 1 ? '+' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{displayError && (
|
||||
<div className="isInvalid keyslot-invalid">
|
||||
<Typography color="text-status-critical">{`${errorText} ${error}`}</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
87
gui/src/components/commons/KeybindRecorderModal.tsx
Normal file
87
gui/src/components/commons/KeybindRecorderModal.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { BaseModal } from './BaseModal';
|
||||
import { Controller, Control, useFormContext } from 'react-hook-form';
|
||||
import { KeybindRecorder } from './KeybindRecorder';
|
||||
import { Typography } from './Typography';
|
||||
import { Button } from './Button';
|
||||
import './KeybindRow.scss';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
|
||||
export function KeybindRecorderModal({
|
||||
id,
|
||||
control,
|
||||
name,
|
||||
isVisisble,
|
||||
onClose,
|
||||
onUnbind,
|
||||
onSubmit,
|
||||
}: {
|
||||
id?: string;
|
||||
control: Control<any>;
|
||||
name: string;
|
||||
isVisisble: boolean;
|
||||
onClose: () => void;
|
||||
onUnbind: () => void;
|
||||
onSubmit: () => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
const keybindlocalization = 'settings-keybinds_' + id;
|
||||
const {
|
||||
formState: { errors },
|
||||
resetField,
|
||||
handleSubmit,
|
||||
} = useFormContext();
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={isVisisble}
|
||||
onRequestClose={onClose}
|
||||
appendClasses="w-full max-w-xl"
|
||||
>
|
||||
<div className="flex flex-col gap-3 w-full justify-between h-full">
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString('settings-keybinds-recorder-modal-title')}{' '}
|
||||
{l10n.getString(keybindlocalization)}
|
||||
</Typography>
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<KeybindRecorder
|
||||
keys={field.value ?? []}
|
||||
onKeysChange={field.onChange}
|
||||
ref={field.ref}
|
||||
error={errors.keybinds?.message as string}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-row justify-start gap-4">
|
||||
<Button
|
||||
id="settings-keybinds-recorder-modal-reset-button"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
resetField(name);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
id="settings-keybinds-recorder-modal-unbind-button"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
onUnbind();
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row justify-end">
|
||||
<Button
|
||||
id="settings-keybinds-recorder-modal-done-button"
|
||||
variant="primary"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
65
gui/src/components/commons/KeybindRow.scss
Normal file
65
gui/src/components/commons/KeybindRow.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
.keybind-row {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
height: auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@keyframes keyslot {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: translate(1px, 1px) rotate(0deg);
|
||||
}
|
||||
10% {
|
||||
transform: translate(-1px, -2px) rotate(-1deg);
|
||||
}
|
||||
20% {
|
||||
transform: translate(-3px, 0px) rotate(1deg);
|
||||
}
|
||||
30% {
|
||||
transform: translate(3px, 2px) rotate(0deg);
|
||||
}
|
||||
40% {
|
||||
transform: translate(1px, -1px) rotate(1deg);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-1px, 2px) rotate(-1deg);
|
||||
}
|
||||
60% {
|
||||
transform: translate(-3px, 1px) rotate(0deg);
|
||||
}
|
||||
70% {
|
||||
transform: translate(3px, 1px) rotate(-1deg);
|
||||
}
|
||||
80% {
|
||||
transform: translate(-1px, -1px) rotate(1deg);
|
||||
}
|
||||
90% {
|
||||
transform: translate(1px, 2px) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(1px, -2px) rotate(-1deg);
|
||||
}
|
||||
}
|
||||
|
||||
.keyslot-animate {
|
||||
animation: keyslot 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.keyslot-invalid {
|
||||
animation: shake 0.35s ease;
|
||||
}
|
||||
83
gui/src/components/commons/KeybindsRow.tsx
Normal file
83
gui/src/components/commons/KeybindsRow.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Typography } from './Typography';
|
||||
import './KeybindRow.scss';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Control, UseFormGetValues } from 'react-hook-form';
|
||||
import { NumberSelector } from './NumberSelector';
|
||||
import { useLocaleConfig } from '@/i18n/config';
|
||||
|
||||
function KeyBindKeyList({ keybind }: { keybind: string[] }) {
|
||||
if (keybind.length <= 1) {
|
||||
return (
|
||||
<div className="flex h-full text-section-title items-center justifiy-center">
|
||||
Click to edit keybind
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return keybind.map((key, i) => {
|
||||
return (
|
||||
<div key={i} className="flex flex-row">
|
||||
<div className="flex flex-wrap p-2 rounded-lg min-w-[50px] text-standard-bold justify-center items-center bg-background-80 mobile:text-sm">
|
||||
{key ?? ''}
|
||||
</div>
|
||||
<div className="flex justify-center items-center text-section-title mobile:text-sm gap-2 pl-3">
|
||||
{i < keybind.length - 1 ? '+' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function KeybindsRow({
|
||||
id,
|
||||
control,
|
||||
index,
|
||||
getValue,
|
||||
openKeybindRecorderModal,
|
||||
}: {
|
||||
id?: string;
|
||||
control: Control<any>;
|
||||
index: number;
|
||||
getValue: UseFormGetValues<any>;
|
||||
openKeybindRecorderModal: (index: number) => void;
|
||||
}) {
|
||||
const [binding, setBinding] = useState<string[]>();
|
||||
const { currentLocales } = useLocaleConfig();
|
||||
const secondsFormat = new Intl.NumberFormat(currentLocales, {
|
||||
style: 'unit',
|
||||
unit: 'second',
|
||||
unitDisplay: 'narrow',
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
const handleOpenModal = () => {
|
||||
openKeybindRecorderModal(index);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setBinding(getValue(`keybinds.${index}.binding`));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="keybind-row bg-background-60 rounded-xl h-full keybinds-small:flex keybinds-small:flex-col keybinds-small:justify-center keybinds-small:items-center p-2">
|
||||
<label className="text-sm font-medium text-background-10 keybinds-small:flex keybinds-small:py-2 keybinds-small:justify-center keybinds-small:align-middle">
|
||||
<Typography id={`settings-keybinds_${id}`} />
|
||||
</label>
|
||||
<div
|
||||
className="flex gap-2 h-full items-center rounded-lg bg-background-70 hover:bg-background-50 w-full"
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
<div className="flex flex-grow gap-2 justify-center p-2 h-[50px]">
|
||||
{binding != null && <KeyBindKeyList keybind={binding} />}
|
||||
</div>
|
||||
</div>
|
||||
<NumberSelector
|
||||
control={control}
|
||||
name={`keybinds.${index}.delay`}
|
||||
valueLabelFormat={(value) => secondsFormat.format(value)}
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,60 +57,62 @@ export function NumberSelector({
|
||||
<Controller
|
||||
control={control}
|
||||
name={name}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<Typography bold>{label}</Typography>
|
||||
<div className="flex gap-5 bg-background-60 p-2 rounded-lg">
|
||||
<div className="flex gap-1">
|
||||
{doubleStep !== undefined && (
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
{label?.length != 0 ? <Typography bold>{label}</Typography> : <></>}
|
||||
<div className="flex gap-5 bg-background-60 p-2 rounded-lg">
|
||||
<div className="flex gap-1">
|
||||
{doubleStep !== undefined && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, false))}
|
||||
disabled={doubleStepFn(value, false) < min || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(-doubleStep)
|
||||
: '--'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, false))}
|
||||
disabled={doubleStepFn(value, false) < min || disabled}
|
||||
onClick={() => onChange(stepFn(value, false))}
|
||||
disabled={stepFn(value, false) < min || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(-doubleStep)
|
||||
: '--'}
|
||||
-
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(stepFn(value, false))}
|
||||
disabled={stepFn(value, false) < min || disabled}
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center text-center items-center w-10 text-standard">
|
||||
{valueLabelFormat ? valueLabelFormat(value) : value}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(stepFn(value, true))}
|
||||
disabled={stepFn(value, true) > max || disabled}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
{doubleStep !== undefined && (
|
||||
</div>
|
||||
<div className="flex flex-grow justify-center text-center items-center w-10 text-standard">
|
||||
{valueLabelFormat ? valueLabelFormat(value) : value}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, true))}
|
||||
disabled={doubleStepFn(value, true) > max || disabled}
|
||||
onClick={() => onChange(stepFn(value, true))}
|
||||
disabled={stepFn(value, true) > max || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(doubleStep)
|
||||
: '++'}
|
||||
+
|
||||
</Button>
|
||||
)}
|
||||
{doubleStep !== undefined && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
rounded
|
||||
onClick={() => onChange(doubleStepFn(value, true))}
|
||||
disabled={doubleStepFn(value, true) > max || disabled}
|
||||
>
|
||||
{showButtonWithNumber
|
||||
? decimalFormat.format(doubleStep)
|
||||
: '++'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
13
gui/src/components/commons/icon/ResetSettingIcon.tsx
Normal file
13
gui/src/components/commons/icon/ResetSettingIcon.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export function ResetSettingIcon({ size = 24 }: { size?: number }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={size}
|
||||
viewBox="0 -960 960 960"
|
||||
width={size}
|
||||
fill="#00000"
|
||||
>
|
||||
<path d="M520-330v-60h160v60H520Zm60 210v-50h-60v-60h60v-50h60v160h-60Zm100-50v-60h160v60H680Zm40-110v-160h60v50h60v60h-60v50h-60Zm111-280h-83q-26-88-99-144t-169-56q-117 0-198.5 81.5T200-480q0 72 32.5 132t87.5 98v-110h80v240H160v-80h94q-62-50-98-122.5T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q129 0 226.5 79.5T831-560Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
97
gui/src/components/onboarding/UdevRulesModal.tsx
Normal file
97
gui/src/components/onboarding/UdevRulesModal.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { BaseModal } from '@/components/commons/BaseModal';
|
||||
import { CheckboxInternal } from '@/components/commons/Checkbox';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useElectron } from '@/hooks/electron';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
|
||||
export function UdevRulesModal() {
|
||||
const { config, setConfig } = useConfig();
|
||||
const electron = useElectron();
|
||||
const [udevContent, setUdevContent] = useState('');
|
||||
const [showUdevWarning, setShowUdevWarning] = useState(false);
|
||||
const [dontShowThisSession, setDontShowThisSession] = useState(false);
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
const { l10n } = useLocalization();
|
||||
const { installInfo } = useAppContext();
|
||||
|
||||
const handleUdevContent = async () => {
|
||||
if (electron.isElectron) {
|
||||
const dir = await electron.api.getInstallDir();
|
||||
const rulesPath = `${dir}/69-slimevr-devices.rules`;
|
||||
setUdevContent(
|
||||
`cat ${rulesPath} | sudo sh -c 'tee /etc/udev/rules.d/69-slimevr-devices.rules >/dev/null && udevadm control --reload-rules && udevadm trigger'`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleUdevContent();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) throw 'Invalid state!';
|
||||
if (electron.isElectron) {
|
||||
const isLinux = electron.data().os.type === 'linux';
|
||||
const udevMissing = !installInfo?.isUdevInstalled;
|
||||
const notHiddenGlobally = !config.dontShowUdevModal;
|
||||
const notHiddenThisSession = !dontShowThisSession;
|
||||
const shouldShow =
|
||||
isLinux && udevMissing && notHiddenGlobally && notHiddenThisSession;
|
||||
setShowUdevWarning(shouldShow);
|
||||
}
|
||||
}, [config, dontShowThisSession]);
|
||||
|
||||
const handleModalClose = () => {
|
||||
if (!config) throw 'Invalid State!';
|
||||
setConfig({ dontShowUdevModal: dontShowAgain });
|
||||
setDontShowThisSession(true);
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(udevContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal isOpen={showUdevWarning} appendClasses="w-full max-w-2xl">
|
||||
<div className="flex w-full h-full flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography
|
||||
variant="main-title"
|
||||
id="install-info_udev-rules_modal_title"
|
||||
/>
|
||||
<Typography id="install-info_udev-rules_warning" />
|
||||
</div>
|
||||
<div className="relative w-full max-w-2xl">
|
||||
<div className="absolute right-2 top-2">
|
||||
<Button variant="secondary" onClick={copyToClipboard}>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bg-background-80 rounded-lg overflow-auto p-2 w-full h-[300px]">
|
||||
<pre className="text-wrap">{udevContent}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2">
|
||||
<CheckboxInternal
|
||||
label={l10n.getString(
|
||||
'install-info_udev-rules_modal-dont-show-again_checkbox'
|
||||
)}
|
||||
name="dismiss-udev-rules-checkbox"
|
||||
onChange={(e) => setDontShowAgain(e.currentTarget.checked)}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleModalClose}
|
||||
id="install-info_udev-rules_modal_button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Localized } from '@fluent/react';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { useConfig } from '@/hooks/config';
|
||||
|
||||
export function ErrorCollectingConsentPage() {
|
||||
const { setConfig } = useConfig();
|
||||
|
||||
const accept = () => {
|
||||
setConfig({ errorTracking: true });
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
setConfig({ errorTracking: false });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full flex-col gap-3 p-4">
|
||||
<div className="max-w-2xl flex flex-col gap-4">
|
||||
<div className="flex flex-col w-full gap-4">
|
||||
<Typography variant="main-title" id="error_collection_modal-title" />
|
||||
<Localized
|
||||
id="error_collection_modal-description_v2"
|
||||
elems={{
|
||||
b: <b />,
|
||||
h1: <span className="text-md font-bold" />,
|
||||
}}
|
||||
>
|
||||
<Typography variant="standard" whitespace="whitespace-pre-line" />
|
||||
</Localized>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-between">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
to="/onboarding/wifi-creds"
|
||||
onClick={cancel}
|
||||
id="error_collection_modal-cancel"
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/onboarding/wifi-creds"
|
||||
onClick={accept}
|
||||
id="error_collection_modal-confirm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,9 +19,11 @@ export function HomePage() {
|
||||
<Typography variant="mobile-title">
|
||||
{l10n.getString('onboarding-home')}
|
||||
</Typography>
|
||||
<Button variant="primary" to="/onboarding/wifi-creds">
|
||||
{l10n.getString('onboarding-home-start')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
id="onboarding-home-start"
|
||||
to="/onboarding/error-collecting-consent"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-4 bottom-4 z-50">
|
||||
<LangSelector />
|
||||
|
||||
@@ -58,6 +58,10 @@ export function SettingSelectorMobile() {
|
||||
label: l10n.getString('settings-sidebar-advanced'),
|
||||
value: { url: '/settings/advanced' },
|
||||
},
|
||||
{
|
||||
label: l10n.getString('settings-sidebar-keybinds'),
|
||||
value: { url: '/settings/keybinds' },
|
||||
},
|
||||
{
|
||||
label: l10n.getString('navbar-onboarding'),
|
||||
value: { url: '/onboarding/home' },
|
||||
|
||||
@@ -73,6 +73,13 @@ export function SettingsSidebar() {
|
||||
scrollTo="gestureControl"
|
||||
id="settings-sidebar-gesture_control"
|
||||
/>
|
||||
{
|
||||
<SettingsLink
|
||||
to="/settings/keybinds"
|
||||
scrollTo="keybinds"
|
||||
id="settings-sidebar-keybinds"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
15
gui/src/components/settings/pages/KeybindSettings.scss
Normal file
15
gui/src/components/settings/pages/KeybindSettings.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
.keybind-settings {
|
||||
display: grid;
|
||||
|
||||
grid-template:
|
||||
'n v d ' auto
|
||||
'k k k ' auto
|
||||
'b . . ' auto
|
||||
/ 1fr 1fr 90px;
|
||||
|
||||
grid-template-columns: subgrid;
|
||||
grid-template-rows: subgrid;
|
||||
|
||||
align-items: left;
|
||||
gap: 10px;
|
||||
}
|
||||
264
gui/src/components/settings/pages/KeybindSettings.tsx
Normal file
264
gui/src/components/settings/pages/KeybindSettings.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { KeybindRecorderModal } from '@/components/commons/KeybindRecorderModal';
|
||||
import {
|
||||
SettingsPageLayout,
|
||||
SettingsPagePaneLayout,
|
||||
} from '@/components/settings/SettingsPageLayout';
|
||||
import { WrenchIcon } from '@/components/commons/icon/WrenchIcons';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useLocalization } from '@fluent/react';
|
||||
import './KeybindSettings.scss';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { KeybindsRow } from '@/components/commons/KeybindsRow';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ChangeKeybindRequestT,
|
||||
KeybindRequestT,
|
||||
KeybindResponseT,
|
||||
KeybindT,
|
||||
OpenUriRequestT,
|
||||
RpcMessage,
|
||||
} from 'solarxr-protocol';
|
||||
import { FormProvider, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useAppContext } from '@/hooks/app';
|
||||
import { useElectron } from '@/hooks/electron';
|
||||
|
||||
export type KeybindForm = {
|
||||
keybinds: {
|
||||
id: number;
|
||||
name: string;
|
||||
binding: string[];
|
||||
delay: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export function KeybindSettings() {
|
||||
const electron = useElectron();
|
||||
const { l10n } = useLocalization();
|
||||
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [defaultKeybindsState, setDefaultKeybindsState] = useState<KeybindForm>(
|
||||
{
|
||||
keybinds: [],
|
||||
}
|
||||
);
|
||||
const currentIndex = useRef<number | null>(null);
|
||||
const { installInfo } = useAppContext();
|
||||
|
||||
const methods = useForm<KeybindForm>({
|
||||
defaultValues: defaultKeybindsState,
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
getValues,
|
||||
setError,
|
||||
clearErrors,
|
||||
resetField,
|
||||
} = methods;
|
||||
|
||||
const { fields } = useFieldArray({
|
||||
control,
|
||||
name: 'keybinds',
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
const value = getValues();
|
||||
if (checkDuplicates(value)) {
|
||||
return;
|
||||
}
|
||||
clearErrors('keybinds');
|
||||
|
||||
value.keybinds.forEach((kb) => {
|
||||
const changeKeybindRequest = new ChangeKeybindRequestT();
|
||||
|
||||
const keybind = new KeybindT();
|
||||
keybind.keybindId = kb.id;
|
||||
keybind.keybindNameId = kb.name;
|
||||
keybind.keybindValue = kb.binding.join('+');
|
||||
keybind.keybindDelay = kb.delay;
|
||||
|
||||
changeKeybindRequest.keybind = keybind;
|
||||
|
||||
sendRPCPacket(RpcMessage.ChangeKeybindRequest, changeKeybindRequest);
|
||||
setIsOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
const checkDuplicates = (value: KeybindForm) => {
|
||||
const normalized = value.keybinds
|
||||
.filter((kb) => kb.binding.length > 0)
|
||||
.map((kb) => JSON.stringify([...kb.binding].sort()));
|
||||
|
||||
const unique = new Set(normalized);
|
||||
|
||||
if (unique.size !== normalized.length) {
|
||||
setError('keybinds', {
|
||||
type: 'manual',
|
||||
message: 'Duplicate keybind combinations are not allowed',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleOpenSystemSettingsButton = () => {
|
||||
sendRPCPacket(RpcMessage.OpenUriRequest, new OpenUriRequestT());
|
||||
};
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.KeybindResponse,
|
||||
({ keybind, defaultKeybinds }: KeybindResponseT) => {
|
||||
if (!keybind) return;
|
||||
|
||||
const mappedDefaults = defaultKeybinds.map((kb) => ({
|
||||
id: kb.keybindId,
|
||||
name: typeof kb.keybindNameId === 'string' ? kb.keybindNameId : '',
|
||||
binding:
|
||||
typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
|
||||
delay: kb.keybindDelay,
|
||||
}));
|
||||
|
||||
setDefaultKeybindsState({ keybinds: mappedDefaults });
|
||||
reset({ keybinds: mappedDefaults });
|
||||
|
||||
const mapped = keybind.map((kb) => ({
|
||||
id: kb.keybindId,
|
||||
name: typeof kb.keybindNameId === 'string' ? kb.keybindNameId : '',
|
||||
binding:
|
||||
typeof kb.keybindValue === 'string' ? kb.keybindValue.split('+') : [],
|
||||
delay: kb.keybindDelay,
|
||||
}));
|
||||
|
||||
mapped.forEach((keybind, index) => {
|
||||
setValue(`keybinds.${index}.binding`, keybind.binding);
|
||||
setValue(`keybinds.${index}.delay`, keybind.delay);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const handleOpenRecorderModal = (index: number) => {
|
||||
currentIndex.current = index;
|
||||
if (currentIndex !== null) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
if (currentIndex.current != null) {
|
||||
resetField(`keybinds.${currentIndex.current}.binding`);
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const createKeybindRows = (): ReactNode => {
|
||||
return fields.map((field, index) => {
|
||||
return (
|
||||
<div className="keybind-row" key={index}>
|
||||
<KeybindsRow
|
||||
id={typeof field.name === 'string' ? field.name : ''}
|
||||
control={control}
|
||||
index={index}
|
||||
getValue={getValues}
|
||||
openKeybindRecorderModal={handleOpenRecorderModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.KeybindRequest, new KeybindRequestT());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingsPageLayout>
|
||||
<SettingsPagePaneLayout icon={<WrenchIcon />} id="keybinds">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="main-title" id="settings-keybinds" />
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
{l10n
|
||||
.getString('settings-keybinds-description')
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography key={i}>{line}</Typography>
|
||||
))}
|
||||
</div>
|
||||
{!installInfo?.isWayland ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Typography id="settings-keybinds-wayland-description" />
|
||||
<div>
|
||||
<Button
|
||||
id="settings-keybinds-wayland-open-system-settings-button"
|
||||
className="flex flex-col"
|
||||
onClick={handleOpenSystemSettingsButton}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
electron.isElectron &&
|
||||
electron.data().os.type !== 'windows' && (
|
||||
<>
|
||||
<FormProvider {...methods}>
|
||||
<div className="keybind-settings">
|
||||
<Typography
|
||||
id="keybind_config-keybind_name"
|
||||
variant="section-title"
|
||||
/>
|
||||
<Typography
|
||||
id="keybind_config-keybind_value"
|
||||
variant="section-title"
|
||||
/>
|
||||
<Typography
|
||||
id="keybind_config-keybind_delay"
|
||||
variant="section-title"
|
||||
/>
|
||||
{createKeybindRows()}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
id="settings-keybinds_reset-all-button"
|
||||
onClick={() => {
|
||||
reset(defaultKeybindsState);
|
||||
handleSubmit(onSubmit)();
|
||||
}}
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
<KeybindRecorderModal
|
||||
id={
|
||||
currentIndex.current != null
|
||||
? fields[currentIndex.current].name
|
||||
: ''
|
||||
}
|
||||
control={control}
|
||||
name={
|
||||
currentIndex.current != null
|
||||
? `keybinds.${currentIndex.current}.binding`
|
||||
: ''
|
||||
}
|
||||
isVisisble={isOpen}
|
||||
onClose={onClose}
|
||||
onUnbind={() => {
|
||||
if (currentIndex.current != null)
|
||||
setValue(
|
||||
`keybinds.${currentIndex.current}.binding`,
|
||||
[]
|
||||
);
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</SettingsPagePaneLayout>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -39,8 +39,8 @@ function StaAlignedPoseModal({
|
||||
<Typography variant="main-title">{l10n.getString(title)}</Typography>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{descriptionKeys.map((descriptionKey) => (
|
||||
<Typography>{l10n.getString(descriptionKey)}</Typography>
|
||||
{descriptionKeys.map((descriptionKey, i) => (
|
||||
<Typography key={i}>{l10n.getString(descriptionKey)}</Typography>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex pt-1 items-center fill-background-50 justify-center px-12">
|
||||
|
||||
@@ -38,7 +38,7 @@ export function TrackerBattery({
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
disabled={charging || !runtime || debug}
|
||||
disabled={!charging && (!runtime || debug)}
|
||||
preferedDirection="left"
|
||||
content=<Typography>{percentFormatter.format(value)}</Typography>
|
||||
>
|
||||
|
||||
@@ -199,7 +199,7 @@ export function TrackerSettingsPage() {
|
||||
shakeHighlight={false}
|
||||
/>
|
||||
)}
|
||||
{tracker?.device?.hardwareInfo?.hardwareIdentifier != 'Unknown' && (
|
||||
{
|
||||
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2">
|
||||
<Typography
|
||||
variant="section-title"
|
||||
@@ -223,38 +223,34 @@ export function TrackerSettingsPage() {
|
||||
whitespace="whitespace-pre-wrap"
|
||||
textAlign="text-end"
|
||||
>
|
||||
{tracker?.device?.hardwareInfo?.firmwareVersion
|
||||
? `v${tracker?.device?.hardwareInfo?.firmwareVersion}`
|
||||
: '--'}
|
||||
v{tracker?.device?.hardwareInfo?.firmwareVersion}
|
||||
</Typography>
|
||||
</div>
|
||||
{!!tracker?.device?.hardwareInfo?.officialBoardType && (
|
||||
<div className="flex justify-between gap-2">
|
||||
<Typography id="tracker-settings-latest-version" />
|
||||
{!updateUnavailable && (
|
||||
<>
|
||||
{currentFirmwareRelease && (
|
||||
<Typography
|
||||
color={
|
||||
needUpdate === 'updated'
|
||||
? undefined
|
||||
: 'text-accent-background-10'
|
||||
}
|
||||
textAlign="text-end"
|
||||
whitespace="whitespace-pre-wrap"
|
||||
>
|
||||
{currentFirmwareRelease.name}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{updateUnavailable && (
|
||||
<Typography id="tracker-settings-update-unavailable-v2">
|
||||
No releases found
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between gap-2">
|
||||
<Typography id="tracker-settings-latest-version" />
|
||||
{!updateUnavailable && (
|
||||
<>
|
||||
{currentFirmwareRelease && (
|
||||
<Typography
|
||||
color={
|
||||
needUpdate === 'updated'
|
||||
? undefined
|
||||
: 'text-accent-background-10'
|
||||
}
|
||||
textAlign="text-end"
|
||||
whitespace="whitespace-pre-wrap"
|
||||
>
|
||||
{currentFirmwareRelease.name}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{updateUnavailable && (
|
||||
<Typography id="tracker-settings-update-unavailable-v2">
|
||||
No releases found
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!updateUnavailable && (
|
||||
<Tooltip
|
||||
@@ -293,7 +289,7 @@ export function TrackerSettingsPage() {
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
|
||||
<div className="flex flex-col bg-background-70 p-3 rounded-lg gap-2 overflow-x-auto">
|
||||
<div className="flex justify-between">
|
||||
@@ -321,11 +317,10 @@ export function TrackerSettingsPage() {
|
||||
<div className="flex justify-between">
|
||||
<Typography>{l10n.getString('tracker-infos-url')}</Typography>
|
||||
<Typography>
|
||||
{tracker?.device?.hardwareInfo?.ipAddress?.addr
|
||||
? `udp://${IPv4.fromNumber(
|
||||
tracker?.device?.hardwareInfo?.ipAddress?.addr || 0
|
||||
).toString()}`
|
||||
: '--'}
|
||||
udp://
|
||||
{IPv4.fromNumber(
|
||||
tracker?.device?.hardwareInfo?.ipAddress?.addr || 0
|
||||
).toString()}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ResetResponseT,
|
||||
RpcMessage,
|
||||
StartDataFeedT,
|
||||
InstalledInfoResponseT,
|
||||
} from 'solarxr-protocol';
|
||||
import { handleResetSounds } from '@/sounds/sounds';
|
||||
import { useConfig } from './config';
|
||||
@@ -18,11 +19,17 @@ import { DEFAULT_LOCALE, LangContext } from '@/i18n/config';
|
||||
|
||||
export interface AppContext {
|
||||
currentFirmwareRelease: FirmwareRelease | null;
|
||||
installInfo: InstalledInfoResponseT | null;
|
||||
}
|
||||
|
||||
export function useProvideAppContext(): AppContext {
|
||||
const { useRPCPacket, sendDataFeedPacket, useDataFeedPacket, isConnected } =
|
||||
useWebsocketAPI();
|
||||
const {
|
||||
useRPCPacket,
|
||||
sendRPCPacket,
|
||||
sendDataFeedPacket,
|
||||
useDataFeedPacket,
|
||||
isConnected,
|
||||
} = useWebsocketAPI();
|
||||
const { changeLocales } = useContext(LangContext);
|
||||
const { config } = useConfig();
|
||||
const { dataFeedConfig } = useDataFeedConfig();
|
||||
@@ -34,6 +41,8 @@ export function useProvideAppContext(): AppContext {
|
||||
const [currentFirmwareRelease, setCurrentFirmwareRelease] =
|
||||
useState<FirmwareRelease | null>(null);
|
||||
|
||||
const [installInfo, setInstallInfo] = useState<InstalledInfoResponseT | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
const startDataFeed = new StartDataFeedT();
|
||||
@@ -70,6 +79,17 @@ export function useProvideAppContext(): AppContext {
|
||||
};
|
||||
}, [config?.uuid]);
|
||||
|
||||
useEffect(() => {
|
||||
sendRPCPacket(RpcMessage.InstalledInfoRequest, new InstalledInfoResponseT());
|
||||
}, []);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.InstalledInfoResponse,
|
||||
({ isUdevInstalled, isWayland }: InstalledInfoResponseT) => {
|
||||
setInstallInfo(new InstalledInfoResponseT(isUdevInstalled, isWayland));
|
||||
}
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
changeLocales([config?.lang || DEFAULT_LOCALE]);
|
||||
}, []);
|
||||
@@ -85,6 +105,7 @@ export function useProvideAppContext(): AppContext {
|
||||
|
||||
return {
|
||||
currentFirmwareRelease,
|
||||
installInfo,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface Config {
|
||||
homeLayout: 'default' | 'table';
|
||||
skeletonPreview: boolean;
|
||||
lastUsedProportions: 'manual' | 'autobone' | 'scaled' | null;
|
||||
dontShowUdevModal: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigContext {
|
||||
@@ -79,6 +80,7 @@ export const defaultConfig: Config = {
|
||||
homeLayout: 'default',
|
||||
skeletonPreview: true,
|
||||
lastUsedProportions: null,
|
||||
dontShowUdevModal: false,
|
||||
};
|
||||
|
||||
const localStore: CrossStorage = {
|
||||
|
||||
@@ -130,6 +130,9 @@ export function checkForUpdate(
|
||||
|
||||
if (
|
||||
!device.hardwareInfo?.officialBoardType ||
|
||||
![BoardType.SLIMEVR, BoardType.SLIMEVR_V1_2].includes(
|
||||
device.hardwareInfo.officialBoardType
|
||||
) ||
|
||||
!semver.valid(currentFirmwareRelease.version) ||
|
||||
!semver.valid(device.hardwareInfo.firmwareVersion?.toString() ?? 'none')
|
||||
) {
|
||||
@@ -141,14 +144,6 @@ export function checkForUpdate(
|
||||
currentFirmwareRelease.version
|
||||
);
|
||||
|
||||
if (
|
||||
![BoardType.SLIMEVR, BoardType.SLIMEVR_V1_2].includes(
|
||||
device.hardwareInfo.officialBoardType
|
||||
)
|
||||
) {
|
||||
return canUpdate ? 'unavailable' : 'updated';
|
||||
}
|
||||
|
||||
if (
|
||||
canUpdate &&
|
||||
device.hardwareStatus?.batteryPctEstimate != null &&
|
||||
|
||||
@@ -11,8 +11,12 @@ import { DeviceDataT } from 'solarxr-protocol';
|
||||
|
||||
export function getSentryOrCompute(enabled = false, uuid: string) {
|
||||
Sentry.setUser({ id: uuid });
|
||||
|
||||
// if sentry is already initialized - SKIP
|
||||
if (enabled && Sentry.isInitialized()) return;
|
||||
if (enabled && Sentry.isInitialized()) {
|
||||
log('Sentry already enabled, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
const client = Sentry.getClient();
|
||||
if (client) {
|
||||
|
||||
@@ -183,6 +183,7 @@ const config = {
|
||||
lg: '1300px',
|
||||
xl: '1600px',
|
||||
tall: { raw: '(min-height: 860px)' },
|
||||
'keybinds-small': { raw: 'not (min-width: 1230px)' },
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"gui"
|
||||
],
|
||||
"scripts": {
|
||||
"gui": "pnpm run update-solarxr && cd gui && pnpm run gui",
|
||||
"gui": "pnpm run update-solarxr && cd gui && pnpm run dev:clean",
|
||||
"lint:fix": "cd gui && pnpm lint:fix",
|
||||
"skipbundler": "cd gui && pnpm run skipbundler",
|
||||
"build": "cd gui && pnpm build",
|
||||
|
||||
@@ -79,6 +79,8 @@ dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||
implementation("com.mayakapps.kache:kache:2.1.1")
|
||||
|
||||
implementation("com.github.HannahPadd:DbusGlobalShortcutsWayland:v0.1.0")
|
||||
|
||||
api("com.github.loucass003:EspflashKotlin:v0.11.0")
|
||||
|
||||
// Allow the use of reflection
|
||||
|
||||
6
server/core/src/main/java/dev/slimevr/FeatureFlags.kt
Normal file
6
server/core/src/main/java/dev/slimevr/FeatureFlags.kt
Normal file
@@ -0,0 +1,6 @@
|
||||
package dev.slimevr
|
||||
|
||||
data class FeatureFlags(
|
||||
var steam: Boolean = false,
|
||||
var skipCheckUdev: Boolean = false,
|
||||
)
|
||||
@@ -2,51 +2,30 @@ package dev.slimevr
|
||||
|
||||
import com.melloware.jintellitype.HotkeyListener
|
||||
import com.melloware.jintellitype.JIntellitype
|
||||
import dev.hannah.portals.PortalManager
|
||||
import dev.hannah.portals.globalShortcuts.Shortcut
|
||||
import dev.hannah.portals.globalShortcuts.ShortcutTuple
|
||||
import dev.slimevr.config.KeybindingsConfig
|
||||
import dev.slimevr.tracking.trackers.TrackerUtils
|
||||
import io.eiren.util.OperatingSystem
|
||||
import io.eiren.util.OperatingSystem.Companion.currentPlatform
|
||||
import io.eiren.util.ann.AWTThread
|
||||
import io.eiren.util.logging.LogManager
|
||||
import solarxr_protocol.rpc.KeybindId
|
||||
|
||||
class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
|
||||
val config: KeybindingsConfig = server.configManager.vrConfig.keybindings
|
||||
|
||||
init {
|
||||
if (currentPlatform != OperatingSystem.WINDOWS) {
|
||||
LogManager
|
||||
.info(
|
||||
"[Keybinding] Currently only supported on Windows. Keybindings will be disabled.",
|
||||
)
|
||||
} else {
|
||||
if (currentPlatform == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
if (JIntellitype.getInstance() != null) {
|
||||
JIntellitype.getInstance().addHotKeyListener(this)
|
||||
|
||||
val fullResetBinding = config.fullResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(FULL_RESET, fullResetBinding)
|
||||
LogManager.info("[Keybinding] Bound full reset to $fullResetBinding")
|
||||
|
||||
val yawResetBinding = config.yawResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(YAW_RESET, yawResetBinding)
|
||||
LogManager.info("[Keybinding] Bound yaw reset to $yawResetBinding")
|
||||
|
||||
val mountingResetBinding = config.mountingResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(MOUNTING_RESET, mountingResetBinding)
|
||||
LogManager.info("[Keybinding] Bound reset mounting to $mountingResetBinding")
|
||||
|
||||
val feetMountingResetBinding = config.feetMountingResetBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(FEET_MOUNTING_RESET, feetMountingResetBinding)
|
||||
LogManager.info("[Keybinding] Bound feet reset mounting to $feetMountingResetBinding")
|
||||
|
||||
val pauseTrackingBinding = config.pauseTrackingBinding
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(PAUSE_TRACKING, pauseTrackingBinding)
|
||||
LogManager.info("[Keybinding] Bound pause tracking to $pauseTrackingBinding")
|
||||
config.keybinds.forEach { (i, keybind) ->
|
||||
JIntellitype.getInstance()
|
||||
.registerHotKey(keybind.id, keybind.binding)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
LogManager
|
||||
@@ -55,32 +34,107 @@ class Keybinding @AWTThread constructor(val server: VRServer) : HotkeyListener {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (currentPlatform == OperatingSystem.LINUX) {
|
||||
val portalManager = PortalManager(SLIMEVR_IDENTIFIER)
|
||||
val fullReset = Shortcut("Full Reset", "CTRL+ALT+SHIFT+Y")
|
||||
val yawReset = Shortcut("Yaw Reset", "CTRL+ALT+SHIFT+U")
|
||||
val mountingReset = Shortcut("Mounting Reset", "CTRL+ALT+SHIFT+I")
|
||||
val feetMountingReset = Shortcut("Feet Mounting Reset", "CTRL+ALT+SHIFT+P")
|
||||
val pauseTracking = Shortcut("Pause Tracking", "CTRL+ALT+SHIFT+O")
|
||||
val shortcutsList = mutableListOf(
|
||||
ShortcutTuple("FULL_RESET", fullReset.shortcut),
|
||||
ShortcutTuple("YAW_RESET", yawReset.shortcut),
|
||||
ShortcutTuple("MOUNTING_RESET", mountingReset.shortcut),
|
||||
ShortcutTuple("FEET_MOUNTING_RESET", feetMountingReset.shortcut),
|
||||
ShortcutTuple("PAUSE_TRACKING", pauseTracking.shortcut),
|
||||
)
|
||||
val globalShortcutsHandler = portalManager.globalShortcutsRequest(shortcutsList)
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
Thread {
|
||||
println("Closing connection")
|
||||
globalShortcutsHandler.close()
|
||||
},
|
||||
)
|
||||
|
||||
globalShortcutsHandler.onShortcutActivated = { shortcutId ->
|
||||
when (shortcutId) {
|
||||
"FULL_RESET" -> {
|
||||
val delay = config.keybinds[KeybindId.FULL_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersFull(RESET_SOURCE_NAME, delay)
|
||||
}
|
||||
|
||||
"YAW_RESET" -> {
|
||||
val delay = config.keybinds[KeybindId.YAW_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, delay)
|
||||
}
|
||||
|
||||
"MOUNTING_RESET" -> {
|
||||
val delay = config.keybinds[KeybindId.MOUNTING_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
delay,
|
||||
)
|
||||
}
|
||||
|
||||
"FEET_MOUNTING_RESET" -> {
|
||||
val delay = config.keybinds[KeybindId.FEET_MOUNTING_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
delay,
|
||||
TrackerUtils.feetsBodyParts,
|
||||
)
|
||||
}
|
||||
|
||||
"PAUSE_TRACKING" -> {
|
||||
val delay = config.keybinds[KeybindId.PAUSE_TRACKING]?.delay?.toLong() ?: 0L
|
||||
server.scheduleTogglePauseTracking(
|
||||
RESET_SOURCE_NAME,
|
||||
delay,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AWTThread
|
||||
override fun onHotKey(identifier: Int) {
|
||||
when (identifier) {
|
||||
FULL_RESET -> server.scheduleResetTrackersFull(RESET_SOURCE_NAME, config.fullResetDelay)
|
||||
FULL_RESET -> {
|
||||
val delay = config.keybinds[KeybindId.FULL_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersFull(RESET_SOURCE_NAME, delay)
|
||||
}
|
||||
|
||||
YAW_RESET -> server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, config.yawResetDelay)
|
||||
YAW_RESET -> {
|
||||
val delay = config.keybinds[KeybindId.YAW_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersYaw(RESET_SOURCE_NAME, delay)
|
||||
}
|
||||
|
||||
MOUNTING_RESET -> server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
config.mountingResetDelay,
|
||||
)
|
||||
MOUNTING_RESET -> {
|
||||
val delay = config.keybinds[KeybindId.FEET_MOUNTING_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
delay,
|
||||
TrackerUtils.feetsBodyParts,
|
||||
)
|
||||
}
|
||||
|
||||
FEET_MOUNTING_RESET -> server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
config.feetMountingResetDelay,
|
||||
TrackerUtils.feetsBodyParts,
|
||||
)
|
||||
FEET_MOUNTING_RESET -> {
|
||||
val delay = config.keybinds[KeybindId.FEET_MOUNTING_RESET]?.delay?.toLong() ?: 0L
|
||||
server.scheduleResetTrackersMounting(
|
||||
RESET_SOURCE_NAME,
|
||||
delay,
|
||||
TrackerUtils.feetsBodyParts,
|
||||
)
|
||||
}
|
||||
|
||||
PAUSE_TRACKING ->
|
||||
server
|
||||
.scheduleTogglePauseTracking(
|
||||
RESET_SOURCE_NAME,
|
||||
config.pauseTrackingDelay,
|
||||
)
|
||||
PAUSE_TRACKING -> {
|
||||
val delay = config.keybinds[KeybindId.PAUSE_TRACKING]?.delay?.toLong() ?: 0L
|
||||
server.scheduleTogglePauseTracking(
|
||||
RESET_SOURCE_NAME,
|
||||
delay,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import dev.slimevr.games.vrchat.VRCConfigHandler
|
||||
import dev.slimevr.games.vrchat.VRCConfigHandlerStub
|
||||
import dev.slimevr.games.vrchat.VRChatConfigManager
|
||||
import dev.slimevr.guards.ServerGuards
|
||||
import dev.slimevr.keybind.KeybindHandler
|
||||
import dev.slimevr.osc.OSCHandler
|
||||
import dev.slimevr.osc.OSCRouter
|
||||
import dev.slimevr.osc.VMCHandler
|
||||
@@ -56,6 +57,7 @@ const val SLIMEVR_IDENTIFIER = "dev.slimevr.SlimeVR"
|
||||
|
||||
class VRServer @JvmOverloads constructor(
|
||||
bridgeProvider: BridgeProvider = { _, _ -> sequence {} },
|
||||
featureFlagsProvider: (VRServer) -> FeatureFlags = { _ -> FeatureFlags() },
|
||||
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
|
||||
flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null },
|
||||
vrcConfigHandlerProvider: (VRServer) -> VRCConfigHandler = { _ -> VRCConfigHandlerStub() },
|
||||
@@ -83,6 +85,9 @@ class VRServer @JvmOverloads constructor(
|
||||
@JvmField
|
||||
val deviceManager: DeviceManager
|
||||
|
||||
// UwU
|
||||
val featureFlags: FeatureFlags = featureFlagsProvider(this)
|
||||
|
||||
@JvmField
|
||||
val bvhRecorder: BVHRecorder
|
||||
|
||||
@@ -123,10 +128,11 @@ class VRServer @JvmOverloads constructor(
|
||||
|
||||
val networkProfileChecker: NetworkProfileChecker
|
||||
|
||||
val keybindHandler: KeybindHandler
|
||||
|
||||
val serverGuards = ServerGuards()
|
||||
|
||||
init {
|
||||
// UwU
|
||||
deviceManager = DeviceManager(this)
|
||||
serialHandler = serialHandlerProvider(this)
|
||||
serialFlashingHandler = flashingHandlerProvider(this)
|
||||
@@ -140,6 +146,7 @@ class VRServer @JvmOverloads constructor(
|
||||
vrcConfigManager = VRChatConfigManager(this, vrcConfigHandlerProvider(this))
|
||||
networkProfileChecker = networkProfileProvider(this)
|
||||
trackingChecklistManager = TrackingChecklistManager(this)
|
||||
keybindHandler = KeybindHandler(this)
|
||||
protocolAPI = ProtocolAPI(this)
|
||||
val computedTrackers = humanPoseManager.computedTrackers
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package dev.slimevr.config;
|
||||
|
||||
public class KeybindingsConfig {
|
||||
|
||||
private String fullResetBinding = "CTRL+ALT+SHIFT+Y";
|
||||
|
||||
private String yawResetBinding = "CTRL+ALT+SHIFT+U";
|
||||
|
||||
private String mountingResetBinding = "CTRL+ALT+SHIFT+I";
|
||||
|
||||
private String feetMountingResetBinding = "CTRL+ALT+SHIFT+P";
|
||||
|
||||
private String pauseTrackingBinding = "CTRL+ALT+SHIFT+O";
|
||||
|
||||
private long fullResetDelay = 0L;
|
||||
|
||||
private long yawResetDelay = 0L;
|
||||
|
||||
private long mountingResetDelay = 0L;
|
||||
|
||||
private long feetMountingResetDelay = 0L;
|
||||
|
||||
private long pauseTrackingDelay = 0L;
|
||||
|
||||
|
||||
public KeybindingsConfig() {
|
||||
}
|
||||
|
||||
public String getFullResetBinding() {
|
||||
return fullResetBinding;
|
||||
}
|
||||
|
||||
public String getYawResetBinding() {
|
||||
return yawResetBinding;
|
||||
}
|
||||
|
||||
public String getMountingResetBinding() {
|
||||
return mountingResetBinding;
|
||||
}
|
||||
|
||||
public String getFeetMountingResetBinding() {
|
||||
return feetMountingResetBinding;
|
||||
}
|
||||
|
||||
public String getPauseTrackingBinding() {
|
||||
return pauseTrackingBinding;
|
||||
}
|
||||
|
||||
public long getFullResetDelay() {
|
||||
return fullResetDelay;
|
||||
}
|
||||
|
||||
public void setFullResetDelay(long delay) {
|
||||
fullResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getYawResetDelay() {
|
||||
return yawResetDelay;
|
||||
}
|
||||
|
||||
public void setYawResetDelay(long delay) {
|
||||
yawResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getMountingResetDelay() {
|
||||
return mountingResetDelay;
|
||||
}
|
||||
|
||||
public void setMountingResetDelay(long delay) {
|
||||
mountingResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getFeetMountingResetDelay() {
|
||||
return feetMountingResetDelay;
|
||||
}
|
||||
|
||||
public void setFeetMountingResetDelay(long delay) {
|
||||
feetMountingResetDelay = delay;
|
||||
}
|
||||
|
||||
public long getPauseTrackingDelay() {
|
||||
return pauseTrackingDelay;
|
||||
}
|
||||
|
||||
public void setPauseTrackingDelay(long delay) {
|
||||
pauseTrackingDelay = delay;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package dev.slimevr.config
|
||||
|
||||
import solarxr_protocol.rpc.KeybindId
|
||||
|
||||
class KeybindingsConfig {
|
||||
val keybinds: MutableMap<Int, KeybindData> = mutableMapOf()
|
||||
|
||||
init {
|
||||
keybinds[KeybindId.FULL_RESET] =
|
||||
KeybindData(
|
||||
KeybindId.FULL_RESET,
|
||||
"full-reset",
|
||||
"CTRL+ALT+SHIFT+Y",
|
||||
0f,
|
||||
)
|
||||
keybinds[KeybindId.YAW_RESET] =
|
||||
KeybindData(
|
||||
KeybindId.YAW_RESET,
|
||||
"yaw-reset",
|
||||
"CTRL+ALT+SHIFT+U",
|
||||
0f,
|
||||
)
|
||||
keybinds[KeybindId.MOUNTING_RESET] =
|
||||
KeybindData(
|
||||
KeybindId.MOUNTING_RESET,
|
||||
"mounting-reset",
|
||||
"CTRL+ALT+SHIFT+I",
|
||||
0f,
|
||||
)
|
||||
keybinds[KeybindId.FEET_MOUNTING_RESET] =
|
||||
KeybindData(
|
||||
KeybindId.FEET_MOUNTING_RESET,
|
||||
"feet-mounting-reset",
|
||||
"CTRL+ALT+SHIFT+P",
|
||||
0f,
|
||||
)
|
||||
keybinds[KeybindId.PAUSE_TRACKING] =
|
||||
KeybindData(
|
||||
KeybindId.PAUSE_TRACKING,
|
||||
"pause-tracking",
|
||||
"CTRL+ALT+SHIFT+O",
|
||||
0f,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class KeybindData(
|
||||
var id: Int,
|
||||
var name: String,
|
||||
var binding: String,
|
||||
var delay: Float,
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
package dev.slimevr.keybind
|
||||
|
||||
import dev.slimevr.VRServer
|
||||
import dev.slimevr.config.KeybindingsConfig
|
||||
import solarxr_protocol.rpc.KeybindT
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class KeybindHandler(val vrServer: VRServer) {
|
||||
private val listeners: MutableList<KeybindListener> = CopyOnWriteArrayList()
|
||||
var keybinds: MutableList<KeybindT> = mutableListOf()
|
||||
var defaultKeybinds: MutableList<KeybindT> = mutableListOf()
|
||||
|
||||
init {
|
||||
createKeybinds()
|
||||
}
|
||||
|
||||
fun addListener(listener: KeybindListener) {
|
||||
this.listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: KeybindListener) {
|
||||
listeners.removeIf { listener == it }
|
||||
}
|
||||
|
||||
private fun createKeybinds() {
|
||||
keybinds.clear()
|
||||
defaultKeybinds.clear()
|
||||
vrServer.configManager.vrConfig.keybindings.keybinds.forEach { (i, keybind) ->
|
||||
keybinds.add(
|
||||
KeybindT().apply {
|
||||
keybindId = keybind.id
|
||||
keybindNameId = keybind.name
|
||||
keybindValue = keybind.binding
|
||||
keybindDelay = keybind.delay
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val binds = KeybindingsConfig().keybinds
|
||||
binds.forEach { (i, keybind) ->
|
||||
defaultKeybinds.add(
|
||||
KeybindT().apply {
|
||||
keybindId = keybind.id
|
||||
keybindNameId = keybind.name
|
||||
keybindValue = keybind.binding
|
||||
keybindDelay = keybind.delay
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateKeybinds() {
|
||||
createKeybinds()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.slimevr.keybind
|
||||
|
||||
interface KeybindListener {
|
||||
fun sendKeybind()
|
||||
fun onKeybindUpdate()
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import dev.slimevr.protocol.datafeed.createTrackerId
|
||||
import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler
|
||||
import dev.slimevr.protocol.rpc.firmware.RPCFirmwareUpdateHandler
|
||||
import dev.slimevr.protocol.rpc.games.vrchat.RPCVRChatHandler
|
||||
import dev.slimevr.protocol.rpc.installinfo.RPCInstallInfoHandler
|
||||
import dev.slimevr.protocol.rpc.keybinds.RPCKeybindHandler
|
||||
import dev.slimevr.protocol.rpc.openuri.RPCOpenUriHandler
|
||||
import dev.slimevr.protocol.rpc.reset.RPCResetHandler
|
||||
import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler
|
||||
import dev.slimevr.protocol.rpc.serial.RPCSerialHandler
|
||||
@@ -52,6 +55,15 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler<RpcMessageHeade
|
||||
RPCVRChatHandler(this, api)
|
||||
RPCTrackingChecklistHandler(this, api)
|
||||
RPCUserHeightCalibration(this, api)
|
||||
RPCInstallInfoHandler(this, api)
|
||||
RPCOpenUriHandler(this, api)
|
||||
try {
|
||||
RPCKeybindHandler(this, api)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} catch (t: Throwable) {
|
||||
t.printStackTrace()
|
||||
}
|
||||
|
||||
registerPacketListener(
|
||||
RpcMessage.AssignTrackerRequest,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package dev.slimevr.protocol.rpc.installinfo
|
||||
|
||||
import com.google.flatbuffers.FlatBufferBuilder
|
||||
import dev.slimevr.protocol.GenericConnection
|
||||
import dev.slimevr.protocol.ProtocolAPI
|
||||
import dev.slimevr.protocol.rpc.RPCHandler
|
||||
import io.eiren.util.logging.LogManager
|
||||
import solarxr_protocol.rpc.InstalledInfoResponse.createInstalledInfoResponse
|
||||
import solarxr_protocol.rpc.RpcMessage
|
||||
import solarxr_protocol.rpc.RpcMessageHeader
|
||||
import java.io.IOException
|
||||
|
||||
class RPCInstallInfoHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
|
||||
|
||||
init {
|
||||
rpcHandler.registerPacketListener(RpcMessage.InstalledInfoRequest, ::onInstalledInfoRequest)
|
||||
}
|
||||
|
||||
fun onInstalledInfoRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
|
||||
if (api.server.featureFlags.skipCheckUdev) {
|
||||
return
|
||||
}
|
||||
|
||||
val command = if (api.server.featureFlags.steam) {
|
||||
arrayOf("steam-runtime-launch-client", "--alongside-steam", "--", "udevadm", "cat")
|
||||
} else {
|
||||
arrayOf("udevadm", "cat")
|
||||
}
|
||||
val udevResponse = executeShellCommand(*command)
|
||||
|
||||
if (udevResponse == null) {
|
||||
LogManager.warning("Server couldn't verify if udev is installed")
|
||||
return
|
||||
}
|
||||
val isUdevInstalled = udevResponse.contains("slime")
|
||||
val isWayland = System.getenv("XDG_SESSION_TYPE").lowercase().contains("wayland")
|
||||
val fbb = FlatBufferBuilder(1024)
|
||||
val outbound = this.rpcHandler.createRPCMessage(
|
||||
fbb,
|
||||
RpcMessage.InstalledInfoResponse,
|
||||
createInstalledInfoResponse(fbb, isUdevInstalled, isWayland),
|
||||
)
|
||||
fbb.finish(outbound)
|
||||
conn.send(fbb.dataBuffer())
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeShellCommand(vararg command: String): String? = try {
|
||||
val process = ProcessBuilder(*command)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.inputStream.bufferedReader().readText().also {
|
||||
process.waitFor()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
LogManager.warning("Error executing shell command", e)
|
||||
null
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package dev.slimevr.protocol.rpc.keybinds
|
||||
|
||||
import com.google.flatbuffers.FlatBufferBuilder
|
||||
import dev.slimevr.config.KeybindData
|
||||
import dev.slimevr.keybind.KeybindListener
|
||||
import dev.slimevr.protocol.GenericConnection
|
||||
import dev.slimevr.protocol.ProtocolAPI
|
||||
import dev.slimevr.protocol.rpc.RPCHandler
|
||||
import jdk.internal.joptsimple.internal.Messages.message
|
||||
import solarxr_protocol.rpc.ChangeKeybindRequest
|
||||
import solarxr_protocol.rpc.KeybindId
|
||||
import solarxr_protocol.rpc.KeybindRequest
|
||||
import solarxr_protocol.rpc.KeybindResponse
|
||||
import solarxr_protocol.rpc.KeybindResponseT
|
||||
import solarxr_protocol.rpc.KeybindT
|
||||
import solarxr_protocol.rpc.RpcMessage
|
||||
import solarxr_protocol.rpc.RpcMessageHeader
|
||||
|
||||
class RPCKeybindHandler(
|
||||
var rpcHandler: RPCHandler,
|
||||
var api: ProtocolAPI,
|
||||
) : KeybindListener {
|
||||
|
||||
val keybindingConfig = api.server.configManager.vrConfig.keybindings
|
||||
|
||||
init {
|
||||
this.api.server.keybindHandler.addListener(this)
|
||||
|
||||
rpcHandler.registerPacketListener(RpcMessage.KeybindRequest, ::onKeybindRequest)
|
||||
rpcHandler.registerPacketListener(RpcMessage.ChangeKeybindRequest, ::onChangeKeybindRequest)
|
||||
}
|
||||
|
||||
// TODO: Figure out a way to "refresh" the keybind array here.
|
||||
private fun buildKeybindResponse(fbb: FlatBufferBuilder): Int = KeybindResponse.pack(
|
||||
fbb,
|
||||
KeybindResponseT().apply {
|
||||
keybind = api.server.keybindHandler.keybinds.toTypedArray()
|
||||
defaultKeybinds = api.server.keybindHandler.defaultKeybinds.toTypedArray()
|
||||
},
|
||||
)
|
||||
|
||||
private fun onKeybindRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
|
||||
val fbb = FlatBufferBuilder(32)
|
||||
val response = buildKeybindResponse(fbb)
|
||||
val outbound = rpcHandler.createRPCMessage(
|
||||
fbb,
|
||||
RpcMessage.KeybindResponse,
|
||||
response,
|
||||
)
|
||||
fbb.finish(outbound)
|
||||
conn.send(fbb.dataBuffer())
|
||||
}
|
||||
|
||||
private fun onChangeKeybindRequest(conn: GenericConnection, messageHeader: RpcMessageHeader) {
|
||||
val req = (messageHeader.message(ChangeKeybindRequest()) as ChangeKeybindRequest).unpack()
|
||||
|
||||
keybindingConfig.keybinds[req.keybind.keybindId] = KeybindData(req.keybind.keybindId, req.keybind.keybindNameId, req.keybind.keybindValue, req.keybind.keybindDelay)
|
||||
|
||||
api.server.configManager.saveConfig()
|
||||
api.server.keybindHandler.updateKeybinds()
|
||||
}
|
||||
|
||||
override fun onKeybindUpdate() {
|
||||
}
|
||||
|
||||
override fun sendKeybind() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.slimevr.protocol.rpc.openuri
|
||||
|
||||
import dev.hannah.portals.PortalManager
|
||||
import dev.slimevr.SLIMEVR_IDENTIFIER
|
||||
import dev.slimevr.protocol.GenericConnection
|
||||
import dev.slimevr.protocol.ProtocolAPI
|
||||
import dev.slimevr.protocol.rpc.RPCHandler
|
||||
import solarxr_protocol.rpc.RpcMessage
|
||||
import solarxr_protocol.rpc.RpcMessageHeader
|
||||
|
||||
class RPCOpenUriHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) {
|
||||
|
||||
init {
|
||||
rpcHandler.registerPacketListener(RpcMessage.OpenUriRequest, ::onOpenUriRequest)
|
||||
}
|
||||
|
||||
fun onOpenUriRequest(conn: GenericConnection, messageHeader: RpcMessageHeader?) {
|
||||
val portalManager = PortalManager(SLIMEVR_IDENTIFIER)
|
||||
portalManager.openGlobalShortcutsSettings()
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public class ProvisioningHandler implements SerialListener {
|
||||
this.provisioningTickTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isRunning || provisioningStatus == ProvisioningStatus.DONE)
|
||||
if (!isRunning)
|
||||
return;
|
||||
provisioningTick();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
Math.PI.toFloat(),
|
||||
0f,
|
||||
).toQuaternion()
|
||||
private val QuarterPitch = Quaternion.rotationAroundXAxis(FastMath.HALF_PI)
|
||||
|
||||
private var driftAmount = 0f
|
||||
private var averagedDriftQuat = Quaternion.IDENTITY
|
||||
private var rotationSinceReset = Quaternion.IDENTITY
|
||||
@@ -52,16 +54,6 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
|
||||
// Reference adjustment quats
|
||||
|
||||
/**
|
||||
* Gyro fix is set by full reset. This sets the current y rotation to 0, correcting
|
||||
* for initial yaw rotation and the rotation incurred by mounting orientation. This
|
||||
* is a local offset in rotation and does not affect the axes of rotation.
|
||||
*
|
||||
* This rotation is only used to compute [attachmentFix], otherwise [yawFix] would
|
||||
* correct for the same rotation.
|
||||
*/
|
||||
private var gyroFix = Quaternion.IDENTITY
|
||||
|
||||
/**
|
||||
* Attachment fix is set by full reset. This sets the current x and z rotations to
|
||||
* 0, correcting for initial pitch and roll rotation. This is a global offset in
|
||||
@@ -188,12 +180,9 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
/**
|
||||
* Get the reference adjusted accel.
|
||||
*/
|
||||
// TODO: Make this actually adjusted to the corrected IMU heading. The current
|
||||
// implementation for heading correction doesn't appear to be correct and may simply
|
||||
// make acceleration worse, so I'm just leaving this until we work that out. The
|
||||
// output of this will be world space, but with an unknown offset to heading (yaw).
|
||||
// - Butterscotch
|
||||
fun getReferenceAdjustedAccel(rawRot: Quaternion, accel: Vector3): Vector3 = rawRot.sandwich(accel)
|
||||
// All IMU axis corrections are inverse to undo `adjustToReference` after local yaw offsets are added
|
||||
// Order is VERY important here! Please be extremely careful! >~>
|
||||
fun getReferenceAdjustedAccel(rawRot: Quaternion, accel: Vector3): Vector3 = (adjustToReference(rawRot) * (attachmentFix * mountingOrientation * mountRotFix * tposeDownFix).inv()).sandwich(accel)
|
||||
|
||||
/**
|
||||
* Converts raw or filtered rotation into reference- and
|
||||
@@ -202,22 +191,17 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
*/
|
||||
private fun adjustToReference(rotation: Quaternion): Quaternion {
|
||||
var rot = rotation
|
||||
// Align heading axis with bone space
|
||||
if (!tracker.isHmd || tracker.trackerPosition != TrackerPosition.HEAD) {
|
||||
rot *= mountingOrientation
|
||||
}
|
||||
// Heading correction assuming manual orientation is correct
|
||||
rot = gyroFix * rot
|
||||
// Align attitude axes with bone space
|
||||
// Correct for global pitch/roll offset
|
||||
rot *= attachmentFix
|
||||
// Secondary heading axis alignment with bone space for automatic mounting
|
||||
// Note: Applying an inverse amount of heading correction corresponding to the
|
||||
// axis alignment quaternion will leave the correction to another variable
|
||||
rot = mountRotFix.inv() * (rot * mountRotFix)
|
||||
// More attitude axes alignment specifically for the t-pose configuration, this
|
||||
// probably shouldn't be a separate variable from attachmentFix?
|
||||
// Correct for global yaw offset without affecting local yaw so we can change this
|
||||
// later without invalidating local yaw offset corrections
|
||||
if (!tracker.isHmd || tracker.trackerPosition != TrackerPosition.HEAD) {
|
||||
rot = mountingOrientation.inv() * rot * mountingOrientation
|
||||
}
|
||||
rot = mountRotFix.inv() * rot * mountRotFix
|
||||
// T-pose global correction
|
||||
rot *= tposeDownFix
|
||||
// More heading correction
|
||||
// Align local yaw with reference
|
||||
rot = yawFix * rot
|
||||
rot = constraintFix * rot
|
||||
return rot
|
||||
@@ -227,8 +211,6 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
* Converts raw or filtered rotation into zero-reference-adjusted by
|
||||
* applying quaternions produced after full reset and yaw reset only
|
||||
*/
|
||||
// This is essentially just adjustToReference but aligning to quaternion identity
|
||||
// rather than to the bone.
|
||||
private fun adjustToIdentity(rotation: Quaternion): Quaternion {
|
||||
var rot = rotation
|
||||
rot = gyroFixNoMounting * rot
|
||||
@@ -283,15 +265,23 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
lastResetQuaternion = oldRot
|
||||
|
||||
// Adjust raw rotation to mountingOrientation
|
||||
val mountingAdjustedRotation = tracker.getRawRotation() * mountingOrientation
|
||||
val rotation = tracker.getRawRotation()
|
||||
|
||||
// Gyrofix
|
||||
if (tracker.allowMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) {
|
||||
gyroFix = if (tracker.isComputed) {
|
||||
fixGyroscope(tracker.getRawRotation())
|
||||
val gyroFix = if (tracker.allowMounting || (tracker.trackerPosition == TrackerPosition.HEAD && !tracker.isHmd)) {
|
||||
if (tracker.isComputed) {
|
||||
fixGyroscope(rotation)
|
||||
} else {
|
||||
fixGyroscope(mountingAdjustedRotation * tposeDownFix)
|
||||
if (tracker.trackerPosition.isFoot()) {
|
||||
// Feet are rotated by 90 deg pitch, this means we're relying on IMU rotation
|
||||
// to be set correctly here.
|
||||
fixGyroscope(rotation * tposeDownFix * QuarterPitch)
|
||||
} else {
|
||||
fixGyroscope(rotation * tposeDownFix)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Quaternion.IDENTITY
|
||||
}
|
||||
|
||||
// Mounting for computed trackers
|
||||
@@ -306,7 +296,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
if (resetHmdPitch) {
|
||||
// Reset the HMD's pitch if it's assigned to head and resetHmdPitch is true
|
||||
// Get rotation without yaw (make sure to use the raw rotation directly!)
|
||||
val rotBuf = getYawQuaternion(tracker.getRawRotation()).inv() * tracker.getRawRotation()
|
||||
val rotBuf = getYawQuaternion(rotation).inv() * rotation
|
||||
// Isolate pitch
|
||||
Quaternion(rotBuf.w, -rotBuf.x, 0f, 0f).unit()
|
||||
} else {
|
||||
@@ -314,7 +304,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
Quaternion.IDENTITY
|
||||
}
|
||||
} else {
|
||||
fixAttachment(mountingAdjustedRotation)
|
||||
(gyroFix * rotation).inv()
|
||||
}
|
||||
|
||||
// Rotate attachmentFix by 180 degrees as a workaround for t-pose (down)
|
||||
@@ -326,7 +316,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
|
||||
// Don't adjust yaw if head and computed
|
||||
if (tracker.trackerPosition != TrackerPosition.HEAD || !tracker.isComputed) {
|
||||
yawFix = fixYaw(mountingAdjustedRotation, reference)
|
||||
yawFix = gyroFix * reference.project(Vector3.POS_Y).unit()
|
||||
tracker.yawResetSmoothing.reset()
|
||||
}
|
||||
|
||||
@@ -370,7 +360,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
lastResetQuaternion = oldRot
|
||||
|
||||
val yawFixOld = yawFix
|
||||
yawFix = fixYaw(tracker.getRawRotation() * mountingOrientation, reference)
|
||||
yawFix = fixYaw(tracker.getRawRotation(), reference)
|
||||
tracker.yawResetSmoothing.reset()
|
||||
|
||||
makeIdentityAdjustmentQuatsYaw()
|
||||
@@ -409,9 +399,9 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
constraintFix = Quaternion.IDENTITY
|
||||
|
||||
// Get the current calibrated rotation
|
||||
var rotBuf = adjustToDrift(tracker.getRawRotation() * mountingOrientation)
|
||||
rotBuf = gyroFix * rotBuf
|
||||
var rotBuf = adjustToDrift(tracker.getRawRotation())
|
||||
rotBuf *= attachmentFix
|
||||
rotBuf = mountingOrientation.inv() * rotBuf * mountingOrientation
|
||||
rotBuf = yawFix * rotBuf
|
||||
|
||||
// Adjust buffer to reference
|
||||
@@ -467,14 +457,22 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
mountRotFix = Quaternion.IDENTITY
|
||||
}
|
||||
|
||||
private fun fixGyroscope(sensorRotation: Quaternion): Quaternion = getYawQuaternion(sensorRotation).inv()
|
||||
|
||||
private fun fixAttachment(sensorRotation: Quaternion): Quaternion = (gyroFix * sensorRotation).inv()
|
||||
// EulerOrder.YXZ is actually better for gyroscope fix, as it can get yaw at any roll.
|
||||
// Consequentially, instead of the roll being limited, the pitch is limited to
|
||||
// 90 degrees from the yaw plane. This means trackers may be mounted upside down
|
||||
// or with incorrectly configured IMU rotation, but we will need to compensate for
|
||||
// the pitch.
|
||||
private fun fixGyroscope(sensorRotation: Quaternion): Quaternion = getYawQuaternion(sensorRotation, EulerOrder.YXZ).inv()
|
||||
|
||||
private fun fixYaw(sensorRotation: Quaternion, reference: Quaternion): Quaternion {
|
||||
var rot = gyroFix * sensorRotation
|
||||
rot *= attachmentFix
|
||||
rot = mountRotFix.inv() * (rot * mountRotFix)
|
||||
var rot = sensorRotation * attachmentFix
|
||||
// We need to fix the global yaw offset for the euler yaw calculation
|
||||
if (!tracker.isHmd || tracker.trackerPosition != TrackerPosition.HEAD) {
|
||||
rot = mountingOrientation.inv() * rot * mountingOrientation
|
||||
}
|
||||
rot = mountRotFix.inv() * rot * mountRotFix
|
||||
// TODO: Get diff from ref to rot, use euler angle (YZX) yaw as output.
|
||||
// This prevents pitch and roll from affecting the alignment.
|
||||
rot = getYawQuaternion(rot)
|
||||
return rot.inv() * reference.project(Vector3.POS_Y).unit()
|
||||
}
|
||||
@@ -485,7 +483,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
// In both cases, the isolated yaw value changes
|
||||
// with the tracker's roll when pointing forward.
|
||||
// calling twinNearest() makes sure this rotation has the wanted polarity (+-).
|
||||
private fun getYawQuaternion(rot: Quaternion): Quaternion = EulerAngles(EulerOrder.YZX, 0f, rot.toEulerAngles(EulerOrder.YZX).y, 0f).toQuaternion().twinNearest(rot)
|
||||
private fun getYawQuaternion(rot: Quaternion, order: EulerOrder = EulerOrder.YZX): Quaternion = EulerAngles(order, 0f, rot.toEulerAngles(order).y, 0f).toQuaternion().twinNearest(rot)
|
||||
|
||||
private fun makeIdentityAdjustmentQuatsFull() {
|
||||
val sensorRotation = tracker.getRawRotation()
|
||||
|
||||
@@ -11,7 +11,6 @@ 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 io.github.axisangles.ktmath.Quaternion
|
||||
import solarxr_protocol.datatypes.DeviceIdT
|
||||
import solarxr_protocol.datatypes.TrackerIdT
|
||||
import solarxr_protocol.rpc.*
|
||||
@@ -200,8 +199,7 @@ class TrackingChecklistManager(private val vrServer: VRServer) : VRCConfigListen
|
||||
}
|
||||
// We ask for a full reset if you need to do mounting calibration but cant because you haven't done full reset in a while
|
||||
// or if you have trackers that need reset after re-assigning
|
||||
val usingSavedCalibration = vrServer.configManager.vrConfig.resetsConfig.saveMountingReset && imuTrackers.all { it.resetsHandler.mountRotFix != Quaternion.IDENTITY }
|
||||
val needFullReset = (vrServer.configManager.vrConfig.resetsConfig.lastMountingMethod == MountingMethods.AUTOMATIC && !usingSavedCalibration && !resetMountingCompleted && !vrServer.serverGuards.canDoMounting) || trackerRequireReset.isNotEmpty()
|
||||
val needFullReset = (!resetMountingCompleted && !vrServer.serverGuards.canDoMounting) || trackerRequireReset.isNotEmpty()
|
||||
updateValidity(TrackingChecklistStepId.FULL_RESET, !needFullReset) {
|
||||
it.enabled = imuTrackers.isNotEmpty()
|
||||
if (trackerRequireReset.isNotEmpty()) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.eiren.util
|
||||
|
||||
import dev.slimevr.SLIMEVR_IDENTIFIER
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
@@ -38,6 +39,12 @@ enum class OperatingSystem(
|
||||
var dir = System.getenv("SLIMEVR_SOCKET_DIR")
|
||||
if (dir != null) return dir
|
||||
if (currentPlatform == LINUX) {
|
||||
val isPressureVessel = System.getenv("PRESSURE_VESSEL_RUNTIME")?.isNotEmpty() == true
|
||||
if (isPressureVessel) {
|
||||
dir = System.getenv("XDG_CONFIG_HOME")?.let { Path(it, SLIMEVR_IDENTIFIER).toString() }
|
||||
?: System.getenv("HOME")?.let { Path(it, ".local", "share", SLIMEVR_IDENTIFIER).toString() }
|
||||
if (dir != null) return dir
|
||||
}
|
||||
dir = System.getenv("XDG_RUNTIME_DIR")
|
||||
if (dir != null) return dir
|
||||
}
|
||||
|
||||
@@ -93,13 +93,13 @@ class SkeletonResetTests {
|
||||
TrackerPosition.HIP,
|
||||
TrackerPosition.LEFT_LOWER_LEG,
|
||||
TrackerPosition.RIGHT_LOWER_LEG,
|
||||
-> mountRot * Quaternion.SLIMEVR.FRONT
|
||||
-> mountRot
|
||||
|
||||
TrackerPosition.LEFT_UPPER_LEG,
|
||||
TrackerPosition.RIGHT_UPPER_LEG,
|
||||
-> mountRot
|
||||
-> mountRot * Quaternion.SLIMEVR.FRONT
|
||||
|
||||
else -> mountRot
|
||||
else -> mountRot * Quaternion.SLIMEVR.FRONT
|
||||
}
|
||||
val actualMounting = tracker.resetsHandler.mountRotFix
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ tasks.withType<Javadoc> {
|
||||
allprojects {
|
||||
repositories {
|
||||
// Use jcenter for resolving dependencies.
|
||||
// You can declare any Maven/Ivy/file repository here.
|
||||
mavenCentral()
|
||||
maven(url = "https://jitpack.io")
|
||||
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
|
||||
@@ -74,7 +73,7 @@ tasks.shadowJar {
|
||||
exclude(dependency("com.fazecast:jSerialComm:.*"))
|
||||
exclude(dependency("net.java.dev.jna:.*:.*"))
|
||||
exclude(dependency("com.google.flatbuffers:flatbuffers-java:.*"))
|
||||
|
||||
exclude(dependency("com.github.HannahPadd:DbusGlobalShortcutsWayland:v0.1.0"))
|
||||
exclude(project(":solarxr-protocol"))
|
||||
}
|
||||
archiveBaseName.set("slimevr")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
package dev.slimevr.desktop
|
||||
|
||||
import dev.slimevr.FeatureFlags
|
||||
import dev.slimevr.Keybinding
|
||||
import dev.slimevr.SLIMEVR_IDENTIFIER
|
||||
import dev.slimevr.VRServer
|
||||
@@ -9,6 +10,7 @@ import dev.slimevr.bridge.Bridge
|
||||
import dev.slimevr.config.ConfigManager
|
||||
import dev.slimevr.desktop.firmware.DesktopSerialFlashingHandler
|
||||
import dev.slimevr.desktop.games.vrchat.DesktopVRCConfigHandler
|
||||
import dev.slimevr.desktop.install.drivers.InstallDrivers
|
||||
import dev.slimevr.desktop.platform.SteamVRBridge
|
||||
import dev.slimevr.desktop.platform.linux.UnixSocketBridge
|
||||
import dev.slimevr.desktop.platform.linux.UnixSocketRpcBridge
|
||||
@@ -43,6 +45,8 @@ val VERSION =
|
||||
(GIT_VERSION_TAG.ifEmpty { GIT_COMMIT_HASH }) +
|
||||
if (GIT_CLEAN) "" else "-dirty"
|
||||
|
||||
val featureFlags = FeatureFlags()
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
System.setProperty("awt.useSystemAAFontSettings", "on")
|
||||
System.setProperty("swing.aatext", "true")
|
||||
@@ -50,23 +54,38 @@ fun main(args: Array<String>) {
|
||||
val parser: CommandLineParser = DefaultParser()
|
||||
val formatter = HelpFormatter()
|
||||
val options = Options()
|
||||
val isLinux = OperatingSystem.currentPlatform == OperatingSystem.LINUX
|
||||
options.addOption("h", "help", false, "Show help")
|
||||
options.addOption("V", "version", false, "Show version")
|
||||
options.addOption("i", "install", true, "Run the driver install")
|
||||
options.addOption("s", "steam", true, "Run the server in steam mode")
|
||||
if (isLinux) {
|
||||
options.addOption("u", "no-udev", false, "Skip checking if udev rules are installed")
|
||||
}
|
||||
|
||||
val cmd: CommandLine = try {
|
||||
parser.parse(options, args, true)
|
||||
} catch (e: org.apache.commons.cli.ParseException) {
|
||||
formatter.printHelp("slimevr.jar", options)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
if (cmd.hasOption("help")) {
|
||||
formatter.printHelp("slimevr.jar", options)
|
||||
exitProcess(0)
|
||||
}
|
||||
if (cmd.hasOption("version")) {
|
||||
println("SlimeVR Server $VERSION")
|
||||
LogManager.info("SlimeVR Server $VERSION")
|
||||
exitProcess(0)
|
||||
}
|
||||
if (cmd.hasOption("install")) {
|
||||
val installDrivers = InstallDrivers()
|
||||
installDrivers.runInstaller()
|
||||
exitProcess(0)
|
||||
}
|
||||
if (cmd.hasOption("steam")) {
|
||||
featureFlags.steam = true
|
||||
}
|
||||
featureFlags.skipCheckUdev = !isLinux || cmd.hasOption("no-udev")
|
||||
|
||||
if (cmd.args.isEmpty()) {
|
||||
System.err.println("No command specified, expected 'run'")
|
||||
@@ -99,6 +118,12 @@ fun main(args: Array<String>) {
|
||||
return
|
||||
}
|
||||
|
||||
val isInstallDisabled = System.getenv("SLIME_SERVER_DISABLE_INSTALLER")?.toInt()
|
||||
if (featureFlags.steam && isInstallDisabled != 1) {
|
||||
val installDrivers = InstallDrivers()
|
||||
installDrivers.runInstaller()
|
||||
}
|
||||
|
||||
val configDir = resolveConfig()
|
||||
LogManager.info("Using config dir: $configDir")
|
||||
|
||||
@@ -126,6 +151,7 @@ fun main(args: Array<String>) {
|
||||
try {
|
||||
val vrServer = VRServer(
|
||||
::provideBridges,
|
||||
{ _ -> featureFlags },
|
||||
{ _ -> DesktopSerialHandler() },
|
||||
{ _ -> DesktopSerialFlashingHandler() },
|
||||
{ _ -> DesktopVRCConfigHandler() },
|
||||
@@ -133,7 +159,6 @@ fun main(args: Array<String>) {
|
||||
configManager = configManager,
|
||||
)
|
||||
vrServer.start()
|
||||
|
||||
// Start service for USB HID trackers
|
||||
DesktopHIDManager(
|
||||
"Sensors HID service",
|
||||
@@ -218,7 +243,6 @@ fun provideBridges(
|
||||
)
|
||||
yield(linuxBridge)
|
||||
}
|
||||
|
||||
yield(
|
||||
UnixSocketBridge(
|
||||
server,
|
||||
|
||||
@@ -19,6 +19,7 @@ abstract class AbstractRegEdit {
|
||||
abstract fun getQwordValue(path: String, key: String): Double?
|
||||
abstract fun getDwordValue(path: String, key: String): Int?
|
||||
abstract fun getVRChatKeys(path: String): Map<String, String>
|
||||
abstract fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map<String, String>
|
||||
}
|
||||
|
||||
class RegEditWindows : AbstractRegEdit() {
|
||||
@@ -74,6 +75,19 @@ class RegEditWindows : AbstractRegEdit() {
|
||||
}
|
||||
return keysMap
|
||||
}
|
||||
|
||||
override fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map<String, String> {
|
||||
val keysMap = mutableMapOf<String, String>()
|
||||
|
||||
try {
|
||||
Advapi32Util.registryGetValues(hkey, path).forEach {
|
||||
keysMap[it.key.replace("""_h\d+$""".toRegex(), "")] = it.value.toString()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.severe("[RegEdit] Error reading values from registry", e)
|
||||
}
|
||||
return keysMap
|
||||
}
|
||||
}
|
||||
|
||||
class RegEditLinux : AbstractRegEdit() {
|
||||
@@ -141,6 +155,9 @@ class RegEditLinux : AbstractRegEdit() {
|
||||
return keysMap
|
||||
}
|
||||
|
||||
// This function should never run on Linux.
|
||||
override fun getKeyByPath(hkey: WinReg.HKEY, path: String): Map<String, String> = mutableMapOf<String, String>()
|
||||
|
||||
companion object {
|
||||
const val USER_REG_SUBPATH = "steamapps/compatdata/438100/pfx/user.reg"
|
||||
val USER_REG_PATH =
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.slimevr.desktop.install.drivers
|
||||
|
||||
import io.eiren.util.logging.LogManager
|
||||
|
||||
class InstallDrivers {
|
||||
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
|
||||
fun runInstaller() {
|
||||
if (os.contains("linux")) {
|
||||
val linuxUpdater = Linux()
|
||||
linuxUpdater.updateLinux()
|
||||
} else if (os.contains("windows")) {
|
||||
val windowsUpdater = Windows()
|
||||
windowsUpdater.updateWindows()
|
||||
} else {
|
||||
LogManager.warning("Updater doesn't support operating system '$os'")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package dev.slimevr.desktop.install.drivers
|
||||
|
||||
import io.eiren.util.logging.LogManager
|
||||
import java.io.IOException
|
||||
|
||||
fun executeShellCommand(vararg command: String): String? = try {
|
||||
val process = ProcessBuilder(*command)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.inputStream.bufferedReader().readText().also {
|
||||
process.waitFor()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
LogManager.warning("Error executing shell command", e)
|
||||
null
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package dev.slimevr.desktop.install.drivers
|
||||
|
||||
import dev.slimevr.desktop.featureFlags
|
||||
import io.eiren.util.logging.LogManager
|
||||
|
||||
class Linux {
|
||||
|
||||
val path: String = System.getProperty("user.dir")
|
||||
|
||||
fun updateLinux() {
|
||||
updateLinuxSteamVRDriver()
|
||||
feeder()
|
||||
}
|
||||
|
||||
fun updateLinuxSteamVRDriver() {
|
||||
val pathRegPath = "${System.getProperty("user.home")}/.steam/steam/steamapps/common/SteamVR/bin/vrpathreg.sh"
|
||||
val vrPathRegContents = executeShellCommand(pathRegPath)
|
||||
if (vrPathRegContents == null) {
|
||||
LogManager.warning("SteamVR driver installation failed")
|
||||
return
|
||||
}
|
||||
if (vrPathRegContents.contains("slimevr")) {
|
||||
LogManager.info("SteamVR driver is already installed")
|
||||
return
|
||||
}
|
||||
|
||||
executeShellCommand(pathRegPath, "adddriver", "$path/$LINUX_STEAM_DRIVER_DIRECTORY")
|
||||
|
||||
if (executeShellCommand(pathRegPath)?.contains("slimevr") != true) {
|
||||
LogManager.warning("Failed to install SteamVR driver")
|
||||
return
|
||||
}
|
||||
LogManager.info("SteamVR driver successfully installed")
|
||||
}
|
||||
|
||||
fun feeder() {
|
||||
executeShellCommand("chmod", "+x", "$path/$LINUX_FEEDER_DIRECTORY/SlimeVR-Feeder-App")
|
||||
|
||||
val command = if (featureFlags.steam) {
|
||||
arrayOf("steam-runtime-launch-client", "--alongside-steam", "--", "$path/$LINUX_FEEDER_DIRECTORY/SlimeVR-Feeder-App", "--install")
|
||||
} else {
|
||||
arrayOf("$path/$LINUX_FEEDER_DIRECTORY/SlimeVR-Feeder-App", "--install")
|
||||
}
|
||||
val feederOutput = executeShellCommand(*command)
|
||||
if (feederOutput == null) {
|
||||
LogManager.warning("Error installing feeder")
|
||||
return
|
||||
}
|
||||
if (feederOutput.lowercase().contains("manifest is not installed")) {
|
||||
LogManager.warning("Could not install feeder application")
|
||||
} else {
|
||||
LogManager.info("Successfully installed feeder application")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LINUX_STEAM_DRIVER_DIRECTORY = "slimevr-openvr-driver-x64-linux"
|
||||
private const val LINUX_FEEDER_DIRECTORY = "SlimeVR-Feeder-App-Linux"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package dev.slimevr.desktop.install.drivers
|
||||
|
||||
import com.sun.jna.platform.win32.WinReg
|
||||
import dev.slimevr.desktop.games.vrchat.RegEditWindows
|
||||
import io.eiren.util.logging.LogManager
|
||||
|
||||
class Windows {
|
||||
|
||||
val path: String = System.getProperty("user.dir")
|
||||
|
||||
fun updateWindows() {
|
||||
steamVRDriver()
|
||||
feeder()
|
||||
}
|
||||
|
||||
fun feeder() {
|
||||
val feederOutput = executeShellCommand("${path}\\${WINDOWS_FEEDER_DIRECTORY}\\SlimeVR-Feeder-App.exe", "--install")
|
||||
if (feederOutput == null) {
|
||||
LogManager.warning("Error installing feeder")
|
||||
return
|
||||
}
|
||||
if (feederOutput.lowercase().contains("manifest is not installed")) {
|
||||
LogManager.warning("Could not install feeder application")
|
||||
} else {
|
||||
LogManager.info("Successfully installed feeder application")
|
||||
}
|
||||
}
|
||||
|
||||
fun steamVRDriver() {
|
||||
val regEdit = RegEditWindows()
|
||||
val regQuery = regEdit.getKeyByPath(WinReg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App 250820")
|
||||
val steamVRLocation = regQuery["InstallLocation"]
|
||||
if (steamVRLocation == null || !steamVRLocation.contains("SteamVR")) {
|
||||
LogManager.warning("Can't find SteamVR, unable to install SteamVR driver")
|
||||
return
|
||||
}
|
||||
|
||||
val pathRegPath = "${steamVRLocation}\\bin\\win64\\vrpathreg.exe"
|
||||
val vrPathRegContents = executeShellCommand(pathRegPath, "finddriver", "slimevr")
|
||||
if (vrPathRegContents == null) {
|
||||
LogManager.warning("Error installing SteamVR driver")
|
||||
return
|
||||
}
|
||||
if (vrPathRegContents.contains("slimevr")) {
|
||||
LogManager.info("SteamVR driver is already installed")
|
||||
return
|
||||
}
|
||||
|
||||
executeShellCommand(pathRegPath, "adddriver", "${path}\\${WINDOWS_STEAMVR_DRIVER_DIRECTORY}")
|
||||
|
||||
if (executeShellCommand(pathRegPath, "finddriver", "slimevr")?.contains("slimevr") != true) {
|
||||
LogManager.warning("Failed to install SlimeVR driver")
|
||||
return
|
||||
}
|
||||
LogManager.info("SteamVR driver successfully installed")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WINDOWS_STEAMVR_DRIVER_DIRECTORY = "slimevr-openvr-driver-win64"
|
||||
private const val WINDOWS_FEEDER_DIRECTORY = "SlimeVR-Feeder-App-win64"
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import dev.slimevr.desktop.platform.ProtobufMessages.*
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import dev.slimevr.tracking.trackers.TrackerStatus
|
||||
import dev.slimevr.tracking.trackers.TrackerStatus.Companion.getById
|
||||
import dev.slimevr.tracking.trackers.TrackerUtils
|
||||
import dev.slimevr.util.ann.VRServerThread
|
||||
import io.eiren.util.ann.Synchronize
|
||||
import io.eiren.util.ann.ThreadSafe
|
||||
@@ -219,11 +218,6 @@ abstract class ProtobufBridge(@JvmField protected val bridgeName: String) : ISte
|
||||
|
||||
"mounting_reset" -> instance.resetTrackersMounting(resetSourceName)
|
||||
|
||||
"feet_mounting_reset" -> instance.resetTrackersMounting(
|
||||
resetSourceName,
|
||||
TrackerUtils.feetsBodyParts,
|
||||
)
|
||||
|
||||
"pause_tracking" ->
|
||||
instance
|
||||
.togglePauseTracking(resetSourceName)
|
||||
|
||||
@@ -3558,20 +3558,6 @@ public final class ProtobufMessages {
|
||||
* @return The trackerRole.
|
||||
*/
|
||||
int getTrackerRole();
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @return The manufacturer.
|
||||
*/
|
||||
java.lang.String getManufacturer();
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @return The bytes for manufacturer.
|
||||
*/
|
||||
com.google.protobuf.ByteString getManufacturerBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3602,7 +3588,6 @@ public final class ProtobufMessages {
|
||||
private TrackerAdded() {
|
||||
trackerSerial_ = "";
|
||||
trackerName_ = "";
|
||||
manufacturer_ = "";
|
||||
}
|
||||
|
||||
public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
|
||||
@@ -3728,48 +3713,6 @@ public final class ProtobufMessages {
|
||||
return trackerRole_;
|
||||
}
|
||||
|
||||
public static final int MANUFACTURER_FIELD_NUMBER = 5;
|
||||
@SuppressWarnings("serial")
|
||||
private volatile java.lang.Object manufacturer_ = "";
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @return The manufacturer.
|
||||
*/
|
||||
@java.lang.Override
|
||||
public java.lang.String getManufacturer() {
|
||||
java.lang.Object ref = manufacturer_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
return (java.lang.String) ref;
|
||||
} else {
|
||||
com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
manufacturer_ = s;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @return The bytes for manufacturer.
|
||||
*/
|
||||
@java.lang.Override
|
||||
public com.google.protobuf.ByteString getManufacturerBytes() {
|
||||
java.lang.Object ref = manufacturer_;
|
||||
if (ref instanceof java.lang.String) {
|
||||
com.google.protobuf.ByteString b = com.google.protobuf.ByteString
|
||||
.copyFromUtf8(
|
||||
(java.lang.String) ref
|
||||
);
|
||||
manufacturer_ = b;
|
||||
return b;
|
||||
} else {
|
||||
return (com.google.protobuf.ByteString) ref;
|
||||
}
|
||||
}
|
||||
|
||||
private byte memoizedIsInitialized = -1;
|
||||
|
||||
@java.lang.Override
|
||||
@@ -3799,9 +3742,6 @@ public final class ProtobufMessages {
|
||||
if (trackerRole_ != 0) {
|
||||
output.writeInt32(4, trackerRole_);
|
||||
}
|
||||
if (!com.google.protobuf.GeneratedMessage.isStringEmpty(manufacturer_)) {
|
||||
com.google.protobuf.GeneratedMessage.writeString(output, 5, manufacturer_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@@ -3826,9 +3766,6 @@ public final class ProtobufMessages {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeInt32Size(4, trackerRole_);
|
||||
}
|
||||
if (!com.google.protobuf.GeneratedMessage.isStringEmpty(manufacturer_)) {
|
||||
size += com.google.protobuf.GeneratedMessage.computeStringSize(5, manufacturer_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSize = size;
|
||||
return size;
|
||||
@@ -3864,11 +3801,6 @@ public final class ProtobufMessages {
|
||||
!= other.getTrackerRole()
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
!getManufacturer()
|
||||
.equals(other.getManufacturer())
|
||||
)
|
||||
return false;
|
||||
if (!getUnknownFields().equals(other.getUnknownFields()))
|
||||
return false;
|
||||
return true;
|
||||
@@ -3889,8 +3821,6 @@ public final class ProtobufMessages {
|
||||
hash = (53 * hash) + getTrackerName().hashCode();
|
||||
hash = (37 * hash) + TRACKER_ROLE_FIELD_NUMBER;
|
||||
hash = (53 * hash) + getTrackerRole();
|
||||
hash = (37 * hash) + MANUFACTURER_FIELD_NUMBER;
|
||||
hash = (53 * hash) + getManufacturer().hashCode();
|
||||
hash = (29 * hash) + getUnknownFields().hashCode();
|
||||
memoizedHashCode = hash;
|
||||
return hash;
|
||||
@@ -4063,7 +3993,6 @@ public final class ProtobufMessages {
|
||||
trackerSerial_ = "";
|
||||
trackerName_ = "";
|
||||
trackerRole_ = 0;
|
||||
manufacturer_ = "";
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -4115,9 +4044,6 @@ public final class ProtobufMessages {
|
||||
if (((from_bitField0_ & 0x00000008) != 0)) {
|
||||
result.trackerRole_ = trackerRole_;
|
||||
}
|
||||
if (((from_bitField0_ & 0x00000010) != 0)) {
|
||||
result.manufacturer_ = manufacturer_;
|
||||
}
|
||||
}
|
||||
|
||||
@java.lang.Override
|
||||
@@ -4157,11 +4083,6 @@ public final class ProtobufMessages {
|
||||
if (other.getTrackerRole() != 0) {
|
||||
setTrackerRole(other.getTrackerRole());
|
||||
}
|
||||
if (!other.getManufacturer().isEmpty()) {
|
||||
manufacturer_ = other.manufacturer_;
|
||||
bitField0_ |= 0x00000010;
|
||||
onChanged();
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
onChanged();
|
||||
return this;
|
||||
@@ -4209,11 +4130,6 @@ public final class ProtobufMessages {
|
||||
bitField0_ |= 0x00000008;
|
||||
break;
|
||||
} // case 32
|
||||
case 42: {
|
||||
manufacturer_ = input.readStringRequireUtf8();
|
||||
bitField0_ |= 0x00000010;
|
||||
break;
|
||||
} // case 42
|
||||
default: {
|
||||
if (!super.parseUnknownField(input, extensionRegistry, tag)) {
|
||||
done = true; // was an endgroup tag
|
||||
@@ -4482,93 +4398,6 @@ public final class ProtobufMessages {
|
||||
return this;
|
||||
}
|
||||
|
||||
private java.lang.Object manufacturer_ = "";
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @return The manufacturer.
|
||||
*/
|
||||
public java.lang.String getManufacturer() {
|
||||
java.lang.Object ref = manufacturer_;
|
||||
if (!(ref instanceof java.lang.String)) {
|
||||
com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
|
||||
java.lang.String s = bs.toStringUtf8();
|
||||
manufacturer_ = s;
|
||||
return s;
|
||||
} else {
|
||||
return (java.lang.String) ref;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @return The bytes for manufacturer.
|
||||
*/
|
||||
public com.google.protobuf.ByteString getManufacturerBytes() {
|
||||
java.lang.Object ref = manufacturer_;
|
||||
if (ref instanceof String) {
|
||||
com.google.protobuf.ByteString b = com.google.protobuf.ByteString
|
||||
.copyFromUtf8(
|
||||
(java.lang.String) ref
|
||||
);
|
||||
manufacturer_ = b;
|
||||
return b;
|
||||
} else {
|
||||
return (com.google.protobuf.ByteString) ref;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @param value The manufacturer to set.
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder setManufacturer(
|
||||
java.lang.String value
|
||||
) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
manufacturer_ = value;
|
||||
bitField0_ |= 0x00000010;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder clearManufacturer() {
|
||||
manufacturer_ = getDefaultInstance().getManufacturer();
|
||||
bitField0_ = (bitField0_ & ~0x00000010);
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <code>string manufacturer = 5;</code>
|
||||
*
|
||||
* @param value The bytes for manufacturer to set.
|
||||
* @return This builder for chaining.
|
||||
*/
|
||||
public Builder setManufacturerBytes(
|
||||
com.google.protobuf.ByteString value
|
||||
) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
checkByteStringIsUtf8(value);
|
||||
manufacturer_ = value;
|
||||
bitField0_ |= 0x00000010;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:messages.TrackerAdded)
|
||||
}
|
||||
|
||||
@@ -9043,55 +8872,53 @@ public final class ProtobufMessages {
|
||||
+
|
||||
"ctionArgumentsEntry\0326\n\024ActionArgumentsEn"
|
||||
+
|
||||
"try\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"|\n\014T"
|
||||
"try\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"f\n\014T"
|
||||
+
|
||||
"rackerAdded\022\022\n\ntracker_id\030\001 \001(\005\022\026\n\016track"
|
||||
+
|
||||
"er_serial\030\002 \001(\t\022\024\n\014tracker_name\030\003 \001(\t\022\024\n"
|
||||
+
|
||||
"\014tracker_role\030\004 \001(\005\022\024\n\014manufacturer\030\005 \001("
|
||||
"\014tracker_role\030\004 \001(\005\"\374\002\n\rTrackerStatus\022\022\n"
|
||||
+
|
||||
"\t\"\374\002\n\rTrackerStatus\022\022\n\ntracker_id\030\001 \001(\005\022"
|
||||
"\ntracker_id\030\001 \001(\005\022.\n\006status\030\002 \001(\0162\036.mess"
|
||||
+
|
||||
".\n\006status\030\002 \001(\0162\036.messages.TrackerStatus"
|
||||
"ages.TrackerStatus.Status\0221\n\005extra\030\003 \003(\013"
|
||||
+
|
||||
".Status\0221\n\005extra\030\003 \003(\0132\".messages.Tracke"
|
||||
"2\".messages.TrackerStatus.ExtraEntry\022;\n\n"
|
||||
+
|
||||
"rStatus.ExtraEntry\022;\n\nconfidence\030\004 \001(\0162\""
|
||||
"confidence\030\004 \001(\0162\".messages.TrackerStatu"
|
||||
+
|
||||
".messages.TrackerStatus.ConfidenceH\000\210\001\001\032"
|
||||
"s.ConfidenceH\000\210\001\001\032,\n\nExtraEntry\022\013\n\003key\030\001"
|
||||
+
|
||||
",\n\nExtraEntry\022\013\n\003key\030\001 \001(\t\022\r\n\005value\030\002 \001("
|
||||
" \001(\t\022\r\n\005value\030\002 \001(\t:\0028\001\"E\n\006Status\022\020\n\014DIS"
|
||||
+
|
||||
"\t:\0028\001\"E\n\006Status\022\020\n\014DISCONNECTED\020\000\022\006\n\002OK\020"
|
||||
"CONNECTED\020\000\022\006\n\002OK\020\001\022\010\n\004BUSY\020\002\022\t\n\005ERROR\020\003"
|
||||
+
|
||||
"\001\022\010\n\004BUSY\020\002\022\t\n\005ERROR\020\003\022\014\n\010OCCLUDED\020\004\"3\n\n"
|
||||
"\022\014\n\010OCCLUDED\020\004\"3\n\nConfidence\022\006\n\002NO\020\000\022\007\n\003"
|
||||
+
|
||||
"Confidence\022\006\n\002NO\020\000\022\007\n\003LOW\020\001\022\n\n\006MEDIUM\020\005\022"
|
||||
"LOW\020\001\022\n\n\006MEDIUM\020\005\022\010\n\004HIGH\020\nB\r\n\013_confiden"
|
||||
+
|
||||
"\010\n\004HIGH\020\nB\r\n\013_confidence\"I\n\007Battery\022\022\n\nt"
|
||||
"ce\"I\n\007Battery\022\022\n\ntracker_id\030\001 \001(\005\022\025\n\rbat"
|
||||
+
|
||||
"racker_id\030\001 \001(\005\022\025\n\rbattery_level\030\002 \001(\002\022\023"
|
||||
"tery_level\030\002 \001(\002\022\023\n\013is_charging\030\003 \001(\010\"\241\002"
|
||||
+
|
||||
"\n\013is_charging\030\003 \001(\010\"\241\002\n\017ProtobufMessage\022"
|
||||
"\n\017ProtobufMessage\022&\n\010position\030\001 \001(\0132\022.me"
|
||||
+
|
||||
"&\n\010position\030\001 \001(\0132\022.messages.PositionH\000\022"
|
||||
"ssages.PositionH\000\022+\n\013user_action\030\002 \001(\0132\024"
|
||||
+
|
||||
"+\n\013user_action\030\002 \001(\0132\024.messages.UserActi"
|
||||
".messages.UserActionH\000\022/\n\rtracker_added\030"
|
||||
+
|
||||
"onH\000\022/\n\rtracker_added\030\003 \001(\0132\026.messages.T"
|
||||
"\003 \001(\0132\026.messages.TrackerAddedH\000\0221\n\016track"
|
||||
+
|
||||
"rackerAddedH\000\0221\n\016tracker_status\030\004 \001(\0132\027."
|
||||
"er_status\030\004 \001(\0132\027.messages.TrackerStatus"
|
||||
+
|
||||
"messages.TrackerStatusH\000\022$\n\007battery\030\005 \001("
|
||||
"H\000\022$\n\007battery\030\005 \001(\0132\021.messages.BatteryH\000"
|
||||
+
|
||||
"\0132\021.messages.BatteryH\000\022$\n\007version\030\006 \001(\0132"
|
||||
"\022$\n\007version\030\006 \001(\0132\021.messages.VersionH\000B\t"
|
||||
+
|
||||
"\021.messages.VersionH\000B\t\n\007messageB2\n\034dev.s"
|
||||
"\n\007messageB2\n\034dev.slimevr.desktop.platfor"
|
||||
+
|
||||
"limevr.desktop.platformB\020ProtobufMessage"
|
||||
+
|
||||
"sH\003b\006proto3"
|
||||
"mB\020ProtobufMessagesH\003b\006proto3"
|
||||
};
|
||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||
.internalBuildGeneratedFileFrom(
|
||||
@@ -9130,8 +8957,7 @@ public final class ProtobufMessages {
|
||||
internal_static_messages_TrackerAdded_descriptor = getDescriptor().getMessageTypes().get(4);
|
||||
internal_static_messages_TrackerAdded_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_messages_TrackerAdded_descriptor,
|
||||
new java.lang.String[] { "TrackerId", "TrackerSerial", "TrackerName", "TrackerRole",
|
||||
"Manufacturer", }
|
||||
new java.lang.String[] { "TrackerId", "TrackerSerial", "TrackerName", "TrackerRole", }
|
||||
);
|
||||
internal_static_messages_TrackerStatus_descriptor = getDescriptor()
|
||||
.getMessageTypes()
|
||||
|
||||
@@ -147,13 +147,23 @@ abstract class SteamVRBridge(
|
||||
val device = instance.deviceManager
|
||||
.createDevice(
|
||||
trackerAdded.trackerName,
|
||||
null,
|
||||
trackerAdded.manufacturer.ifEmpty { "OpenVR" },
|
||||
trackerAdded.trackerSerial,
|
||||
"OpenVR", // TODO : We need the manufacturer
|
||||
)
|
||||
|
||||
// Display name, needsReset and isHmd
|
||||
val displayName: String = trackerAdded.trackerName
|
||||
val isHmd = trackerAdded.trackerId == 0
|
||||
val displayName: String
|
||||
val isHmd = if (trackerAdded.trackerId == 0) {
|
||||
displayName = if (trackerAdded.trackerName == "HMD") {
|
||||
"SteamVR Driver HMD"
|
||||
} else {
|
||||
"Feeder App HMD"
|
||||
}
|
||||
true
|
||||
} else {
|
||||
displayName = trackerAdded.trackerName
|
||||
false
|
||||
}
|
||||
|
||||
// trackerPosition
|
||||
val role = getById(trackerAdded.trackerRole)
|
||||
|
||||
Submodule solarxr-protocol updated: fa2895b19a...088daeeaa1
Reference in New Issue
Block a user