Render buildings and resources

- create LuaJsonList to handle empty Lua JSON lists being sent as empty objects
- fix exporting of resources and entities
- split up map tiles into different layer directories
- add 'chunk size' to MapChunkPosition
- move tests to correct packages
- bump tstl
- decrease tstl logging
- remove some defined, but unused, kafka topics
- syslog, rest, websocket servers & web-map: decrease logging
- webmap: add additional layers for buildings and resources
This commit is contained in:
Adam 2022-07-16 23:10:32 +02:00
parent a805d2ad63
commit b507081eac
72 changed files with 2167 additions and 1068 deletions

View file

@ -1,19 +1,17 @@
package dev.adamko.kafkatorio.library
import kotlin.reflect.KClass
import kotlinx.serialization.DeserializationStrategy
import kotlin.jvm.JvmInline
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonDecoder
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
@ -22,55 +20,60 @@ val jsonMapperKafkatorio = Json {
prettyPrintIndent = " "
serializersModule = SerializersModule {
contextual(List::class) { args -> FactorioJsonListSerializer(args[0]) }
// contextual(List::class) { args -> FactorioListJsonSerializer(args[0]) }
}
}
/** @see LuaJsonListSerializer */
@Serializable(with = LuaJsonListSerializer::class)
@JvmInline
value class LuaJsonList<T>(private val list: List<T>) : List<T> by list
/**
* Factorio outputs an *empty* list as an *empty* Json object. This serde performs the same
* conversion.
* Factorio outputs **empty** lists `[]` as *empty* Json objects `{}`.
*
* When deserializing JSON, this serializer transforms any incoming empty objects to empty
* [array][JsonArray]s.
*/
class FactorioJsonListSerializer<T>(dataSerializer: KSerializer<T>) :
JsonTransformingSerializer<List<T>>(ListSerializer(dataSerializer)) {
class LuaJsonListSerializer<T>(
elementSerializer: KSerializer<T>
) : KSerializer<LuaJsonList<T>> {
override fun transformDeserialize(element: JsonElement): JsonElement {
return when (element) {
is JsonObject -> JsonArray(emptyList())
else -> element
private val listSerializer: KSerializer<List<T>> = ListSerializer(elementSerializer)
override val descriptor: SerialDescriptor =
SerialDescriptor("kafkatorio.LuaList", listSerializer.descriptor)
override fun deserialize(decoder: Decoder): LuaJsonList<T> = LuaJsonList(
when (decoder) {
is JsonDecoder -> deserializeJson(decoder)
else -> decoder.decodeSerializableValue(listSerializer)
}
}
)
override fun transformSerialize(element: JsonElement): JsonElement {
return when {
element is JsonArray && element.isEmpty() -> buildJsonObject { }
else -> element
private fun deserializeJson(decoder: JsonDecoder): List<T> =
when (val element = decoder.decodeJsonElement()) {
is JsonObject -> when {
element.entries.isEmpty() -> emptyList()
else -> deserializeError(
"expected empty Json object, but contained ${element.entries.size} entries ${element.entries}"
)
}
is JsonArray -> decoder.json.decodeFromJsonElement(listSerializer, element)
else -> deserializeError(
"expected empty Json object or a Json array, but was $element"
)
}
override fun serialize(encoder: Encoder, value: LuaJsonList<T>) {
encoder.encodeSerializableValue(listSerializer, value)
}
}
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
companion object {
private fun deserializeError(msg: String): Nothing = throw SerializationException(
"Error deserializing LuaList: $msg"
)
}
}

View file

@ -0,0 +1,103 @@
package dev.adamko.kafkatorio.library
import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
import dev.adamko.kafkatorio.schema.common.MapTilePosition
import kotlin.math.floor
operator fun MapTilePosition.times(factor: Int): MapTilePosition =
MapTilePosition(x * factor, y * factor)
operator fun MapTilePosition.div(divisor: Int): MapTilePosition =
MapTilePosition(x / divisor, y / divisor)
operator fun MapTilePosition.plus(addend: Int): MapTilePosition =
MapTilePosition(x + addend, y + addend)
operator fun MapTilePosition.plus(addend: MapTilePosition): MapTilePosition =
MapTilePosition(x + addend.x, y + addend.y)
operator fun MapTilePosition.minus(subtrahend: MapTilePosition): MapTilePosition =
MapTilePosition(x - subtrahend.x, y - subtrahend.y)
/** Get the [MapChunkPosition] of the tile */
fun MapTilePosition.toMapChunkPosition(chunkSize: ChunkSize): MapChunkPosition =
MapChunkPosition(
x = floor(x.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
y = floor(y.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
chunkSize = chunkSize,
)
/** Get the [MapTilePosition] of the entity */
fun MapEntityPosition.toMapTilePosition(): MapTilePosition =
MapTilePosition(
x = floor(x).toInt(),
y = floor(y).toInt(),
)
/** Get the [MapChunkPosition] of the entity */
fun MapEntityPosition.toMapChunkPosition(chunkSize: ChunkSize): MapChunkPosition =
MapChunkPosition(
x = floor(x / chunkSize.lengthInTiles.toDouble()).toInt(),
y = floor(y / chunkSize.lengthInTiles.toDouble()).toInt(),
chunkSize = chunkSize,
)
fun MapChunkPosition.resize(chunkSize: ChunkSize): MapChunkPosition =
leftTopTile().toMapChunkPosition(chunkSize)
operator fun MapChunkPosition.times(factor: Int): MapChunkPosition =
copy(x = x * factor, y = y * factor)
operator fun MapChunkPosition.plus(addend: Int): MapChunkPosition =
copy(x = x + addend, y = y + addend)
operator fun MapChunkPosition.minus(subtrahend: Int): MapChunkPosition =
copy(x = x - subtrahend, y = y - subtrahend)
fun MapChunkPosition.leftTopTile(): MapTilePosition =
MapTilePosition(
x * chunkSize.lengthInTiles,
y * chunkSize.lengthInTiles,
)
fun MapChunkPosition.rightBottomTile(): MapTilePosition =
leftTopTile() + (chunkSize.lengthInTiles - 1)
/** Iterate over all possible [MapTilePosition]s in the chunk. */
fun MapChunkPosition.iterateTiles(): Iterator<MapTilePosition> {
val leftTop = leftTopTile()
val rightBottom = rightBottomTile()
return iterator {
(leftTop.x..rightBottom.x).forEach { x ->
(leftTop.y..rightBottom.y).forEach { y ->
yield(MapTilePosition(x, y))
}
}
}
}
//val MapChunkPosition.rightBottomTile: MapTilePosition
// get() = leftTopTile + (MAP_CHUNK_SIZE - 1)
//
//val MapChunkPosition.boundingBox: MapBoundingBox
// get() = MapBoundingBox(leftTopTile, rightBottomTile)

View file

@ -52,6 +52,7 @@ data class ColourHex(
) {
companion object {
val TRANSPARENT = ColourHex(0u, 0u, 0u, 0u)
val WHITE = ColourHex(UByte.MAX_VALUE, UByte.MAX_VALUE, UByte.MAX_VALUE, UByte.MAX_VALUE)
}
}

View file

@ -1,24 +1,29 @@
package dev.adamko.kafkatorio.schema.common
import kotlin.jvm.JvmInline
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// TODO change all booleans to be optional, and default to 'false' (also in events-mod)
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityData")
sealed interface FactorioEntityData {
val protoId: PrototypeId
val status: Status?
val position: MapEntityPosition
/** Non-specific entity data */
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityData.Standard")
@SerialName("kafkatorio.entity.FactorioEntityData")
data class Standard(
override val protoId: PrototypeId,
override val status: Status? = null,
val status: Status? = null,
val unitNumber: UnitNumber? = null,
override val position: MapEntityPosition,
val graphicsVariation: UByte? = null,
@ -26,121 +31,230 @@ sealed interface FactorioEntityData {
val isActive: Boolean? = null,
val isRotatable: Boolean? = null,
val lastUser: UInt? = null,
val localisedDescription: String? = null,
val localisedName: String? = null,
) : FactorioEntityData
) : FactorioEntityData {
constructor(
position: MapEntityPosition,
protoId: PrototypeId,
element: FactorioEntityUpdateElement.Entity
) : this(
protoId = protoId,
position = position,
status = element.status,
unitNumber = element.unitNumber,
graphicsVariation = element.graphicsVariation,
health = element.health,
isActive = element.isActive,
isRotatable = element.isRotatable,
lastUser = element.lastUser
)
}
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityData.Resource")
@SerialName("kafkatorio.entity.FactorioResourceData")
data class Resource(
override val protoId: PrototypeId,
override val status: Status? = null,
override val position: MapEntityPosition,
val amount: UInt,
/** Count of initial resource units contained. `null` if resource is not infinite. */
val initialAmount: UInt? = null,
) : FactorioEntityData
) : FactorioEntityData {
constructor(
position: MapEntityPosition,
protoId: PrototypeId,
element: FactorioEntityUpdateElement.Resource
) : this(
protoId = protoId,
position = position,
amount = element.amount,
initialAmount = element.initialAmount,
)
}
/**
* [`https://lua-api.factorio.com/latest/defines.html#defines.entity_status`](https://lua-api.factorio.com/latest/defines.html#defines.entity_status)
*/
@Suppress("unused") // not directly used at the moment - maybe later
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityData.EntityStatus")
@SerialName("kafkatorio.entity.FactorioEntityData.FactorioEntityStatus")
enum class Status {
CANT_DIVIDE_SEGMENTS,
CHARGING,
CLOSED_BY_CIRCUIT_NETWORK,
DISABLED,
DISABLED_BY_CONTROL_BEHAVIOR,
DISABLED_BY_SCRIPT,
DISCHARGING,
FLUID_INGREDIENT_SHORTAGE,
FULLY_CHARGED,
FULL_OUTPUT,
ITEM_INGREDIENT_SHORTAGE,
LAUNCHING_ROCKET,
LOW_INPUT_FLUID,
LOW_POWER,
LOW_TEMPERATURE,
MARKED_FOR_DECONSTRUCTION,
MISSING_REQUIRED_FLUID,
MISSING_SCIENCE_PACKS,
NETWORKS_CONNECTED,
NETWORKS_DISCONNECTED,
NORMAL,
NOT_CONNECTED_TO_RAIL,
NOT_PLUGGED_IN_ELECTRIC_NETWORK,
NO_AMMO,
NO_FUEL,
NO_INGREDIENTS,
NO_INPUT_FLUID,
NO_MINABLE_RESOURCES,
NO_MODULES_TO_TRANSMIT,
NO_POWER,
NO_RECIPE,
NO_RESEARCH_IN_PROGRESS,
OPENED_BY_CIRCUIT_NETWORK,
OUT_OF_LOGISTIC_NETWORK,
PREPARING_ROCKET_FOR_LAUNCH,
RECHARGING_AFTER_POWER_OUTAGE,
TURNED_OFF_DURING_DAYTIME,
WAITING_FOR_SOURCE_ITEMS,
WAITING_FOR_SPACE_IN_DESTINATION,
WAITING_FOR_TARGET_TO_BE_BUILT,
WAITING_FOR_TRAIN,
WAITING_TO_LAUNCH_ROCKET,
WORKING,
NORMAL,
NO_POWER,
LOW_POWER,
NO_FUEL,
DISABLED_BY_CONTROL_BEHAVIOR,
OPENED_BY_CIRCUIT_NETWORK,
CLOSED_BY_CIRCUIT_NETWORK,
DISABLED_BY_SCRIPT,
MARKED_FOR_DECONSTRUCTION,
/** Used by generators and solar panels. */
NOT_PLUGGED_IN_ELECTRIC_NETWORK,
/** Used by power switches. */
NETWORKS_CONNECTED,
/** Used by power switches. */
NETWORKS_DISCONNECTED,
/** Used by accumulators. */
CHARGING,
/** Used by accumulators. */
DISCHARGING,
/** Used by accumulators. */
FULLY_CHARGED,
/** Used by logistic containers. */
OUT_OF_LOGISTIC_NETWORK,
/** Used by assembling machines. */
NO_RECIPE,
/** Used by furnaces. */
NO_INGREDIENTS,
/** USED BY BOILERS, fluid turrets and fluid energy sources: Boiler has no fluid to work with. */
NO_INPUT_FLUID,
/** Used by labs. */
NO_RESEARCH_IN_PROGRESS,
/** Used by mining drills. */
NO_MINABLE_RESOURCES,
/** Used by boilers and fluid turrets: Boiler still has some fluid but is about to run out. */
LOW_INPUT_FLUID,
/** Used by crafting machines. */
FLUID_INGREDIENT_SHORTAGE,
/**
* Used by crafting machines, boilers, burner energy sources and reactors: Reactor/burner has
* full burnt result inventory, boiler has full output fluid box.
*/
FULL_OUTPUT,
/** Used by crafting machines. */
ITEM_INGREDIENT_SHORTAGE,
/** Used by mining drills when the mining fluid is missing. */
MISSING_REQUIRED_FLUID,
/** Used by labs. */
MISSING_SCIENCE_PACKS,
/** Used by inserters. */
WAITING_FOR_SOURCE_ITEMS,
/** Used by inserters and mining drills. */
WAITING_FOR_SPACE_IN_DESTINATION,
/** Used by the rocket silo. */
PREPARING_ROCKET_FOR_LAUNCH,
/** Used by the rocket silo. */
WAITING_TO_LAUNCH_ROCKET,
/** Used by the rocket silo. */
LAUNCHING_ROCKET,
/** Used by beacons. */
NO_MODULES_TO_TRANSMIT,
/** Used by roboports. */
RECHARGING_AFTER_POWER_OUTAGE,
/** Used by inserters targeting entity ghosts. */
WAITING_FOR_TARGET_TO_BE_BUILT,
/** Used by inserters targeting rails. */
WAITING_FOR_TRAIN,
/** Used by ammo turrets. */
NO_AMMO,
/** Used by heat energy sources. */
LOW_TEMPERATURE,
/** Used by constant combinators: Combinator is turned off via switch in GUI. */
DISABLED,
/** Used by lamps. */
TURNED_OFF_DURING_DAYTIME,
/** Used by rail signals. */
NOT_CONNECTED_TO_RAIL,
/** Used by rail signals. */
CANT_DIVIDE_SEGMENTS,
}
}
/*
enum entity_status {
working,
normal,
no_power,
low_power,
no_fuel,
disabled_by_control_behavior,
opened_by_circuit_network,
closed_by_circuit_network,
disabled_by_script,
marked_for_deconstruction,
not_plugged_in_electric_network,
networks_connected,
networks_disconnected,
charging,
discharging,
fully_charged,
out_of_logistic_network,
no_recipe,
no_ingredients,
no_input_fluid,
no_research_in_progress,
no_minable_resources,
low_input_fluid,
fluid_ingredient_shortage,
full_output,
item_ingredient_shortage,
missing_required_fluid,
missing_science_packs,
waiting_for_source_items,
waiting_for_space_in_destination,
preparing_rocket_for_launch,
waiting_to_launch_rocket,
launching_rocket,
no_modules_to_transmit,
recharging_after_power_outage,
waiting_for_target_to_be_built,
waiting_for_train,
no_ammo,
low_temperature,
disabled,
turned_off_during_daytime,
not_connected_to_rail,
cant_divide_segments,
}
*/
sealed interface FactorioEntityUpdateDictionary<T : FactorioEntityUpdateElement>
: Iterable<Pair<MapEntityPosition, T>> {
val entitiesXY: Map<String, Map<String, T>>
operator fun get(position: MapEntityPosition): T? =
entitiesXY[position.x.toString()]?.get(position.y.toString())
override fun iterator(): Iterator<Pair<MapEntityPosition, T>> =
iterator {
entitiesXY.forEach { (x, column) ->
column.forEach { (y, entity) ->
yield(
MapEntityPosition(x.toDouble(), y.toDouble()) to entity
)
}
}
}
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateResourceDictionary")
@JvmInline
value class Resource(
override val entitiesXY: Map<String, Map<String, FactorioEntityUpdateElement.Resource>>
) : FactorioEntityUpdateDictionary<FactorioEntityUpdateElement.Resource>
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateEntityDictionary")
@JvmInline
value class Entity(
override val entitiesXY: Map<String, Map<String, FactorioEntityUpdateElement.Entity>>
) : FactorioEntityUpdateDictionary<FactorioEntityUpdateElement.Entity>
}
sealed interface FactorioEntityUpdateElement {
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateResource")
data class Resource(
val amount: UInt,
/** Count of initial resource units contained. `null` if resource is not infinite. */
val initialAmount: UInt? = null,
) : FactorioEntityUpdateElement
@Serializable
@SerialName("kafkatorio.entity.FactorioEntityUpdateEntity")
data class Entity(
val status: FactorioEntityData.Status? = null,
val unitNumber: UnitNumber? = null,
val graphicsVariation: UByte? = null,
val health: Float? = null,
val isActive: Boolean? = null,
val isRotatable: Boolean? = null,
val lastUser: UInt? = null,
) : FactorioEntityUpdateElement
}
/*
AssemblingMachine

View file

@ -1,6 +1,6 @@
package dev.adamko.kafkatorio.schema.common
import kotlinx.serialization.Contextual
import dev.adamko.kafkatorio.library.LuaJsonList
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -20,8 +20,8 @@ sealed interface FactorioPrototype {
val mapColour: Colour,
val layer: UInt,
@Contextual
val collisionMasks: List<String>,
// @Contextual
val collisionMasks: LuaJsonList<String>,
val order: String,
/** Can the tile be mined for resources? */
val canBeMined: Boolean,
@ -49,17 +49,23 @@ sealed interface FactorioPrototype {
val mapColourEnemy: Colour? = null,
val maxHealth: Float,
val isBuilding: Boolean,
val isEntityWithOwner: Boolean,
val isMilitaryTarget: Boolean,
val isBuilding: Boolean = false,
val isEntityWithOwner: Boolean = false,
val isMilitaryTarget: Boolean = false,
val miningProperties: MiningProperties? = null,
val collisionBox: MapBoundingBox? = null,
) : FactorioPrototype {
val tileHeight: Int? get() = collisionBox?.tileHeight
val tileWidth: Int? get() = collisionBox?.tileWidth
@Serializable
@SerialName("kafkatorio.prototype.EntityMiningProperties")
data class MiningProperties(
val canBeMined: Boolean,
val products: List<MinedProduct>?,
// @Contextual
val products: LuaJsonList<MinedProduct>?,
)
@Serializable

View file

@ -1,10 +1,11 @@
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.schema.common.MapTileDictionary.PrototypeKey
import kotlin.jvm.JvmInline
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
const val MAP_CHUNK_SIZE = 32
@ -24,11 +25,11 @@ data class MapTile(
* [PrototypeKey] is an arbitrary integer value, which maps to one [PrototypeId] only for a
* specific instance of [MapTileDictionary].
*
* Each X/Y coordinate in [tilesXY] maps to a [PrototypeKey].
* @param[tilesXY] Map an X/Y coordinate to a [PrototypeKey].
*/
@Serializable
@SerialName("kafkatorio.packet.keyed.MapTileDictionary")
data class MapTileDictionary(
/** Map an X,Y coordinate a prototype name */
val tilesXY: Map<String, Map<String, PrototypeKey>>,
val protos: Map<PrototypeId, PrototypeKey>,
) {
@ -55,59 +56,68 @@ data class MapTileDictionary(
/** An arbitrary ID for a prototype in [MapTileDictionary.protos]. */
@Serializable
@JvmInline
value class PrototypeKey(private val index: Int)
value class PrototypeKey(private val index: Int) : Comparable<Int> by index
companion object {
fun MapTileDictionary?.isNullOrEmpty(): Boolean {
return this == null || (tilesXY.isEmpty() && tilesXY.all { it.value.isEmpty() })
}
}
// 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,
)
@Serializable
data class ServerMapChunkId(
val serverId: FactorioServerId,
val layer: ServerMapTileLayer,
val chunkPosition: MapChunkPosition,
val surfaceIndex: SurfaceIndex,
val chunkSize: ChunkSize,
)
) {
val chunkSize: ChunkSize get() = chunkPosition.chunkSize
}
/** `${serverDataDir}/servers/{serverId}/map/tiles/s{surfaceIndex}/z{zoomLevel}/x{chunkX}/y{chunkY}.png` */
@Serializable
enum class ServerMapTileLayer(val dir: String) {
@SerialName("terrain")
TERRAIN("terrain"),
@SerialName("resource")
RESOURCE("resource"),
@SerialName("building")
BUILDING("building"),
}
/** `${serverDataDir}/servers/{serverId}/map/${layer}/s{surfaceIndex}/z{zoomLevel}/x{chunkX}/y{chunkY}.png` */
@Serializable
@JvmInline
value class TilePngFilename(
val value: String,
) {
value class ServerMapTilePngFilename(val value: String) {
constructor(id: ServerMapChunkId) : this(
id.serverId,
id.surfaceIndex,
id.chunkSize.zoomLevel,
id.chunkPosition.x,
id.chunkPosition.y,
serverId = id.serverId,
layer = id.layer,
surfaceIndex = id.surfaceIndex,
zoomLevel = id.chunkSize.zoomLevel,
chunkX = id.chunkPosition.x,
chunkY = id.chunkPosition.y,
)
constructor(
serverId: FactorioServerId,
layer: ServerMapTileLayer,
surfaceIndex: SurfaceIndex,
zoomLevel: Int,
chunkX: Int,
chunkY: Int,
) : this(buildString {
append("servers/${serverId}")
append("/map/tiles")
append("/s${surfaceIndex}")
append("/z${zoomLevel}")
append("/x${chunkX}")
append("/y${chunkY}")
append(".png")
})
) : this(
"servers/${serverId}" +
"/map" +
"/layers/${layer.dir}" +
"/s${surfaceIndex}" +
"/z${zoomLevel}" +
"/x${chunkX}" +
"/y${chunkY}" +
".png"
)
}

View file

@ -1,83 +1,171 @@
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kxstsgen.core.*
import dev.adamko.kafkatorio.library.leftTopTile
import dev.adamko.kafkatorio.library.rightBottomTile
import dev.adamko.kxstsgen.core.experiments.TupleSerializer
import kotlin.math.floor
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Coordinates of a [EntityData] on a map.
* The position of something that exists in-game (not conceptually, like a
* [chunk][MapChunkPosition])
*/
sealed interface MapPosition : Comparable<MapPosition> {
val x: Number
val y: Number
}
/**
* Coordinates of an [entity][FactorioEntityData.Standard] on a map.
*
* Positive y points south, positive x points east.
*
* A [MapEntityPosition] can be translated to a [MapChunkPosition] by dividing the `x`/`y` values
* by [32][MAP_CHUNK_SIZE].
*/
@Serializable(with = MapEntityPosition.Serializer::class)
@Serializable(with = MapEntityPosition.Companion.Serializer::class)
data class MapEntityPosition(
val x: Double,
val y: Double,
) {
object Serializer : TupleSerializer<MapEntityPosition>(
"MapEntityPosition",
{
element(MapEntityPosition::x)
element(MapEntityPosition::y)
override val x: Double,
override val y: Double,
) : MapPosition {
override fun compareTo(other: MapPosition): Int =
when (val resultX = x.compareTo(other.x.toDouble())) {
0 -> y.compareTo(other.y.toDouble())
else -> resultX
}
) {
override fun tupleConstructor(elements: Iterator<*>): MapEntityPosition {
return MapEntityPosition(
elements.next() as Double,
elements.next() as Double,
)
companion object {
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
/**
* Coordinates of a tile on a surface, where each `x`/`y` coordinate represents a different
* [MapTile].
*
* Positive y points south, positive x points east.
*
* It rounds any `x`/`y` down to whole numbers.
*/
@Serializable(with = MapTilePosition.Companion.Serializer::class)
data class MapTilePosition(
override val x: Int,
override val y: Int,
) : MapPosition {
override fun compareTo(other: MapPosition): Int =
when (val resultX = x.compareTo(other.x.toInt())) {
0 -> y.compareTo(other.y.toInt())
else -> resultX
}
companion object {
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)
}
}
}
}
/**
* Coordinates of a chunk on a surface, where each `x`/`y` coordinate represents a different
* chunk.
*
* Positive y points south, positive x points east.
*
* A [MapChunkPosition] can be translated to a [MapEntityPosition] by multiplying the `x`/`y`
* values by [32][MAP_CHUNK_SIZE].
*/
@Serializable(with = MapChunkPosition.Serializer::class)
@Serializable(with = MapChunkPosition.Companion.Serializer::class)
data class MapChunkPosition(
val x: Int,
val y: Int,
// Don't set a default for ChunkSize. It could default to 32, but it's clearer to make it explicit
// and avoid accidentally not setting it. Also, a default doesn't save much in serialization size.
val chunkSize: ChunkSize,
) {
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,
)
}
}
private val bounds: MapChunkPositionBounds by lazy {
MapChunkPositionBounds(this, ChunkSize.CHUNK_032)
val bounds: MapChunkPositionBounds by lazy {
MapChunkPositionBounds(this)
}
operator fun contains(tilePosition: MapTilePosition): Boolean = tilePosition in bounds
operator fun contains(entityPosition: MapEntityPosition): Boolean = entityPosition in bounds
companion object {
object Serializer : TupleSerializer<MapChunkPosition>(
"MapChunkPosition",
{
element(MapChunkPosition::x)
element(MapChunkPosition::y)
element(MapChunkPosition::chunkSize)
}
) {
override fun tupleConstructor(elements: Iterator<*>): MapChunkPosition {
return MapChunkPosition(
elements.next() as Int,
elements.next() as Int,
elements.next() as ChunkSize,
)
}
}
}
}
/**
* The bounds of a [MapChunkPosition], that may contain a [MapPosition].
*
* Because Factorio uses screen-coordinates, the lowest `x/y` coordinate is the [leftTop], and
* the highest is [rightBottomTile].
*/
@Serializable
data class MapChunkPositionBounds(
val leftTop: MapTilePosition,
val rightBottom: MapTilePosition,
) : ClosedRange<MapTilePosition> {
val chunkSize: ChunkSize, // should be computable based on the tiles, but include it for completeness
) : ClosedRange<MapPosition> {
constructor(chunkPosition: MapChunkPosition, chunkSize: ChunkSize) : this(
chunkPosition.leftTopTile(chunkSize),
chunkPosition.rightBottomTile(chunkSize),
constructor(chunkPosition: MapChunkPosition) : this(
chunkPosition.leftTopTile(),
chunkPosition.rightBottomTile(),
chunkPosition.chunkSize,
)
override val start: MapTilePosition
@ -88,117 +176,6 @@ data class MapChunkPositionBounds(
}
/**
* 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,
) : Comparable<MapTilePosition> {
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)
}
}
override operator fun compareTo(other: MapTilePosition): Int =
when {
other.x != x -> x.compareTo(other.x)
else -> y.compareTo(other.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)
operator fun MapTilePosition.plus(addend: MapTilePosition) =
MapTilePosition(x + addend.x, y + addend.y)
operator fun MapTilePosition.minus(subtrahend: MapTilePosition) =
MapTilePosition(x - subtrahend.x, y - subtrahend.y)
fun MapTilePosition.toMapChunkPosition(chunkSize: ChunkSize) =
MapChunkPosition(
floor(x.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
floor(y.toDouble() / chunkSize.lengthInTiles.toDouble()).toInt(),
)
fun MapEntityPosition.toMapTilePosition() =
MapChunkPosition(floor(x).toInt(), floor(y).toInt())
fun MapEntityPosition.toMapChunkPosition(chunkSize: ChunkSize) =
MapChunkPosition(
floor(x / chunkSize.lengthInTiles.toDouble()).toInt(),
floor(y / chunkSize.lengthInTiles.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: ChunkSize): MapTilePosition =
MapTilePosition(
x * chunkSize.lengthInTiles,
y * chunkSize.lengthInTiles,
)
fun MapChunkPosition.rightBottomTile(chunkSize: ChunkSize): MapTilePosition =
leftTopTile(chunkSize) + (chunkSize.lengthInTiles - 1)
fun MapChunkPosition.iterateTiles(chunkSize: ChunkSize): Iterator<MapTilePosition> {
val leftTop = leftTopTile(chunkSize)
val rightBottom = rightBottomTile(chunkSize)
return iterator {
(leftTop.x..rightBottom.x).forEach { x ->
(leftTop.y..rightBottom.y).forEach { y ->
yield(MapTilePosition(x, y))
}
}
}
}
//val MapChunkPosition.rightBottomTile: MapTilePosition
// get() = leftTopTile + (MAP_CHUNK_SIZE - 1)
//
//val MapChunkPosition.boundingBox: MapBoundingBox
// get() = MapBoundingBox(leftTopTile, rightBottomTile)
@Serializable
enum class ChunkSize(
val zoomLevel: Int,
@ -231,3 +208,17 @@ enum class ChunkSize(
val STANDARD: ChunkSize = entries.first { it.zoomLevel == 0 }
}
}
@Serializable
@SerialName("kafkatorio.position.MapBoundingBox")
data class MapBoundingBox(
val leftTop: MapEntityPosition,
val rightBottom: MapEntityPosition,
) {
val width: Double get() = abs(leftTop.x - rightBottom.x)
val height: Double get() = abs(leftTop.y - rightBottom.y)
val tileWidth: Int get() = ceil(width).toInt()
val tileHeight: Int get() = ceil(height).toInt()
}

View file

@ -55,6 +55,15 @@ value class PrototypeId private constructor(
}
/** Serialize a [PrototypeId] as an integer - more efficient storage */
@JvmInline
@Serializable
value class PrototypeHashCode private constructor(val code: Int) {
constructor(id: PrototypeId) : this(id.hashCode())
constructor(tile: FactorioPrototype) : this(tile.protoId)
}
//@Suppress("unused")
//enum class PrototypeTypeId(
// val id: String

View file

@ -1,7 +1,7 @@
package dev.adamko.kafkatorio.schema.packets
import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.schema.common.TilePngFilename
import dev.adamko.kafkatorio.schema.common.ServerMapTilePngFilename
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -21,7 +21,7 @@ sealed class EventServerPacket {
@SerialName("kafkatorio.event-server-packet.ChunkTileSaved")
data class ChunkTileSaved(
val chunkId: ServerMapChunkId,
val filename: TilePngFilename,
val filename: ServerMapTilePngFilename,
) : EventServerPacket()
}

View file

@ -16,13 +16,12 @@ data class KafkatorioPacket(
@Serializable
@SerialName("kafkatorio.packet.KafkatorioPacketData")
sealed class KafkatorioPacketData {
sealed class KafkatorioPacketData
@Serializable
@SerialName("kafkatorio.packet.KafkatorioPacketData.Error")
data class Error(
val message: String? = null,
val rawValue: String? = null,
) : KafkatorioPacketData()
}
@Serializable
@SerialName("kafkatorio.packet.KafkatorioPacketData.Error")
data class KafkatorioPacketDataError(
val message: String? = null,
val rawValue: String? = null,
) : KafkatorioPacketData()

View file

@ -1,5 +1,6 @@
package dev.adamko.kafkatorio.schema.packets
import dev.adamko.kafkatorio.library.LuaJsonList
import dev.adamko.kafkatorio.schema.common.FactorioPrototype
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
@ -20,7 +21,8 @@ sealed class KafkatorioInstantPacketData : KafkatorioPacketData()
@SerialName("kafkatorio.packet.instant.ConfigurationUpdate")
data class ConfigurationUpdate(
val factorioData: ConfigurationUpdateGameData,
val allMods: List<ConfigurationUpdateModData>,
// @Contextual
val allMods: LuaJsonList<ConfigurationUpdateModData>,
val modStartupSettingsChange: Boolean,
val migrationApplied: Boolean,
) : KafkatorioInstantPacketData() {
@ -62,7 +64,8 @@ data class ConsoleCommandUpdate(
@Serializable
@SerialName("kafkatorio.packet.instant.PrototypesUpdate")
data class PrototypesUpdate(
val prototypes: List<FactorioPrototype>,
// @Contextual
val prototypes: LuaJsonList<FactorioPrototype>,
) : KafkatorioInstantPacketData()

View file

@ -1,10 +1,12 @@
package dev.adamko.kafkatorio.schema.packets
import dev.adamko.kafkatorio.library.LuaJsonList
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.EventName
import dev.adamko.kafkatorio.schema.common.FactorioEntityData
import dev.adamko.kafkatorio.schema.common.FactorioEntityUpdateDictionary
import dev.adamko.kafkatorio.schema.common.ForceIndex
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
@ -26,7 +28,7 @@ sealed class KafkatorioKeyedPacketData : KafkatorioPacketData() {
/** A list of all events that have been aggregated into this packet. */
// Maybe change to be Map<EventName, Tick>, where Tick is the most recent event?
// A complete event timeline isn't necessary. The most recent tick of an event is good enough, and requires less space.
abstract val events: Map<EventName, List<Tick>>?
abstract val events: Map<EventName, LuaJsonList<Tick>>?
}
@ -52,13 +54,13 @@ data class EntityUpdateKey(
data class EntityUpdate(
override val key: EntityUpdateKey,
override val events: Map<EventName, List<Tick>>? = null,
override val events: Map<EventName, LuaJsonList<Tick>>? = null,
val chunkPosition: MapEntityPosition? = null,
val graphicsVariation: UByte? = null,
val health: Float? = null,
val isActive: Boolean? = null,
val isRotatable: Boolean? = null,
val isActive: Boolean = false,
val isRotatable: Boolean = false,
val lastUser: UInt? = null,
val localisedDescription: String? = null,
val localisedName: String? = null,
@ -82,14 +84,14 @@ data class MapChunkTileUpdateKey(
data class MapChunkTileUpdate(
override val key: MapChunkTileUpdateKey,
override val events: Map<EventName, List<Tick>>? = null,
override val events: Map<EventName, LuaJsonList<Tick>>? = 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,
val isDeleted: Boolean = false,
) : KafkatorioKeyedPacketData()
@ -108,7 +110,7 @@ data class PlayerUpdateKey(
data class PlayerUpdate(
override val key: PlayerUpdateKey,
override val events: Map<EventName, List<Tick>>? = null,
override val events: Map<EventName, LuaJsonList<Tick>>? = null,
val characterUnitNumber: UnitNumber? = null,
val chatColour: Colour? = null,
@ -118,10 +120,10 @@ data class PlayerUpdate(
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 isAdmin: Boolean = false,
val isConnected: Boolean = false,
val isShowOnMap: Boolean = false,
val isSpectator: Boolean = false,
val lastOnline: Tick? = null,
val onlineTime: Tick? = null,
val position: MapEntityPosition? = null,
@ -149,20 +151,58 @@ data class MapChunkEntityUpdateKey(
) : KafkatorioKeyedPacketKey()
/**
* @property[distinctEntities] Distinct entities. The key is arbitrary and is only used
* for de-duplicating updates. It should only contain data that can be obtained from
* [the value][FactorioEntityData].
*/
@Serializable
@SerialName("kafkatorio.packet.keyed.MapChunkEntityUpdate")
data class MapChunkEntityUpdate(
override val key: MapChunkEntityUpdateKey,
override val events: Map<EventName, List<Tick>>? = null,
override val events: Map<EventName, LuaJsonList<Tick>>? = null,
val distinctEntities: Map<String, FactorioEntityData>? = null,
) : KafkatorioKeyedPacketData()
val entitiesXY: FactorioEntityUpdateDictionary.Entity,
) : KafkatorioKeyedPacketData() {
fun entities(): List<FactorioEntityData.Standard> =
entitiesXY.map { (position, entity) ->
FactorioEntityData.Standard(
protoId = key.protoId,
position = position,
element = entity,
)
}
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
@Serializable
@SerialName("kafkatorio.packet.keyed.MapChunkResourceUpdateKey")
data class MapChunkResourceUpdateKey(
val protoId: PrototypeId,
val surfaceIndex: SurfaceIndex,
val chunkPosition: MapChunkPosition,
) : KafkatorioKeyedPacketKey()
@Serializable
@SerialName("kafkatorio.packet.keyed.MapChunkResourceUpdate")
data class MapChunkResourceUpdate(
override val key: MapChunkResourceUpdateKey,
override val events: Map<EventName, LuaJsonList<Tick>>? = null,
val resourcesXY: FactorioEntityUpdateDictionary.Resource,
) : KafkatorioKeyedPacketData() {
fun resources(): List<FactorioEntityData.Resource> =
resourcesXY.map { (position, resource) ->
FactorioEntityData.Resource(
protoId = key.protoId,
position = position,
element = resource,
)
}
}
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */

View file

@ -1,10 +1,6 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
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 io.kotest.assertions.withClue
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.floats.shouldBeWithinPercentageOf

View file

@ -2,10 +2,12 @@ package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.library.kxsBinary
import io.kotest.assertions.asClue
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.ints.shouldBeExactly
import io.kotest.matchers.maps.shouldContainExactly
import io.kotest.matchers.shouldBe
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
@ -17,18 +19,17 @@ class MapChunkPositionBoundsTest : FunSpec({
context("chunk size 32") {
val chunkPos = MapChunkPosition(0, 0)
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_032)
val chunkPos = MapChunkPosition(0, 0, ChunkSize.CHUNK_032)
test("leftTop should be 0,0") {
bounds.leftTop.asClue {
chunkPos.bounds.leftTop.asClue {
it.x shouldBeExactly 0
it.y shouldBeExactly 0
}
}
test("rightBottom should be 31,31") {
bounds.rightBottom.asClue {
chunkPos.bounds.rightBottom.asClue {
it.x shouldBeExactly 31
it.y shouldBeExactly 31
}
@ -38,18 +39,17 @@ class MapChunkPositionBoundsTest : FunSpec({
context("chunk size 256") {
val chunkPos = MapChunkPosition(0, 0)
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_256)
val chunkPos = MapChunkPosition(0, 0, ChunkSize.CHUNK_256)
test("leftTop should be 0,0") {
bounds.leftTop.asClue {
chunkPos.bounds.leftTop.asClue {
it.x shouldBeExactly 0
it.y shouldBeExactly 0
}
}
test("rightBottom should be 255,255") {
bounds.rightBottom.asClue {
chunkPos.bounds.rightBottom.asClue {
it.x shouldBeExactly 255
it.y shouldBeExactly 255
}
@ -61,18 +61,17 @@ class MapChunkPositionBoundsTest : FunSpec({
context("chunk size 32") {
val chunkPos = MapChunkPosition(-10, -10)
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_032)
val chunkPos = MapChunkPosition(-10, -10, ChunkSize.CHUNK_032)
test("leftTop should be -320,-320") {
bounds.leftTop.asClue {
chunkPos.bounds.leftTop.asClue {
it.x shouldBeExactly -320
it.y shouldBeExactly -320
}
}
test("rightBottom should be -289,-289") {
bounds.rightBottom.asClue {
chunkPos.bounds.rightBottom.asClue {
it.x shouldBeExactly -289
it.y shouldBeExactly -289
}
@ -81,18 +80,17 @@ class MapChunkPositionBoundsTest : FunSpec({
context("chunk size 256") {
val chunkPos = MapChunkPosition(-10, -10)
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_256)
val chunkPos = MapChunkPosition(-10, -10, ChunkSize.CHUNK_256)
test("leftTop should be -2560") {
bounds.leftTop.asClue {
chunkPos.bounds.leftTop.asClue {
it.x shouldBeExactly -2560
it.y shouldBeExactly -2560
}
}
test("rightBottom should be (-2560 + 255)") {
bounds.rightBottom.asClue {
chunkPos.bounds.rightBottom.asClue {
it.x shouldBeExactly (-2560 + 255)
it.y shouldBeExactly (-2560 + 255)
}
@ -134,4 +132,57 @@ class MapChunkPositionBoundsTest : FunSpec({
decoded.toMapTileList() shouldContainExactly actual.toMapTileList()
}
}
context("verify map chunk 'contains'") {
context("MapChunkPosition(1, 1)") {
val chunkPos = MapChunkPosition(1, 1, ChunkSize.CHUNK_032) // from 32,32 to 63,63
test("tile outside of chunk") {
val outerTilePos = MapTilePosition(1, 1)
withClue("$outerTilePos in ${chunkPos.bounds}") {
(outerTilePos in chunkPos) shouldBe false
}
}
test("tile inside of chunk") {
val innerTilePos = MapTilePosition(34, 33)
withClue("$innerTilePos in ${chunkPos.bounds}") {
(innerTilePos in chunkPos) shouldBe true
}
}
test("entity outside of chunk") {
val outerEntityPos = MapEntityPosition(1.0, 1.0)
withClue("$outerEntityPos in ${chunkPos.bounds}") {
(outerEntityPos !in chunkPos) shouldBe true
(outerEntityPos in chunkPos) shouldBe false
}
}
test("entity inside of chunk") {
val innerEntityPos = MapEntityPosition(34.0, 33.0)
withClue("$innerEntityPos in ${chunkPos.bounds}") {
(innerEntityPos !in chunkPos) shouldBe false
(innerEntityPos in chunkPos) shouldBe true
}
}
}
context(" MapChunkPosition(9, -5)") {
val chunkPos2 = MapChunkPosition(9, -5, ChunkSize.CHUNK_032)
test("tile inside of chunk") {
val innerTilePos = MapTilePosition(288, -160)
withClue("$innerTilePos in ${chunkPos2.bounds}") {
(innerTilePos in chunkPos2) shouldBe true
}
}
}
}
})

View file

@ -14,7 +14,7 @@ class MapChunkPositionTest : FunSpec({
context("MapChunkPosition(0, 0)") {
val chunkPos = MapChunkPosition(0, 0)
val chunkPos = MapChunkPosition(0, 0, ChunkSize.CHUNK_032)
test("should contain tiles from (0,0) to (31,31)") {
checkAll(mapTilePositionArb(0..31)) { tilePos ->
@ -40,7 +40,7 @@ class MapChunkPositionTest : FunSpec({
}
context("MapChunkPosition(-10, -10)") {
val chunkPos = MapChunkPosition(-10, -10)
val chunkPos = MapChunkPosition(-10, -10, ChunkSize.CHUNK_032)
test("should contain tiles from (-320,-320) to (-289,-289)") {
checkAll(mapTilePositionArb(-320..-289)) { tilePos ->

View file

@ -1,5 +1,7 @@
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.library.minus
import dev.adamko.kafkatorio.library.plus
import dev.adamko.kafkatorio.schema.common.MapChunkPositionTest.Companion.mapTilePositionArb
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.forAll
@ -17,15 +19,27 @@ class MapTilePositionTest : FunSpec({
}
}
test("less than or equal to <=") {
test("equal comparing to ==") {
forAll(mapTilePositionArb()) { tilePos ->
tilePos <= (tilePos)
tilePos.compareTo(tilePos) == 0
}
}
test("less than or equal to <=") {
forAll(
mapTilePositionArb(),
mapTilePositionArb(0..1),
) { tilePos, delta ->
tilePos <= (tilePos + delta)
}
}
test("greater than or equal to >=") {
forAll(mapTilePositionArb()) { tilePos ->
tilePos >= (tilePos)
forAll(
mapTilePositionArb(),
mapTilePositionArb(0..1),
) { tilePos, delta ->
tilePos >= (tilePos - delta)
}
}
@ -37,7 +51,5 @@ class MapTilePositionTest : FunSpec({
tilePos > (tilePos - delta)
}
}
}
})

View file

@ -1,9 +1,6 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema.common
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
import dev.adamko.kafkatorio.schema.common.Colour
import dev.adamko.kafkatorio.schema.common.FactorioPrototype
import dev.adamko.kafkatorio.schema.common.PrototypeId
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import io.kotest.core.spec.style.BehaviorSpec

View file

@ -1,22 +1,21 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema.packets
import dev.adamko.kafkatorio.library.LuaJsonList
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.EventName
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapTileDictionary
import dev.adamko.kafkatorio.schema.common.PrototypeId
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.common.tick
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.MapChunkTileUpdate
import dev.adamko.kafkatorio.schema.packets.MapChunkTileUpdateKey
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.decodeFromString
class MapChunkUpdateTest : FunSpec({
class MapChunkTileUpdateTest : FunSpec({
context("MapChunkUpdate update") {
context("MapChunkTileUpdate update") {
//language=json
val json = """
{
@ -26,7 +25,8 @@ class MapChunkUpdateTest : FunSpec({
"surfaceIndex": 1,
"chunkPosition": [
-6,
-3
-3,
"CHUNK_032"
]
},
"tileDictionary": {
@ -61,11 +61,11 @@ class MapChunkUpdateTest : FunSpec({
modVersion = "0.4.0",
data = MapChunkTileUpdate(
key = MapChunkTileUpdateKey(
MapChunkPosition(-6, -3),
MapChunkPosition(-6, -3, ChunkSize.CHUNK_032),
SurfaceIndex(1u),
),
events = mapOf(
EventName("on_chunk_generated") to listOf(1u.tick)
EventName("on_chunk_generated") to LuaJsonList(listOf(1u.tick))
),
tileDictionary = MapTileDictionary(
tilesXY = mapOf(

View file

@ -1,5 +1,6 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema.packets
import dev.adamko.kafkatorio.library.LuaJsonList
import dev.adamko.kafkatorio.library.jsonMapperKafkatorio
import dev.adamko.kafkatorio.schema.common.Colour
import dev.adamko.kafkatorio.schema.common.EntityIdentifiersData
@ -12,15 +13,12 @@ import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.common.UnitNumber
import dev.adamko.kafkatorio.schema.common.tick
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.schema.packets.PlayerUpdateKey
import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
class UpdatePacketsTest : FunSpec({
context("player update packet") {
@ -72,8 +70,8 @@ class UpdatePacketsTest : FunSpec({
diedCause = null,
disconnectReason = null,
events = mapOf(
EventName("on_player_changed_position") to listOf(7u.tick, 99u.tick),
EventName("on_player_joined_game") to listOf(14u.tick),
EventName("on_player_changed_position") to LuaJsonList(listOf(7u.tick, 99u.tick)),
EventName("on_player_joined_game") to LuaJsonList(listOf(14u.tick)),
),
forceIndex = ForceIndex(1u),
isAdmin = true,
@ -194,7 +192,7 @@ class UpdatePacketsTest : FunSpec({
),
disconnectReason = null,
events = mapOf(
EventName("on_player_died") to listOf(2u.tick),
EventName("on_player_died") to LuaJsonList(listOf(2u.tick)),
),
forceIndex = null,
key = PlayerUpdateKey(PlayerIndex(1u)),

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.events.schema
package dev.adamko.kafkatorio.schema
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
@ -85,13 +85,21 @@ class MySourceCodeGenerator(
config: KxsTsConfig,
) : TsSourceCodeGenerator.Default(config) {
private val seenTypeAliases: MutableSet<TsElementId> = mutableSetOf()
override fun generateTypeAlias(element: TsTypeAlias): String {
if (!seenTypeAliases.add(element.id)) {
return ""
}
return if (element.id.name in listOf("PlayerIndex", "SurfaceIndex", "UnitNumber")) {
""
} else if (
(element.typeRef as? TsTypeRef.Literal)?.element is TsLiteral.Custom
|| element.id.name == "Tick"
|| element.id.name == "FactorioEntityUpdateEntityDictionary"
|| element.id.name == "FactorioEntityUpdateResourceDictionary"
) {
val aliasedRef = generateTypeReference(element.typeRef)
"""

View file

@ -1,11 +1,12 @@
package dev.adamko.kafkatorio.library
import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.schema.common.ChunkSize
import dev.adamko.kafkatorio.schema.common.ColourHex
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.MapChunkPosition
import dev.adamko.kafkatorio.schema.common.MapTilePosition
import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
import dev.adamko.kafkatorio.schema.common.ServerMapTileLayer
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
@ -51,7 +52,7 @@ class KxsBinaryTests : FunSpec({
test("byte array") {
val ba = byteArrayOf(
4, 116, 101, 115, 116, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 3
4, 116, 101, 115, 116, 0, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 1
)
val result = kxsBinary.decodeFromByteArray(ServerMapChunkId.serializer(), ba)
@ -60,9 +61,9 @@ class KxsBinaryTests : FunSpec({
result shouldBeEqualToComparingFields ServerMapChunkId(
serverId = FactorioServerId("test"),
chunkPosition = MapChunkPosition(1, 2),
layer = ServerMapTileLayer.TERRAIN,
chunkPosition = MapChunkPosition(1, 2, ChunkSize.CHUNK_064),
surfaceIndex = SurfaceIndex(1u),
chunkSize = ChunkSize.CHUNK_064,
)
}
@ -72,15 +73,15 @@ class KxsBinaryTests : FunSpec({
val ba = kxsBinary.encodeToByteArray(
ServerMapChunkId(
serverId = FactorioServerId("test"),
chunkPosition = MapChunkPosition(1, 2),
layer = ServerMapTileLayer.TERRAIN,
chunkPosition = MapChunkPosition(1, 2, ChunkSize.CHUNK_064),
surfaceIndex = SurfaceIndex(1u),
chunkSize = ChunkSize.CHUNK_064,
)
)
withClue(ba.toString(Charsets.UTF_8)) {
ba shouldBe byteArrayOf(
4, 116, 101, 115, 116, 2, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 3
4, 116, 101, 115, 116, 0, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 1
)
}
}
@ -112,10 +113,10 @@ class KxsBinaryTests : FunSpec({
context("ServerMapChunkTiles") {
val initial = ServerMapChunkTiles(
ServerMapChunkId(
FactorioServerId("server-id"),
MapChunkPosition(33, 44),
SurfaceIndex(4u),
ChunkSize.CHUNK_032,
serverId = FactorioServerId("server-id"),
layer = ServerMapTileLayer.TERRAIN,
chunkPosition = MapChunkPosition(33, 44, ChunkSize.CHUNK_032),
surfaceIndex = SurfaceIndex(4u),
),
mapOf(
MapTilePosition(1, 2) to ColourHex(1u, 2u, 3u, 4u),