improve event-schema polymorphism with enums

This commit is contained in:
Adam 2022-01-08 23:09:27 +01:00
parent 3875b9c309
commit 51e0aff823
18 changed files with 120 additions and 67 deletions

View file

@ -15,5 +15,6 @@ interface FactorioConfigurationUpdate extends KafkatorioPacket {
allMods: FactorioModInfo[]; allMods: FactorioModInfo[];
factorioData: FactorioGameDataUpdate; factorioData: FactorioGameDataUpdate;
modVersion: string; modVersion: string;
packetType: PacketType;
prototypes: FactorioPrototype[]; prototypes: FactorioPrototype[];
} }

View file

@ -1,13 +1,16 @@
// Generated by TypeScriptGenerator - do not edit this file manually // Generated by TypeScriptGenerator - do not edit this file manually
type ObjectName = "LuaPlayer" | "LuaEntity" | "LuaSurface" | "FactorioMapChunk" | "LuaTile" | "ConsoleChatMessage";
interface FactorioObjectData { interface FactorioObjectData {
objectName: string; objectName: ObjectName;
} }
interface FactorioEvent<T extends FactorioObjectData> extends KafkatorioPacket { interface FactorioEvent<T extends FactorioObjectData> extends KafkatorioPacket {
data: T; data: T;
eventType: string; eventType: string;
modVersion: string; modVersion: string;
packetType: PacketType;
tick: uint; tick: uint;
} }
@ -18,7 +21,7 @@ interface PlayerData extends FactorioObjectData {
colour: Colour; colour: Colour;
lastOnline: uint; lastOnline: uint;
name: string; name: string;
objectName: string; objectName: ObjectName;
position: PositionData; position: PositionData;
} }
@ -27,7 +30,7 @@ interface EntityData extends FactorioObjectData {
health: double | null; health: double | null;
healthRatio: double; healthRatio: double;
name: string; name: string;
objectName: string; objectName: ObjectName;
playerIndex: uint | null; playerIndex: uint | null;
position: PositionData; position: PositionData;
surfaceIndex: int; surfaceIndex: int;
@ -39,23 +42,23 @@ interface SurfaceData extends FactorioObjectData {
daytime: double; daytime: double;
index: uint; index: uint;
name: string; name: string;
objectName: string; objectName: ObjectName;
} }
interface FactorioMapTile extends FactorioObjectData { interface FactorioMapTile extends FactorioObjectData {
objectName: string; objectName: ObjectName;
position: PositionData; position: PositionData;
prototypeName: string; prototypeName: string;
surfaceIndex: int; surfaceIndex: int;
} }
interface FactorioMapChunk extends FactorioObjectData { interface FactorioMapChunk extends FactorioObjectData {
objectName: string; objectName: ObjectName;
tiles: FactorioMapTile[]; tiles: FactorioMapTile[];
} }
interface ConsoleChatMessage extends FactorioObjectData { interface ConsoleChatMessage extends FactorioObjectData {
authorPlayerIndex: uint | null; authorPlayerIndex: uint | null;
content: string; content: string;
objectName: string; objectName: ObjectName;
} }

View file

