Trying to get the basic shape of the state system out. Testing it with the udp part of the server to confirm the implementation choices

This commit is contained in:
loucass003
2026-03-17 21:22:56 +01:00
parent d691619b98
commit e060bc7cc5
14 changed files with 623 additions and 19 deletions

View File

@@ -12,8 +12,10 @@
perSystem = { pkgs, ... }:
let
java = pkgs.jdk24;
runtimeLibs = pkgs: (with pkgs; [
jdk22
java
alsa-lib at-spi2-atk at-spi2-core cairo cups dbus expat
gdk-pixbuf glib gtk3 libdrm libgbm libglvnd libnotify
@@ -33,8 +35,8 @@
name = "slimevr-env";
targetPkgs = runtimeLibs;
profile = ''
export JAVA_HOME=${pkgs.jdk22}
export PATH="${pkgs.jdk22}/bin:$PATH"
export JAVA_HOME=${java}
export PATH="${java}/bin:$PATH"
# Tell electron-builder to use system tools instead of downloading them
export USE_SYSTEM_FPM=true

View File

@@ -22,12 +22,12 @@ plugins {
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(22))
languageVersion.set(JavaLanguageVersion.of(24))
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(22))
languageVersion.set(JavaLanguageVersion.of(24))
}
}
@@ -217,7 +217,7 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_22
targetCompatibility = JavaVersion.VERSION_22
sourceCompatibility = JavaVersion.VERSION_24
targetCompatibility = JavaVersion.VERSION_24
}
}

View File

@@ -42,6 +42,7 @@ configure<com.diffplug.gradle.spotless.SpotlessExtension> {
",dev.slimevr.tracking.trackers.*,dev.slimevr.desktop.platform.ProtobufMessages.*" +
",solarxr_protocol.rpc.*,kotlinx.coroutines.*,com.illposed.osc.*,android.app.*",
"ij_kotlin_allow_trailing_comma" to true,
"ktlint_standard_filename" to "disabled",
)
val ktlintVersion = "1.8.0"
kotlinGradle {

View File

@@ -16,17 +16,17 @@ plugins {
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(22))
languageVersion.set(JavaLanguageVersion.of(24))
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(22))
languageVersion.set(JavaLanguageVersion.of(24))
}
}
tasks.withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_22)
jvmTarget.set(JvmTarget.JVM_24)
freeCompilerArgs.set(listOf("-Xvalue-classes"))
}
}
@@ -67,6 +67,8 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("com.mayakapps.kache:kache:2.1.1")
implementation("io.ktor:ktor-network:3.0.0")
implementation("io.ktor:ktor-utils:3.0.3")
api("com.github.loucass003:EspflashKotlin:v0.11.0")

View File

@@ -1,4 +0,0 @@
package dev.slimevr
class VRServer {
}

View File

@@ -0,0 +1,67 @@
package dev.slimevr.config
import dev.slimevr.context.Context
import dev.slimevr.context.BasicModule
import dev.slimevr.context.createContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.Serializable
@Serializable
data class UserConfigState(
val userHeight: Float = 1.6f,
)
@Serializable
data class SettingsConfigState(
val trackerPort: Int = 6969,
)
@Serializable
data class GlobalConfigState(
val selectedUserProfile: String = "default",
val selectedSettingsProfile: String = "default",
)
data class ConfigState(
val userConfig: UserConfigState = UserConfigState(),
val settingsConfig: SettingsConfigState = SettingsConfigState(),
val globalConfig: GlobalConfigState = GlobalConfigState(),
)
sealed interface ConfigAction {
data class ChangeProfile(val settingsProfile: String? = null, val userProfile: String? = null) : ConfigAction
}
typealias ConfigContext = Context<ConfigState, ConfigAction>
typealias ConfigModule = BasicModule<ConfigState, ConfigAction>
val ConfigModuleTest = ConfigModule(
reducer = { s, a ->
when (a) {
is ConfigAction.ChangeProfile -> if (a.settingsProfile != null) {
s.copy(globalConfig = s.globalConfig.copy(selectedSettingsProfile = a.settingsProfile))
} else if (a.userProfile != null) {
s.copy(globalConfig = s.globalConfig.copy(selectedUserProfile = a.userProfile))
} else {
s
}
}
},
)
suspend fun createConfig(scope: CoroutineScope): ConfigContext {
val modules = listOf(ConfigModuleTest)
val context = createContext(
initialState = ConfigState(),
reducers = modules.map { it.reducer },
scope = scope,
)
modules.map { it.observer }.forEach { it?.invoke(context) }
context.dispatch(ConfigAction.ChangeProfile(settingsProfile = "Test"))
return context
}

