biggest refactor ever...

- use kxs-ts-gen for TypeScript schemas
- update schema files, so they're tidier (no need for typescript-generator workarounds)
- version bumps (kotlin, gradle, tstl, factorio, kotest)
- improve kxs binary format (use okio)
- move schema files package ('events' wasn't needed)
- tidy up topic creation, naming. base it off classes, make util functions
This commit is contained in:
Adam 2022-04-19 23:25:10 +02:00
parent db14865ca9
commit 3fff57d46d
82 changed files with 2545 additions and 1914 deletions

View file

@ -1,57 +0,0 @@
package dev.adamko.kafkatorio.events.schema.converters
import dev.adamko.kafkatorio.events.schema.MAP_CHUNK_SIZE
import dev.adamko.kafkatorio.events.schema.MapChunkPosition
import dev.adamko.kafkatorio.events.schema.MapEntityPosition
import dev.adamko.kafkatorio.events.schema.MapTilePosition
import kotlin.math.floor
operator fun MapTilePosition.times(factor: Int) =
MapTilePosition(x * factor, y * factor)
operator fun MapTilePosition.div(divisor: Int) =
MapTilePosition(x / divisor, y / divisor)
operator fun MapTilePosition.plus(addend: Int) =
MapTilePosition(x + addend, y + addend)
fun MapTilePosition.toMapChunkPosition(chunkSize: Int = MAP_CHUNK_SIZE) =
MapChunkPosition(
floor(x.toDouble() / chunkSize.toDouble()).toInt(),
floor(y.toDouble() / chunkSize.toDouble()).toInt(),
)
fun MapEntityPosition.toMapTilePosition() =
MapChunkPosition(floor(x).toInt(), floor(y).toInt())
fun MapEntityPosition.toMapChunkPosition(chunkSize: Int = MAP_CHUNK_SIZE) =
MapChunkPosition(
floor(x / chunkSize.toDouble()).toInt(),
floor(y / chunkSize.toDouble()).toInt(),
)
operator fun MapChunkPosition.times(factor: Int) =
MapChunkPosition(x * factor, y * factor)
operator fun MapChunkPosition.plus(addend: Int) =
MapChunkPosition(x + addend, y + addend)
operator fun MapChunkPosition.minus(subtrahend: Int) =
MapChunkPosition(x - subtrahend, y - subtrahend)
fun MapChunkPosition.leftTopTile(chunkSize: Int = MAP_CHUNK_SIZE): MapTilePosition =
MapTilePosition(
x * chunkSize,
y * chunkSize,
)
//val MapChunkPosition.rightBottomTile: MapTilePosition
// get() = leftTopTile + (MAP_CHUNK_SIZE - 1)
//
//
//val MapChunkPosition.boundingBox: MapBoundingBox
// get() = MapBoundingBox(leftTopTile, rightBottomTile)

View file

@ -1,158 +0,0 @@
package dev.adamko.kafkatorio.events.schema
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
const val MAP_CHUNK_SIZE = 32
@Serializable
@JvmInline
value class Tick(val value: UInt)
/**
* Coordinates of a [EntityData] on a map.
*
* A [MapEntityPosition] can be translated to a [MapChunkPosition] by dividing the `x`/`y` values
* by [32][MAP_CHUNK_SIZE].
*/
@Serializable
data class MapEntityPosition(
val x: Double,
val y: Double,
)
/** Coordinates of a chunk in a [SurfaceData] where each integer `x`/`y` represents a different
* chunk.
*
* A [MapChunkPosition] can be translated to a [MapEntityPosition] by multiplying the `x`/`y`
* values by [32][MAP_CHUNK_SIZE].
*/
@Serializable
data class MapChunkPosition(
val x: Int,
val y: Int,
)
/**
* Coordinates of a tile in a chunk on a [SurfaceData] where each integer `x`/`y` represents a
* different [MapTile].
*
* It rounds any `x`/`y` down to whole numbers.
*/
@Serializable
data class MapTilePosition(
val x: Int,
val y: Int,
)
@Serializable
data class MapBoundingBox(
val topLeft: MapTilePosition,
val bottomRight: MapTilePosition,
)
/**
* Red, green, blue and alpha values, all in range `[0, 1]` or all in range `[0, 255]` if any
* value is > 1.
*
* All values here are optional. Colour channels default to `0`, the alpha channel defaults to `1`.
*/
@Serializable
data class Colour(
val red: Float = 0f,
val green: Float = 0f,
val blue: Float = 0f,
val alpha: Float = 1f,
)
/** Size-efficient version of [Colour] (`4*4` bytes vs `4*1` bytes) */
@Serializable
data class ColourHex(
val red: UByte = UByte.MIN_VALUE,
val green: UByte = UByte.MIN_VALUE,
val blue: UByte = UByte.MIN_VALUE,
val alpha: UByte = UByte.MAX_VALUE,
) {
companion object {
val TRANSPARENT = ColourHex(0u, 0u, 0u, 0u)
}
}
@Serializable
data class MapTiles(
val surfaceIndex: SurfaceIndex,
val tiles: List<MapTile>,
)
@Serializable
data class MapTile(
val x: Int,
val y: Int,
val proto: PrototypeName,
)
interface EntityIdentifiers {
val unitNumber: UnitNumber?
val name: String
val type: String
}
// instant of EntityIdentifiers, because kxs needs a concrete instance
@Serializable
data class EntityIdentifiersData(
override val unitNumber: UnitNumber? = null,
override val name: String,
/** The prototype-type of the entity. */
override val type: String,
) : EntityIdentifiers
/**
* Size-optimised 2D map of x/y tile positions and prototype-name.
*
* Each X/Y coordinate maps to an arbitrary [PrototypeKey] (which is only valid for any specific
* instance). [protos] is specific to each instance of [MapTileDictionary], can be used to convert from a
*/
@Serializable
data class MapTileDictionary(
/** Map an X,Y coordinate a prototype name */
val tilesXY: Map<String, Map<String, PrototypeKey>>,
val protos: Map<PrototypeKey, PrototypeName>,
) {
fun toMapTileList(): List<MapTile> = buildList {
tilesXY.forEach { (xString, row) ->
row.forEach { (yString, protoIndex) ->
val x = xString.toIntOrNull()
val y = yString.toIntOrNull()
val protoName = protos[protoIndex]
if (x != null && y != null && protoName != null) {
add(MapTile(x, y, protoName))
}
}
}
}
companion object {
fun MapTileDictionary?.isNullOrEmpty(): Boolean {
return this == null || (tilesXY.isEmpty() && tilesXY.all { it.value.isEmpty() })
}
}
}
@Serializable
@JvmInline
value class PrototypeKey(val index: String)

View file

@ -1,287 +0,0 @@
package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.events.schema.FactorioUpdateData.FactorioUpdateDataType
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@Serializable
sealed class KafkatorioPacket {
/** Schema versioning */
abstract val modVersion: String
abstract val tick: Tick
abstract val update: FactorioUpdateData
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
sealed interface FactorioUpdateDataKey {
val updateType: FactorioUpdateDataType
}
@Serializable(with = FactorioUpdateData.JsonSerializer::class)
sealed class FactorioUpdateData : FactorioUpdateDataKey {
/** Discriminator for [FactorioUpdateData] */
@EncodeDefault
abstract override val updateType: FactorioUpdateDataType
/** the initial Factorio event (`defines.events`) trigger */
abstract val eventCounts: Map<String, UInt>?
@Serializable
enum class FactorioUpdateDataType {
PLAYER,
MAP_CHUNK,
ENTITY,
CONSOLE_CHAT,
CONSOLE_COMMAND,
SURFACE,
CONFIG,
PROTOTYPES,
;
companion object {
val values: List<FactorioUpdateDataType> = values().toList()
}
}
object JsonSerializer : JsonContentPolymorphicSerializer<FactorioUpdateData>(
FactorioUpdateData::class
) {
private val key = FactorioUpdateData::updateType.name
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out FactorioUpdateData> {
val type = element
.jsonObject[key]
?.jsonPrimitive
?.contentOrNull
?.let { json ->
FactorioUpdateDataType.values.firstOrNull { it.name == json }
}
requireNotNull(type) { "Unknown FactorioEventUpdate $key: $element" }
return when (type) {
FactorioUpdateDataType.PLAYER -> PlayerUpdate.serializer()
FactorioUpdateDataType.MAP_CHUNK -> MapChunkUpdate.serializer()
FactorioUpdateDataType.ENTITY -> EntityUpdate.serializer()
FactorioUpdateDataType.CONSOLE_CHAT -> ConsoleChatUpdate.serializer()
FactorioUpdateDataType.CONSOLE_COMMAND -> ConsoleCommandUpdate.serializer()
FactorioUpdateDataType.SURFACE -> SurfaceUpdate.serializer()
FactorioUpdateDataType.CONFIG -> ConfigurationUpdate.serializer()
FactorioUpdateDataType.PROTOTYPES -> PrototypesUpdate.serializer()
}
}
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
sealed interface PlayerUpdateKey : FactorioUpdateDataKey {
val index: PlayerIndex
}
@Serializable
data class PlayerUpdate(
override val index: PlayerIndex,
override val eventCounts: Map<String, UInt>? = null,
val characterUnitNumber: UnitNumber? = null,
val chatColour: Colour? = null,
val colour: Colour? = null,
val name: String? = null,
val afkTime: Tick? = null,
val ticksToRespawn: Tick? = null,
val forceIndex: ForceIndex? = null,
val isAdmin: Boolean? = null,
val isConnected: Boolean? = null,
val isShowOnMap: Boolean? = null,
val isSpectator: Boolean? = null,
val lastOnline: Tick? = null,
val onlineTime: Tick? = null,
val position: MapEntityPosition? = null,
val surfaceIndex: SurfaceIndex? = null,
val tag: String? = null,
val diedCause: EntityIdentifiersData? = null,
val bannedReason: String? = null,
val kickedReason: String? = null,
val disconnectReason: String? = null,
/** `true` when a player is removed (deleted) from the game */
val isRemoved: Boolean? = null,
) : FactorioUpdateData(), PlayerUpdateKey {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.PLAYER
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
sealed interface EntityUpdateKey : EntityIdentifiers, FactorioUpdateDataKey {
/** A [unitNumber] is required for caching */
override val unitNumber: UnitNumber
override val name: String
override val type: String
}
@Serializable
data class EntityUpdate(
override val unitNumber: UnitNumber,
override val name: String,
override val type: String,
override val eventCounts: Map<String, UInt>? = null,
val chunkPosition: MapChunkPosition? = null,
val graphicsVariation: UByte? = null,
val health: Float? = null,
val isActive: Boolean? = null,
val isRotatable: Boolean? = null,
val lastUser: UInt? = null,
val localisedDescription: String? = null,
val localisedName: String? = null,
val prototype: PrototypeName? = null,
) : FactorioUpdateData(), EntityUpdateKey {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.ENTITY
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
sealed interface MapChunkUpdateKey : FactorioUpdateDataKey {
val chunkPosition: MapChunkPosition
val surfaceIndex: SurfaceIndex
}
@Serializable
data class MapChunkUpdate(
override val chunkPosition: MapChunkPosition,
override val surfaceIndex: SurfaceIndex,
override val eventCounts: Map<String, UInt>? = null,
val player: PlayerIndex? = null,
val robot: EntityIdentifiersData? = null,
val force: ForceIndex? = null,
/** updated tiles - might be partial and not all tiles in the chunk */
val tileDictionary: MapTileDictionary? = null,
val isDeleted: Boolean? = null,
) : FactorioUpdateData(), MapChunkUpdateKey {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.MAP_CHUNK
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@Serializable
data class ConsoleChatUpdate(
override val eventCounts: Map<String, UInt>? = null,
val authorPlayerIndex: PlayerIndex?,
val content: String,
) : FactorioUpdateData() {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.CONSOLE_CHAT
}
@Serializable
data class ConsoleCommandUpdate(
override val eventCounts: Map<String, UInt>? = null,
val authorPlayerIndex: PlayerIndex?,
val command: String,
val parameters: String,
) : FactorioUpdateData() {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.CONSOLE_COMMAND
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
sealed interface SurfaceUpdateKey : FactorioUpdateDataKey {
val index: SurfaceIndex
}
@Serializable
data class SurfaceUpdate(
override val index: SurfaceIndex,
override val eventCounts: Map<String, UInt>? = null,
val daytime: Double,
val name: String,
) : FactorioUpdateData(), SurfaceUpdateKey {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.SURFACE
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@Serializable
data class ConfigurationUpdate(
override val eventCounts: Map<String, UInt>? = null,
val factorioData: GameData,
val allMods: List<ModData>,
) : FactorioUpdateData() {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.CONFIG
@Serializable
data class GameData(
val oldVersion: String? = null,
val newVersion: String? = null,
)
@Serializable
data class ModData(
val modName: String,
val currentVersion: String? = null,
val previousVersion: String? = null,
)
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
@Serializable
data class PrototypesUpdate(
override val eventCounts: Map<String, UInt>? = null,
val prototypes: List<FactorioPrototype>,
) : FactorioUpdateData() {
@EncodeDefault
override val updateType: FactorioUpdateDataType = FactorioUpdateDataType.PROTOTYPES
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

View file

@ -1,8 +1,38 @@
package dev.adamko.kafkatorio.events.schema.converters
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.events.schema.Colour
import dev.adamko.kafkatorio.events.schema.ColourHex
import kotlin.math.roundToInt
import kotlinx.serialization.Serializable
/**
* Red, green, blue and alpha values, all in range `[0, 1]` or all in range `[0, 255]` if any
* value is > 1.
*
* All values here are optional. Colour channels default to `0`, the alpha channel defaults to `1`.
*/
@Serializable
data class Colour(
val red: Float = 0f,
val green: Float = 0f,
val blue: Float = 0f,
val alpha: Float = 1f,
)
/** Size-efficient version of [Colour] (`4*4` bytes vs `4*1` bytes) */
@Serializable
data class ColourHex(
val red: UByte = UByte.MIN_VALUE,
val green: UByte = UByte.MIN_VALUE,
val blue: UByte = UByte.MIN_VALUE,
val alpha: UByte = UByte.MAX_VALUE,
) {
companion object {
val TRANSPARENT = ColourHex(0u, 0u, 0u, 0u)
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/** True if any value is greater than 1, so the values are hexadecimal. */
@ -74,8 +104,10 @@ fun Colour.toHex(): ColourHex {
)
}
private fun UByte.toPercentileFloat() = (this.toFloat() / ubyteMaxFloat).coerceIn(0f..1f)
fun ColourHex.toPercentile(): Colour {
return Colour(
red.toPercentileFloat(),

View file

@ -1,9 +1,14 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema.common
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
@Serializable
@JvmInline
value class Tick(val value: UInt)
/**
* Unique ID associated with this surface
*
@ -44,3 +49,20 @@ value class UnitNumber(private val id: UInt) {
value class PrototypeName(private val id: String) {
override fun toString(): String = id
}
interface EntityIdentifiers {
val unitNumber: UnitNumber?
val name: String
/** The prototype-type of the entity. */
val protoType: String
}
// instant of EntityIdentifiers, because kxs needs a concrete instance
@Serializable
data class EntityIdentifiersData(
override val unitNumber: UnitNumber? = null,
override val name: String,
override val protoType: String,
) : EntityIdentifiers

View file

@ -0,0 +1,75 @@
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.schema.common.MapTileDictionary.PrototypeKey
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
const val MAP_CHUNK_SIZE = 32
@Serializable
data class MapTiles(
val surfaceIndex: SurfaceIndex,
val tiles: List<MapTile>,
)
@Serializable
data class MapTile(
val x: Int,
val y: Int,
val proto: PrototypeName,
)
/**
* 2D map of x/y tile positions and prototype-name, optimised for JSON size.
*
* [protos] is a one-to-one map between a [PrototypeName] and [PrototypeKey].
*
* [PrototypeKey] is an arbitrary integer value, which maps to one [PrototypeName] only for a
* specific instance of [MapTileDictionary].
*
* Each X/Y coordinate in [tilesXY] maps to a [PrototypeKey].
*/
@Serializable
data class MapTileDictionary(
/** Map an X,Y coordinate a prototype name */
val tilesXY: Map<String, Map<String, PrototypeKey>>,
val protos: Map<PrototypeName, PrototypeKey>,
) {
private val protosIndexToName: Map<PrototypeKey, PrototypeName> by lazy {
protos.entries.associate { (name, key) -> key to name }
}
fun toMapTileList(): List<MapTile> = buildList {
tilesXY.forEach { (xString, row) ->
row.forEach { (yString, protoIndex) ->
val x = xString.toIntOrNull()
val y = yString.toIntOrNull()
val protoName = protosIndexToName[protoIndex]
if (x != null && y != null && protoName != null) {
add(MapTile(x, y, protoName))
}
}
}
}
/** The collection-index of a prototype in [MapTileDictionary.protos] */
@Serializable
@JvmInline
value class PrototypeKey(val index: Int)
companion object {
fun MapTileDictionary?.isNullOrEmpty(): Boolean {
return this == null || (tilesXY.isEmpty() && tilesXY.all { it.value.isEmpty() })
}
}
}
@Serializable
data class MapBoundingBox(
val topLeft: MapTilePosition,
val bottomRight: MapTilePosition,
)

View file

@ -0,0 +1,142 @@
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kxstsgen.core.*
import dev.adamko.kxstsgen.core.experiments.TupleSerializer
import kotlin.math.floor
import kotlinx.serialization.Serializable
/**
* Coordinates of a [EntityData] on a map.
*
* A [MapEntityPosition] can be translated to a [MapChunkPosition] by dividing the `x`/`y` values
* by [32][MAP_CHUNK_SIZE].
*/
@Serializable(with = MapEntityPosition.Serializer::class)
data class MapEntityPosition(
val x: Double,
val y: Double,
) {
object Serializer : TupleSerializer<MapEntityPosition>(
"MapEntityPosition",
{
element(MapEntityPosition::x)
element(MapEntityPosition::y)
}
) {
override fun tupleConstructor(elements: Iterator<*>): MapEntityPosition {
return MapEntityPosition(
elements.next() as Double,
elements.next() as Double,
)
}
}
}
/** Coordinates of a chunk in a [SurfaceData] where each integer `x`/`y` represents a different
* chunk.
*
* A [MapChunkPosition] can be translated to a [MapEntityPosition] by multiplying the `x`/`y`
* values by [32][MAP_CHUNK_SIZE].
*/
@Serializable(with = MapChunkPosition.Serializer::class)
data class MapChunkPosition(
val x: Int,
val y: Int,
) {
object Serializer : TupleSerializer<MapChunkPosition>(
"MapChunkPosition",
{
element(MapChunkPosition::x)
element(MapChunkPosition::y)
}
) {
override fun tupleConstructor(elements: Iterator<*>): MapChunkPosition {
return MapChunkPosition(
elements.next() as Int,
elements.next() as Int,
)
}
}
}
/**
* Coordinates of a tile in a chunk on a [SurfaceData] where each integer `x`/`y` represents a
* different [MapTile].
*
* It rounds any `x`/`y` down to whole numbers.
*/
@Serializable(with = MapTilePosition.Serializer::class)
data class MapTilePosition(
val x: Int,
val y: Int,
) {
object Serializer : TupleSerializer<MapTilePosition>(
"MapTilePosition",
{
element(MapTilePosition::x)
element(MapTilePosition::y)
}
) {
override fun tupleConstructor(elements: Iterator<*>): MapTilePosition {
val x = requireNotNull(elements.next() as? Int)
val y = requireNotNull(elements.next() as? Int)
return MapTilePosition(x, y)
}
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
operator fun MapTilePosition.times(factor: Int) =
MapTilePosition(x * factor, y * factor)
operator fun MapTilePosition.div(divisor: Int) =
MapTilePosition(x / divisor, y / divisor)
operator fun MapTilePosition.plus(addend: Int) =
MapTilePosition(x + addend, y + addend)
fun MapTilePosition.toMapChunkPosition(chunkSize: Int = MAP_CHUNK_SIZE) =
MapChunkPosition(
floor(x.toDouble() / chunkSize.toDouble()).toInt(),
floor(y.toDouble() / chunkSize.toDouble()).toInt(),
)
fun MapEntityPosition.toMapTilePosition() =
MapChunkPosition(floor(x).toInt(), floor(y).toInt())
fun MapEntityPosition.toMapChunkPosition(chunkSize: Int = MAP_CHUNK_SIZE) =
MapChunkPosition(
floor(x / chunkSize.toDouble()).toInt(),
floor(y / chunkSize.toDouble()).toInt(),
)
operator fun MapChunkPosition.times(factor: Int) =
MapChunkPosition(x * factor, y * factor)
operator fun MapChunkPosition.plus(addend: Int) =
MapChunkPosition(x + addend, y + addend)
operator fun MapChunkPosition.minus(subtrahend: Int) =
MapChunkPosition(x - subtrahend, y - subtrahend)
fun MapChunkPosition.leftTopTile(chunkSize: Int = MAP_CHUNK_SIZE): MapTilePosition =
MapTilePosition(
x * chunkSize,
y * chunkSize,
)
//val MapChunkPosition.rightBottomTile: MapTilePosition
// get() = leftTopTile + (MAP_CHUNK_SIZE - 1)
//
//
//val MapChunkPosition.boundingBox: MapBoundingBox
// get() = MapBoundingBox(leftTopTile, rightBottomTile)

View file

@ -0,0 +1,68 @@
package dev.adamko.kafkatorio.schema.events
import dev.adamko.kafkatorio.schema.SerializerProvider
import dev.adamko.kafkatorio.schema.SerializerProviderDeserializer
import dev.adamko.kafkatorio.schema.common.Tick
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
//@Serializable
//data class KafkatorioPacket(
// val modVersion: String,
// val tick: Tick,
// val data: KafkatorioPacketData,
//)
@Serializable(with = KafkatorioPacketData.JsonSerializer::class)
sealed class KafkatorioPacketData {
abstract val updateType: KafkatorioPacketDataType
object JsonSerializer : SerializerProviderDeserializer<KafkatorioPacketData>(
KafkatorioPacketData::class,
KafkatorioPacketData::updateType.name,
{ json -> KafkatorioPacketDataType.entries.firstOrNull { it.name == json } }
)
}
/** Discriminator for [KafkatorioPacketData] */
@Serializable
enum class KafkatorioPacketDataType(
override val serializer: KSerializer<out KafkatorioPacketData>
) : SerializerProvider<KafkatorioPacketData> {
// non-keyed
CONFIG(ConfigurationUpdate.serializer()),
CONSOLE_CHAT(ConsoleChatUpdate.serializer()),
CONSOLE_COMMAND(ConsoleCommandUpdate.serializer()),
PROTOTYPES(PrototypesUpdate.serializer()),
SURFACE(SurfaceUpdate.serializer()),
// keyed
ENTITY(EntityUpdate.serializer()),
MAP_CHUNK(MapChunkUpdate.serializer()),
PLAYER(PlayerUpdate.serializer()),
;
companion object {
val entries: Set<KafkatorioPacketDataType> = values().toSet()
}
}
//inline fun <reified T: KafkatorioPacketData> KafkatorioPacketDataType.isType (): KClass<out T> {
// return when (this) {
// KafkatorioPacketDataType.CONFIG -> ConfigurationUpdate::class
// KafkatorioPacketDataType.CONSOLE_CHAT -> ConsoleChatUpdate::class
// KafkatorioPacketDataType.CONSOLE_COMMAND -> ConsoleCommandUpdate::class
// KafkatorioPacketDataType.PROTOTYPES -> PrototypesUpdate::class
// KafkatorioPacketDataType.SURFACE -> SurfaceUpdate::class
// KafkatorioPacketDataType.ENTITY -> EntityUpdate::class
// KafkatorioPacketDataType.MAP_CHUNK -> MapChunkUpdate::class
// KafkatorioPacketDataType.PLAYER -> PlayerUpdate::class
// }
//}

View file

@ -0,0 +1,95 @@
package dev.adamko.kafkatorio.schema.events
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.prototypes.FactorioPrototype
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
/** No key, no caching. Must be sent instantly. */
sealed class KafkatorioInstantPacketData : KafkatorioPacketData()
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class ConfigurationUpdate(
val factorioData: ConfigurationUpdateGameData,
val allMods: List<ConfigurationUpdateModData>,
val modStartupSettingsChange: Boolean,
val migrationApplied: Boolean,
) : KafkatorioInstantPacketData() {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.CONFIG
@Serializable
data class ConfigurationUpdateGameData(
val oldVersion: String? = null,
val newVersion: String? = null,
)
@Serializable
data class ConfigurationUpdateModData(
val modName: String,
val currentVersion: String? = null,
val previousVersion: String? = null,
)
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class ConsoleChatUpdate(
val authorPlayerIndex: PlayerIndex?,
val content: String,
) : KafkatorioInstantPacketData() {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.CONSOLE_CHAT
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class ConsoleCommandUpdate(
val authorPlayerIndex: PlayerIndex?,
val command: String,
val parameters: String,
) : KafkatorioInstantPacketData() {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.CONSOLE_COMMAND
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class PrototypesUpdate(
val prototypes: List<FactorioPrototype>,
) : KafkatorioInstantPacketData() {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.PROTOTYPES
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class SurfaceUpdate(
val index: SurfaceIndex,
val daytime: Double,
val name: String,
) : KafkatorioInstantPacketData() {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.SURFACE
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */

View file

@ -0,0 +1,135 @@
package dev.adamko.kafkatorio.schema.events
import dev.adamko.kafkatorio.schema.common.Colour
import dev.adamko.kafkatorio.schema.common.EntityIdentifiers
import dev.adamko.kafkatorio.schema.common.EntityIdentifiersData
import dev.adamko.kafkatorio.schema.common.ForceIndex
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
import dev.adamko.kafkatorio.schema.common.MapTileDictionary
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.PrototypeName
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.common.UnitNumber
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
sealed interface KafkatorioKeyedPacketKey
/** Has a key, so it can be debounced */
sealed class KafkatorioKeyedPacketData : KafkatorioPacketData() {
/** Count how many events this packet is aggregating data from */
abstract val eventCounts: Map<String, UInt>?
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
sealed interface EntityUpdateKey : EntityIdentifiers, KafkatorioKeyedPacketKey {
/** A [unitNumber] is required for caching */
override val unitNumber: UnitNumber
override val name: String
override val protoType: String
}
@Serializable
data class EntityUpdate(
override val unitNumber: UnitNumber,
override val name: String,
override val protoType: String,
override val eventCounts: Map<String, UInt>? = null,
val chunkPosition: MapChunkPosition? = null,
val graphicsVariation: UByte? = null,
val health: Float? = null,
val isActive: Boolean? = null,
val isRotatable: Boolean? = null,
val lastUser: UInt? = null,
val localisedDescription: String? = null,
val localisedName: String? = null,
val prototype: PrototypeName? = null,
) : KafkatorioKeyedPacketData(), EntityUpdateKey {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.ENTITY
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
sealed interface MapChunkUpdateKey : KafkatorioKeyedPacketKey {
val chunkPosition: MapChunkPosition
val surfaceIndex: SurfaceIndex
}
@Serializable
data class MapChunkUpdate(
override val chunkPosition: MapChunkPosition,
override val surfaceIndex: SurfaceIndex,
override val eventCounts: Map<String, UInt>? = null,
val player: PlayerIndex? = null,
val robot: EntityIdentifiersData? = null,
val force: ForceIndex? = null,
/** updated tiles - might be partial and not all tiles in the chunk */
val tileDictionary: MapTileDictionary? = null,
val isDeleted: Boolean? = null,
) : KafkatorioKeyedPacketData(), MapChunkUpdateKey {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.MAP_CHUNK
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
sealed interface PlayerUpdateKey : KafkatorioKeyedPacketKey {
val index: PlayerIndex
}
@Serializable
data class PlayerUpdate(
override val index: PlayerIndex,
override val eventCounts: Map<String, UInt>? = null,
val characterUnitNumber: UnitNumber? = null,
val chatColour: Colour? = null,
val colour: Colour? = null,
val name: String? = null,
val afkTime: Tick? = null,
val ticksToRespawn: Tick? = null,
val forceIndex: ForceIndex? = null,
val isAdmin: Boolean? = null,
val isConnected: Boolean? = null,
val isShowOnMap: Boolean? = null,
val isSpectator: Boolean? = null,
val lastOnline: Tick? = null,
val onlineTime: Tick? = null,
val position: MapEntityPosition? = null,
val surfaceIndex: SurfaceIndex? = null,
val tag: String? = null,
val diedCause: EntityIdentifiersData? = null,
val bannedReason: String? = null,
val kickedReason: String? = null,
val disconnectReason: String? = null,
/** `true` when a player is removed (deleted) from the game */
val isRemoved: Boolean? = null,
) : KafkatorioKeyedPacketData(), PlayerUpdateKey {
@EncodeDefault
override val updateType: KafkatorioPacketDataType = KafkatorioPacketDataType.PLAYER
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */

View file

@ -1,13 +1,19 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema
import kotlin.reflect.KClass
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.modules.SerializersModule
@ -44,3 +50,28 @@ class FactorioJsonListSerializer<T>(dataSerializer: KSerializer<T>) :
}
}
interface SerializerProvider<T : Any> {
val serializer: KSerializer<out T>
}
open class SerializerProviderDeserializer<T : Any>(
private val baseClass: KClass<T>,
private val key: String,
private val selector: (String) -> SerializerProvider<out T>?,
) : JsonContentPolymorphicSerializer<T>(baseClass) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out T> {
val type = element
.jsonObject[key]
?.jsonPrimitive
?.contentOrNull
?.let { json -> selector(json) }
requireNotNull(type) { "Unknown ${baseClass.simpleName} $key: $element" }
return type.serializer
}
}

View file

@ -1,9 +1,14 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema.prototypes
import dev.adamko.kafkatorio.events.schema.FactorioPrototype.PrototypeObjectName
import dev.adamko.kafkatorio.schema.SerializerProvider
import dev.adamko.kafkatorio.schema.SerializerProviderDeserializer
import dev.adamko.kafkatorio.schema.common.Colour
import dev.adamko.kafkatorio.schema.common.PrototypeName
import dev.adamko.kafkatorio.schema.prototypes.FactorioPrototype.PrototypeObjectName
import kotlinx.serialization.Contextual
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
@ -12,46 +17,30 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
@Serializable(with = FactorioPrototypeJsonSerializer::class)
@Serializable(with = FactorioPrototype.JsonSerializer::class)
sealed class FactorioPrototype {
@EncodeDefault
abstract val prototypeObjectName: PrototypeObjectName
enum class PrototypeObjectName {
LuaTilePrototype,
enum class PrototypeObjectName(
override val serializer: KSerializer<out FactorioPrototype>
) : SerializerProvider<FactorioPrototype> {
LuaTilePrototype(MapTilePrototype.serializer()),
;
companion object {
val values: List<PrototypeObjectName> = values().toList()
val entries: Set<PrototypeObjectName> = values().toSet()
}
}
object JsonSerializer : SerializerProviderDeserializer<FactorioPrototype>(
FactorioPrototype::class,
FactorioPrototype::prototypeObjectName.name,
{ json -> PrototypeObjectName.entries.firstOrNull { it.name == json } }
)
}
object FactorioPrototypeJsonSerializer : JsonContentPolymorphicSerializer<FactorioPrototype>(
FactorioPrototype::class
) {
private val key = FactorioPrototype::prototypeObjectName.name
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out FactorioPrototype> {
val type = element
.jsonObject[key]
?.jsonPrimitive
?.contentOrNull
?.let { json ->
PrototypeObjectName.values.firstOrNull { it.name == json }
}
requireNotNull(type) { "Unknown FactorioPrototype ${key}: $element" }
return when (type) {
PrototypeObjectName.LuaTilePrototype -> MapTilePrototype.serializer()
}
}
}
@Serializable
data class MapTilePrototype(

View file

@ -0,0 +1,23 @@
package dev.adamko.kafkatorio.schema.prototypes
import dev.adamko.kafkatorio.schema.common.Colour
import dev.adamko.kafkatorio.schema.common.PrototypeName
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
sealed class FactorioPrototype2 {
@Serializable
data class MapTile(
val name: PrototypeName,
val layer: UInt,
val mapColour: Colour,
@Contextual
val collisionMasks: List<String>,
val order: String,
/** Can the tile be mined for resources? */
val canBeMined: Boolean,
) : FactorioPrototype2()
}

View file

@ -0,0 +1,16 @@
package dev.adamko.kafkatorio.schema2
import dev.adamko.kafkatorio.schema.common.Tick
import kotlinx.serialization.Serializable
@Serializable
data class KafkatorioPacket2(
val modVersion: String,
val tick: Tick,
val data: KafkatorioPacketData2,
)
@Serializable
sealed class KafkatorioPacketData2

View file

@ -0,0 +1,70 @@
package dev.adamko.kafkatorio.schema2
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.prototypes.FactorioPrototype
import dev.adamko.kafkatorio.schema.prototypes.FactorioPrototype2
import kotlinx.serialization.Serializable
/** No key, no caching. Must be sent instantly. */
@Serializable
sealed class KafkatorioInstantPacketData2 : KafkatorioPacketData2()
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class ConfigurationUpdate(
val factorioData: ConfigurationUpdateGameData,
val allMods: List<ConfigurationUpdateModData>,
val modStartupSettingsChange: Boolean,
val migrationApplied: Boolean,
) : KafkatorioInstantPacketData2() {
@Serializable
data class ConfigurationUpdateGameData(
val oldVersion: String? = null,
val newVersion: String? = null,
)
@Serializable
data class ConfigurationUpdateModData(
val modName: String,
val currentVersion: String? = null,
val previousVersion: String? = null,
)
}
@Serializable
data class ConsoleChatUpdate(
val authorPlayerIndex: PlayerIndex?,
val content: String,
) : KafkatorioInstantPacketData2()
@Serializable
data class ConsoleCommandUpdate(
val authorPlayerIndex: PlayerIndex?,
val command: String,
val parameters: String,
) : KafkatorioInstantPacketData2()
@Serializable
data class PrototypesUpdate(
val prototypes: List<FactorioPrototype2>,
) : KafkatorioInstantPacketData2()
@Serializable
data class SurfaceUpdate(
val index: SurfaceIndex,
val daytime: Double,
val name: String,
) : KafkatorioInstantPacketData2()
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */

View file

@ -0,0 +1,126 @@
package dev.adamko.kafkatorio.schema2
import dev.adamko.kafkatorio.schema.common.Colour
import dev.adamko.kafkatorio.schema.common.EntityIdentifiers
import dev.adamko.kafkatorio.schema.common.EntityIdentifiersData
import dev.adamko.kafkatorio.schema.common.ForceIndex
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
import dev.adamko.kafkatorio.schema.common.MapTileDictionary
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.PrototypeName
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.common.UnitNumber
import kotlinx.serialization.Serializable
/** Has a key, so it can be debounced */
@Serializable
sealed class KafkatorioKeyedPacketData2 : KafkatorioPacketData2() {
/** Count how many events this packet is aggregating data from */
abstract val key: KafkatorioKeyedPacketKey2
abstract val eventCounts: Map<String, UInt>?
}
@Serializable
sealed class KafkatorioKeyedPacketKey2
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class EntityUpdateKey(
/** While normally optional, here a [unitNumber] is required for caching */
override val unitNumber: UnitNumber,
override val name: String,
override val protoType: String,
) : EntityIdentifiers, KafkatorioKeyedPacketKey2()
@Serializable
data class EntityUpdate(
override val key: EntityUpdateKey,
override val eventCounts: Map<String, UInt>? = null,
val chunkPosition: MapChunkPosition? = null,
val graphicsVariation: UByte? = null,
val health: Float? = null,
val isActive: Boolean? = null,
val isRotatable: Boolean? = null,
val lastUser: UInt? = null,
val localisedDescription: String? = null,
val localisedName: String? = null,
val prototype: PrototypeName? = null,
) : KafkatorioKeyedPacketData2()
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class MapChunkUpdateKey(
val chunkPosition: MapChunkPosition,
val surfaceIndex: SurfaceIndex,
) : KafkatorioKeyedPacketKey2()
@Serializable
data class MapChunkUpdate(
override val key: MapChunkUpdateKey,
override val eventCounts: Map<String, UInt>? = null,
val player: PlayerIndex? = null,
val robot: EntityIdentifiersData? = null,
val force: ForceIndex? = null,
/** updated tiles - might be partial and not all tiles in the chunk */
val tileDictionary: MapTileDictionary? = null,
val isDeleted: Boolean? = null,
) : KafkatorioKeyedPacketData2()
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
data class PlayerUpdateKey(
val index: PlayerIndex,
) : KafkatorioKeyedPacketKey2()
@Serializable
data class PlayerUpdate(
override val key: PlayerUpdateKey,
override val eventCounts: Map<String, UInt>? = null,
val characterUnitNumber: UnitNumber? = null,
val chatColour: Colour? = null,
val colour: Colour? = null,
val name: String? = null,
val afkTime: Tick? = null,
val ticksToRespawn: Tick? = null,
val forceIndex: ForceIndex? = null,
val isAdmin: Boolean? = null,
val isConnected: Boolean? = null,
val isShowOnMap: Boolean? = null,
val isSpectator: Boolean? = null,
val lastOnline: Tick? = null,
val onlineTime: Tick? = null,
val position: MapEntityPosition? = null,
val surfaceIndex: SurfaceIndex? = null,
val tag: String? = null,
val diedCause: EntityIdentifiersData? = null,
val bannedReason: String? = null,
val kickedReason: String? = null,
val disconnectReason: String? = null,
/** `true` when a player is removed (deleted) from the game */
val isRemoved: Boolean? = null,
) : KafkatorioKeyedPacketData2()
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */

View file

@ -1,7 +1,10 @@
package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.events.schema.converters.toHex
import dev.adamko.kafkatorio.events.schema.converters.toPercentile
import dev.adamko.kafkatorio.schema.common.Colour
import dev.adamko.kafkatorio.schema.common.ColourHex
import dev.adamko.kafkatorio.schema.common.toHex
import dev.adamko.kafkatorio.schema.common.toPercentile
import dev.adamko.kafkatorio.schema.jsonMapperKafkatorio
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.floats.shouldBeWithinPercentageOf
@ -81,6 +84,4 @@ class ColourHexTest : BehaviorSpec({
}
}
})

View file

@ -1,342 +0,0 @@
package dev.adamko.kafkatorio.events.schema
import io.kotest.assertions.json.shouldEqualJson
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
class FactorioEventTest : FunSpec({
// context("Given: json on_player_joined_game") {
//
// // language=JSON
// val json = """
// {
// "data": {
// "objectName": "LuaPlayer",
// "force": 1,
// "surface": 1,
// "name": "fredthedeadhead",
// "characterUnitNumber": 1,
// "associatedCharactersUnitNumbers": {},
// "position": {
// "x": 30.6,
// "y": 84.1
// },
// "colour": {
// "red": 0.86,
// "green": 0.5,
// "blue": 0.12,
// "alpha": 0.5
// },
// "chatColour": {
// "red": 1,
// "green": 0.62,
// "blue": 0.25
// },
// "lastOnline": 2287061
// },
// "packetType": "EVENT",
// "eventType": "on_player_joined_game",
// "modVersion": "0.2.1",
// "tick": 2287072
// }
// """.trimIndent()
// context("When: decoded") {
// val actual: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
//
// test("Then: parse") {
//
// val expected = FactorioEvent(
// data = PlayerData(
// name = "fredthedeadhead",
// surface = SurfaceIndex(1u),
// force = ForceIndex(1u),
// characterUnitNumber = UnitNumber(1u),
// associatedCharactersUnitNumbers = emptyList(),
// position = MapEntityPosition(
// x = 30.6,
// y = 84.1,
// ),
// colour = Colour(
// red = 0.86f,
// green = 0.5f,
// blue = 0.12f,
// alpha = 0.5f
// ),
// chatColour = Colour(
// red = 1f,
// green = 0.62f,
// blue = 0.25f,
// alpha = 1f
// ),
// lastOnline = Tick(2287061u)
// ),
// eventType = "on_player_joined_game",
// modVersion = "0.2.1",
// tick = Tick(2287072u)
// )
//
// actual shouldBe expected
//
// expected.data.objectName shouldBe FactorioObjectData.ObjectName.LuaPlayer
// }
//
// test("Then: expect encode equals json") {
// val encoded = jsonMapperKafkatorio.encodeToString(actual)
// encoded.shouldEqualJson(json)
// }
// }
// }
//
// context("Given: json LuaSurface") {
// // language=JSON
// val json = """
// {
// "data": {
// "objectName": "LuaSurface",
// "name": "nauvis",
// "index": 1,
// "daytime": 0.745
// },
// "packetType": "EVENT",
// "eventType": "on_tick",
// "modVersion": "0.2.1",
// "tick": 2301240
// }
// """.trimIndent()
//
// context("When: decoded") {
// val actual: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
//
// test("should equal obj") {
//
// val expected = FactorioEvent(
// data = SurfaceData(
// name = "nauvis",
// index = SurfaceIndex(1u),
// daytime = 0.745
// ),
// eventType = "on_tick",
// modVersion = "0.2.1",
// tick = Tick(2301240u)
// )
//
// actual shouldBe expected
//
// expected.data.objectName shouldBe FactorioObjectData.ObjectName.LuaSurface
// }
//
// test("Then: expect encode equals json") {
// val encoded = jsonMapperKafkatorio.encodeToString(actual)
// encoded.shouldEqualJson(json)
// }
// }
// }
//
// context("Given: LuaEntity on_player_changed_position") {
// // language=JSON
// val json = """
// {
// "data": {
// "objectName": "LuaEntity",
// "name": "character",
// "type": "character",
// "active": true,
// "health": 250,
// "healthRatio": 1,
// "surface": 1,
// "force": 1,
// "unitNumber": 1,
// "position": {
// "x": 37.5,
// "y": 81.1
// }
// },
// "packetType": "EVENT",
// "eventType": "on_player_changed_position",
// "modVersion": "0.2.1",
// "tick": 2301231
// }
// """.trimIndent()
// context("When: decoded") {
// val actual: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
//
// test("parse") {
//
// val expected = FactorioEvent(
// data = EntityData(
// name = "character",
// type = "character",
// active = true,
// health = 250.0,
// healthRatio = 1.0,
// surface = SurfaceIndex(1u),
// unitNumber = UnitNumber(1u),
// force = ForceIndex(1u),
// playerIndex = null,
// position = MapEntityPosition(
// x = 37.5,
// y = 81.1,
// )
// ),
// eventType = "on_player_changed_position",
// modVersion = "0.2.1",
// tick = Tick(2301231u)
// )
//
// actual shouldBe expected
//
// expected.data.objectName shouldBe FactorioObjectData.ObjectName.LuaEntity
// }
//
// test("Then: expect encode equals json") {
// val encoded = jsonMapperKafkatorio.encodeToString(actual)
// encoded.shouldEqualJson(json)
// }
// }
//
// }
//
// context("Given: LuaTiles on_player_built_tile") {
// // language=JSON
// val json = """
// {
// "data": {
// "objectName": "LuaTiles",
// "surfaceIndex": 1,
// "tiles": [
// {
// "y": 10,
// "x": -65,
// "proto": "refined-concrete"
// },
// {
// "y": 11,
// "x": -65,
// "proto": "red-earth"
// },
// {
// "y": 10,
// "x": -64,
// "proto": "blue-cheese"
// },
// {
// "y": 11,
// "x": -64,
// "proto": "refined-concrete"
// }
// ]
// },
// "packetType": "EVENT",
// "eventType": "on_player_built_tile",
// "modVersion": "0.2.2",
// "tick": 2429929
// }
// """.trimIndent()
// context("When: decoded") {
// val actual: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
//
// test("parse") {
//
// val expected = FactorioEvent(
// data = MapTiles(
// surfaceIndex = SurfaceIndex(1u),
// tiles = listOf(
// MapTile(
// proto = PrototypeName("refined-concrete"),
// x = -65,
// y = 10,
// ),
// MapTile(
// proto = PrototypeName("red-earth"),
// x = -65,
// y = 11,
// ),
// MapTile(
// proto = PrototypeName("blue-cheese"),
// x = -64,
// y = 10,
// ),
// MapTile(
// proto = PrototypeName("refined-concrete"),
// x = -64,
// y = 11,
// ),
// )
// ),
// eventType = "on_player_built_tile",
// modVersion = "0.2.2",
// tick = Tick(2429929u)
// )
//
// actual shouldBe expected
//
// expected.data.objectName shouldBe FactorioObjectData.ObjectName.LuaTiles
// }
//
// test("Then: expect encode equals json") {
// val encoded = jsonMapperKafkatorio.encodeToString(actual)
// encoded.shouldEqualJson(json)
// }
// }
// }
//
// context("Given: packet FactorioConfigurationUpdate") {
// // language=JSON
// val json = """
// {
// "modVersion": "0.2.8",
// "packetType": "CONFIG",
// "tick": 534,
// "allMods": [
// {
// "modName": "kafkatorio-events",
// "currentVersion": "0.2.8",
// "previousVersion": "0.2.7"
// },
// {
// "modName": "base",
// "currentVersion": "1.1.53"
// }
// ],
// "factorioData": {}
// }
// """.trimIndent()
// context("When: decoded") {
// val actual: FactorioConfigurationUpdate = jsonMapperKafkatorio.decodeFromString(json)
//
// test("parse") {
//
// val expected = FactorioConfigurationUpdate(
// modVersion = "0.2.8",
// tick = Tick(534u),
// factorioData = FactorioGameDataUpdate(null, null),
// allMods = listOf(
// FactorioModInfo(
// modName = "kafkatorio-events",
// currentVersion = "0.2.8",
// previousVersion = "0.2.7"
// ),
// FactorioModInfo(
// modName = "base",
// currentVersion = "1.1.53",
// previousVersion = null
// ),
// )
// )
//
// actual shouldBe expected
//
// expected.packetType shouldBe KafkatorioPacket.PacketType.CONFIG
// }
//
// test("Then: expect encode equals json") {
// val encoded = jsonMapperKafkatorio.encodeToString(actual)
// encoded.shouldEqualJson(json)
// }
// }
//
// }
})

View file

@ -0,0 +1,93 @@
package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapTileDictionary
import dev.adamko.kafkatorio.schema.common.PrototypeName
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.jsonMapperKafkatorio
import dev.adamko.kafkatorio.schema2.KafkatorioPacket2
import dev.adamko.kafkatorio.schema2.MapChunkUpdate
import dev.adamko.kafkatorio.schema2.MapChunkUpdateKey
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import kotlin.math.exp
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
class MapChunkUpdateTest : FunSpec({
context("MapChunkUpdate update") {
//language=json
val json = """
{
"data": {
"type": "dev.adamko.kafkatorio.schema2.MapChunkUpdate",
"key": {
"surfaceIndex": 1,
"chunkPosition": [
-6,
-3
]
},
"tileDictionary": {
"tilesXY": {
"-192": {
"-65": 2
},
"161": {
"-96": 1,
"-95": 2,
"94": 2,
"93": 32
}
},
"protos": {
"water": 1,
"grass-4": 2,
"grass-3": 32
}
},
"eventCounts": {
"on_chunk_generated": 1
}
},
"modVersion": "0.4.0",
"tick": 36
}
""".trimIndent()
val expected = KafkatorioPacket2(
tick = Tick(36u),
modVersion = "0.4.0",
data = MapChunkUpdate(
key = MapChunkUpdateKey(
MapChunkPosition(-6, -3),
SurfaceIndex(1u),
),
eventCounts = mapOf("on_chunk_generated" to 1u),
tileDictionary = MapTileDictionary(
tilesXY = mapOf(
"-192" to mapOf("-65" to MapTileDictionary.PrototypeKey(2)),
"161" to mapOf(
"-96" to MapTileDictionary.PrototypeKey(1),
"-95" to MapTileDictionary.PrototypeKey(2),
"94" to MapTileDictionary.PrototypeKey(2),
"93" to MapTileDictionary.PrototypeKey(32),
),
),
protos = mapOf(
PrototypeName("water") to MapTileDictionary.PrototypeKey(1),
PrototypeName("grass-4") to MapTileDictionary.PrototypeKey(2),
PrototypeName("grass-3") to MapTileDictionary.PrototypeKey(32),
)
)
)
)
test("decode") {
val packet: KafkatorioPacket2 = jsonMapperKafkatorio.decodeFromString(json)
packet shouldBe expected
}
}
})

View file

@ -0,0 +1,42 @@
package dev.adamko.kafkatorio.schema.common
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainAll
class MapTileDictionaryTest : FunSpec({
context("MapTileDictionary") {
val actual = MapTileDictionary(
tilesXY = mapOf(
"-192" to mapOf("-65" to MapTileDictionary.PrototypeKey(2)),
"161" to mapOf(
"-96" to MapTileDictionary.PrototypeKey(1),
"-95" to MapTileDictionary.PrototypeKey(2),
"94" to MapTileDictionary.PrototypeKey(2),
"93" to MapTileDictionary.PrototypeKey(32),
),
// unknown prototype
"44" to mapOf("1" to MapTileDictionary.PrototypeKey(44)),
),
protos = mapOf(
PrototypeName("water") to MapTileDictionary.PrototypeKey(1),
PrototypeName("grass-4") to MapTileDictionary.PrototypeKey(2),
PrototypeName("grass-3") to MapTileDictionary.PrototypeKey(32),
PrototypeName("not-used") to MapTileDictionary.PrototypeKey(99),
)
)
test("toMapTileList") {
actual.toMapTileList() shouldContainAll listOf(
MapTile(-192, -65, PrototypeName("grass-4")),
MapTile(161, -96, PrototypeName("water")),
MapTile(161, -95, PrototypeName("grass-4")),
MapTile(161, 94, PrototypeName("grass-4")),
MapTile(161, 93, PrototypeName("grass-3")),
)
}
}
})

View file

@ -1,5 +1,10 @@
package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.schema.events.KafkatorioInstantPacketData
import dev.adamko.kafkatorio.schema.events.KafkatorioKeyedPacketData
import dev.adamko.kafkatorio.schema.events.KafkatorioKeyedPacketKey
//import dev.adamko.kafkatorio.schema.events.KafkatorioPacket
import dev.adamko.kafkatorio.schema.prototypes.FactorioPrototype
import kotlin.io.path.Path
import kotlin.io.path.createDirectories
import me.ntrrgc.tsGenerator.TypeScriptGenerator
@ -10,40 +15,38 @@ import org.intellij.lang.annotations.Language
*/
fun main(args: Array<String>) {
val gen =
TypeScriptGenerator(
rootClasses = buildSet {
addAll(KafkatorioPacket::class.sealedSubclasses)
val gen = TypeScriptGenerator(
rootClasses = buildSet {
// add(KafkatorioPacket::class)
// addAll(KafkatorioPacket::class.sealedSubclasses)
addAll(FactorioObjectData::class.sealedSubclasses)
add(KafkatorioInstantPacketData::class)
addAll(KafkatorioInstantPacketData::class.sealedSubclasses)
addAll(FactorioPrototype::class.sealedSubclasses)
add(FactorioPrototypes::class)
add(FactorioConfigurationUpdate::class)
add(KafkatorioKeyedPacketData::class)
addAll(KafkatorioKeyedPacketData::class.sealedSubclasses)
addAll(KafkatorioKeyedPacketKey::class.sealedSubclasses)
add(FactorioEvent::class)
add(FactorioEventUpdatePacket::class)
addAll(FactorioEventUpdate::class.sealedSubclasses)
addAll(FactorioEventUpdateKey::class.sealedSubclasses)
},
mappings = mapOf(
// builtin Factorio numeric types > `typed-factorio/generated/builtin-types.d.ts`
Float::class to "float",
Double::class to "double",
Int::class to "int",
Byte::class to "int8",
UInt::class to "uint",
UByte::class to "uint8",
UShort::class to "uint16",
ULong::class to "uint64",
),
classTransformers = listOf(
ValueClassTransformer,
add(FactorioPrototype::class)
addAll(FactorioPrototype::class.sealedSubclasses)
},
mappings = mapOf(
// builtin Factorio numeric types > `typed-factorio/generated/builtin-types.d.ts`
Float::class to "float",
Double::class to "double",
Int::class to "int",
Byte::class to "int8",
UInt::class to "uint",
UByte::class to "uint8",
UShort::class to "uint16",
ULong::class to "uint64",
),
classTransformers = listOf(
ValueClassTransformer,
// PropertyNameTransformer,
),
),
// voidType = VoidType.UNDEFINED,
)
)
val definitions = gen.definitionsText
@ -57,7 +60,7 @@ fun main(args: Array<String>) {
.forEach { (defFile, def) ->
val outFile = outPath.resolve(defFile).toFile()
outFile.printWriter().use {
it.println(header)
it.println(kt2tsHeader)
it.print(def)
}
println("Generated file: $outFile")
@ -67,6 +70,7 @@ fun main(args: Array<String>) {
}
}
/** Split the TypeScript interfaces into different files */
private fun splitDefinitions(definitions: String): Map<String, String> {
return definitions
@ -74,51 +78,64 @@ private fun splitDefinitions(definitions: String): Map<String, String> {
.groupBy { def ->
when {
def isInterface "KafkatorioPacket"
|| def extendsAny listOf("KafkatorioPacket")
|| def isType "KafkatorioPacketDataType"
-> "kafkatorio-packet.d.ts"
"interface FactorioObjectData " in def ||
"type ObjectName " in def ||
def.contains(Regex("interface.+extends.+(FactorioObjectData).*"))
-> "object-data.d.ts"
def isInterface "KafkatorioInstantPacketData"
|| def extends "KafkatorioInstantPacketData"
|| def isType "KafkatorioInstantPacketDataType"
"extends FactorioPrototype " in def ||
"interface FactorioPrototypes " in def ||
"interface FactorioPrototype " in def ||
"type PrototypeObjectName " in def -> "prototype.d.ts"
|| def isInterface "ConfigurationUpdateGameData"
|| def isInterface "ConfigurationUpdateModData"
-> "kafkatorio-instant-packet.d.ts"
"extends FactorioConfigurationUpdate " in def ||
"interface FactorioConfigurationUpdate " in def ||
"FactorioGameDataUpdate " in def ||
"FactorioModInfo " in def -> "config-update.d.ts"
def isInterface "KafkatorioKeyedPacket"
|| def isInterface "KafkatorioKeyedPacketData"
|| def extends "KafkatorioKeyedPacket"
|| def isType "KafkatorioKeyedPacketDataType"
"interface FactorioEventUpdatePacket" in def ||
"type FactorioEventUpdateType =" in def ||
"interface FactorioEventUpdateKey {" in def ||
"interface FactorioEventUpdateData {" in def ||
def.contains(
Regex(
"""
interface.+extends.+(FactorioEventUpdate|PlayerUpdateKey|PlayerUpdateData).*
""".trimIndent()
)
)
-> "update.d.ts"
|| def isInterface "KafkatorioKeyedPacketKey"
-> "kafkatorio-keyed-packet.d.ts"
else -> "schema.d.ts"
def isInterface "FactorioPrototypes"
|| def isInterface "FactorioPrototype"
|| def extends "FactorioPrototype"
|| def isType "PrototypeObjectName"
-> "prototype.d.ts"
else -> "schema.d.ts"
}
}
.mapValues { (_, def) ->
def
.sorted()
.joinToString("\n\n", postfix = "\n")
.replace(" ", " ")
}
}
@Language("TypeScript")
val header = """
val kt2tsHeader = """
|// Generated by TypeScriptGenerator - do not edit this file manually
|
""".trimMargin()
private infix fun String.extends(types: String): Boolean =
contains(Regex(".+extends.*(${types})"))
private infix fun String.extendsAny(types: List<String>): Boolean =
extends(types.joinToString("|") { "$it,".replaceAfterLast(',', "") })
private infix fun String.isInterface(type: String): Boolean =
contains(Regex("interface $type.*"))
private infix fun String.isType(type: String): Boolean =
contains(Regex("type $type.*"))
//object PropertyNameTransformer : ClassTransformer {
// override fun transformPropertyName(
// propertyName: String,

View file

@ -0,0 +1,55 @@
package dev.adamko.kafkatorio.events.schema
import dev.adamko.kafkatorio.schema2.KafkatorioPacket2
import dev.adamko.kxstsgen.KxsTsConfig
import dev.adamko.kxstsgen.KxsTsGenerator
import dev.adamko.kxstsgen.core.TsDeclaration.TsTypeAlias
import dev.adamko.kxstsgen.core.TsElementId
import dev.adamko.kxstsgen.core.TsLiteral
import dev.adamko.kxstsgen.core.TsTypeRef
import kotlin.io.path.Path
import kotlin.io.path.createDirectories
import kotlinx.serialization.builtins.serializer
@ExperimentalUnsignedTypes
fun main(args: Array<String>) {
val config = KxsTsConfig(
// typeAliasTyping = BrandTyping
)
val gen2 = KxsTsGenerator(config)
gen2.descriptorOverrides += mapOf(
//@formatter:off
Float .serializer().descriptor to TsTypeAlias(TsElementId("kotlin.Float" ), TsTypeRef.Literal(TsLiteral.Custom("float" ), false)),
Double.serializer().descriptor to TsTypeAlias(TsElementId("kotlin.Double"), TsTypeRef.Literal(TsLiteral.Custom("double"), false)),
Int .serializer().descriptor to TsTypeAlias(TsElementId("kotlin.Int" ), TsTypeRef.Literal(TsLiteral.Custom("int" ), false)),
Byte .serializer().descriptor to TsTypeAlias(TsElementId("kotlin.Byte" ), TsTypeRef.Literal(TsLiteral.Custom("int8" ), false)),
UInt .serializer().descriptor to TsTypeAlias(TsElementId("kotlin.UInt" ), TsTypeRef.Literal(TsLiteral.Custom("uint" ), false)),
UByte .serializer().descriptor to TsTypeAlias(TsElementId("kotlin.UByte" ), TsTypeRef.Literal(TsLiteral.Custom("uint8" ), false)),
UShort.serializer().descriptor to TsTypeAlias(TsElementId("kotlin.UShort"), TsTypeRef.Literal(TsLiteral.Custom("uint16"), false)),
ULong .serializer().descriptor to TsTypeAlias(TsElementId("kotlin.ULong" ), TsTypeRef.Literal(TsLiteral.Custom("uint64"), false)),
//@formatter:on
)
val definitions = gen2.generate(
KafkatorioPacket2.serializer(),
// KafkatorioKeyedPacketKey2.serializer(),
)
when (val filename = args.firstOrNull()) {
null -> println(definitions)
else -> {
val outPath = Path(filename)
outPath.createDirectories()
val outFile = outPath.resolve("kafkatorio-schema.ts").toFile()
outFile.printWriter().use {
it.println(kt2tsHeader)
it.print(definitions)
}
}
}
}