@ -1,12 +1,9 @@
// Generated by TypeScriptGenerator - do not edit this file manually // Generated by TypeScriptGenerator - do not edit this file manually
interface FactorioPrototype { type PrototypeObjectName = "LuaTilePrototype";
objectName: string;
}
interface FactorioPrototypes<T extends FactorioPrototype> extends KafkatorioPacket { interface FactorioPrototype {
modVersion: string; objectName: PrototypeObjectName;
prototypes: T[];
} }
interface FactorioMapTilePrototype extends FactorioPrototype { interface FactorioMapTilePrototype extends FactorioPrototype {
@ -15,6 +12,6 @@ interface FactorioMapTilePrototype extends FactorioPrototype {
layer: uint; layer: uint;
mapColor: Colour; mapColor: Colour;
name: string; name: string;
objectName: string; objectName: PrototypeObjectName;
order: string; order: string;
} }

View file

@ -1,7 +1,10 @@
// Generated by TypeScriptGenerator - do not edit this file manually // Generated by TypeScriptGenerator - do not edit this file manually
type PacketType = "EVENT" | "CONFIG";
interface KafkatorioPacket { interface KafkatorioPacket {
modVersion: string; modVersion: string;
packetType: PacketType;
} }
interface Colour { interface Colour {

View file

@ -7,6 +7,7 @@ export function handleConfigurationUpdate(changeData: ConfigurationChangedData)
emitPacket<FactorioConfigurationUpdate>({ emitPacket<FactorioConfigurationUpdate>({
modVersion: global.MOD_VERSION, modVersion: global.MOD_VERSION,
packetType: "CONFIG",
allMods: allMods(changeData), allMods: allMods(changeData),
factorioData: factorioData(changeData), factorioData: factorioData(changeData),
prototypes: convertPrototypes() prototypes: convertPrototypes()

View file

@ -7,6 +7,7 @@ import {emitPacket} from "../emitKafkatorioPacket";
export function emitEvent<T extends FactorioObjectData>(eventData: T, tick: uint, eventType: string) { export function emitEvent<T extends FactorioObjectData>(eventData: T, tick: uint, eventType: string) {
emitPacket({ emitPacket({
data: eventData, data: eventData,
packetType: "EVENT",
eventType: eventType, eventType: eventType,
modVersion: global.MOD_VERSION, modVersion: global.MOD_VERSION,
tick: tick tick: tick

View file

@ -1,6 +1,7 @@
package dev.adamko.kafkatorio.events.schema package dev.adamko.kafkatorio.events.schema
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable @Serializable
@ -9,7 +10,10 @@ data class FactorioConfigurationUpdate(
val factorioData: FactorioGameDataUpdate, val factorioData: FactorioGameDataUpdate,
val allMods: List<FactorioModInfo>, val allMods: List<FactorioModInfo>,
val prototypes: List<FactorioPrototype>, val prototypes: List<FactorioPrototype>,
) : KafkatorioPacket ) : KafkatorioPacket() {
@Transient
override val packetType = PacketType.CONFIG
}
@Serializable @Serializable
data class FactorioGameDataUpdate( data class FactorioGameDataUpdate(

View file

@ -2,15 +2,18 @@ package dev.adamko.kafkatorio.events.schema
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable @Serializable
data class FactorioEvent<out T : FactorioObjectData>( data class FactorioEvent<out T : FactorioObjectData>(
override val modVersion: String, override val modVersion: String,
/** the initial Factorio event (`defines.events`) trigger */ /** the initial Factorio event (`defines.events`) trigger */
val eventType: String, val eventType: String,
/** game time */ /** game time */
val tick: UInt, val tick: UInt,
val data: T, val data: T,
) : KafkatorioPacket ) : KafkatorioPacket() {
@Transient
override val packetType: PacketType = PacketType.EVENT
}

View file

@ -9,7 +9,8 @@ import kotlinx.serialization.json.JsonClassDiscriminator
@JsonClassDiscriminator(FactorioObjectData.discriminatorKey) @JsonClassDiscriminator(FactorioObjectData.discriminatorKey)
sealed class FactorioObjectData { sealed class FactorioObjectData {
abstract val objectName: String /** @see FactorioObjectData.discriminatorKey */
abstract val objectName: ObjectName
// workaround - https://github.com/Kotlin/kotlinx.serialization/issues/1664 // workaround - https://github.com/Kotlin/kotlinx.serialization/issues/1664
// val objectName: String by lazy { // val objectName: String by lazy {
@ -28,12 +29,22 @@ sealed class FactorioObjectData {
// } // }
// } // }
enum class ObjectName {
LuaPlayer,
LuaEntity,
LuaSurface,
FactorioMapChunk,
LuaTile,
ConsoleChatMessage,
}
companion object { companion object {
/** The [JsonClassDiscriminator] for [FactorioEvent] */ /** The [JsonClassDiscriminator] for [FactorioEvent] */
const val discriminatorKey: String = "objectName" const val discriminatorKey: String = "objectName"
} }
} }
@Serializable @Serializable
@SerialName("LuaPlayer") @SerialName("LuaPlayer")
data class PlayerData( data class PlayerData(
@ -47,7 +58,7 @@ data class PlayerData(
val lastOnline: UInt, val lastOnline: UInt,
) : FactorioObjectData() { ) : FactorioObjectData() {
@Transient @Transient
override val objectName: String = "LuaPlayer" override val objectName = ObjectName.LuaPlayer
} }
@Serializable @Serializable
@ -64,7 +75,7 @@ data class EntityData(
val playerIndex: UInt? = null, val playerIndex: UInt? = null,
) : FactorioObjectData() { ) : FactorioObjectData() {
@Transient @Transient
override val objectName: String = "LuaEntity" override val objectName = ObjectName.LuaEntity
} }
@Serializable @Serializable
@ -75,16 +86,17 @@ data class SurfaceData(
val name: String, val name: String,
) : FactorioObjectData() { ) : FactorioObjectData() {
@Transient @Transient
override val objectName: String = "LuaSurface" override val objectName = ObjectName.LuaSurface
} }
/** Quasi-object. This isn't a Factorio Lua type, but a helpful collection of [FactorioMapTile]s */
@Serializable @Serializable
@SerialName("FactorioMapChunk") @SerialName("FactorioMapChunk")
data class FactorioMapChunk( data class FactorioMapChunk(
val tiles: List<FactorioMapTile>, val tiles: List<FactorioMapTile>,
) : FactorioObjectData() { ) : FactorioObjectData() {
@Transient @Transient
override val objectName: String = "FactorioMapChunk" override val objectName = ObjectName.FactorioMapChunk
} }
@Serializable @Serializable
@ -95,7 +107,7 @@ data class FactorioMapTile(
val surfaceIndex: Int, val surfaceIndex: Int,
) : FactorioObjectData() { ) : FactorioObjectData() {
@Transient @Transient
override val objectName: String = "LuaTile" override val objectName = ObjectName.LuaTile
} }
@Serializable @Serializable
@ -104,5 +116,5 @@ data class ConsoleChatMessage(
val content: String, val content: String,
) : FactorioObjectData() { ) : FactorioObjectData() {
@Transient @Transient
override val objectName: String = "ConsoleChatMessage" override val objectName = ObjectName.ConsoleChatMessage
} }

View file

@ -7,14 +7,17 @@ import kotlinx.serialization.json.JsonClassDiscriminator
@Serializable @Serializable
class FactorioPrototypes<T : FactorioPrototype>( class FactorioPrototypes<T : FactorioPrototype>(
override val modVersion: String,
val prototypes: List<T>, val prototypes: List<T>,
) : KafkatorioPacket )
@Serializable @Serializable
@JsonClassDiscriminator(FactorioPrototype.discriminatorKey) @JsonClassDiscriminator(FactorioPrototype.discriminatorKey)
sealed class FactorioPrototype { sealed class FactorioPrototype {
abstract val objectName: String abstract val objectName: PrototypeObjectName
enum class PrototypeObjectName {
LuaTilePrototype
}
companion object { companion object {
const val discriminatorKey: String = "objectName" const val discriminatorKey: String = "objectName"
@ -34,5 +37,5 @@ data class FactorioMapTilePrototype(
val canBeMined: Boolean, val canBeMined: Boolean,
) : FactorioPrototype() { ) : FactorioPrototype() {
@Transient @Transient
override val objectName: String = "LuaTilePrototype" override val objectName: PrototypeObjectName = PrototypeObjectName.LuaTilePrototype
} }

View file

@ -1,19 +1,22 @@
package dev.adamko.kafkatorio.events.schema; package dev.adamko.kafkatorio.events.schema;
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator
//@Serializable
sealed interface KafkatorioPacket { @Serializable
@JsonClassDiscriminator("packetType")
sealed class KafkatorioPacket {
/** Schema versioning */ /** Schema versioning */
val modVersion: String abstract val modVersion: String
abstract val packetType: PacketType
enum class PacketType {
EVENT, CONFIG,
}
} }
//enum class PacketDataType {
// EVENT, PROTOTYPE, CONFIG_DATA,
// ;
//}
//@Serializable //@Serializable
//@JsonClassDiscriminator(KafkatorioPacketData.discriminatorKey) //@JsonClassDiscriminator(KafkatorioPacketData.discriminatorKey)
//sealed class KafkatorioPacketData( //sealed class KafkatorioPacketData(

View file

@ -48,7 +48,7 @@ fun main(args: Array<String>) {
.forEach { (defFile, def) -> .forEach { (defFile, def) ->
val outFile = outPath.resolve(defFile).toFile() val outFile = outPath.resolve(defFile).toFile()
outFile.printWriter().use { outFile.printWriter().use {
it.print(header) it.println(header)
it.print(def) it.print(def)
} }
println("Generated file: $outFile") println("Generated file: $outFile")
@ -65,14 +65,16 @@ private fun splitDefinitions(definitions: String): Map<String, String> {
.groupBy { def -> .groupBy { def ->
when { when {
"extends FactorioObjectData" in def || "extends FactorioObjectData" in def ||
"interface FactorioObjectData" in def -> "object-data.d.ts" "interface FactorioObjectData" in def ||
"type ObjectName" in def -> "object-data.d.ts"
"extends FactorioPrototype" in def || "extends FactorioPrototype" in def ||
"interface FactorioPrototype" in def -> "prototype.d.ts" "interface FactorioPrototype" in def ||
"type PrototypeObjectName" in def -> "prototype.d.ts"
"extends FactorioConfigurationUpdate" in def || "extends FactorioConfigurationUpdate" in def ||
"interface FactorioConfigurationUpdate" in def || "interface FactorioConfigurationUpdate" in def ||
"FactorioGameDataUpdate" in def || "FactorioGameDataUpdate" in def ||
"FactorioModInfo" in def -> "config-update.d.ts" "FactorioModInfo" in def -> "config-update.d.ts"
else -> "schema.d.ts" else -> "schema.d.ts"
} }
} }
.mapValues { (_, def) -> .mapValues { (_, def) ->
@ -86,7 +88,7 @@ private fun splitDefinitions(definitions: String): Map<String, String> {
val header = """ val header = """
|// Generated by TypeScriptGenerator - do not edit this file manually |// Generated by TypeScriptGenerator - do not edit this file manually
| |
""".trimMargin() + "\n" """.trimMargin()
//object PropertyNameTransformer : ClassTransformer { //object PropertyNameTransformer : ClassTransformer {
// override fun transformPropertyName( // override fun transformPropertyName(

View file

@ -1,7 +1,8 @@
package dev.adamko.kafkatorio.processor package dev.adamko.kafkatorio.processor
import dev.adamko.kafkatorio.events.schema.FactorioConfigurationUpdate
import dev.adamko.kafkatorio.events.schema.FactorioEvent import dev.adamko.kafkatorio.events.schema.FactorioEvent
import dev.adamko.kafkatorio.events.schema.FactorioObjectData import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import java.time.Duration import java.time.Duration
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
@ -27,7 +28,7 @@ class FactorioEventsTopology(
) )
.mapValues { readOnlyKey, value -> .mapValues { readOnlyKey, value ->
println("Mapping $readOnlyKey:$value") println("Mapping $readOnlyKey:$value")
jsonMapper.decodeFromString<FactorioEvent<FactorioObjectData>>(value) jsonMapper.decodeFromString<KafkatorioPacket>(value)
} }
.peek { _, value -> .peek { _, value ->
websocketServer.sendMessage(jsonMapper.encodeToString(value)) websocketServer.sendMessage(jsonMapper.encodeToString(value))
@ -35,9 +36,14 @@ class FactorioEventsTopology(
.to( .to(
{ _, value, _ -> { _, value, _ ->
// println("[$key] sending event:${value.eventType} to topic:${value.data.objectName()}") // println("[$key] sending event:${value.eventType} to topic:${value.data.objectName()}")
value.data.objectName when (value) {
is FactorioEvent<*> ->
"kafkatorio.${value.packetType.name}.${value.data.objectName.name}"
is FactorioConfigurationUpdate ->
"kafkatorio.${value.packetType.name}"
}
}, },
Produced.with(Serdes.String(), JsonSerdes) Produced.with(Serdes.String(), KafkatorioPacketSerde)
) )
val topology = builder.build() val topology = builder.build()

View file

@ -16,4 +16,5 @@ fun main() = runBlocking {
topology.build() topology.build()
wsServer.build().asServer(Undertow(9073)).start().block() wsServer.build().asServer(Undertow(9073)).start().block()
} }

View file

@ -1,7 +1,6 @@
package dev.adamko.kafkatorio.processor package dev.adamko.kafkatorio.processor
import dev.adamko.kafkatorio.events.schema.FactorioEvent import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import dev.adamko.kafkatorio.events.schema.FactorioObjectData
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -14,12 +13,12 @@ import org.http4k.format.asConfigurable
import org.http4k.format.withStandardMappings import org.http4k.format.withStandardMappings
object JsonSerdes : Serde<FactorioEvent<FactorioObjectData>> { object KafkatorioPacketSerde : Serde<KafkatorioPacket> {
override fun serializer() = Serializer<FactorioEvent<FactorioObjectData>> { _, message -> override fun serializer() = Serializer<KafkatorioPacket> { _, message ->
jsonMapper.encodeToString<FactorioEvent<FactorioObjectData>>(message).encodeToByteArray() jsonMapper.encodeToString<KafkatorioPacket>(message).encodeToByteArray()
} }
override fun deserializer() = Deserializer<FactorioEvent<FactorioObjectData>> { _, bytes -> override fun deserializer() = Deserializer<KafkatorioPacket> { _, bytes ->
jsonMapper.decodeFromString(bytes.decodeToString()) jsonMapper.decodeFromString(bytes.decodeToString())
} }
} }

View file

@ -56,7 +56,6 @@ fun main() {
"ws://localhost:9073/ws", "ws://localhost:9073/ws",
reduxStore, reduxStore,
) )
App( App(
reduxStore = reduxStore, reduxStore = reduxStore,
wsService = wsService wsService = wsService

View file

@ -2,9 +2,8 @@ package dev.adamko.kafkatorio.webmap
import dev.adamko.kafkatorio.events.schema.FactorioEvent import dev.adamko.kafkatorio.events.schema.FactorioEvent
import dev.adamko.kafkatorio.events.schema.PlayerData import dev.adamko.kafkatorio.events.schema.PlayerData
import externals.leaflet.geo.LatLng import io.kvision.maps.externals.leaflet.layer.overlay.Tooltip
import externals.leaflet.layer.overlay.Tooltip import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
import externals.leaflet.layer.vector.CircleMarker
import io.kvision.maps.Maps import io.kvision.maps.Maps
import io.kvision.maps.Maps.Companion.L import io.kvision.maps.Maps.Companion.L
import io.kvision.redux.ReduxStore import io.kvision.redux.ReduxStore
@ -12,6 +11,7 @@ import io.kvision.redux.createReduxStore
import io.kvision.utils.obj import io.kvision.utils.obj
import redux.RAction import redux.RAction
fun createFactorioReduxStore(): ReduxStore<FactorioGameState, FactorioUpdate> = fun createFactorioReduxStore(): ReduxStore<FactorioGameState, FactorioUpdate> =
createReduxStore( createReduxStore(
::factorioGameReducer, ::factorioGameReducer,
@ -21,6 +21,7 @@ fun createFactorioReduxStore(): ReduxStore<FactorioGameState, FactorioUpdate> =
) )
) )
data class FactorioGameState( data class FactorioGameState(
val kvMaps: Maps, val kvMaps: Maps,
val players: Map<String, PlayerState>, val players: Map<String, PlayerState>,
@ -32,6 +33,7 @@ sealed class FactorioUpdate : RAction {
FactorioUpdate() FactorioUpdate()
} }
fun factorioGameReducer(state: FactorioGameState, update: FactorioUpdate): FactorioGameState = fun factorioGameReducer(state: FactorioGameState, update: FactorioUpdate): FactorioGameState =
when (update) { when (update) {
is FactorioUpdate.PlayerUpdate -> { is FactorioUpdate.PlayerUpdate -> {
@ -77,7 +79,7 @@ data class PlayerState(
fillColor = color fillColor = color
fillOpacity = 1f fillOpacity = 1f
radius = 10 radius = 10
className = playerData.objectName className = playerData.objectName.name
// interactive = false // interactive = false
} }

View file

@ -1,13 +1,20 @@
package dev.adamko.kafkatorio.webmap package dev.adamko.kafkatorio.webmap
import dev.adamko.kafkatorio.events.schema.ConsoleChatMessage
import dev.adamko.kafkatorio.events.schema.EntityData
import dev.adamko.kafkatorio.events.schema.FactorioConfigurationUpdate
import dev.adamko.kafkatorio.events.schema.FactorioEvent import dev.adamko.kafkatorio.events.schema.FactorioEvent
import dev.adamko.kafkatorio.events.schema.FactorioObjectData import dev.adamko.kafkatorio.events.schema.FactorioMapChunk
import dev.adamko.kafkatorio.events.schema.FactorioMapTile
import dev.adamko.kafkatorio.events.schema.KafkatorioPacket
import dev.adamko.kafkatorio.events.schema.PlayerData import dev.adamko.kafkatorio.events.schema.PlayerData
import dev.adamko.kafkatorio.events.schema.SurfaceData
import io.kvision.redux.ReduxStore import io.kvision.redux.ReduxStore
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import org.w3c.dom.MessageEvent import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket import org.w3c.dom.WebSocket
class WebsocketService( class WebsocketService(
private val wsUrl: String, private val wsUrl: String,
private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>, private val reduxStore: ReduxStore<FactorioGameState, FactorioUpdate>,
@ -24,22 +31,28 @@ class WebsocketService(
if ( if (
data != null data != null
&& data.trim().startsWith("{") && data.trim().run { startsWith("{") && endsWith("}") }
&& data.trim().endsWith("}")
) { ) {
println(data.replace('\n', ' ')) println(data.replace('\n', ' '))
val event: FactorioEvent<FactorioObjectData> = jsonMapper.decodeFromString(data)
when (val eventData = event.data) { when (val event: KafkatorioPacket = jsonMapper.decodeFromString(data)) {
is PlayerData -> { is FactorioEvent<*> -> {
reduxStore.dispatch(FactorioUpdate.PlayerUpdate(event, eventData)) when (val eventData = event.data) {
is PlayerData ->
reduxStore.dispatch(FactorioUpdate.PlayerUpdate(event, eventData))
is ConsoleChatMessage -> {}
is EntityData -> {}
is FactorioMapChunk -> {}
is FactorioMapTile -> {}
is SurfaceData -> {}
}
} }
else -> {} is FactorioConfigurationUpdate -> {}
} }
} else { } else {
println(data) println(data)
} }
} }