Karth

Karth is a wrapper around GEarth with as main goal to provide a type-safe way to listen for and send messages.

Type-safe

Karth provides a type-safe way to listen for and send messages. There are Message.Outgoing (to server) and Message.Incoming (to client) typed messages. These are sealed classes and define the specific Messageimplementations.

Below follows an example of Karth’s Message implementation:

sealed class Message(val name: String, val direction: HMessage.Direction) {
    
    sealed class Incoming(name: String) : Message(name, HMessage.Direction.TOCLIENT) {

        @Decoder(MessageDecoder.IncomingChatDecoder::class)
        data class Chat(val userIndex: Int, val message: String) : Incoming("Chat")
    }
   
    sealed class Outgoing(name: String) : Message(name, HMessage.Direction.TOSERVER) {

        @Encoder(MessageEncoder.OutgoingChatEncoder::class)
        data class Chat(val message: String, val colorIndex: Int, val userIndex: Int) : Outgoing("Chat")
    }
}

Usage

To use Karth, create a new KarthSession.

val session = KarthSession(extension)

Listen to a message of a specific type during lifetime of program.

 // Listen for incoming messages of type Chat
session.on<Message.Incoming.Chat> {
    // access properties of Chat messages within lambda scope
    // behaves like `this` reference, could do `this.userIndex`
    println("$userIndex send $message")
}

Send a message of a specific type.

val heyChat = Message.Outgoing.Chat(userIndex = 69, "Hey", color = 1)
session.send(heyChat)

Send and expect a message, blocking the current thread until expected message is received, or when a timeout occurs.

 // Listen for the first Message.Incoming.Chat message after sending the packet
session.sendAndReceive<Outgoing.Chat, Incoming.Chat>(
    toSend = heyChat,
    onReceive = { println("Received a message from $userIndex containing $contents") }
)

// Add a condition, only accept the first message that fulfills it
session.sendAndReceive<Outgoing.Chat, Incoming.Chat>(
    toSend = heyChat,
    condition = { userIndex == 69 }, // accept first message for user with index `69`
    onReceive = { println("Received a message from $userIndex containing $contents") }
)

// Specify max duration of an attempt at receiving the expected packet
session.sendAndReceive<Outgoing.Chat, Incoming.Chat>(
    toSend = heyChat,        
    maxWaitTime = 1000.milliseconds,
    condition = { userIndex == 69 }, // accept first message for user with index `69`
    onReceive = { println("Received a message from $userIndex containing $contents") },
)

// Specify how many times the method should retry in case of a timeout/read failure.
session.sendAndReceive<Outgoing.Chat, Incoming.Chat>(
    toSend = heyChat,
    maxWaitTime = 1000.milliseconds,
    maxAttempts = 10, // if failed 10 times in a row, stop blocking
    condition = { userIndex == 69 }, // accept first message for user with index `69`
    onReceive = { println("Received a message from $userIndex containing $contents") },
)

// Handle exceptions
session.sendAndReceive<Outgoing.Chat, Incoming.Chat>(
    toSend = heyChat,
    maxWaitTime = 1000.milliseconds,
    maxAttempts = 10, // if failed 10 times in a row, stop blocking
    condition = { userIndex == 69 }, // accept first message for user with index `69`
    onReceive = { println("Received a message from $userIndex containing $contents") },
    onException = { it.printStackTrace() }
)

Live Entities

These are entities with observable states whose state reflect the current state in the client. Live Entities maintain their state by listening to relevant incoming messages.

Create a LiveRoom instance.

val liveRoom = LiveRoom(session)

For example, a LiveRoom entitiy listens to all Room related packets in order to maintain its state. It uses these packets for one to maintaina a list of all the items in the room.

val fxFloorItemNodes = FXCollections.observableArrayList<Label>() // e.g. used as backing list for some ListView
liveRoom.floorItemList.addListener(ListChangeListener {
    Platform.runLater {
        fxFloorItemNodes.setAll(it.list.map { floorItem -> Label("FloorItem(id=${floorItem.id})") })
    }
})

GitHub

View Github