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;
}
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 {
daytime: double;
index: uint;
@ -39,7 +15,7 @@ interface SurfaceData extends FactorioObjectData {
interface MapTiles extends FactorioObjectData {
objectName: ObjectName;
surfaceIndex: int;
surfaceIndex: uint;
tiles: MapTile[];
}

View file

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

View file

@ -15,6 +15,17 @@ interface FactorioEvent extends KafkatorioPacket {
tick: uint;
}
interface MapEntityPosition {
x: double;
y: double;
}
interface FactorioLuaControl {
force: uint;
position: MapEntityPosition;
surface: uint;
}
interface Colour {
alpha: float;
blue: float;
@ -22,9 +33,31 @@ interface Colour {
red: float;
}
interface MapEntityPosition {
x: double;
y: double;
interface PlayerData extends FactorioObjectData, FactorioLuaControl {
associatedCharactersUnitNumbers: uint[];
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 {
@ -46,3 +79,53 @@ interface MapTile {
position: MapTilePosition;
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
import kotlin.jvm.JvmInline
import kotlinx.serialization.EncodeDefault
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.
*
@ -60,7 +67,7 @@ data class MapBoundingBox(
*/
@Serializable
data class Colour(
@EncodeDefault
@EncodeDefault // remove encode default?
val red: Float = 0f,
@EncodeDefault
val green: Float = 0f,
@ -74,7 +81,7 @@ data class Colour(
/** Size-efficient version of [Colour] (`4*4` bytes vs `4*1` bytes) */
@Serializable
data class ColourHex(
@EncodeDefault
@EncodeDefault // remove encode default?
val red: UByte = UByte.MIN_VALUE,
@EncodeDefault
val green: UByte = UByte.MIN_VALUE,
@ -91,6 +98,6 @@ data class ColourHex(
@Serializable
data class MapTile(
val prototypeName: String,
val prototypeName: PrototypeName,
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 */
val eventType: String,
/** game time */
val tick: UInt,
val tick: Tick,
val data: FactorioObjectData,
) : KafkatorioPacket() {
@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.jsonPrimitive
@Serializable(with = FactorioObjectDataJsonSerializer::class)
sealed class FactorioObjectData {
@ -41,9 +42,15 @@ sealed class FactorioObjectData {
// LuaTile,
LuaTiles,
ConsoleChatMessage,
;
companion object {
val values: List<ObjectName> = values().toList()
}
}
}
object FactorioObjectDataJsonSerializer : JsonContentPolymorphicSerializer<FactorioObjectData>(
FactorioObjectData::class
) {
@ -56,7 +63,7 @@ object FactorioObjectDataJsonSerializer : JsonContentPolymorphicSerializer<Facto
?.jsonPrimitive
?.contentOrNull
?.let { json ->
FactorioObjectData.ObjectName.values().firstOrNull { it.name == json }
FactorioObjectData.ObjectName.values.firstOrNull { it.name == json }
}
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
data class PlayerData(
// @Serializable(with = FactorioJsonListSerializer::class)
override val force: ForceIndex,
override val position: MapEntityPosition,
override val surface: SurfaceIndex,
@Contextual
val associatedCharactersUnitNumbers: List<UInt>,
val characterUnitNumber: UInt?,
val name: String,
val position: MapEntityPosition,
val colour: Colour,
val associatedCharactersUnitNumbers: List<UnitNumber>,
val characterUnitNumber: UnitNumber?,
val chatColour: Colour,
val lastOnline: UInt,
) : FactorioObjectData() {
val colour: Colour,
val lastOnline: Tick,
val name: String,
) : FactorioObjectData(), FactorioLuaControl {
@EncodeDefault
override val objectName = ObjectName.LuaPlayer
}
@Serializable
data class EntityData(
override val force: ForceIndex,
override val position: MapEntityPosition,
override val surface: SurfaceIndex,
val active: Boolean,
val health: Double?,
val healthRatio: Double,
val name: String,
val position: MapEntityPosition,
val surfaceIndex: Int,
val playerIndex: PlayerIndex? = null,
val type: String,
val unitNumber: UInt? = null,
val playerIndex: UInt? = null,
) : FactorioObjectData() {
val unitNumber: UnitNumber? = null,
) : FactorioObjectData(), FactorioLuaControl {
@EncodeDefault
override val objectName = ObjectName.LuaEntity
}
@Serializable
data class SurfaceData(
val daytime: Double,
val index: UInt,
val index: SurfaceIndex,
val name: String,
) : FactorioObjectData() {
@EncodeDefault
override val objectName = ObjectName.LuaSurface
}
/** This isn't a Factorio Lua type, but a helpful collection of Chunk info */
@Serializable
data class MapChunk(
@ -127,19 +147,21 @@ data class MapChunk(
override val objectName = ObjectName.MapChunk
}
/** This isn't a Factorio Lua type, but a helpful collection of [MapTile]s */
@Serializable
data class MapTiles(
val surfaceIndex: Int,
val surfaceIndex: SurfaceIndex,
val tiles: List<MapTile>,
) : FactorioObjectData() {
@EncodeDefault
override val objectName = ObjectName.LuaTiles
}
@Serializable
data class ConsoleChatMessage(
val authorPlayerIndex: UInt?,
val authorPlayerIndex: PlayerIndex?,
val content: String,
) : FactorioObjectData() {
@EncodeDefault

View file

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

View file

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

View file

@ -50,7 +50,9 @@ class FactorioEventTest : FunSpec({
val expected = FactorioEvent(
data = PlayerData(
name = "fredthedeadhead",
characterUnitNumber = 1u,
surface = SurfaceIndex(1u),
force = ForceIndex(1u),
characterUnitNumber = UnitNumber(1u),
associatedCharactersUnitNumbers = emptyList(),
position = MapEntityPosition(
x = 30.6,
@ -68,11 +70,11 @@ class FactorioEventTest : FunSpec({
blue = 0.25f,
alpha = 1f
),
lastOnline = 2287061u
lastOnline = Tick(2287061u)
),
eventType = "on_player_joined_game",
modVersion = "0.2.1",
tick = 2287072u
tick = Tick(2287072u)
)
actual shouldBe expected
@ -112,12 +114,12 @@ class FactorioEventTest : FunSpec({
val expected = FactorioEvent(
data = SurfaceData(
name = "nauvis",
index = 1u,
index = SurfaceIndex(1u),
daytime = 0.745
),
eventType = "on_tick",
modVersion = "0.2.1",
tick = 2301240u
tick = Tick(2301240u)
)
actual shouldBe expected
@ -168,8 +170,9 @@ class FactorioEventTest : FunSpec({
active = true,
health = 250.0,
healthRatio = 1.0,
surfaceIndex = 1,
unitNumber = 1u,
surface = SurfaceIndex(1u),
unitNumber = UnitNumber(1u),
force = ForceIndex(1u),
playerIndex = null,
position = MapEntityPosition(
x = 37.5,
@ -178,7 +181,7 @@ class FactorioEventTest : FunSpec({
),
eventType = "on_player_changed_position",
modVersion = "0.2.1",
tick = 2301231u
tick = Tick(2301231u)
)
actual shouldBe expected
@ -245,7 +248,7 @@ class FactorioEventTest : FunSpec({
val expected = FactorioEvent(
data = MapTiles(
surfaceIndex = 1,
surfaceIndex = SurfaceIndex(1u),
tiles = listOf(
MapTile(
prototypeName = "refined-concrete",
@ -267,7 +270,7 @@ class FactorioEventTest : FunSpec({
),
eventType = "on_player_built_tile",
modVersion = "0.2.2",
tick = 2429929u
tick = Tick(2429929u)
)
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)
add(FactorioPrototypes::class)
add(FactorioConfigurationUpdate::class)
addAll(FactorioEventUpdate::class.sealedSubclasses)
},
mappings = mapOf(
// builtin Factorio numeric types > `typed-factorio/generated/builtin-types.d.ts`
@ -31,16 +32,16 @@ fun main(args: Array<String>) {
ULong::class to "uint64",
),
classTransformers = listOf(
ValueClassTransformer,
// PropertyNameTransformer,
)
),
// voidType = VoidType.UNDEFINED,
)
val definitions = gen.definitionsText
when (val filename = args.firstOrNull()) {
null -> {
println(definitions)
}
null -> println(definitions)
else -> {
val outPath = Path(filename)
outPath.createDirectories()