View File

@@ -0,0 +1,44 @@
package dev.slimevr.context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
data class BasicModule<S, A>(
val reducer: (S, A) -> S,
val observer: ((Context<S, A>) -> Unit)? = null,
)
data class Context<S, in A>(
val state: StateFlow<S>,
val dispatch: suspend (A) -> Unit,
val dispatchAll: suspend (List<A>) -> Unit,
val scope: CoroutineScope,
)
fun <S, A> createContext(
initialState: S,
scope: CoroutineScope,
reducers: List<(S, A) -> S>,
): Context<S, A> {
val mutableStateFlow = MutableStateFlow(initialState)
val applyAction: (S, A) -> S = { currentState, action ->
reducers.fold(currentState) { s, reducer -> reducer(s, action) }
}
val dispatch: suspend (A) -> Unit = { action ->
mutableStateFlow.update { applyAction(it, action) }
}
val dispatchAll: suspend (List<A>) -> Unit = { actions ->
mutableStateFlow.update { currentState ->
actions.fold(currentState) { s, action -> applyAction(s, action) }
}
}
val context = Context(mutableStateFlow.asStateFlow(), dispatch, dispatchAll, scope)
return context
}

View File

@@ -0,0 +1,18 @@
package dev.slimevr.tracker
import dev.slimevr.context.Context
import dev.slimevr.context.BasicModule
import io.github.axisangles.ktmath.Quaternion
data class TrackerState(
val id: Int,
val name: String,
val rawRotation: Quaternion,
)
sealed interface TrackerActions {
data class UpdateRotation(val rotation: Quaternion)
}
typealias TrackerContext = Context<TrackerState, TrackerActions>
typealias TrackerModule = BasicModule<TrackerState, TrackerActions>

View File

@@ -0,0 +1,138 @@
package dev.slimevr.tracker.udp
import dev.slimevr.context.Context
import dev.slimevr.context.createContext
import io.ktor.network.sockets.BoundDatagramSocket
import io.ktor.network.sockets.Datagram
import io.ktor.network.sockets.InetSocketAddress
import io.ktor.utils.io.core.buildPacket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
data class LastPing(
val id: Int,
val startTime: Long,
val duration: Long,
)
data class UDPConnectionState(
val id: String,
val lastPacket: Long,
val lastPacketNum: Int,
val lastPing: LastPing,
val didHandshake: Boolean,
val address: String,
val port: Int,
)
sealed interface UDPConnectionActions {
data class StartPing(val startTime: Long): UDPConnectionActions
data class ReceivedPong(val id: Int, val duration: Long): UDPConnectionActions
}
typealias UDPConnectionContext = Context<UDPConnectionState, UDPConnectionActions>
data class UDPConnection(
val context: UDPConnectionContext,
val packetEvents: PacketDispatcher,
val send: (Packet) -> Unit
)
data class UDPConnectionModule(
val reducer: (UDPConnectionState, UDPConnectionActions) -> UDPConnectionState,
val observer: ((UDPConnection) -> Unit)? = null,
)
val PingModule = UDPConnectionModule(
reducer = { s, a ->
when (a) {
is UDPConnectionActions.StartPing -> {
s.copy(lastPing = s.lastPing.copy(startTime = a.startTime))
}
is UDPConnectionActions.ReceivedPong -> {
s.copy(lastPing = s.lastPing.copy(duration = a.duration, id = a.id))
}
else -> s
}
},
observer = {
// Send the ping every 1s
it.context.scope.launch {
while (isActive) {
val state = it.context.state.value
if (state.didHandshake) {
it.context.dispatch(UDPConnectionActions.StartPing(startTime = System.currentTimeMillis()))
it.send(PingPong(state.lastPacketNum + 1))
}
delay(1000)
}
}
// listen for the pong
it.packetEvents.on<PingPong> { packet ->
val state = it.context.state.value
if (packet.pingId != state.lastPing.id + 1) {
println("Ping ID does not match, ignoring")
return@on
}
val ping = System.currentTimeMillis() - state.lastPing.startTime;
it.context.scope.launch {
it.context.dispatch(UDPConnectionActions.ReceivedPong(id = packet.pingId, duration = ping))
// TODO update the device ping delay
}
}
}
)
fun createUDPConnectionContext(
id: String,
socket: BoundDatagramSocket,
remoteAddress: InetSocketAddress,
scope: CoroutineScope
): UDPConnection {
val modules = listOf(PingModule)
val context = createContext(
initialState = UDPConnectionState(
id = id,
lastPacket = System.currentTimeMillis(),
lastPacketNum = 0,
lastPing = LastPing(id = 0, duration = 0, startTime = 0),
didHandshake = false,
address = remoteAddress.hostname,
port = remoteAddress.port,
),
reducers = modules.map { it.reducer },
scope = scope
)
val dispatcher = PacketDispatcher()
val sendFunc = { packet: Packet ->
scope.launch {
val bytePacket = buildPacket {
writePacket(this, packet)
}
socket.send(Datagram(bytePacket, remoteAddress))
}
Unit
}
val conn = UDPConnection(
context,
dispatcher,
send = sendFunc
)
modules.map { it.observer }.forEach { it?.invoke(conn) }
return conn
}

