update events schema

- unwrap value classes when generating TS
- add value classes for some data types
- add FactorioEventUpdates (for new incremental event updates)
- use static lists for enum values (better performance)
- add LuaControl for sharing Player/Entity fields
This commit is contained in:
Adam 2022-02-20 12:29:34 +01:00
parent e989d25a3c
commit 8f9ebdb1e1
13 changed files with 396 additions and 67 deletions

View file

@ -6,30 +6,6 @@ interface FactorioObjectData {
objectName: ObjectName; objectName: ObjectName;
} }
interface PlayerData extends FactorioObjectData {
associatedCharactersUnitNumbers: uint[];
characterUnitNumber: uint | null;
chatColour: Colour;
colour: Colour;
lastOnline: uint;
name: string;
objectName: ObjectName;
position: MapEntityPosition;
}
interface EntityData extends FactorioObjectData {
active: boolean;
health: double | null;
healthRatio: double;
name: string;
objectName: ObjectName;
playerIndex: uint | null;
position: MapEntityPosition;
surfaceIndex: int;
type: string;
unitNumber: uint | null;
}
interface SurfaceData extends FactorioObjectData { interface SurfaceData extends FactorioObjectData {
daytime: double; daytime: double;
index: uint; index: uint;
@ -39,7 +15,7 @@ interface SurfaceData extends FactorioObjectData {
interface MapTiles extends FactorioObjectData { interface MapTiles extends FactorioObjectData {
objectName: ObjectName; objectName: ObjectName;
surfaceIndex: int; surfaceIndex: uint;
tiles: MapTile[]; tiles: MapTile[];
} }

View file

@ -15,7 +15,7 @@ interface FactorioPrototypes extends KafkatorioPacket {
interface MapTilePrototype extends FactorioPrototype { interface MapTilePrototype extends FactorioPrototype {
canBeMined: boolean; canBeMined: boolean;
collisionMasks: string[]; collisionMasks: string[];
layer: uint; layer: int;
mapColour: Colour; mapColour: Colour;
name: string; name: string;
order: string; order: string;

View file

@ -15,6 +15,17 @@ interface FactorioEvent extends KafkatorioPacket {
tick: uint; tick: uint;
} }
interface MapEntityPosition {
x: double;
y: double;
}
interface FactorioLuaControl {
force: uint;
position: MapEntityPosition;
surface: uint;
}
interface Colour { interface Colour {
alpha: float; alpha: float;
blue: float; blue: float;
@ -22,9 +33,31 @@ interface Colour {
red: float; red: float;
} }
interface MapEntityPosition { interface PlayerData extends FactorioObjectData, FactorioLuaControl {
x: double; associatedCharactersUnitNumbers: uint[];
y: double; characterUnitNumber: uint | null;
chatColour: Colour;
colour: Colour;
force: uint;
lastOnline: uint;
name: string;
objectName: ObjectName;
position: MapEntityPosition;
surface: uint;
}
interface EntityData extends FactorioObjectData, FactorioLuaControl {
active: boolean;
force: uint;
health: double | null;
healthRatio: double;
name: string;
objectName: ObjectName;
playerIndex: uint | null;
position: MapEntityPosition;
surface: uint;
type: string;
unitNumber: uint | null;
} }
interface MapTilePosition { interface MapTilePosition {
@ -46,3 +79,53 @@ interface MapTile {
position: MapTilePosition; position: MapTilePosition;
prototypeName: string; prototypeName: string;
} }
type FactorioEventUpdateType = "PLAYER" | "MAP_CHUNK" | "ENTITY";
interface FactorioEventUpdate {
updateType: FactorioEventUpdateType;
}
interface PlayerUpdate extends FactorioEventUpdate {
characterUnitNumber: uint | null;
chatColour: Colour | null;
colour: Colour | null;
disconnectReason: string | null;
forceIndex: uint | null;
index: uint;
isAdmin: boolean | null;
isConnected: boolean | null;
isShowOnMap: boolean | null;
isSpectator: boolean | null;
lastOnline: uint | null;
name: string | null;
onlineTime: uint | null;
position: MapEntityPosition | null;
surfaceIndex: uint | null;
tag: string | null;
updateType: FactorioEventUpdateType;
}
interface EntityUpdate extends FactorioEventUpdate {
chunkPosition: MapChunkPosition | null;
graphicsVariation: number | null;
health: float | null;
isActive: boolean | null;
isRotatable: boolean | null;
lastUser: int | null;
localisedDescription: string | null;
localisedName: string | null;
name: string | null;
prototype: string | null;
type: string | null;
unitNumber: uint;
updateType: FactorioEventUpdateType;
}
interface MapChunkUpdate extends FactorioEventUpdate {
chunkPosition: MapChunkPosition;
isDeleted: boolean | null;
surfaceIndex: uint;
tiles: MapTiles | null;
updateType: FactorioEventUpdateType;
}

View file

@ -1,11 +1,18 @@
package dev.adamko.kafkatorio.events.schema package dev.adamko.kafkatorio.events.schema
import kotlin.jvm.JvmInline
import kotlinx.serialization.EncodeDefault import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
const val MAP_CHUNK_SIZE = 32 const val MAP_CHUNK_SIZE = 32
@Serializable
@JvmInline
value class Tick(val value: UInt)
/** /**
* Coordinates of a [EntityData] on a map. * Coordinates of a [EntityData] on a map.
* *
@ -60,7 +67,7 @@ data class MapBoundingBox(
*/ */
@Serializable @Serializable
data class Colour( data class Colour(
@EncodeDefault @EncodeDefault // remove encode default?
val red: Float = 0f, val red: Float = 0f,
@EncodeDefault @EncodeDefault
val green: Float = 0f, val green: Float = 0f,
@ -74,7 +81,7 @@ data class Colour(
/** Size-efficient version of [Colour] (`4*4` bytes vs `4*1` bytes) */ /** Size-efficient version of [Colour] (`4*4` bytes vs `4*1` bytes) */
@Serializable @Serializable
data class ColourHex( data class ColourHex(
@EncodeDefault @EncodeDefault // remove encode default?
val red: UByte = UByte.MIN_VALUE, val red: UByte = UByte.MIN_VALUE,
@EncodeDefault @EncodeDefault
val green: UByte = UByte.MIN_VALUE, val green: UByte = UByte.MIN_VALUE,
@ -91,6 +98,6 @@ data class ColourHex(
@Serializable @Serializable
data class MapTile( data class MapTile(
val prototypeName: String, val prototypeName: PrototypeName,
val position: MapTilePosition, val position: MapTilePosition,
) )

View file

@ -0,0 +1,124 @@
package dev.adamko.kafkatorio.events.schema
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(with = FactorioEventUpdate.Companion.JsonSerializer::class)
sealed class FactorioEventUpdate {
@EncodeDefault
abstract val updateType: FactorioEventUpdateType
@Serializable
enum class FactorioEventUpdateType {
PLAYER,
MAP_CHUNK,
ENTITY,
;
companion object {
val values: List<FactorioEventUpdateType> = values().toList()
}
}
companion object {
object JsonSerializer : JsonContentPolymorphicSerializer<FactorioEventUpdate>(
FactorioEventUpdate::class
) {
private val key = FactorioEventUpdate::updateType.name
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out FactorioEventUpdate> {
val type = element
.jsonObject[key]
?.jsonPrimitive
?.contentOrNull
?.let { json ->
FactorioEventUpdateType.values.firstOrNull { it.name == json }
}
requireNotNull(type) { "Unknown FactorioEventUpdate $key: $element" }
return when (type) {
FactorioEventUpdateType.PLAYER -> PlayerUpdate.serializer()
FactorioEventUpdateType.MAP_CHUNK -> TODO()
FactorioEventUpdateType.ENTITY -> EntityUpdate.serializer()
}
}
}
}
}
@Serializable
data class PlayerUpdate(
val index: PlayerIndex,
val characterUnitNumber: UnitNumber? = null,
val chatColour: Colour? = null,
val colour: Colour? = null,
val name: String? = null,
val disconnectReason: String? = 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,
) : FactorioEventUpdate() {
@EncodeDefault
override val updateType: FactorioEventUpdateType = FactorioEventUpdateType.PLAYER
}
@Serializable
data class EntityUpdate(
/**
* A [unitNumber] is required for caching, else the Entity Update should be emitted without
* debouncing/throttling
*/
val unitNumber: UnitNumber,
val name: String? = null,
val type: String? = null,
val chunkPosition: MapChunkPosition? = null,
val graphicsVariation: UShort? = 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,
) : FactorioEventUpdate() {
@EncodeDefault
override val updateType: FactorioEventUpdateType = FactorioEventUpdateType.ENTITY
}
@Serializable
data class MapChunkUpdate(
val chunkPosition: MapChunkPosition,
val surfaceIndex: SurfaceIndex,
/** Might be a partial list of updated tiles */
val tiles: MapTiles? = null,
val isDeleted: Boolean? = null,
) : FactorioEventUpdate() {
@EncodeDefault
override val updateType: FactorioEventUpdateType = FactorioEventUpdateType.MAP_CHUNK
}

View file

@ -10,7 +10,7 @@ data class FactorioEvent(
/** 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: Tick,
val data: FactorioObjectData, val data: FactorioObjectData,
) : KafkatorioPacket() { ) : KafkatorioPacket() {
@EncodeDefault @EncodeDefault

View file

@ -0,0 +1,36 @@
package dev.adamko.kafkatorio.events.schema
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
/**
* Unique ID associated with this surface
*
* See [`LuaSurface.index`](https://lua-api.factorio.com/latest/LuaSurface.html#LuaSurface.index)
*/
@Serializable
@JvmInline
value class SurfaceIndex(val value: UInt)
@Serializable
@JvmInline
value class ForceIndex(val value: UInt)
/** The player's index in `LuaGameScript::players` */
@Serializable
@JvmInline
value class PlayerIndex(val value: UInt)
/** This is universally unique for every entity that has one, for the lifetime of a whole game. */
@Serializable
@JvmInline
value class UnitNumber(val value: UInt)
@Serializable
@JvmInline
value class PrototypeName(val value: String)

View file

@ -10,6 +10,7 @@ import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
@Serializable(with = FactorioObjectDataJsonSerializer::class) @Serializable(with = FactorioObjectDataJsonSerializer::class)
sealed class FactorioObjectData { sealed class FactorioObjectData {
@ -41,9 +42,15 @@ sealed class FactorioObjectData {
// LuaTile, // LuaTile,
LuaTiles, LuaTiles,
ConsoleChatMessage, ConsoleChatMessage,
;
companion object {
val values: List<ObjectName> = values().toList()
}
} }
} }
object FactorioObjectDataJsonSerializer : JsonContentPolymorphicSerializer<FactorioObjectData>( object FactorioObjectDataJsonSerializer : JsonContentPolymorphicSerializer<FactorioObjectData>(
FactorioObjectData::class FactorioObjectData::class
) { ) {
@ -56,7 +63,7 @@ object FactorioObjectDataJsonSerializer : JsonContentPolymorphicSerializer<Facto
?.jsonPrimitive ?.jsonPrimitive
?.contentOrNull ?.contentOrNull
?.let { json -> ?.let { json ->
FactorioObjectData.ObjectName.values().firstOrNull { it.name == json } FactorioObjectData.ObjectName.values.firstOrNull { it.name == json }
} }
requireNotNull(type) { "Unknown FactorioObjectData $key: $element" } requireNotNull(type) { "Unknown FactorioObjectData $key: $element" }
@ -74,48 +81,61 @@ object FactorioObjectDataJsonSerializer : JsonContentPolymorphicSerializer<Facto
} }
/** [`LuaControl`](https://lua-api.factorio.com/latest/LuaControl.html) */
interface FactorioLuaControl {
val position: MapEntityPosition
val surface: SurfaceIndex
val force: ForceIndex
}
@Serializable @Serializable
data class PlayerData( data class PlayerData(
// @Serializable(with = FactorioJsonListSerializer::class) override val force: ForceIndex,
override val position: MapEntityPosition,
override val surface: SurfaceIndex,
@Contextual @Contextual
val associatedCharactersUnitNumbers: List<UInt>, val associatedCharactersUnitNumbers: List<UnitNumber>,
val characterUnitNumber: UInt?, val characterUnitNumber: UnitNumber?,
val name: String,
val position: MapEntityPosition,
val colour: Colour,
val chatColour: Colour, val chatColour: Colour,
val lastOnline: UInt, val colour: Colour,
) : FactorioObjectData() { val lastOnline: Tick,
val name: String,
) : FactorioObjectData(), FactorioLuaControl {
@EncodeDefault @EncodeDefault
override val objectName = ObjectName.LuaPlayer override val objectName = ObjectName.LuaPlayer
} }
@Serializable @Serializable
data class EntityData( data class EntityData(
override val force: ForceIndex,
override val position: MapEntityPosition,
override val surface: SurfaceIndex,
val active: Boolean, val active: Boolean,
val health: Double?, val health: Double?,
val healthRatio: Double, val healthRatio: Double,
val name: String, val name: String,
val position: MapEntityPosition, val playerIndex: PlayerIndex? = null,
val surfaceIndex: Int,
val type: String, val type: String,
val unitNumber: UInt? = null, val unitNumber: UnitNumber? = null,
val playerIndex: UInt? = null, ) : FactorioObjectData(), FactorioLuaControl {
) : FactorioObjectData() {
@EncodeDefault @EncodeDefault
override val objectName = ObjectName.LuaEntity override val objectName = ObjectName.LuaEntity
} }
@Serializable @Serializable
data class SurfaceData( data class SurfaceData(
val daytime: Double, val daytime: Double,
val index: UInt, val index: SurfaceIndex,
val name: String, val name: String,
) : FactorioObjectData() { ) : FactorioObjectData() {
@EncodeDefault @EncodeDefault
override val objectName = ObjectName.LuaSurface override val objectName = ObjectName.LuaSurface
} }
/** This isn't a Factorio Lua type, but a helpful collection of Chunk info */ /** This isn't a Factorio Lua type, but a helpful collection of Chunk info */
@Serializable @Serializable
data class MapChunk( data class MapChunk(
@ -127,19 +147,21 @@ data class MapChunk(
override val objectName = ObjectName.MapChunk override val objectName = ObjectName.MapChunk
} }
/** This isn't a Factorio Lua type, but a helpful collection of [MapTile]s */ /** This isn't a Factorio Lua type, but a helpful collection of [MapTile]s */
@Serializable @Serializable
data class MapTiles( data class MapTiles(
val surfaceIndex: Int, val surfaceIndex: SurfaceIndex,
val tiles: List<MapTile>, val tiles: List<MapTile>,
) : FactorioObjectData() { ) : FactorioObjectData() {
@EncodeDefault @EncodeDefault
override val objectName = ObjectName.LuaTiles override val objectName = ObjectName.LuaTiles
} }
@Serializable @Serializable
data class ConsoleChatMessage( data class ConsoleChatMessage(
val authorPlayerIndex: UInt?, val authorPlayerIndex: PlayerIndex?,
val content: String, val content: String,
) : FactorioObjectData() { ) : FactorioObjectData() {
@EncodeDefault @EncodeDefault

View file

@ -28,7 +28,12 @@ sealed class FactorioPrototype {
abstract val prototypeObjectName: PrototypeObjectName abstract val prototypeObjectName: PrototypeObjectName
enum class PrototypeObjectName { enum class PrototypeObjectName {
LuaTilePrototype LuaTilePrototype,
;
companion object {
val values: List<PrototypeObjectName> = values().toList()
}
} }
} }
@ -45,7 +50,7 @@ object FactorioPrototypeJsonSerializer : JsonContentPolymorphicSerializer<Factor
?.jsonPrimitive ?.jsonPrimitive
?.contentOrNull ?.contentOrNull
?.let { json -> ?.let { json ->
PrototypeObjectName.values().firstOrNull { it.name == json } PrototypeObjectName.values.firstOrNull { it.name == json }
} }
requireNotNull(type) { "Unknown FactorioPrototype ${key}: $element" } requireNotNull(type) { "Unknown FactorioPrototype ${key}: $element" }

View file

@ -21,6 +21,11 @@ sealed class KafkatorioPacket {
EVENT, EVENT,
CONFIG, CONFIG,
PROTOTYPES, PROTOTYPES,
;
companion object {
val values: List<PacketType> = values().toList()
}
} }
} }
@ -37,7 +42,7 @@ object KafkatorioPacketJsonSerializer : JsonContentPolymorphicSerializer<Kafkato
?.jsonPrimitive ?.jsonPrimitive
?.contentOrNull ?.contentOrNull
?.let { json -> ?.let { json ->
KafkatorioPacket.PacketType.values().firstOrNull { it.name == json } KafkatorioPacket.PacketType.values.firstOrNull { it.name == json }
} }
requireNotNull(type) { "Unknown KafkatorioPacket ${key}: $element" } requireNotNull(type) { "Unknown KafkatorioPacket ${key}: $element" }

View file

@ -50,7 +50,9 @@ class FactorioEventTest : FunSpec({
val expected = FactorioEvent( val expected = FactorioEvent(
data = PlayerData( data = PlayerData(
name = "fredthedeadhead", name = "fredthedeadhead",
characterUnitNumber = 1u, surface = SurfaceIndex(1u),
force = ForceIndex(1u),
characterUnitNumber = UnitNumber(1u),
associatedCharactersUnitNumbers = emptyList(), associatedCharactersUnitNumbers = emptyList(),
position = MapEntityPosition( position = MapEntityPosition(
x = 30.6, x = 30.6,
@ -68,11 +70,11 @@ class FactorioEventTest : FunSpec({
blue = 0.25f, blue = 0.25f,
alpha = 1f alpha = 1f
), ),
lastOnline = 2287061u lastOnline = Tick(2287061u)
), ),
eventType = "on_player_joined_game", eventType = "on_player_joined_game",
modVersion = "0.2.1", modVersion = "0.2.1",
tick = 2287072u tick = Tick(2287072u)
) )
actual shouldBe expected actual shouldBe expected
@ -112,12 +114,12 @@ class FactorioEventTest : FunSpec({
val expected = FactorioEvent( val expected = FactorioEvent(
data = SurfaceData( data = SurfaceData(
name = "nauvis", name = "nauvis",
index = 1u, index = SurfaceIndex(1u),
daytime = 0.745 daytime = 0.745
), ),
eventType = "on_tick", eventType = "on_tick",
modVersion = "0.2.1", modVersion = "0.2.1",
tick = 2301240u tick = Tick(2301240u)
) )
actual shouldBe expected actual shouldBe expected
@ -168,8 +170,9 @@ class FactorioEventTest : FunSpec({
active = true, active = true,
health = 250.0, health = 250.0,
healthRatio = 1.0, healthRatio = 1.0,
surfaceIndex = 1, surface = SurfaceIndex(1u),
unitNumber = 1u, unitNumber = UnitNumber(1u),
force = ForceIndex(1u),
playerIndex = null, playerIndex = null,
position = MapEntityPosition( position = MapEntityPosition(
x = 37.5, x = 37.5,
@ -178,7 +181,7 @@ class FactorioEventTest : FunSpec({
), ),
eventType = "on_player_changed_position", eventType = "on_player_changed_position",
modVersion = "0.2.1", modVersion = "0.2.1",
tick = 2301231u tick = Tick(2301231u)
) )
actual shouldBe expected actual shouldBe expected
@ -245,7 +248,7 @@ class FactorioEventTest : FunSpec({
val expected = FactorioEvent( val expected = FactorioEvent(
data = MapTiles( data = MapTiles(
surfaceIndex = 1, surfaceIndex = SurfaceIndex(1u),
tiles = listOf( tiles = listOf(
MapTile( MapTile(
prototypeName = "refined-concrete", prototypeName = "refined-concrete",
@ -267,7 +270,7 @@ class FactorioEventTest : FunSpec({
), ),
eventType = "on_player_built_tile", eventType = "on_player_built_tile",
modVersion = "0.2.2", modVersion = "0.2.2",
tick = 2429929u tick = Tick(2429929u)
) )
actual shouldBe expected actual shouldBe expected

View file

@ -0,0 +1,67 @@
package dev.adamko.kafkatorio.events.schema
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.withNullability
import me.ntrrgc.tsGenerator.ClassTransformer
/**
* 'Unwrap' value classes
*/
object ValueClassTransformer : ClassTransformer {
override fun transformPropertyType(type: KType, property: KProperty<*>, klass: KClass<*>): KType {
val classifier = type.classifier ?: return super.transformPropertyType(type, property, klass)
val valueClassType = unwrapValueClassType(type)
if (valueClassType != null) {
println("$klass has a value class $type -> $valueClassType")
return valueClassType
}
val hasValueClassTypeParameters = type.arguments
.mapNotNull { it.type?.classifier }
.filterIsInstance<KClass<*>>()
.any { it.isValue }
if (hasValueClassTypeParameters) {
val args = type.arguments.map { arg ->
when (val vct = unwrapValueClassType(arg.type)) {
null -> arg
else -> KTypeProjection(arg.variance, vct.withNullability(arg.type!!.isMarkedNullable))
}
}
println("$klass has a prop with value class type parameters ${type.arguments} -> $args")
return classifier.createType(
arguments = args,
nullable = type.isMarkedNullable,
annotations = type.annotations,
)
}
return super.transformPropertyType(type, property, klass)
}
private fun unwrapValueClassType(type: KType?): KType? {
val klass = type?.classifier as? KClass<*> ?: return null
return when {
klass.isValue -> klass
.declaredMemberProperties
.first()
.returnType
.withNullability(type.isMarkedNullable)
else -> null
}
}
}

View file

@ -18,6 +18,7 @@ fun main(args: Array<String>) {
addAll(FactorioPrototype::class.sealedSubclasses) addAll(FactorioPrototype::class.sealedSubclasses)
add(FactorioPrototypes::class) add(FactorioPrototypes::class)
add(FactorioConfigurationUpdate::class) add(FactorioConfigurationUpdate::class)
addAll(FactorioEventUpdate::class.sealedSubclasses)
}, },
mappings = mapOf( mappings = mapOf(
// builtin Factorio numeric types > `typed-factorio/generated/builtin-types.d.ts` // builtin Factorio numeric types > `typed-factorio/generated/builtin-types.d.ts`
@ -31,16 +32,16 @@ fun main(args: Array<String>) {
ULong::class to "uint64", ULong::class to "uint64",
), ),
classTransformers = listOf( classTransformers = listOf(
ValueClassTransformer,
// PropertyNameTransformer, // PropertyNameTransformer,
) ),
// voidType = VoidType.UNDEFINED,
) )
val definitions = gen.definitionsText val definitions = gen.definitionsText
when (val filename = args.firstOrNull()) { when (val filename = args.firstOrNull()) {
null -> { null -> println(definitions)
println(definitions)
}
else -> { else -> {
val outPath = Path(filename) val outPath = Path(filename)
outPath.createDirectories() outPath.createDirectories()