Merge branch 'main' into aed/grammar-police

This commit is contained in:
Aed
2026-04-03 18:48:18 +01:00
committed by GitHub
12 changed files with 143 additions and 44 deletions

View File

@@ -34,6 +34,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- name: Setup PNPM
uses: pnpm/action-setup@v4
- name: Setup Node
@@ -71,6 +73,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
@@ -98,6 +102,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- name: Setup PNPM
uses: pnpm/action-setup@v4
- name: Setup Node
@@ -129,6 +135,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- name: Setup PNPM
uses: pnpm/action-setup@v4
- name: Setup Node
@@ -201,6 +209,8 @@ jobs:
uses: actions/checkout@v6
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no --force
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
@@ -231,6 +241,24 @@ jobs:
name: release-android
path: SlimeVR-android.apk
- name: Build Google Play release bundle
if: startsWith(github.ref, 'refs/tags/')
run: ./gradlew :server:android:bundleRelease
env:
ANDROID_STORE_FILE: ${{ secrets.ANDROID_GPLAY_STORE_FILE }}
ANDROID_STORE_PASSWD: ${{ secrets.ANDROID_GPLAY_STORE_PASSWD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_GPLAY_KEY_ALIAS }}
ANDROID_KEY_PASSWD: ${{ secrets.ANDROID_GPLAY_KEY_PASSWD }}
- name: Upload the Google Play artifact
uses: actions/upload-artifact@v6
if: startsWith(github.ref, 'refs/tags/')
with:
# Artifact name
name: 'SlimeVR-Android-GPDev' # optional, default is artifact
# A file, directory or wildcard pattern that describes what to upload
path: server/android/build/outputs/bundle/release/*
create-release:
name: Finalize Release Draft
needs: [package-desktop, bundle-android, build-server-jar, build-gui-frontend]

View File

@@ -14,7 +14,7 @@ import { IPC_CHANNELS } from '../shared';
import path, { dirname, join } from 'path';
import open from 'open';
import trayIcon from '../resources/icons/icon.png?asset';
import appleTrayIcon from '../resources/icons/appleTrayIcon.png?asset';
import appleTrayIcon from '../resources/icons/Square30x30Logo.png?asset';
import { readFile, stat } from 'fs/promises';
import { getPlatform, handleIpc, isPortAvailable } from './utils';
import {
@@ -26,7 +26,7 @@ import {
getWindowStateFile,
} from './paths';
import { stores } from './store';
import { logger } from './logger';
import { closeLogger, logger } from './logger';
import { writeFileSync } from 'node:fs';
import { spawn } from 'node:child_process';
@@ -36,11 +36,16 @@ import { ServerStatusEvent } from 'electron/preload/interface';
import { mkdir } from 'node:fs/promises';
import { MenuItem } from 'electron/main';
// Fixes colors looking washed on linux
// Might affect hdr
if (process.platform === 'linux') {
app.commandLine.appendSwitch('disable-features', 'WaylandWpColorManagerV1');
app.commandLine.appendSwitch('force-color-profile', 'srgb');
}
app.setPath('userData', getGuiDataFolder())
app.setPath('sessionData', join(getGuiDataFolder(), 'electron'))
app.setPath('userData', getGuiDataFolder());
app.setPath('sessionData', join(getGuiDataFolder(), 'electron'));
// Register custom protocol to handle asset paths with leading slashes
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
@@ -268,6 +273,9 @@ function createWindow() {
case 'close':
mainWindow?.close();
break;
case 'hide':
mainWindow?.hide();
break;
case 'minimize':
mainWindow?.minimize();
break;
@@ -339,8 +347,7 @@ function createWindow() {
menu.append(new MenuItem({ label: 'Copy', role: 'copy' }));
menu.append(new MenuItem({ label: 'Paste', role: 'paste' }));
if (mainWindow)
menu.popup({ window: mainWindow });
if (mainWindow) menu.popup({ window: mainWindow });
});
}
@@ -353,7 +360,7 @@ const checkEnvironmentVariables = () => {
'SlimeVR',
`You have environment variables ${set.join(', ')} set, which may cause the SlimeVR Server to fail to launch properly.`
);
app.exit(0);
app.quit();
}
};
@@ -380,36 +387,60 @@ const spawnServer = async () => {
'SlimeVR',
`Couldn't find a compatible Java version, please download Java 17 or higher`
);
app.exit(0);
app.quit()
return;
}
logger.info({ javaBin, serverJar }, 'Found Java and server jar');
const process = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run']);
process.stdout?.on('data', (message) => {
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {
message: message.toString(),
type: 'stdout',
} satisfies ServerStatusEvent);
const platform = getPlatform();
const serverWorkdir = getServerDataFolder()
const serverProcess = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run'], {
cwd: serverWorkdir,
shell: false,
env:
platform === 'windows'
? {
...process.env,
APPDATA: app.getPath('appData'),
LOCALAPPDATA: process.env['USERPROFILE'] ? path.join(process.env['USERPROFILE'], 'AppData', 'Local') : undefined,
}
: undefined,
});
process.stderr?.on('data', (message) => {
mainWindow?.webContents.send(IPC_CHANNELS.SERVER_STATUS, {
message: message.toString(),
type: 'stderr',
} satisfies ServerStatusEvent);
const sendToWindow = (event: ServerStatusEvent) => {
if (mainWindow && !mainWindow.webContents.isDestroyed()) {
mainWindow.webContents.send(IPC_CHANNELS.SERVER_STATUS, event);
}
};
serverProcess.stdout?.on('data', (message) => {
sendToWindow({ message: message.toString(), type: 'stdout' });
});
serverProcess.stderr?.on('data', (message) => {
sendToWindow({ message: message.toString(), type: 'stderr' });
});
serverProcess.on('error', (err) => {
logger.info({ err }, 'Error launching the java server');
if (!isQuitting) app.quit();
})
serverProcess.on('exit', () => {
logger.info('Server process exiting');
})
const exited = new Promise<void>((resolve) => serverProcess.once('exit', resolve));
return {
process: process,
close: () => {
process.kill('SIGTERM');
},
process: serverProcess,
close: () => serverProcess.kill(),
waitForExit: () => exited,
};
};
let isQuitting = false;
app.whenReady().then(async () => {
// Register protocol handler for app:// scheme to handle assets with leading slashes
protocol.handle('app', (request) => {
@@ -426,23 +457,21 @@ app.whenReady().then(async () => {
logger.info('SlimeVR started!');
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
app.quit();
});
process.on('exit', () => {
server?.close();
});
app.on('before-quit', async () => {
app.on('before-quit', async (event) => {
if (isQuitting) return;
isQuitting = true;
event.preventDefault();
logger.info('App quitting, saving...');
server?.close();
await server?.waitForExit();
stores.settings.save();
stores.cache.save();
discordPresence.destroy();
await saveWindowState();
await closeLogger();
app.exit(0);
});
});

View File

@@ -24,3 +24,11 @@ const transport = pino.transport({
});
export const logger = pino(transport);
export const closeLogger = () =>
new Promise<void>((resolve) => {
logger.flush(() => {
transport.once('close', resolve);
transport.end();
});
});

View File

@@ -103,12 +103,12 @@ export const findSystemJRE = async (sharedDir: string) => {
export const findServerJar = () => {
const paths = [
options.path ? path.resolve(options.path) : undefined,
app.isPackaged ? path.resolve(process.resourcesPath) : undefined,
// AppImage passes the fakeroot in `APPDIR` env var.
process.env['APPDIR']
? path.resolve(join(process.env['APPDIR'], 'usr/share/slimevr/'))
: undefined,
path.dirname(app.getPath('exe')),
// For flatpack container
path.resolve('/app/share/slimevr/'),
path.resolve('/usr/share/slimevr/'),

View File

@@ -12,6 +12,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
openUrl: (url) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_URL, url),
osStats: () => ipcRenderer.invoke(IPC_CHANNELS.OS_STATS),
close: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'close'),
hide: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'hide'),
minimize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'minimize'),
maximize: () => ipcRenderer.invoke(IPC_CHANNELS.WINDOW_ACTIONS, 'maximize'),
getStorage: async (type) => {

View File

@@ -43,6 +43,7 @@ export interface IElectronAPI {
openLogsFolder: () => Promise<void>;
openConfigFolder: () => Promise<void>;
close: () => void;
hide: () => void;
minimize: () => void;
maximize: () => void;
showDecorations: (decorations: boolean) => void;

View File

@@ -25,7 +25,7 @@ export const IPC_CHANNELS = {
export interface IpcInvokeMap {
[IPC_CHANNELS.OPEN_URL]: (url: string) => void;
[IPC_CHANNELS.OS_STATS]: () => Promise<OSStats>;
[IPC_CHANNELS.WINDOW_ACTIONS]: (action: 'close' | 'minimize' | 'maximize') => void;
[IPC_CHANNELS.WINDOW_ACTIONS]: (action: 'close' | 'minimize' | 'maximize' | 'hide') => void;
[IPC_CHANNELS.LOG]: (type: 'info' | 'error' | 'warn', ...args: unknown[]) => void;
[IPC_CHANNELS.OPEN_DIALOG]: (
options: OpenDialogOptions

View File

@@ -81,7 +81,7 @@ export function TopBar({
}
if (config?.useTray && !dontTray) {
electron.api.minimize();
electron.api.hide();
} else if (
config?.connectedTrackersWarning &&
connectedIMUTrackers.filter(

View File

@@ -160,7 +160,7 @@ android {
// adds an offset of the version code as we might do apk releases in the middle of actual
// releases if we failed on bundling or stuff
val versionCodeOffset = 4
val versionCodeOffset = 5
// Defines the version number of your app.
versionCode = (extra["gitVersionCode"] as? Int)?.plus(versionCodeOffset) ?: 0

View File

@@ -72,7 +72,7 @@ dependencies {
implementation("org.apache.commons:commons-lang3:3.20.0")
implementation("org.apache.commons:commons-collections4:4.5.0")
implementation("com.illposed.osc:javaosc-core:0.9")
implementation("com.illposed.osc:javaosc-core:0.8")
implementation("org.java-websocket:Java-WebSocket:1.+")
implementation("com.melloware:jintellitype:1.+")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")

View File

@@ -19,6 +19,10 @@ class BVHRecorder(server: VRServer) {
val file = if (filePath.isDirectory()) {
getBvhFile(filePath) ?: return
} else {
if (filePath.extension != "bvh") {
LogManager.severe("[BVH] Invalid file extension for bvh file \"${filePath}\".")
return
}
filePath
}

View File

@@ -44,6 +44,18 @@ class UDPDevice(
@JvmField
var lastPacketNumber: Long = -1
@JvmField
var totalPacketsReceived: Int = 0
@JvmField
var acceptedPackets: Int = 0
@JvmField
var lastPacketCounterReset: Long = System.currentTimeMillis()
val packetLossPercent: Float
get() = if (totalPacketsReceived == 0) 0f else (1f - acceptedPackets.toFloat() / totalPacketsReceived.toFloat())
@JvmField
var protocol: NetworkProtocol? = null
@@ -68,9 +80,25 @@ class UDPDevice(
var firmwareFeatures = FirmwareFeatures()
fun isNextPacket(packetId: Long): Boolean {
if (packetId != 0L && packetId <= lastPacketNumber) return false
lastPacketNumber = packetId
return true
val now = System.currentTimeMillis()
if (now - lastPacketCounterReset >= 10_000L) {
totalPacketsReceived = 0
acceptedPackets = 0
lastPacketCounterReset = now
}
totalPacketsReceived++
val accepted = packetId == 0L || packetId > lastPacketNumber
if (accepted) {
lastPacketNumber = packetId
acceptedPackets++
}
val lost = totalPacketsReceived - acceptedPackets
trackers.values.forEach {
it.packetsReceived = totalPacketsReceived
it.packetsLost = lost
it.packetLoss = packetLossPercent
}
return accepted
}
override fun toString(): String = "udp:/$ipAddress"