View File

@@ -0,0 +1,187 @@
package dev.slimevr.tracker.udp
import io.github.axisangles.ktmath.Quaternion
import io.github.axisangles.ktmath.Vector3
import io.ktor.utils.io.core.remaining
import kotlinx.io.*
import kotlin.reflect.KClass
// ── Enums ───────────────────────────────────────────────────────────────────
enum class PacketType(val id: Int) {
HEARTBEAT(0), ROTATION(1), HANDSHAKE(3), ACCEL(4), PING_PONG(10),
SERIAL(11), BATTERY_LEVEL(12), TAP(13), ERROR(14), SENSOR_INFO(15),
ROTATION_2(16), ROTATION_DATA(17), MAGNETOMETER_ACCURACY(18),
SIGNAL_STRENGTH(19), TEMPERATURE(20), USER_ACTION(21), PROTOCOL_CHANGE(200);
companion object {
private val map = entries.associateBy { it.id }
fun fromId(id: Int) = map[id]
}
}
enum class UserAction(val id: Int) {
RESET_FULL(2), RESET_YAW(3), RESET_MOUNTING(4), PAUSE_TRACKING(5);
companion object {
private val map = entries.associateBy { it.id }
fun fromId(id: Int) = map[id]
}
}
sealed interface Packet
sealed interface SensorSpecificPacket : Packet { val sensorId: Int }
sealed interface RotationPacket : SensorSpecificPacket { val rotation: Quaternion }
data object Heartbeat : Packet
data class Handshake(val board: Int = 0, val imu: Int = 0, val mcu: Int = 0, val pVer: Int = 0, val firmware: String? = null, val mac: String? = null) : Packet
data class Rotation(override val sensorId: Int = 0, override val rotation: Quaternion = Quaternion.IDENTITY) : RotationPacket
data class Accel(val acceleration: Vector3 = Vector3.NULL, override val sensorId: Int = 0) : SensorSpecificPacket
data class PingPong(val pingId: Int = 0) : Packet
data class Serial(val serial: String = "") : Packet
data class BatteryLevel(val voltage: Float = 0f, val level: Float = 0f) : Packet
data class Tap(override val sensorId: Int = 0, val tap: Int = 0) : SensorSpecificPacket
data class Error(override val sensorId: Int = 0, val errorNumber: Int = 0) : SensorSpecificPacket
data class SensorInfo(override val sensorId: Int = 0, val status: Int = 0, val type: Int = 0) : SensorSpecificPacket
data class Rotation2(override val sensorId: Int = 1, override val rotation: Quaternion = Quaternion.IDENTITY) : RotationPacket
data class RotationData(override val sensorId: Int = 0, val dataType: Int = 0, val rotation: Quaternion = Quaternion.IDENTITY, val calibration: Int = 0) : SensorSpecificPacket
data class MagnetometerAccuracy(override val sensorId: Int = 0, val accuracy: Float = 0f) : SensorSpecificPacket
data class SignalStrength(override val sensorId: Int = 0, val signal: Int = 0) : SensorSpecificPacket
data class Temperature(override val sensorId: Int = 0, val temp: Float = 0f) : SensorSpecificPacket
data class UserActionPacket(val action: UserAction? = null) : Packet
data class ProtocolChange(val targetProtocol: Int = 0, val targetVersion: Int = 0) : Packet
private fun Source.readU8() = readByte().toInt() and 0xFF
private fun Sink.writeU8(v: Int) = writeByte(v.toByte())
private fun Source.readSafeFloat() = readFloat().let { if (it.isNaN()) 0f else it }
private fun Source.readSafeQuat() = Quaternion(readFloat(), readFloat(), readFloat(), readFloat()).let {
if (it.x.isNaN() || (it.x == 0f && it.y == 0f && it.z == 0f && it.w == 0f)) Quaternion.IDENTITY else it
}
private fun Sink.writeQuat(q: Quaternion) {
writeFloat(q.x); writeFloat(q.y); writeFloat(q.z); writeFloat(q.w)
}
private fun Source.readStr(len: Int) = buildString {
repeat(len) { readByte().toInt().takeIf { it != 0 }?.let { append(it.toChar()) } }
}
object PacketCodec {
fun read(type: PacketType, src: Source): Packet = when (type) {
PacketType.HEARTBEAT -> Heartbeat
PacketType.HANDSHAKE -> with(src) {
if (exhausted()) return Handshake()
val b = if (remaining >= 4) readInt() else 0
val i = if (remaining >= 4) readInt() else 0
val m = if (remaining >= 4) readInt() else 0
if (remaining >= 12) skip(12)
val p = if (remaining >= 4) readInt() else 0
val f = if (remaining >= 1) readStr(readByte().toInt()) else null
val mac = if (remaining >= 6) readByteArray(6).joinToString(":") { "%02X".format(it) }.takeIf { it != "00:00:00:00:00:00" } else null
Handshake(b, i, m, p, f, mac)
}
PacketType.ROTATION -> Rotation(rotation = src.readSafeQuat())
PacketType.ACCEL -> Accel(Vector3(src.readSafeFloat(), src.readSafeFloat(), src.readSafeFloat()), if (!src.exhausted()) src.readU8() else 0)
PacketType.PING_PONG -> PingPong(src.readInt())
PacketType.SERIAL -> Serial(src.readStr(src.readInt()))
PacketType.BATTERY_LEVEL -> src.readSafeFloat().let { f ->
if (src.remaining >= 4) BatteryLevel(f, src.readSafeFloat()) else BatteryLevel(0f, f)
}
PacketType.TAP -> Tap(src.readU8(), src.readU8())
PacketType.ERROR -> Error(src.readU8(), src.readU8())
PacketType.SENSOR_INFO -> SensorInfo(src.readU8(), src.readU8(), if (!src.exhausted()) src.readU8() else 0)
PacketType.ROTATION_2 -> Rotation2(rotation = src.readSafeQuat())
PacketType.ROTATION_DATA -> RotationData(src.readU8(), src.readU8(), src.readSafeQuat(), src.readU8())
PacketType.MAGNETOMETER_ACCURACY -> MagnetometerAccuracy(src.readU8(), src.readSafeFloat())
PacketType.SIGNAL_STRENGTH -> SignalStrength(src.readU8(), src.readByte().toInt())
PacketType.TEMPERATURE -> Temperature(src.readU8(), src.readSafeFloat())
PacketType.USER_ACTION -> UserActionPacket(UserAction.fromId(src.readU8()))
PacketType.PROTOCOL_CHANGE -> ProtocolChange(src.readU8(), src.readU8())
}
fun write(dst: Sink, packet: Packet) = when (packet) {
is Heartbeat -> {}
is Handshake -> {
dst.writeU8(PacketType.HANDSHAKE.id)
dst.write("Hey OVR =D 5".toByteArray(Charsets.US_ASCII))
}
is Rotation -> dst.writeQuat(packet.rotation)
is Accel -> { dst.writeFloat(packet.acceleration.x); dst.writeFloat(packet.acceleration.y); dst.writeFloat(packet.acceleration.z) }
is PingPong -> dst.writeInt(packet.pingId)
is Serial -> { dst.writeInt(packet.serial.length); packet.serial.forEach { dst.writeU8(it.code) } }
is BatteryLevel -> { dst.writeFloat(packet.voltage); dst.writeFloat(packet.level) }
is Tap -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.tap) }
is Error -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.errorNumber) }
is SensorInfo -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.status); dst.writeU8(packet.type) }
is Rotation2 -> dst.writeQuat(packet.rotation)
is RotationData -> { dst.writeU8(packet.sensorId); dst.writeU8(packet.dataType); dst.writeQuat(packet.rotation); dst.writeU8(packet.calibration) }
is MagnetometerAccuracy -> { dst.writeU8(packet.sensorId); dst.writeFloat(packet.accuracy) }
is SignalStrength -> { dst.writeU8(packet.sensorId); dst.writeByte(packet.signal.toByte()) }
is Temperature -> { dst.writeU8(packet.sensorId); dst.writeFloat(packet.temp) }
is UserActionPacket -> dst.writeU8(packet.action?.id ?: 0)
is ProtocolChange -> { dst.writeU8(packet.targetProtocol); dst.writeU8(packet.targetVersion) }
}
}
// ── Entry Points ────────────────────────────────────────────────────────────
fun readPacket(src: Source): Packet? {
if (src.exhausted()) return null
val type = PacketType.fromId(src.readInt()) ?: return null
if (type != PacketType.HANDSHAKE) src.skip(8) // Skip sequence number
return PacketCodec.read(type, src)
}
fun writePacket(dst: Sink, packet: Packet) {
val type = when(packet) {
is Heartbeat -> PacketType.HEARTBEAT
is Handshake -> PacketType.HANDSHAKE
is Rotation -> PacketType.ROTATION
is Accel -> PacketType.ACCEL
is PingPong -> PacketType.PING_PONG
is Serial -> PacketType.SERIAL
is BatteryLevel -> PacketType.BATTERY_LEVEL
is Tap -> PacketType.TAP
is Error -> PacketType.ERROR
is SensorInfo -> PacketType.SENSOR_INFO
is Rotation2 -> PacketType.ROTATION_2
is RotationData -> PacketType.ROTATION_DATA
is MagnetometerAccuracy -> PacketType.MAGNETOMETER_ACCURACY
is SignalStrength -> PacketType.SIGNAL_STRENGTH
is Temperature -> PacketType.TEMPERATURE
is UserActionPacket -> PacketType.USER_ACTION
is ProtocolChange -> PacketType.PROTOCOL_CHANGE
}
if (type != PacketType.HANDSHAKE) {
dst.writeInt(type.id)
dst.writeLong(type.id.toLong()) // Sequence number placeholder
}
PacketCodec.write(dst, packet)
}
class PacketDispatcher {
val listeners = mutableMapOf<KClass<out Packet>, MutableList<(Packet) -> Unit>>()
/**
* Listen for a specific packet type.
* Usage: dispatcher.on<Rotation> { packet -> println(packet.rotation) }
*/
inline fun <reified T : Packet> on(crossinline callback: (T) -> Unit) {
val list = listeners.getOrPut(T::class) { mutableListOf() }
synchronized(list) {
list.add { callback(it as T) }
}
}
/**
* Broadcasts a packet to all registered listeners for its type.
*/
fun emit(packet: Packet) {
val list = listeners[packet::class] ?: return
synchronized(list) {
list.forEach { it(packet) }
}
}
}

