mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Lint
This commit is contained in:
@@ -21,11 +21,11 @@ The `Context<S, A>` type (`context/context.kt`) is the building block of every m
|
||||
|
||||
```kotlin
|
||||
class Context<S, in A>(
|
||||
val state: StateFlow<S>, // current state, readable by anyone
|
||||
val scope: CoroutineScope, // lifetime of this module
|
||||
val state: StateFlow<S>, // current state, readable by anyone
|
||||
val scope: CoroutineScope, // lifetime of this module
|
||||
) {
|
||||
fun dispatch(action: A)
|
||||
fun dispatchAll(actions: List<A>)
|
||||
fun dispatch(action: A)
|
||||
fun dispatchAll(actions: List<A>)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -44,8 +44,8 @@ A `Behaviour` is an interface with two methods, both with no-op defaults:
|
||||
|
||||
```kotlin
|
||||
interface Behaviour<S, A, C> {
|
||||
fun reduce(state: S, action: A): S = state
|
||||
fun observe(receiver: C) {}
|
||||
fun reduce(state: S, action: A): S = state
|
||||
fun observe(receiver: C) {}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -68,9 +68,9 @@ Every module follows the same construction pattern:
|
||||
val behaviours = listOf(BehaviourA, BehaviourB, BehaviourC)
|
||||
|
||||
val context = Context.create(
|
||||
initialState = ...,
|
||||
scope = scope,
|
||||
behaviours = behaviours,
|
||||
initialState = ...,
|
||||
scope = scope,
|
||||
behaviours = behaviours,
|
||||
)
|
||||
|
||||
val module = MyModule(context, ...)
|
||||
@@ -102,11 +102,11 @@ Group behaviours that share the same receiver type in a single file. Behaviours
|
||||
|
||||
```kotlin
|
||||
object PacketBehaviour : UDPConnectionBehaviour {
|
||||
override fun reduce(state: UDPConnectionState, action: UDPConnectionActions) = when (action) {
|
||||
is UDPConnectionActions.LastPacket -> state.copy(...)
|
||||
else -> state
|
||||
}
|
||||
override fun observe(receiver: UDPConnection) { ... }
|
||||
override fun reduce(state: UDPConnectionState, action: UDPConnectionActions) = when (action) {
|
||||
is UDPConnectionActions.LastPacket -> state.copy(...)
|
||||
else -> state
|
||||
}
|
||||
override fun observe(receiver: UDPConnection) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -114,14 +114,14 @@ object PacketBehaviour : UDPConnectionBehaviour {
|
||||
|
||||
```kotlin
|
||||
class FirmwareBehaviour(private val firmwareManager: FirmwareManager) : SolarXRConnectionBehaviour {
|
||||
override fun observe(receiver: SolarXRConnection) { ... }
|
||||
override fun observe(receiver: SolarXRConnection) { ... }
|
||||
}
|
||||
|
||||
// At the call site:
|
||||
listOf(
|
||||
DataFeedInitBehaviour,
|
||||
FirmwareBehaviour(firmwareManager),
|
||||
SerialBehaviour(serialServer),
|
||||
DataFeedInitBehaviour,
|
||||
FirmwareBehaviour(firmwareManager),
|
||||
SerialBehaviour(serialServer),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -180,16 +180,16 @@ To add a new major section of the server (say, a HID device connection):
|
||||
1. **Define the state**:
|
||||
```kotlin
|
||||
data class HIDConnectionState(
|
||||
val deviceId: Int?,
|
||||
val connected: Boolean,
|
||||
val deviceId: Int?,
|
||||
val connected: Boolean,
|
||||
)
|
||||
```
|
||||
|
||||
2. **Define sealed actions**:
|
||||
```kotlin
|
||||
sealed interface HIDConnectionActions {
|
||||
data class Connected(val deviceId: Int) : HIDConnectionActions
|
||||
data object Disconnected : HIDConnectionActions
|
||||
data class Connected(val deviceId: Int) : HIDConnectionActions
|
||||
data object Disconnected : HIDConnectionActions
|
||||
}
|
||||
```
|
||||
|
||||
@@ -202,37 +202,37 @@ typealias HIDConnectionBehaviour = Behaviour<HIDConnectionState, HIDConnectionAc
|
||||
4. **Define the module class** (holds context + extra runtime state):
|
||||
```kotlin
|
||||
class HIDConnection(
|
||||
val context: HIDConnectionContext,
|
||||
val serverContext: VRServer,
|
||||
private val onSend: suspend (ByteArray) -> Unit,
|
||||
val context: HIDConnectionContext,
|
||||
val serverContext: VRServer,
|
||||
private val onSend: suspend (ByteArray) -> Unit,
|
||||
) {
|
||||
suspend fun send(bytes: ByteArray) = onSend(bytes)
|
||||
suspend fun send(bytes: ByteArray) = onSend(bytes)
|
||||
}
|
||||
```
|
||||
|
||||
5. **Write behaviours** in a separate `behaviours.kt` file:
|
||||
```kotlin
|
||||
object HIDHandshakeBehaviour : HIDConnectionBehaviour {
|
||||
override fun reduce(state: HIDConnectionState, action: HIDConnectionActions) = when (action) {
|
||||
is HIDConnectionActions.Connected -> state.copy(deviceId = action.deviceId, connected = true)
|
||||
is HIDConnectionActions.Disconnected -> state.copy(connected = false)
|
||||
}
|
||||
override fun observe(receiver: HIDConnection) {
|
||||
// launch coroutines, subscribe to events, etc.
|
||||
}
|
||||
override fun reduce(state: HIDConnectionState, action: HIDConnectionActions) = when (action) {
|
||||
is HIDConnectionActions.Connected -> state.copy(deviceId = action.deviceId, connected = true)
|
||||
is HIDConnectionActions.Disconnected -> state.copy(connected = false)
|
||||
}
|
||||
override fun observe(receiver: HIDConnection) {
|
||||
// launch coroutines, subscribe to events, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **Write a `companion object { fun create() }`**:
|
||||
```kotlin
|
||||
companion object {
|
||||
fun create(serverContext: VRServer, scope: CoroutineScope, send: suspend (ByteArray) -> Unit): HIDConnection {
|
||||
val behaviours = listOf(HIDHandshakeBehaviour, ...)
|
||||
val context = Context.create(initialState = ..., scope = scope, behaviours = behaviours)
|
||||
val conn = HIDConnection(context, serverContext, send)
|
||||
behaviours.forEach { it.observe(conn) }
|
||||
return conn
|
||||
}
|
||||
fun create(serverContext: VRServer, scope: CoroutineScope, send: suspend (ByteArray) -> Unit): HIDConnection {
|
||||
val behaviours = listOf(HIDHandshakeBehaviour, ...)
|
||||
val context = Context.create(initialState = ..., scope = scope, behaviours = behaviours)
|
||||
val conn = HIDConnection(context, serverContext, send)
|
||||
behaviours.forEach { it.observe(conn) }
|
||||
return conn
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -252,7 +252,7 @@ Example: adding battery tracking to a HID connection requires only adding a `HID
|
||||
2. In a behaviour's `observe`, register a listener:
|
||||
```kotlin
|
||||
receiver.packetEvents.on<MyNewPacket> { event ->
|
||||
// handle it
|
||||
// handle it
|
||||
}
|
||||
```
|
||||
3. In `udp/server.kt`, route the new packet type to `emit`.
|
||||
@@ -313,4 +313,4 @@ Each client runs in its own `launch` block. When the socket disconnects, the cor
|
||||
- Module creation lives in `companion object { fun create(...) }`.
|
||||
- State data classes use `copy(...)` inside reducers and `Update { copy(...) }` actions — never expose a `MutableStateFlow` directly.
|
||||
- **Never use `var` in a state data class** — state must be immutable, all fields `val`. Using `var` in any data class is almost certainly a design mistake; if you need mutable fields, prefer a plain class or rethink the structure.
|
||||
- Use `sealed interface` for action types, not `sealed class`, to avoid the extra constructor overhead.
|
||||
- Use `sealed interface` for action types, not `sealed class`, to avoid the extra constructor overhead.
|
||||
|
||||
@@ -157,8 +157,8 @@ suspend fun doOtaFlash(
|
||||
onStatus(FirmwareUpdateStatus.REBOOTING, 0)
|
||||
|
||||
// Wait for the device to come back online after reboot.
|
||||
// which don't emit a new VRServerState, are also observed.
|
||||
// flatMapLatest switches to the matched device's own state flow so that status changes,
|
||||
// which don't emit a new VRServerState, are also observed.
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
val connected = withTimeoutOrNull(60_000) {
|
||||
server.context.state
|
||||
|
||||
@@ -37,6 +37,7 @@ private fun UserHeightCalibrationStatus.isTerminal() = when (this) {
|
||||
UserHeightCalibrationStatus.ERROR_TOO_HIGH,
|
||||
UserHeightCalibrationStatus.ERROR_TOO_SMALL,
|
||||
-> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
@@ -51,13 +52,12 @@ private fun isHmdLeveled(snapshot: TrackerSnapshot): Boolean {
|
||||
}
|
||||
|
||||
object CalibrationBehaviour : HeightCalibrationBehaviourType {
|
||||
override fun reduce(state: HeightCalibrationState, action: HeightCalibrationActions) =
|
||||
when (action) {
|
||||
is HeightCalibrationActions.Update -> state.copy(
|
||||
status = action.status,
|
||||
currentHeight = action.currentHeight,
|
||||
)
|
||||
}
|
||||
override fun reduce(state: HeightCalibrationState, action: HeightCalibrationActions) = when (action) {
|
||||
is HeightCalibrationActions.Update -> state.copy(
|
||||
status = action.status,
|
||||
currentHeight = action.currentHeight,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun runCalibrationSession(
|
||||
@@ -84,7 +84,6 @@ internal suspend fun runCalibrationSession(
|
||||
dispatch(UserHeightCalibrationStatus.RECORDING_FLOOR)
|
||||
|
||||
withTimeoutOrNull(TIMEOUT_MS) {
|
||||
|
||||
// Floor phase: collect controller updates until the floor level is locked in
|
||||
controllerUpdates
|
||||
.sample(SAMPLE_INTERVAL_MS)
|
||||
|
||||
@@ -64,12 +64,14 @@ class HeightCalibrationManager(
|
||||
(bodyPart == BodyPart.LEFT_HAND || bodyPart == BodyPart.RIGHT_HAND) && it.context.state.value.position != null
|
||||
}
|
||||
if (controllers.isEmpty()) return@flatMapLatest emptyFlow()
|
||||
combine(controllers.map { controller ->
|
||||
controller.context.state.map { s ->
|
||||
val position = s.position ?: error("hands (or Controller) will always have a position in this case")
|
||||
TrackerSnapshot(position = position, rotation = s.rawRotation)
|
||||
}
|
||||
}) { snapshots -> snapshots.minByOrNull { it.position.y }!! }
|
||||
combine(
|
||||
controllers.map { controller ->
|
||||
controller.context.state.map { s ->
|
||||
val position = s.position ?: error("hands (or Controller) will always have a position in this case")
|
||||
TrackerSnapshot(position = position, rotation = s.rawRotation)
|
||||
}
|
||||
},
|
||||
) { snapshots -> snapshots.minByOrNull { it.position.y }!! }
|
||||
}
|
||||
|
||||
fun start() {
|
||||
|
||||
@@ -20,7 +20,7 @@ object HIDRegistrationBehaviour : HIDReceiverBehaviour {
|
||||
deviceId = action.deviceId,
|
||||
trackerId = null,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
else -> state
|
||||
|
||||
@@ -55,10 +55,12 @@ class SerialServer(
|
||||
if (conn is SerialConnection.Console) {
|
||||
conn.handle.close()
|
||||
}
|
||||
context.dispatchAll(listOf(
|
||||
SerialServerActions.RemoveConnection(portLocation),
|
||||
SerialServerActions.PortLost(portLocation)
|
||||
))
|
||||
context.dispatchAll(
|
||||
listOf(
|
||||
SerialServerActions.RemoveConnection(portLocation),
|
||||
SerialServerActions.PortLost(portLocation),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onDataReceived(portLocation: String, line: String) {
|
||||
|
||||
@@ -25,6 +25,7 @@ private interface RegistryNotify : Library {
|
||||
const val REG_NOTIFY_CHANGE_LAST_SET = 0x00000004
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun RegNotifyChangeKeyValue(
|
||||
hKey: WinReg.HKEY,
|
||||
bWatchSubtree: Boolean,
|
||||
@@ -86,15 +87,25 @@ internal fun windowsVRCConfigFlow(): Flow<VRCConfigValues?> = flow {
|
||||
try {
|
||||
if (hEvent != null) {
|
||||
RegistryNotify.INSTANCE.RegNotifyChangeKeyValue(
|
||||
phkResult.value, false, RegistryNotify.REG_NOTIFY_CHANGE_LAST_SET, hEvent, true,
|
||||
phkResult.value,
|
||||
false,
|
||||
RegistryNotify.REG_NOTIFY_CHANGE_LAST_SET,
|
||||
hEvent,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
val keys = windowsGetVRChatKeys(VRC_REG_PATH)
|
||||
emit(if (keys.isEmpty()) null else buildVRCConfigValues(
|
||||
intValue = { key -> keys[key]?.let { windowsGetDwordValue(VRC_REG_PATH, it) } },
|
||||
doubleValue = { key -> keys[key]?.let { windowsGetQwordValue(VRC_REG_PATH, it) } },
|
||||
))
|
||||
emit(
|
||||
if (keys.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
buildVRCConfigValues(
|
||||
intValue = { key -> keys[key]?.let { windowsGetDwordValue(VRC_REG_PATH, it) } },
|
||||
doubleValue = { key -> keys[key]?.let { windowsGetQwordValue(VRC_REG_PATH, it) } },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if (hEvent != null) {
|
||||
withContext(Dispatchers.IO) { Kernel32.INSTANCE.WaitForSingleObject(hEvent, WinBase.INFINITE) }
|
||||
|
||||
Reference in New Issue
Block a user