Skip to content

Commit

Permalink
Add Actor-based RPC handler (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bluexin authored Feb 22, 2019
1 parent ede8c10 commit 5751ff8
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 14 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies {

implementation("org.slf4j", "slf4j-api")
implementation("io.github.microutils", "kotlin-logging", prop("kotlinLoggingVersion"))
compileOnly("org.slf4j", "slf4j-simple", prop("slf4jVersion"))
testRuntime("org.slf4j", "slf4j-simple", prop("slf4jVersion"))

shade("net.java.dev.jna", "jna", prop("jnaVersion"))
shadeInPlace(files("libs"))
Expand Down
13 changes: 5 additions & 8 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
#Plugins versions
kotlin_version=1.3.0
kotlin_version=1.3.11
undercouch_dl_version=3.4.3
bintray_version=1.8.4
artifactory_version=4.7.5

#Dependencies versions
discord_rpc_version=3.3.0
coroutinesVersion=1.0.0
discord_rpc_version=3.4.0
coroutinesVersion=1.1.0
slf4jVersion=1.7.25
kotlinLoggingVersion=1.6.10
jnaVersion=4.5.0

jnaVersion=5.2.0
#Build variables
build_number=8
build_number=9
version_number=0

#Build settings
org.gradle.caching=true
org.gradle.parallel=true
Expand Down
8 changes: 4 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ Then add the dependency:
```groovy
dependencies {
/* project dependencies */
compile("be.bluexin:drpc4k:0.8")
compile("be.bluexin:drpc4k:0.9")
}
```
Maven :
```xml
<dependency>
<groupId>be.bluexin</groupId>
<artifactId>drpc4k</artifactId>
<version>0.8</version>
<version>0.9</version>
<type>pom</type>
</dependency>
```
Expand All @@ -46,6 +46,6 @@ You can also directly download it from [Bintray](https://bintray.com/bluexin/blu

## Rich Presence

To use Discord Rich Presence, the easiest way is to use the wrapper class [be.bluexin.drpc4k.jna.RPCHandler](src/main/kotlin/be/bluexin/drpc4k/jna/RPCHandler.kt).
To use Discord Rich Presence, the easiest way is to use the Actor wrapper [be.bluexin.drpc4k.jna.RPCActor](src/main/kotlin/be/bluexin/drpc4k/jna/RPCActor.kt).
It will handle everything using lightweight Kotlin Coroutines.
An example usage can be found at [src/test/kotlin/JnaExample.kt](src/test/kotlin/JnaExample.kt).
An example usage can be found at [src/test/kotlin/JnaExampleActor.kt](src/test/kotlin/JnaExampleActor.kt).
260 changes: 260 additions & 0 deletions src/main/kotlin/be/bluexin/drpc4k/jna/RPCActor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
/*
* Copyright (c) 2019 Arnaud 'Bluexin' Solé
*
* This file is part of drpc4k.
*
* drpc4k is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* drpc4k is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with drpc4k. If not, see <http://www.gnu.org/licenses/>.
*/

package be.bluexin.drpc4k.jna

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
import mu.KotlinLogging
import kotlin.coroutines.CoroutineContext

/**
* Start a new Discord RPC Actor in the current [this] [CoroutineScope].
*
* The newly created actor will receive messages of type [RPCInputMessage] and send [RPCOutputMessage] to the
* specified [output].
*
* A typical usage looks like this :
* ```
* val rpcOutput = Channel<RPCOutputMessage>(capacity = Channel.UNLIMITED)
* val rpcInput = rpcActor(rpcOutput)
* // Connect to the client via RPC
* rpcInput.send(RPCInputMessage.Connect(myClientKey))
* // Update rich presence
* rpcInput.send(RPCInputMessage.UpdatePresence(myPresence))
* // Set up receiving of updates from the RPC actor
* launch {
* for (msg in rpcOutput) with(msg) {
* when (this) {
* is RPCOutputMessage.Ready -> with(user) { logger.info("Logged in as $username#$discriminator") }
* is RPCOutputMessage.Disconnected -> logger.warn("Disconnected: #$errorCode $message")
* is RPCOutputMessage.Errored -> logger.error("Error: #$errorCode $message")
* }
* }
* }
* ...
* // Disconnect from the client
* rpcInput.close()
* ```
*
* Note that because we use a [Channel.UNLIMITED] capacity channel, it is safe to use non-suspending [Channel.offer]
* instead of the suspending [Channel.send].
*
* @param output Channel the RPC Actor will be sending [RPCOutputMessage] update messages to.
* @param context additional to [CoroutineScope.coroutineContext] context of the coroutine.
* @see CoroutineScope.actor for more technical information.
*/
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
fun CoroutineScope.rpcActor(output: SendChannel<RPCOutputMessage>, context: CoroutineContext = this.coroutineContext):
SendChannel<RPCInputMessage> = actor(context = context, capacity = Channel.UNLIMITED, start = CoroutineStart.LAZY) {
RPCActor(this, channel, output).start()
}

/**
* Superclass for messages sent to the RPC actor.
*/
@Suppress("MemberVisibilityCanBePrivate")
sealed class RPCInputMessage {
/**
* Make the RPC actor connect to the client.
*
* @param clientId your app's client ID.
* @param autoRegister whether Discord should register your app for automatic launch (untested! Probably broken because Java).
* @param steamId your app's Steam ID, if any.
* @param refreshRate the rate in milliseconds at which this handler will run callbacks and send info to discord.
*/
data class Connect(
val clientId: String,
val autoRegister: Boolean = false,
val steamId: String? = null,
val refreshRate: Long = 500L
) : RPCInputMessage()

/**
* Update the user's Rich Presence.
* The presence will be cached if used before the app has connected, and automatically sent once ready.
*
* @see DiscordRichPresence for all available options.
*/
data class UpdatePresence(val presence: DiscordRichPresence) : RPCInputMessage()
}

/**
* Superclass for messages sent by the RPC actor.
*/
@Suppress("MemberVisibilityCanBePrivate")
sealed class RPCOutputMessage {
/**
* Sent when the RPC actor has logged in, and is ready to be accessed.
*/
data class Ready(val user: DiscordUser) : RPCOutputMessage()

/**
* Sent when the RPC actor has been disconnected.
*
* @param errorCode the error code causing disconnection.
* @param message the message for disconnection.
*/
data class Disconnected(val errorCode: Int, val message: String) : RPCOutputMessage()

/**
* Sent when the RPC actor has detected an error.
*
* @param errorCode the error code causing the error.
* @param message the message for the error.
*/
data class Errored(val errorCode: Int, val message: String) : RPCOutputMessage()

/**
* Sent when the someone accepted a game invitation.
*
* @param joinSecret the game invitation secret.
*/
data class JoinGame(val joinSecret: String) : RPCOutputMessage()

/**
* Sent when the someone accepted a game spectating invitation.
*
* @param spectateSecret the game spectating secret.
*/
data class Spectate(val spectateSecret: String) : RPCOutputMessage()

/**
* Sent when the someone requested to join the game.
*
* @param user the requester.
*/
data class JoinRequest(val user: DiscordUser) : RPCOutputMessage()
}

/**
* RPC Actor implementation.
*
* @param scope the scope for this actor to act in.
* @param input the actor's input channel.
* @param output the actor's output channel.
*/
@ExperimentalCoroutinesApi
private class RPCActor(
private val scope: CoroutineScope,
private val input: ReceiveChannel<RPCInputMessage>,
private val output: SendChannel<RPCOutputMessage>) {

private val logger = KotlinLogging.logger { }

private var connected = false
private var initialized = false
private lateinit var user: DiscordUser
private var queuedPresence: DiscordRichPresence? = null

/**
* Start the actor.
*/
suspend fun start() {
for (m in input) onReceive(m)
}

private suspend fun onReceive(msg: RPCInputMessage) {
when (msg) {
is RPCInputMessage.Connect -> with(msg) { connect(clientId, autoRegister, steamId, refreshRate) }
is RPCInputMessage.UpdatePresence -> if (initialized) DiscordRpc.Discord_UpdatePresence(msg.presence) else queuedPresence = msg.presence
}
}

/**
* Connect the actor to the RPC Client.
*
* @see DiscordRpc.Discord_Initialize
*/
private suspend fun connect(clientId: String, autoRegister: Boolean = false, steamId: String? = null, refreshRate: Long = 500L) {
try {
DiscordRpc.Discord_Initialize(clientId, handlers, autoRegister, steamId)
initialized = true
if (queuedPresence != null) {
DiscordRpc.Discord_UpdatePresence(queuedPresence!!)
queuedPresence = null
}
while (!input.isClosedForReceive) {
var m = input.poll()
while (m != null) {
onReceive(m)
m = input.poll()
}
DiscordRpc.Discord_RunCallbacks()
delay(refreshRate)
}
} catch (e: CancellationException) {
} catch (e: Throwable) {
output.send(RPCOutputMessage.Errored(-1, "Unknown error caused by: ${e.message}"))
} finally {
output.send(RPCOutputMessage.Disconnected(0, "Discord RPC Thread closed."))
output.close()
connected = false
scope.coroutineContext.cancelChildren()
try {
DiscordRpc.Discord_Shutdown()
} catch (e: Throwable) {
}
}
}

private val handlers = DiscordEventHandlers {
onReady {
user = it
connected = true
scope.launch {
output.send(RPCOutputMessage.Ready(it))
}
}
onDisconnected { errorCode, message ->
logger.warn("Disconnected: #$errorCode (${message.takeIf { message.isNotEmpty() }
?: "No message provided"})")
connected = false
scope.launch {
output.send(RPCOutputMessage.Disconnected(errorCode, message))
}
}
onErrored { errorCode, message ->
logger.error("Error: #$errorCode (${message.takeIf { message.isNotEmpty() } ?: "No message provided"})")
connected = false
scope.launch {
output.send(RPCOutputMessage.Errored(errorCode, message))
}
}
onJoinGame {
scope.launch {
output.send(RPCOutputMessage.JoinGame(it))
}
}
onSpectateGame {
scope.launch {
output.send(RPCOutputMessage.Spectate(it))
}
}
onJoinRequest {
scope.launch {
output.send(RPCOutputMessage.JoinRequest(it))
}
}
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/be/bluexin/drpc4k/jna/RPCHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import java.util.concurrent.atomic.AtomicBoolean
*
* @author Bluexin
*/
@Suppress("MemberVisibilityCanPrivate", "unused")
@Suppress("MemberVisibilityCanBePrivate", "unused")
object RPCHandler {
private val logger = KotlinLogging.logger {}

Expand Down
Loading

0 comments on commit 5751ff8

Please sign in to comment.