View File

@@ -0,0 +1,86 @@
package dev.slimevr.tracker.udp
import dev.slimevr.VRServerContext
import dev.slimevr.config.ConfigContext
import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.BoundDatagramSocket
import io.ktor.network.sockets.Datagram
import io.ktor.network.sockets.InetSocketAddress
import io.ktor.network.sockets.aSocket
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
data class UDPTrackerServerState(
val port: Int,
val connections: MutableMap<String, UDPConnection>
)
suspend fun processPacket(
socket: BoundDatagramSocket,
datagram: Datagram,
workerId: Int
) {
}
const val PACKET_WORKERS = 4
suspend fun createUDPTrackerServer(
serverContext: VRServerContext,
configContext: ConfigContext
): UDPTrackerServerState {
val state = UDPTrackerServerState(
port = configContext.state.value.settingsConfig.trackerPort,
connections = mutableMapOf()
)
val selectorManager = SelectorManager(Dispatchers.IO)
val serverSocket =
aSocket(selectorManager).udp().bind(InetSocketAddress("0.0.0.0", state.port))
val packetChannel = Channel<Datagram>(capacity = Channel.BUFFERED)
supervisorScope {
launch(Dispatchers.IO) {
while (isActive) {
val datagram = serverSocket.receive()
packetChannel.send(datagram)
}
}
repeat(PACKET_WORKERS) { workerId ->
launch(Dispatchers.Default) {
for (datagram in packetChannel) {
val packet = readPacket(datagram.packet)
if (packet == null) {
println("null packet")
continue
}
val address = datagram.address as InetSocketAddress
val connContext = state.connections[address.hostname]
if (connContext !== null)
connContext.packetEvents.emit(packet = packet)
else {
val newContext = createUDPConnectionContext(
id = address.hostname,
remoteAddress = address,
socket = serverSocket,
scope = this
)
state.connections[address.hostname] = newContext
newContext.packetEvents.emit(packet = packet)
}
}
}
}
}
return state
}

View File

@@ -0,0 +1,54 @@
package dev.slimevr
import dev.slimevr.context.Context
import dev.slimevr.context.BasicModule
import dev.slimevr.context.createContext
import dev.slimevr.tracker.TrackerContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
data class VRServerState(
val trackers: Map<Int, TrackerContext>,
)
sealed interface VRServerActions {
data class NewTracker(val trackerId: Int, val context: TrackerContext) : VRServerActions
}
typealias VRServerContext = Context<VRServerState, VRServerActions>
typealias VRServerModule = BasicModule<VRServerState, VRServerActions>
val TestModule = VRServerModule(
reducer = { s, a ->
when (a) {
is VRServerActions.NewTracker -> s.copy(
trackers = s.trackers + (a.trackerId to a.context),
)
}
},
observer = { context ->
context.state.distinctUntilChangedBy { state -> state.trackers.size }.onEach {
println("tracker list size changed")
}.launchIn(context.scope)
},
)
fun createVRServer(scope: CoroutineScope): VRServerContext {
val server = VRServerState(
trackers = mapOf(),
)
val modules = listOf(TestModule)
val context = createContext(
initialState = server,
reducers = modules.map { it.reducer },
scope = scope,
)
modules.map { it.observer }.forEach { it?.invoke(context) }
return context
}

View File

@@ -18,17 +18,17 @@ plugins {
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(22))
languageVersion.set(JavaLanguageVersion.of(24))
}
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(22))
languageVersion.set(JavaLanguageVersion.of(24))
}
}
tasks.withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_22)
jvmTarget.set(JvmTarget.JVM_24)
freeCompilerArgs.set(listOf("-Xvalue-classes"))
}
}
@@ -60,6 +60,7 @@ dependencies {
implementation("net.java.dev.jna:jna:5.+")
implementation("net.java.dev.jna:jna-platform:5.+")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("com.fazecast:jSerialComm:2.11.3") {
exclude(group = "com.fazecast", module = "android")
}

View File

@@ -2,6 +2,14 @@
package dev.slimevr.desktop
fun main(args: Array<String>) {
println("Hello world!")
import dev.slimevr.config.createConfig
import dev.slimevr.createVRServer
import dev.slimevr.tracker.udp.createUDPTrackerServer
import kotlinx.coroutines.runBlocking
fun main(args: Array<String>) = runBlocking {
val config = createConfig(this)
val server = createVRServer(this)
val udpServer = createUDPTrackerServer(server, config)
}