finish migrating to ktor!
- remove http4k - renamed 'schema' subproject to 'library', because it contains more than just data models - tiles are now hosted per factorio-server - changed api base to `kafkatorio/data` - tidied up some Gradle conf - remove 'events-processor' subproject - I don't want to set up loads of microservices right now
This commit is contained in:
parent
8a90c4fb5e
commit
c0c82e7c9c
80 changed files with 1878 additions and 1280 deletions
|
|
@ -0,0 +1,76 @@
|
|||
package dev.adamko.kafkatorio.library
|
||||
|
||||
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
|
||||
|
||||
|
||||
val jsonMapperKafkatorio = Json {
|
||||
prettyPrint = true
|
||||
prettyPrintIndent = " "
|
||||
|
||||
serializersModule = SerializersModule {
|
||||
contextual(List::class) { args -> FactorioJsonListSerializer(args[0]) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Factorio outputs an *empty* list as an *empty* Json object. This serde performs the same
|
||||
* conversion.
|
||||
*/
|
||||
class FactorioJsonListSerializer<T>(dataSerializer: KSerializer<T>) :
|
||||
JsonTransformingSerializer<List<T>>(ListSerializer(dataSerializer)) {
|
||||
|
||||
override fun transformDeserialize(element: JsonElement): JsonElement {
|
||||
return when (element) {
|
||||
is JsonObject -> JsonArray(emptyList())
|
||||
else -> element
|
||||
}
|
||||
}
|
||||
|
||||
override fun transformSerialize(element: JsonElement): JsonElement {
|
||||
return when {
|
||||
element is JsonArray && element.isEmpty() -> buildJsonObject { }
|
||||
else -> element
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
package dev.adamko.kafkatorio.library
|
||||
|
||||
import kotlinx.serialization.BinaryFormat
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.SerializationStrategy
|
||||
import kotlinx.serialization.builtins.ByteArraySerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.AbstractDecoder
|
||||
import kotlinx.serialization.encoding.AbstractEncoder
|
||||
import kotlinx.serialization.encoding.CompositeDecoder
|
||||
import kotlinx.serialization.encoding.CompositeEncoder
|
||||
import kotlinx.serialization.modules.EmptySerializersModule
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import okio.Buffer
|
||||
import okio.BufferedSink
|
||||
import okio.BufferedSource
|
||||
|
||||
|
||||
val kxsBinary: KxsBinary = KxsBinary()
|
||||
|
||||
/** [`https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/formats.md#efficient-binary-format`](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/formats.md#efficient-binary-format) */
|
||||
class KxsBinary(
|
||||
override val serializersModule: SerializersModule = EmptySerializersModule
|
||||
) : BinaryFormat {
|
||||
|
||||
override fun <T> encodeToByteArray(serializer: SerializationStrategy<T>, value: T): ByteArray {
|
||||
val output = Buffer()
|
||||
val encoder = KxsDataOutputEncoder(output, serializersModule)
|
||||
encoder.encodeSerializableValue(serializer, value)
|
||||
return output.readByteArray()
|
||||
}
|
||||
|
||||
override fun <T> decodeFromByteArray(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
bytes: ByteArray
|
||||
): T {
|
||||
val input = Buffer().write(bytes)
|
||||
val decoder = KxsDataInputDecoder(input, serializersModule)
|
||||
return decoder.decodeSerializableValue(deserializer)
|
||||
}
|
||||
}
|
||||
|
||||
private val byteArraySerializer = ByteArraySerializer()
|
||||
|
||||
class KxsDataOutputEncoder(
|
||||
private val output: BufferedSink,
|
||||
override val serializersModule: SerializersModule = EmptySerializersModule,
|
||||
) : AbstractEncoder() {
|
||||
|
||||
private operator fun BufferedSink.invoke(action: BufferedSink.() -> Unit): Unit = action()
|
||||
|
||||
override fun encodeBoolean(value: Boolean) = output { writeByte(if (value) 1 else 0) }
|
||||
override fun encodeByte(value: Byte) = output { writeByte(value.toInt()) }
|
||||
override fun encodeChar(value: Char) = output { writeUtf8CodePoint(value.code) }
|
||||
override fun encodeDouble(value: Double) = output { writeLong(value.toRawBits()) }
|
||||
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = output { writeInt(index) }
|
||||
override fun encodeFloat(value: Float) = output { writeInt(value.toRawBits()) }
|
||||
override fun encodeInt(value: Int) = output { writeInt(value) }
|
||||
override fun encodeLong(value: Long) = output { writeLong(value) }
|
||||
override fun encodeShort(value: Short) = output { writeShort(value.toInt()) }
|
||||
override fun encodeString(value: String) = output {
|
||||
encodeCompactSize(value.length)
|
||||
writeUtf8(value)
|
||||
}
|
||||
|
||||
override fun beginCollection(
|
||||
descriptor: SerialDescriptor,
|
||||
collectionSize: Int
|
||||
): CompositeEncoder {
|
||||
encodeCompactSize(collectionSize)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun encodeNull() = encodeBoolean(false)
|
||||
override fun encodeNotNullMark() = encodeBoolean(true)
|
||||
|
||||
override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) {
|
||||
when (serializer.descriptor) {
|
||||
byteArraySerializer.descriptor -> encodeByteArray(value as ByteArray)
|
||||
else -> super.encodeSerializableValue(serializer, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeByteArray(bytes: ByteArray) {
|
||||
encodeCompactSize(bytes.size)
|
||||
output.write(bytes)
|
||||
}
|
||||
|
||||
private fun encodeCompactSize(value: Int) {
|
||||
if (value < 0xff) {
|
||||
output.writeByte(value)
|
||||
} else {
|
||||
output.writeByte(0xff)
|
||||
output.writeInt(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class KxsDataInputDecoder(
|
||||
private val input: BufferedSource,
|
||||
override val serializersModule: SerializersModule = EmptySerializersModule,
|
||||
private var elementsCount: Int = 0,
|
||||
) : AbstractDecoder() {
|
||||
|
||||
private var elementIndex = 0
|
||||
|
||||
override fun decodeBoolean(): Boolean = input.readByte().toInt() != 0
|
||||
override fun decodeByte(): Byte = input.readByte()
|
||||
override fun decodeChar(): Char = input.readUtf8CodePoint().toChar()
|
||||
override fun decodeDouble(): Double = Double.fromBits(input.readLong())
|
||||
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = input.readInt()
|
||||
override fun decodeFloat(): Float = Float.fromBits(input.readInt())
|
||||
override fun decodeInt(): Int = input.readInt()
|
||||
override fun decodeLong(): Long = input.readLong()
|
||||
override fun decodeShort(): Short = input.readShort()
|
||||
override fun decodeString(): String {
|
||||
val size = decodeCompactSize()
|
||||
return input.readUtf8(size.toLong())
|
||||
}
|
||||
|
||||
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
|
||||
if (elementIndex == elementsCount) return CompositeDecoder.DECODE_DONE
|
||||
return elementIndex++
|
||||
}
|
||||
|
||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
|
||||
KxsDataInputDecoder(input, serializersModule, descriptor.elementsCount)
|
||||
|
||||
override fun decodeSequentially(): Boolean = true
|
||||
|
||||
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
|
||||
decodeCompactSize().also { elementsCount = it }
|
||||
|
||||
override fun decodeNotNullMark(): Boolean = decodeBoolean()
|
||||
|
||||
override fun <T> decodeSerializableValue(
|
||||
deserializer: DeserializationStrategy<T>,
|
||||
previousValue: T?
|
||||
): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return when (deserializer.descriptor) {
|
||||
byteArraySerializer.descriptor -> decodeByteArray() as T
|
||||
else -> super.decodeSerializableValue(deserializer, previousValue)
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeByteArray(): ByteArray {
|
||||
val bytes = ByteArray(decodeCompactSize())
|
||||
input.readFully(bytes)
|
||||
return bytes
|
||||
}
|
||||
|
||||
private fun decodeCompactSize(): Int {
|
||||
val byte = input.readByte().toInt() and 0xff
|
||||
if (byte < 0xff) return byte
|
||||
return input.readInt()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import dev.adamko.kxstsgen.core.experiments.TupleSerializer
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.serialization.SerialName
|
||||
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(with = Colour.Serializer::class)
|
||||
@SerialName("kafkatorio.common.Colour")
|
||||
data class Colour(
|
||||
val red: Float = 0f,
|
||||
val green: Float = 0f,
|
||||
val blue: Float = 0f,
|
||||
val alpha: Float = 1f,
|
||||
) {
|
||||
object Serializer : TupleSerializer<Colour>(
|
||||
"Colour",
|
||||
{
|
||||
element(Colour::red)
|
||||
element(Colour::green)
|
||||
element(Colour::blue)
|
||||
element(Colour::alpha)
|
||||
}
|
||||
) {
|
||||
override fun tupleConstructor(elements: Iterator<*>): Colour {
|
||||
return Colour(
|
||||
elements.next() as Float,
|
||||
elements.next() as Float,
|
||||
elements.next() as Float,
|
||||
elements.next() as Float,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Size-efficient version of [Colour] (`4*4` bytes vs `4*1` bytes) */
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.common.ColourHex")
|
||||
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. */
|
||||
fun Colour.isHexadecimal(): Boolean {
|
||||
return red > 1f
|
||||
|| green > 1f
|
||||
|| blue > 1f
|
||||
|| alpha > 1f
|
||||
}
|
||||
|
||||
/** True if all values are between `[0..1]`. */
|
||||
fun Colour.isPercentage(): Boolean = !isHexadecimal()
|
||||
|
||||
|
||||
fun Colour.toHexString(includeAlpha: Boolean = false): String {
|
||||
|
||||
val hexConverter: Float.() -> String = when {
|
||||
isHexadecimal() -> Float::hexadecimalToString
|
||||
else -> Float::percentageToHexadecimalString
|
||||
}
|
||||
|
||||
val length = if (includeAlpha) 9 else 7
|
||||
return buildString(length) {
|
||||
append('#')
|
||||
append(red.hexConverter())
|
||||
append(green.hexConverter())
|
||||
append(blue.hexConverter())
|
||||
if (includeAlpha) {
|
||||
append(alpha.hexConverter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Float.hexadecimalToString(): String =
|
||||
roundToInt().toString(16).padStart(2, ' ')
|
||||
|
||||
fun Float.percentageToHexadecimalString(): String =
|
||||
times(255).roundToInt().toString(16).padStart(2, ' ')
|
||||
|
||||
|
||||
private val ubyteMaxFloat = UByte.MAX_VALUE.toFloat()
|
||||
private val ubyteMinFloat = UByte.MIN_VALUE.toFloat()
|
||||
private val ubyteRangeFloat = ubyteMinFloat..ubyteMaxFloat
|
||||
|
||||
|
||||
private fun Float.toColourHex() = (this * ubyteMaxFloat)
|
||||
.coerceIn(ubyteRangeFloat)
|
||||
.toUInt()
|
||||
.toUByte()
|
||||
|
||||
|
||||
private fun Float.coerceToColourHex() = coerceIn(ubyteRangeFloat)
|
||||
.toUInt()
|
||||
.toUByte()
|
||||
|
||||
|
||||
fun Colour.toHex(): ColourHex {
|
||||
|
||||
val converter: Float.() -> UByte = when {
|
||||
isPercentage() -> Float::toColourHex
|
||||
else -> Float::coerceToColourHex
|
||||
}
|
||||
|
||||
return ColourHex(
|
||||
red.converter(),
|
||||
green.converter(),
|
||||
blue.converter(),
|
||||
alpha.converter(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun UByte.toPercentileFloat() = (this.toFloat() / ubyteMaxFloat).coerceIn(0f..1f)
|
||||
|
||||
|
||||
fun ColourHex.toPercentile(): Colour {
|
||||
return Colour(
|
||||
red.toPercentileFloat(),
|
||||
green.toPercentileFloat(),
|
||||
blue.toPercentileFloat(),
|
||||
alpha.toPercentileFloat(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import kotlin.jvm.JvmInline
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class Tick(val value: UInt)
|
||||
|
||||
val UInt.tick
|
||||
get() = Tick(this)
|
||||
|
||||
/**
|
||||
* Unique ID associated with this surface
|
||||
*
|
||||
* See [`LuaSurface.index`](https://lua-api.factorio.com/latest/LuaSurface.html#LuaSurface.index)
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class SurfaceIndex(private val id: UInt) {
|
||||
override fun toString(): String = "$id"
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class ForceIndex(private val id: UInt) {
|
||||
override fun toString(): String = "$id"
|
||||
}
|
||||
|
||||
|
||||
/** The player's index in `LuaGameScript::players` */
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PlayerIndex(private val id: UInt) {
|
||||
override fun toString(): String = "$id"
|
||||
}
|
||||
|
||||
|
||||
/** > This is universally unique for every entity that has one, for the lifetime of a whole game. */
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class UnitNumber(private val id: UInt) {
|
||||
override fun toString(): String = "$id"
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PrototypeName(private val id: String) {
|
||||
override fun toString(): String = id
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class EventName(private val name: String) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@JvmInline
|
||||
@Serializable
|
||||
value class FactorioServerId(private val id: String) {
|
||||
override fun toString() = id
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.prototype.FactorioPrototype")
|
||||
sealed interface FactorioPrototype {
|
||||
val name: PrototypeName
|
||||
val mapColour: Colour
|
||||
|
||||
@SerialName("kafkatorio.prototype.MapTile")
|
||||
@Serializable
|
||||
data class MapTile(
|
||||
override val name: PrototypeName,
|
||||
override val mapColour: Colour,
|
||||
|
||||
val layer: UInt,
|
||||
@Contextual
|
||||
val collisionMasks: List<String>,
|
||||
val order: String,
|
||||
/** Can the tile be mined for resources? */
|
||||
val canBeMined: Boolean,
|
||||
) : FactorioPrototype
|
||||
|
||||
|
||||
@SerialName("kafkatorio.prototype.Entity")
|
||||
@Serializable
|
||||
data class Entity(
|
||||
override val name: PrototypeName,
|
||||
override val mapColour: Colour,
|
||||
|
||||
val protoType: String,
|
||||
val objectName: String,
|
||||
val colour: Colour,
|
||||
val maxHealth: Float,
|
||||
val isBuilding: Boolean,
|
||||
val isEntityWithOwner: Boolean,
|
||||
val isMilitaryTarget: Boolean,
|
||||
val miningProperties: MiningProperties
|
||||
) : FactorioPrototype {
|
||||
|
||||
@Serializable
|
||||
data class MiningProperties(
|
||||
val canBeMined: Boolean,
|
||||
val products: List<MinedProduct>?,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
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 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))
|
||||
} else if (protoName == null) {
|
||||
println("warning: no prototype name found for index:$protoIndex. $protosIndexToName")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** An arbitrary ID for a prototype in [MapTileDictionary.protos]. */
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PrototypeKey(private 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,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class ServerMapChunkId(
|
||||
val serverId: FactorioServerId,
|
||||
val chunkPosition: MapChunkPosition,
|
||||
val surfaceIndex: SurfaceIndex,
|
||||
val chunkSize: ChunkSize,
|
||||
)
|
||||
|
||||
|
||||
/** `${serverDataDir}/servers/{serverId}/map/tiles/s{surfaceIndex}/z{zoomLevel}/x{chunkX}/y{chunkY}.png` */
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class TilePngFilename(
|
||||
val value: String,
|
||||
) {
|
||||
constructor(id: ServerMapChunkId) : this(
|
||||
id.serverId,
|
||||
id.surfaceIndex,
|
||||
id.chunkSize.zoomLevel,
|
||||
id.chunkPosition.x,
|
||||
id.chunkPosition.y,
|
||||
)
|
||||
|
||||
constructor(
|
||||
serverId: FactorioServerId,
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import dev.adamko.kxstsgen.core.*
|
||||
import dev.adamko.kxstsgen.core.experiments.TupleSerializer
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val bounds: MapChunkPositionBounds by lazy {
|
||||
MapChunkPositionBounds(this, ChunkSize.CHUNK_032)
|
||||
}
|
||||
|
||||
operator fun contains(tilePosition: MapTilePosition): Boolean = tilePosition in bounds
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class MapChunkPositionBounds(
|
||||
val leftTop: MapTilePosition,
|
||||
val rightBottom: MapTilePosition,
|
||||
) : ClosedRange<MapTilePosition> {
|
||||
|
||||
constructor(chunkPosition: MapChunkPosition, chunkSize: ChunkSize) : this(
|
||||
chunkPosition.leftTopTile(chunkSize),
|
||||
chunkPosition.rightBottomTile(chunkSize),
|
||||
)
|
||||
|
||||
override val start: MapTilePosition
|
||||
get() = leftTop
|
||||
|
||||
override val endInclusive: MapTilePosition
|
||||
get() = rightBottom
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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,
|
||||
) : Comparable<ChunkSize> {
|
||||
CHUNK_512(-1),
|
||||
CHUNK_256(0),
|
||||
CHUNK_128(1),
|
||||
CHUNK_064(2),
|
||||
CHUNK_032(3),
|
||||
;
|
||||
|
||||
/** 32, 64, 128, 256, or 512 */
|
||||
val lengthInTiles: Int = 2f.pow(8 - zoomLevel).roundToInt()
|
||||
|
||||
init {
|
||||
require(lengthInTiles > 0) { "tilesPerChunk $lengthInTiles must be positive" }
|
||||
// 1000 & 0111 = 0000 => pow^2
|
||||
// 1001 & 1000 = 1000 => !pow^2
|
||||
require(lengthInTiles and (lengthInTiles - 1) == 0) {
|
||||
"tilesPerChunk $lengthInTiles must be a power-of-two number"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// cache values for better performance. KT-48872
|
||||
val entries: Set<ChunkSize> = values().toSet()
|
||||
|
||||
val MAX: ChunkSize = entries.maxByOrNull { it.lengthInTiles }!!
|
||||
val MIN: ChunkSize = entries.minByOrNull { it.lengthInTiles }!!
|
||||
val STANDARD: ChunkSize = entries.first { it.zoomLevel == 0 }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.resource.MinedProduct")
|
||||
data class MinedProduct(
|
||||
/** `item` or `fluid`. */
|
||||
val type: Type,
|
||||
|
||||
/** Prototype name of the result. */
|
||||
@SerialName("name")
|
||||
val resultPrototype: PrototypeName,
|
||||
|
||||
//Amount of the item or fluid to give. If not specified, amount_min, amount_max and probability must all be specified.
|
||||
val amount: Double?,
|
||||
|
||||
//Minimal amount of the item or fluid to give. Has no effect when amount is specified.
|
||||
//amount_min : Uint or double?,
|
||||
|
||||
//Maximum amount of the item or fluid to give. Has no effect when amount is specified.
|
||||
//amount_max : Uint or double?,
|
||||
|
||||
//A value in range [0, 1]. Item or fluid is only given with this probability; otherwise no product is produced.
|
||||
//probability : Double?,
|
||||
|
||||
//How much of this product is a catalyst.
|
||||
//catalyst_amount : Uint or double?,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.resource.MinedProductType")
|
||||
enum class Type {
|
||||
@SerialName("item")
|
||||
ITEM,
|
||||
@SerialName("fluid")
|
||||
FLUID,
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.adamko.kafkatorio.schema.packets
|
||||
|
||||
import dev.adamko.kafkatorio.schema.common.ServerMapChunkId
|
||||
import dev.adamko.kafkatorio.schema.common.TilePngFilename
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
sealed class EventServerPacket {
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.event-server-packet.Kafkatorio")
|
||||
data class Kafkatorio(
|
||||
val packet: KafkatorioPacket
|
||||
) : EventServerPacket()
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.event-server-packet.ChunkTileSaved")
|
||||
data class ChunkTileSaved(
|
||||
val chunkId: ServerMapChunkId,
|
||||
val filename: TilePngFilename,
|
||||
) : EventServerPacket()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package dev.adamko.kafkatorio.schema.packets
|
||||
|
||||
import dev.adamko.kafkatorio.schema.common.Tick
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.KafkatorioPacket")
|
||||
data class KafkatorioPacket(
|
||||
val modVersion: String,
|
||||
val tick: Tick,
|
||||
val data: KafkatorioPacketData,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.KafkatorioPacketData")
|
||||
sealed class KafkatorioPacketData {
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.KafkatorioPacketData.Error")
|
||||
data class Error(
|
||||
val message: String? = null,
|
||||
val rawValue: String? = null,
|
||||
) : KafkatorioPacketData()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package dev.adamko.kafkatorio.schema.packets
|
||||
|
||||
import dev.adamko.kafkatorio.schema.common.FactorioPrototype
|
||||
import dev.adamko.kafkatorio.schema.common.PlayerIndex
|
||||
import dev.adamko.kafkatorio.schema.common.SurfaceIndex
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
/** No key, no caching. Must be sent instantly. */
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.KafkatorioPacket")
|
||||
sealed class KafkatorioInstantPacketData : KafkatorioPacketData()
|
||||
|
||||
|
||||
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.instant.ConfigurationUpdate")
|
||||
data class ConfigurationUpdate(
|
||||
val factorioData: ConfigurationUpdateGameData,
|
||||
val allMods: List<ConfigurationUpdateModData>,
|
||||
val modStartupSettingsChange: Boolean,
|
||||
val migrationApplied: Boolean,
|
||||
) : KafkatorioInstantPacketData() {
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.instant.ConfigurationUpdate.ConfigurationUpdateGameData")
|
||||
data class ConfigurationUpdateGameData(
|
||||
val oldVersion: String? = null,
|
||||
val newVersion: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.instant.ConfigurationUpdate.ConfigurationUpdateModData")
|
||||
data class ConfigurationUpdateModData(
|
||||
val modName: String,
|
||||
val currentVersion: String? = null,
|
||||
val previousVersion: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.instant.ConsoleChatUpdate")
|
||||
data class ConsoleChatUpdate(
|
||||
val authorPlayerIndex: PlayerIndex?,
|
||||
val content: String,
|
||||
) : KafkatorioInstantPacketData()
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.instant.ConsoleCommandUpdate")
|
||||
data class ConsoleCommandUpdate(
|
||||
val authorPlayerIndex: PlayerIndex?,
|
||||
val command: String,
|
||||
val parameters: String,
|
||||
) : KafkatorioInstantPacketData()
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.instant.PrototypesUpdate")
|
||||
data class PrototypesUpdate(
|
||||
val prototypes: List<FactorioPrototype>,
|
||||
) : KafkatorioInstantPacketData()
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.instant.SurfaceUpdate")
|
||||
data class SurfaceUpdate(
|
||||
val index: SurfaceIndex,
|
||||
val daytime: Double,
|
||||
val name: String,
|
||||
) : KafkatorioInstantPacketData()
|
||||
|
||||
|
||||
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package dev.adamko.kafkatorio.schema.packets
|
||||
|
||||
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.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.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
/** Has a key, so it can be debounced */
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.KafkatorioKeyedPacketData")
|
||||
sealed class KafkatorioKeyedPacketData : KafkatorioPacketData() {
|
||||
abstract val key: KafkatorioKeyedPacketKey
|
||||
/** A list of all events that have been aggregated into this packet. */
|
||||
abstract val events: Map<EventName, List<Tick>>?
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.KafkatorioKeyedPacketKey")
|
||||
sealed class KafkatorioKeyedPacketKey
|
||||
|
||||
|
||||
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.EntityUpdateKey")
|
||||
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, KafkatorioKeyedPacketKey()
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.EntityUpdate")
|
||||
data class EntityUpdate(
|
||||
override val key: EntityUpdateKey,
|
||||
|
||||
override val events: Map<EventName, List<Tick>>? = null,
|
||||
|
||||
val chunkPosition: MapEntityPosition? = 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()
|
||||
|
||||
|
||||
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.MapChunkUpdateKey")
|
||||
data class MapChunkUpdateKey(
|
||||
val chunkPosition: MapChunkPosition,
|
||||
val surfaceIndex: SurfaceIndex,
|
||||
) : KafkatorioKeyedPacketKey()
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.MapChunkUpdate")
|
||||
data class MapChunkUpdate(
|
||||
override val key: MapChunkUpdateKey,
|
||||
|
||||
override val events: Map<EventName, List<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,
|
||||
) : KafkatorioKeyedPacketData()
|
||||
|
||||
|
||||
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.PlayerUpdateKey")
|
||||
data class PlayerUpdateKey(
|
||||
val index: PlayerIndex,
|
||||
) : KafkatorioKeyedPacketKey()
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("kafkatorio.packet.keyed.PlayerUpdate")
|
||||
data class PlayerUpdate(
|
||||
override val key: PlayerUpdateKey,
|
||||
|
||||
override val events: Map<EventName, List<Tick>>? = 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()
|
||||
|
||||
|
||||
/* *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** */
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package dev.adamko.kafkatorio.events.schema
|
||||
|
||||
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
|
||||
import io.kotest.matchers.should
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.property.checkAll
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
||||
|
||||
class ColourHexTest : BehaviorSpec({
|
||||
|
||||
Given("colour json") {
|
||||
|
||||
//Language=JSON
|
||||
val colourJson = """
|
||||
[0.1, 0.4, 0.7, 0.6]
|
||||
""".trimIndent()
|
||||
|
||||
val colour: Colour = jsonMapperKafkatorio.decodeFromString(colourJson)
|
||||
|
||||
Then("expect colour is decoded") {
|
||||
colour shouldBe Colour(
|
||||
0.1f,
|
||||
0.4f,
|
||||
0.7f,
|
||||
0.6f,
|
||||
)
|
||||
}
|
||||
|
||||
Then("expect Colour can be converted to ColourHex and back") {
|
||||
val colourHex = colour.toHex()
|
||||
colourHex shouldBe ColourHex(
|
||||
25u,
|
||||
102u,
|
||||
178u,
|
||||
153u,
|
||||
)
|
||||
|
||||
colourHex.toPercentile() should { back ->
|
||||
withClue(back) {
|
||||
|
||||
val tolerance = 2.6 // 2.6%, because 255/100 = 2.55
|
||||
|
||||
withClue("red") { back.red.shouldBeWithinPercentageOf(0.1f, tolerance) }
|
||||
withClue("green") { back.green.shouldBeWithinPercentageOf(0.4f, tolerance) }
|
||||
withClue("blue") { back.blue.shouldBeWithinPercentageOf(0.7f, tolerance) }
|
||||
withClue("alpha") { back.alpha.shouldBeWithinPercentageOf(0.6f, tolerance) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Given("any colour json") {
|
||||
Then("expect can always convert") {
|
||||
|
||||
checkAll<UByte, UByte, UByte, UByte> { r, g, b, a ->
|
||||
|
||||
//Language=JSON
|
||||
val colourJson = """
|
||||
[
|
||||
${r.toFloat() / UByte.MAX_VALUE.toFloat()},
|
||||
${g.toFloat() / UByte.MAX_VALUE.toFloat()},
|
||||
${b.toFloat() / UByte.MAX_VALUE.toFloat()},
|
||||
${a.toFloat() / UByte.MAX_VALUE.toFloat()}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val colour: Colour = jsonMapperKafkatorio.decodeFromString(colourJson)
|
||||
val colourHex = colour.toHex()
|
||||
colourHex shouldBe ColourHex(r, g, b, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package dev.adamko.kafkatorio.events.schema
|
||||
|
||||
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.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.schema.packets.KafkatorioPacket
|
||||
import dev.adamko.kafkatorio.schema.packets.MapChunkUpdate
|
||||
import dev.adamko.kafkatorio.schema.packets.MapChunkUpdateKey
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
||||
class MapChunkUpdateTest : FunSpec({
|
||||
|
||||
context("MapChunkUpdate update") {
|
||||
//language=json
|
||||
val json = """
|
||||
{
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.keyed.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
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"on_chunk_generated": [1]
|
||||
}
|
||||
},
|
||||
"modVersion": "0.4.0",
|
||||
"tick": 36
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val expected = KafkatorioPacket(
|
||||
tick = 36u.tick,
|
||||
modVersion = "0.4.0",
|
||||
data = MapChunkUpdate(
|
||||
key = MapChunkUpdateKey(
|
||||
MapChunkPosition(-6, -3),
|
||||
SurfaceIndex(1u),
|
||||
),
|
||||
events = mapOf(
|
||||
EventName("on_chunk_generated") to listOf(1u.tick)
|
||||
),
|
||||
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: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
|
||||
packet shouldBe expected
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,681 @@
|
|||
package dev.adamko.kafkatorio.events.schema
|
||||
|
||||
import dev.adamko.kafkatorio.schema.common.Colour
|
||||
import dev.adamko.kafkatorio.schema.common.FactorioPrototype
|
||||
import dev.adamko.kafkatorio.schema.common.PrototypeName
|
||||
import dev.adamko.kafkatorio.schema.jsonMapperKafkatorio
|
||||
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
|
||||
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
|
||||
import io.kotest.core.spec.style.BehaviorSpec
|
||||
import io.kotest.inspectors.forOne
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
||||
class PrototypesTest : BehaviorSpec({
|
||||
Given("Prototypes Json") {
|
||||
// language=JSON
|
||||
val raw = """
|
||||
{
|
||||
"modVersion": "0.2.4",
|
||||
"tick": 534,
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.instant.PrototypesUpdate",
|
||||
"prototypes": [
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "tile-unknown",
|
||||
"order": "z-a",
|
||||
"layer": 0,
|
||||
"collisionMasks": {},
|
||||
"mapColour": [0, 0, 0, 255],
|
||||
"canBeMined": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
Then("expect can be parsed") {
|
||||
val data = jsonMapperKafkatorio.decodeFromString<KafkatorioPacket>(raw)
|
||||
println(data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Given("Big prototypes json") {
|
||||
// language=JSON
|
||||
val raw = """
|
||||
{
|
||||
"modVersion": "0.3.2",
|
||||
"tick": 534,
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.instant.PrototypesUpdate",
|
||||
"prototypes": [
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "stone-path",
|
||||
"order": "a[artificial]-a[tier-1]-a[stone-path]",
|
||||
"layer": 60,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [86, 82, 74, 255],
|
||||
"canBeMined": true
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "concrete",
|
||||
"order": "a[artificial]-b[tier-2]-a[concrete]",
|
||||
"layer": 61,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [63, 61, 59, 255],
|
||||
"canBeMined": true
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "hazard-concrete-left",
|
||||
"order": "a[artificial]-b[tier-2]-b[hazard-concrete-left]",
|
||||
"layer": 62,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [176, 142, 39, 255],
|
||||
"canBeMined": true
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "hazard-concrete-right",
|
||||
"order": "a[artificial]-b[tier-2]-c[hazard-concrete-right]",
|
||||
"layer": 62,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [176, 142, 39, 255],
|
||||
"canBeMined": true
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "refined-concrete",
|
||||
"order": "a[artificial]-c[tier-3]-a[refined-concrete]",
|
||||
"layer": 64,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [49, 48, 45, 255],
|
||||
"canBeMined": true
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "refined-hazard-concrete-left",
|
||||
"order": "a[artificial]-c[tier-3]-b[refined-hazard-concrete-left]",
|
||||
"layer": 65,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [116, 94, 26, 255],
|
||||
"canBeMined": true
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "refined-hazard-concrete-right",
|
||||
"order": "a[artificial]-c[tier-3]-c[refined-hazard-concrete-right]",
|
||||
"layer": 65,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [116, 94, 26, 255],
|
||||
"canBeMined": true
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "landfill",
|
||||
"order": "a[artificial]-d[utility]-a[landfill]",
|
||||
"layer": 57,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [57, 39, 26, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "acid-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-acid",
|
||||
"layer": 97,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [142, 194, 40, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "black-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-black",
|
||||
"layer": 88,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [25, 25, 25, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "blue-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-blue",
|
||||
"layer": 73,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [39, 137, 228, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "brown-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-brown",
|
||||
"layer": 91,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [76, 29, 0, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "cyan-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-cyan",
|
||||
"layer": 94,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [70, 192, 181, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "green-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-green",
|
||||
"layer": 70,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [23, 195, 43, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "orange-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-orange",
|
||||
"layer": 76,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [221, 127, 33, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "pink-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-pink",
|
||||
"layer": 82,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [236, 98, 131, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "purple-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-purple",
|
||||
"layer": 85,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [123, 28, 168, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "red-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-red",
|
||||
"layer": 67,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [207, 6, 0, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "yellow-refined-concrete",
|
||||
"order": "a[artificial]-e[color-concrete]-yellow",
|
||||
"layer": 79,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [212, 169, 19, 127],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "grass-1",
|
||||
"order": "b[natural]-a[grass]-a[grass-1]",
|
||||
"layer": 26,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [55, 53, 11, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "grass-2",
|
||||
"order": "b[natural]-a[grass]-b[grass-2]",
|
||||
"layer": 28,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [66, 57, 15, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "grass-3",
|
||||
"order": "b[natural]-a[grass]-c[grass-3]",
|
||||
"layer": 29,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [65, 52, 28, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "grass-4",
|
||||
"order": "b[natural]-a[grass]-d[grass-4]",
|
||||
"layer": 30,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [59, 40, 18, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dry-dirt",
|
||||
"order": "b[natural]-b[dirt]-a[dry-dirt]",
|
||||
"layer": 18,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [94, 66, 37, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dirt-1",
|
||||
"order": "b[natural]-b[dirt]-b[dirt-1]",
|
||||
"layer": 19,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [141, 104, 60, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dirt-2",
|
||||
"order": "b[natural]-b[dirt]-c[dirt-2]",
|
||||
"layer": 20,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [136, 96, 59, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dirt-3",
|
||||
"order": "b[natural]-b[dirt]-d[dirt-3]",
|
||||
"layer": 21,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [133, 92, 53, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dirt-4",
|
||||
"order": "b[natural]-b[dirt]-e[dirt-4]",
|
||||
"layer": 22,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [103, 72, 43, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dirt-5",
|
||||
"order": "b[natural]-b[dirt]-f[dirt-5]",
|
||||
"layer": 23,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [91, 63, 38, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dirt-6",
|
||||
"order": "b[natural]-b[dirt]-g[dirt-6]",
|
||||
"layer": 24,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [80, 55, 31, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "dirt-7",
|
||||
"order": "b[natural]-b[dirt]-h[dirt-7]",
|
||||
"layer": 25,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [80, 54, 28, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "sand-1",
|
||||
"order": "b[natural]-c[sand]-a[sand-1]",
|
||||
"layer": 8,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [138, 103, 58, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "sand-2",
|
||||
"order": "b[natural]-c[sand]-b[sand-2]",
|
||||
"layer": 9,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [128, 93, 52, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "sand-3",
|
||||
"order": "b[natural]-c[sand]-c[sand-3]",
|
||||
"layer": 10,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [115, 83, 47, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "red-desert-0",
|
||||
"order": "b[natural]-d[red-desert]-a[red-desert-0]",
|
||||
"layer": 31,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [103, 70, 32, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "red-desert-1",
|
||||
"order": "b[natural]-d[red-desert]-b[red-desert-1]",
|
||||
"layer": 14,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [116, 81, 39, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "red-desert-2",
|
||||
"order": "b[natural]-d[red-desert]-c[red-desert-2]",
|
||||
"layer": 15,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [116, 84, 43, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "red-desert-3",
|
||||
"order": "b[natural]-d[red-desert]-d[red-desert-3]",
|
||||
"layer": 16,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [128, 93, 52, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "water",
|
||||
"order": "c[water]-a[water]",
|
||||
"layer": 3,
|
||||
"collisionMasks": [
|
||||
"doodad-layer",
|
||||
"item-layer",
|
||||
"player-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [51, 83, 95, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "deepwater",
|
||||
"order": "c[water]-b[deep-water]",
|
||||
"layer": 3,
|
||||
"collisionMasks": [
|
||||
"doodad-layer",
|
||||
"item-layer",
|
||||
"player-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [38, 64, 73, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "water-green",
|
||||
"order": "c[water]-c[water-green]",
|
||||
"layer": 3,
|
||||
"collisionMasks": [
|
||||
"doodad-layer",
|
||||
"item-layer",
|
||||
"player-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [31, 48, 18, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "deepwater-green",
|
||||
"order": "c[water]-d[deepwater-green]",
|
||||
"layer": 3,
|
||||
"collisionMasks": [
|
||||
"doodad-layer",
|
||||
"item-layer",
|
||||
"player-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [23, 37, 16, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "water-shallow",
|
||||
"order": "c[water]-e[water-shallow]",
|
||||
"layer": 6,
|
||||
"collisionMasks": [
|
||||
"item-layer",
|
||||
"object-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [82, 98, 92, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "water-mud",
|
||||
"order": "c[water]-g[water-mud]",
|
||||
"layer": 7,
|
||||
"collisionMasks": [
|
||||
"item-layer",
|
||||
"object-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [65, 89, 90, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "nuclear-ground",
|
||||
"order": "d[destruction]-a[nuclear]-a[nuclear-ground]",
|
||||
"layer": 33,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [48, 40, 35, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "water-wube",
|
||||
"order": "x[wube]-a[water-wube]",
|
||||
"layer": 2,
|
||||
"collisionMasks": [
|
||||
"doodad-layer",
|
||||
"item-layer",
|
||||
"player-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [0, 0, 0, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "tile-unknown",
|
||||
"order": "z-a",
|
||||
"layer": 0,
|
||||
"collisionMasks": {
|
||||
},
|
||||
"mapColour": [0, 0, 0, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "out-of-map",
|
||||
"order": "z[other]-a[out-of-map]",
|
||||
"layer": 0,
|
||||
"collisionMasks": [
|
||||
"doodad-layer",
|
||||
"floor-layer",
|
||||
"ground-tile",
|
||||
"item-layer",
|
||||
"object-layer",
|
||||
"player-layer",
|
||||
"resource-layer",
|
||||
"water-tile"
|
||||
],
|
||||
"mapColour": [0, 0, 0, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "lab-dark-1",
|
||||
"order": "z[other]-b[lab]-a[lab-dark-1]",
|
||||
"layer": 70,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [49, 49, 49, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "lab-dark-2",
|
||||
"order": "z[other]-b[lab]-b[lab-dark-2]",
|
||||
"layer": 70,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [0, 0, 0, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "lab-white",
|
||||
"order": "z[other]-b[lab]-c[lab-white]",
|
||||
"layer": 70,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [255, 255, 255, 255],
|
||||
"canBeMined": false
|
||||
},
|
||||
{
|
||||
"type": "kafkatorio.prototype.MapTile",
|
||||
"name": "tutorial-grid",
|
||||
"order": "z[other]-c[tutorial]-a[tutorial-grid]",
|
||||
"layer": 55,
|
||||
"collisionMasks": [
|
||||
"ground-tile"
|
||||
],
|
||||
"mapColour": [122, 122, 122, 255],
|
||||
"canBeMined": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
Then("expect can be parsed") {
|
||||
val packet: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(raw)
|
||||
// println(packet)
|
||||
|
||||
packet.data.shouldBeInstanceOf<PrototypesUpdate>()
|
||||
.prototypes
|
||||
.forOne {
|
||||
it.shouldBeInstanceOf<FactorioPrototype.MapTile>()
|
||||
it.name shouldBe PrototypeName("tutorial-grid")
|
||||
it.order shouldBe "z[other]-c[tutorial]-a[tutorial-grid]"
|
||||
it.layer shouldBe 55u
|
||||
it.collisionMasks shouldBe listOf("ground-tile")
|
||||
it.mapColour shouldBe Colour(122f, 122f, 122f, 255f)
|
||||
it.canBeMined shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
package dev.adamko.kafkatorio.events.schema
|
||||
|
||||
import dev.adamko.kafkatorio.schema.common.Colour
|
||||
import dev.adamko.kafkatorio.schema.common.EntityIdentifiersData
|
||||
import dev.adamko.kafkatorio.schema.common.EventName
|
||||
import dev.adamko.kafkatorio.schema.common.ForceIndex
|
||||
import dev.adamko.kafkatorio.schema.common.MapEntityPosition
|
||||
import dev.adamko.kafkatorio.schema.common.PlayerIndex
|
||||
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.jsonMapperKafkatorio
|
||||
import dev.adamko.kafkatorio.schema.packets.KafkatorioKeyedPacketData
|
||||
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") {
|
||||
// language=JSON
|
||||
val json = """
|
||||
{
|
||||
"tick": 66,
|
||||
"modVersion": "0.3.2",
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.keyed.PlayerUpdate",
|
||||
"key": {"index": 1},
|
||||
"afkTime": 0,
|
||||
"chatColour": [1, 0.4, 0.6, 1],
|
||||
"colour": [0.8, 0.5, 0.2, 0.5],
|
||||
"events": {
|
||||
"on_player_changed_position": [7, 99],
|
||||
"on_player_joined_game": [14]
|
||||
},
|
||||
"forceIndex": 1,
|
||||
"isAdmin": true,
|
||||
"isConnected": true,
|
||||
"isShowOnMap": true,
|
||||
"isSpectator": false,
|
||||
"lastOnline": 0,
|
||||
"name": "fredthedeadhead",
|
||||
"onlineTime": 0,
|
||||
"position": [
|
||||
-58,
|
||||
13
|
||||
],
|
||||
"tag": ""
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
test("decode") {
|
||||
val packet: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
|
||||
|
||||
packet shouldBe KafkatorioPacket(
|
||||
modVersion = "0.3.2",
|
||||
tick = Tick(66u),
|
||||
data = PlayerUpdate(
|
||||
key = PlayerUpdateKey(PlayerIndex(1u)),
|
||||
afkTime = Tick(0u),
|
||||
bannedReason = null,
|
||||
characterUnitNumber = null,
|
||||
chatColour = Colour(red = 1f, green = 0.4f, blue = 0.6f, alpha = 1f),
|
||||
colour = Colour(red = 0.8f, green = 0.5f, blue = 0.2f, alpha = 0.5f),
|
||||
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),
|
||||
),
|
||||
forceIndex = ForceIndex(1u),
|
||||
isAdmin = true,
|
||||
isConnected = true,
|
||||
isRemoved = null,
|
||||
isShowOnMap = true,
|
||||
isSpectator = false,
|
||||
kickedReason = null,
|
||||
lastOnline = Tick(0u),
|
||||
name = "fredthedeadhead",
|
||||
onlineTime = Tick(0u),
|
||||
position = MapEntityPosition(-58.0, 13.0),
|
||||
surfaceIndex = null,
|
||||
tag = "",
|
||||
ticksToRespawn = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
test("encode") {
|
||||
val v = KafkatorioPacket(
|
||||
modVersion = "0.3.2",
|
||||
tick = Tick(66u),
|
||||
data = PlayerUpdate(
|
||||
key = PlayerUpdateKey(PlayerIndex(1u)),
|
||||
characterUnitNumber = null,
|
||||
chatColour = null,
|
||||
colour = null,
|
||||
name = "fredthedeadhead",
|
||||
afkTime = Tick(0u),
|
||||
ticksToRespawn = null,
|
||||
forceIndex = null,
|
||||
isAdmin = null,
|
||||
isConnected = null,
|
||||
isShowOnMap = true,
|
||||
isSpectator = false,
|
||||
lastOnline = Tick(0u),
|
||||
onlineTime = Tick(0u),
|
||||
position = MapEntityPosition(-58.0, 13.0),
|
||||
surfaceIndex = null,
|
||||
tag = null,
|
||||
diedCause = null,
|
||||
bannedReason = null,
|
||||
kickedReason = null,
|
||||
disconnectReason = null,
|
||||
isRemoved = null,
|
||||
)
|
||||
)
|
||||
|
||||
val string = jsonMapperKafkatorio.encodeToString(v)
|
||||
// language=JSON
|
||||
string shouldBe """
|
||||
{
|
||||
"modVersion": "0.3.2",
|
||||
"tick": 66,
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.keyed.PlayerUpdate",
|
||||
"key": {
|
||||
"index": 1
|
||||
},
|
||||
"name": "fredthedeadhead",
|
||||
"afkTime": 0,
|
||||
"isShowOnMap": true,
|
||||
"isSpectator": false,
|
||||
"lastOnline": 0,
|
||||
"onlineTime": 0,
|
||||
"position": [
|
||||
-58.0,
|
||||
13.0
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
context("player died") {
|
||||
// language=JSON
|
||||
val json = """
|
||||
{
|
||||
"tick": 227934,
|
||||
"modVersion": "0.3.2",
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.keyed.PlayerUpdate",
|
||||
"key": {
|
||||
"index": 1
|
||||
},
|
||||
"lastOnline": 227932,
|
||||
"onlineTime": 227888,
|
||||
"afkTime": 324,
|
||||
"isConnected": true,
|
||||
"diedCause": {
|
||||
"unitNumber": 66,
|
||||
"name": "small-biter",
|
||||
"protoType": "unit"
|
||||
},
|
||||
"events": {
|
||||
"on_player_died": [2]
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
test("decode") {
|
||||
val packet: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
|
||||
|
||||
packet shouldBe KafkatorioPacket(
|
||||
modVersion = "0.3.2",
|
||||
tick = 227934u.tick,
|
||||
data = PlayerUpdate(
|
||||
afkTime = 324u.tick,
|
||||
bannedReason = null,
|
||||
characterUnitNumber = null,
|
||||
chatColour = null,
|
||||
colour = null,
|
||||
diedCause = EntityIdentifiersData(
|
||||
unitNumber = UnitNumber(66u),
|
||||
name = "small-biter",
|
||||
protoType = "unit",
|
||||
),
|
||||
disconnectReason = null,
|
||||
events = mapOf(
|
||||
EventName("on_player_died") to listOf(2u.tick),
|
||||
),
|
||||
forceIndex = null,
|
||||
key = PlayerUpdateKey(PlayerIndex(1u)),
|
||||
isAdmin = null,
|
||||
isConnected = true,
|
||||
isRemoved = null,
|
||||
isShowOnMap = null,
|
||||
isSpectator = null,
|
||||
kickedReason = null,
|
||||
lastOnline = 227932u.tick,
|
||||
name = null,
|
||||
onlineTime = 227888u.tick,
|
||||
position = null,
|
||||
surfaceIndex = null,
|
||||
tag = null,
|
||||
ticksToRespawn = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context("surface update") {
|
||||
//language=json
|
||||
val json = """
|
||||
{
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.instant.SurfaceUpdate",
|
||||
"name": "nauvis",
|
||||
"index": 1,
|
||||
"daytime": 0.73
|
||||
},
|
||||
"modVersion": "0.3.2",
|
||||
"tick": 176000
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val expected = KafkatorioPacket(
|
||||
tick = Tick(176000u),
|
||||
modVersion = "0.3.2",
|
||||
data = SurfaceUpdate(
|
||||
name = "nauvis",
|
||||
index = SurfaceIndex(1u),
|
||||
daytime = 0.73,
|
||||
)
|
||||
)
|
||||
|
||||
println(jsonMapperKafkatorio.encodeToString(expected))
|
||||
|
||||
test("decode") {
|
||||
val packet: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
|
||||
packet shouldBe expected
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import io.kotest.assertions.asClue
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.ints.shouldBeExactly
|
||||
|
||||
class MapChunkPositionBoundsTest : FunSpec({
|
||||
|
||||
context("create bounds") {
|
||||
|
||||
context("MapChunkPosition(0, 0)") {
|
||||
|
||||
context("chunk size 32") {
|
||||
|
||||
val chunkPos = MapChunkPosition(0, 0)
|
||||
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_032)
|
||||
|
||||
test("leftTop should be 0,0") {
|
||||
bounds.leftTop.asClue {
|
||||
it.x shouldBeExactly 0
|
||||
it.y shouldBeExactly 0
|
||||
}
|
||||
}
|
||||
|
||||
test("rightBottom should be 31,31") {
|
||||
bounds.rightBottom.asClue {
|
||||
it.x shouldBeExactly 31
|
||||
it.y shouldBeExactly 31
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("chunk size 256") {
|
||||
|
||||
val chunkPos = MapChunkPosition(0, 0)
|
||||
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_256)
|
||||
|
||||
test("leftTop should be 0,0") {
|
||||
bounds.leftTop.asClue {
|
||||
it.x shouldBeExactly 0
|
||||
it.y shouldBeExactly 0
|
||||
}
|
||||
}
|
||||
|
||||
test("rightBottom should be 255,255") {
|
||||
bounds.rightBottom.asClue {
|
||||
it.x shouldBeExactly 255
|
||||
it.y shouldBeExactly 255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("MapChunkPosition(-10, -10)") {
|
||||
|
||||
context("chunk size 32") {
|
||||
|
||||
val chunkPos = MapChunkPosition(-10, -10)
|
||||
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_032)
|
||||
|
||||
test("leftTop should be -320,-320") {
|
||||
bounds.leftTop.asClue {
|
||||
it.x shouldBeExactly -320
|
||||
it.y shouldBeExactly -320
|
||||
}
|
||||
}
|
||||
|
||||
test("rightBottom should be -289,-289") {
|
||||
bounds.rightBottom.asClue {
|
||||
it.x shouldBeExactly -289
|
||||
it.y shouldBeExactly -289
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("chunk size 256") {
|
||||
|
||||
val chunkPos = MapChunkPosition(-10, -10)
|
||||
val bounds = MapChunkPositionBounds(chunkPos, ChunkSize.CHUNK_256)
|
||||
|
||||
test("leftTop should be -2560") {
|
||||
bounds.leftTop.asClue {
|
||||
it.x shouldBeExactly -2560
|
||||
it.y shouldBeExactly -2560
|
||||
}
|
||||
}
|
||||
|
||||
test("rightBottom should be (-2560 + 255)") {
|
||||
bounds.rightBottom.asClue {
|
||||
it.x shouldBeExactly (-2560 + 255)
|
||||
it.y shouldBeExactly (-2560 + 255)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.datatest.withData
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.property.Arb
|
||||
import io.kotest.property.arbitrary.arbitrary
|
||||
import io.kotest.property.arbitrary.int
|
||||
import io.kotest.property.checkAll
|
||||
|
||||
class MapChunkPositionTest : FunSpec({
|
||||
|
||||
context("check MapChunkPosition.contains()") {
|
||||
|
||||
context("MapChunkPosition(0, 0)") {
|
||||
|
||||
val chunkPos = MapChunkPosition(0, 0)
|
||||
|
||||
test("should contain tiles from (0,0) to (31,31)") {
|
||||
checkAll(mapTilePositionArb(0..31)) { tilePos ->
|
||||
(tilePos in chunkPos) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
test("should not contain tiles below (0,0)") {
|
||||
checkAll(
|
||||
mapTilePositionArb(-1000..-1)
|
||||
) { tilePos ->
|
||||
(tilePos in chunkPos) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
test("should not contain tiles above (31,31)") {
|
||||
checkAll(
|
||||
mapTilePositionArb(32..100)
|
||||
) { tilePos ->
|
||||
(tilePos in chunkPos) shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
context("MapChunkPosition(-10, -10)") {
|
||||
|
||||
val chunkPos = MapChunkPosition(-10, -10)
|
||||
|
||||
test("should contain tiles from (-320,-320) to (-289,-289)") {
|
||||
checkAll(mapTilePositionArb(-320..-289)) { tilePos ->
|
||||
(tilePos in chunkPos) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
test("should not contain tiles below (-320,-320)") {
|
||||
checkAll(
|
||||
mapTilePositionArb(-400..-321)
|
||||
) { tilePos ->
|
||||
(tilePos in chunkPos) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
test("should not contain tiles above (-289,-289)") {
|
||||
checkAll(
|
||||
mapTilePositionArb(-288..100)
|
||||
) { tilePos ->
|
||||
(tilePos in chunkPos) shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}) {
|
||||
companion object {
|
||||
fun mapTilePositionArb(
|
||||
range: IntRange = -1000..1000
|
||||
) = arbitrary {
|
||||
MapTilePosition(
|
||||
Arb.int(range).bind(),
|
||||
Arb.int(range).bind(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import dev.adamko.kafkatorio.schema.common.MapChunkPositionTest.Companion.mapTilePositionArb
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.property.forAll
|
||||
|
||||
class MapTilePositionTest : FunSpec({
|
||||
|
||||
context("comparison") {
|
||||
|
||||
test("less than <") {
|
||||
forAll(
|
||||
mapTilePositionArb(),
|
||||
mapTilePositionArb(1..100),
|
||||
) { tilePos, delta ->
|
||||
tilePos < (tilePos + delta)
|
||||
}
|
||||
}
|
||||
|
||||
test("less than or equal to <=") {
|
||||
forAll(mapTilePositionArb()) { tilePos ->
|
||||
tilePos <= (tilePos)
|
||||
}
|
||||
}
|
||||
|
||||
test("greater than or equal to >=") {
|
||||
forAll(mapTilePositionArb()) { tilePos ->
|
||||
tilePos >= (tilePos)
|
||||
}
|
||||
}
|
||||
|
||||
test("greater than >") {
|
||||
forAll(
|
||||
mapTilePositionArb(),
|
||||
mapTilePositionArb(1..100),
|
||||
) { tilePos, delta ->
|
||||
tilePos > (tilePos - delta)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package dev.adamko.kafkatorio.schema.packets
|
||||
|
||||
import dev.adamko.kafkatorio.schema.jsonMapperKafkatorio
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import kotlinx.serialization.decodeFromString
|
||||
|
||||
class EntityUpdateTest : FunSpec({
|
||||
|
||||
context("EntityUpdate packet") {
|
||||
/*language=json*/
|
||||
val json = """
|
||||
{
|
||||
"data": {
|
||||
"type": "kafkatorio.packet.keyed.EntityUpdate",
|
||||
"key": {
|
||||
"name": "assembling-machine-2",
|
||||
"protoType": "assembling-machine-2",
|
||||
"unitNumber": 4622
|
||||
},
|
||||
"events": {
|
||||
"on_built_entity": [
|
||||
38023
|
||||
]
|
||||
},
|
||||
"chunkPosition": [
|
||||
-13.5,
|
||||
4.5
|
||||
],
|
||||
"health": 350,
|
||||
"isActive": true,
|
||||
"isRotatable": true,
|
||||
"lastUser": 1,
|
||||
"prototype": "assembling-machine-2"
|
||||
},
|
||||
"modVersion": "0.4.0",
|
||||
"tick": 38388
|
||||
}
|
||||
""".trimIndent()
|
||||
test("decode") {
|
||||
val entityUpdate: KafkatorioPacket = jsonMapperKafkatorio.decodeFromString(json)
|
||||
entityUpdate.data
|
||||
.shouldBeInstanceOf<EntityUpdate>()
|
||||
|
||||
// entityUpdate.health.shouldNotBeNull().shouldBeExactly(350f)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package dev.adamko.kafkatorio.events.schema
|
||||
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KClassifier
|
||||
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.starProjectedType
|
||||
import kotlin.reflect.full.withNullability
|
||||
import me.ntrrgc.tsGenerator.ClassTransformer
|
||||
|
||||
/**
|
||||
* 'Unwrap' value classes
|
||||
*/
|
||||
object ValueClassTransformer : ClassTransformer {
|
||||
|
||||
private val mappedValueClasses: MutableMap<KType, KType> = mutableMapOf(
|
||||
// don't unwrap unsigned numbers - they're mapped to Factorio types
|
||||
UInt::class.starProjectedType to UInt::class.starProjectedType,
|
||||
UByte::class.starProjectedType to UByte::class.starProjectedType,
|
||||
UShort::class.starProjectedType to UShort::class.starProjectedType,
|
||||
ULong::class.starProjectedType to ULong::class.starProjectedType,
|
||||
)
|
||||
|
||||
override fun transformPropertyType(type: KType, property: KProperty<*>, klass: KClass<*>): KType {
|
||||
val unwrappedType =
|
||||
unwrapValueClassType(type, property, klass)
|
||||
?: unwrapArgs(type, property, klass)
|
||||
return when (unwrappedType) {
|
||||
null -> super.transformPropertyType(type, property, klass)
|
||||
else -> super.transformPropertyType(unwrappedType, property, klass)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unwrapArgs(type: KType, property: KProperty<*>, klass: KClass<*>): KType? {
|
||||
|
||||
val classifier: KClassifier = type.classifier ?: return type
|
||||
|
||||
return if (type.arguments.isNotEmpty()) {
|
||||
println("unwrapping args for $type - ${type.arguments}")
|
||||
|
||||
val args = type.arguments.map { arg: KTypeProjection ->
|
||||
// recursively map each argument
|
||||
val argType = arg.type!!
|
||||
val converted = transformPropertyType(argType, property, klass)
|
||||
KTypeProjection(arg.variance, converted.withNullability(argType.isMarkedNullable))
|
||||
}
|
||||
|
||||
classifier.createType(
|
||||
arguments = args,
|
||||
nullable = type.isMarkedNullable,
|
||||
annotations = type.annotations,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun unwrapValueClassType(type: KType, property: KProperty<*>, klass: KClass<*>): KType? {
|
||||
|
||||
val valueKlass: KClass<*> = type.classifier as? KClass<*> ?: return null
|
||||
|
||||
return if (isValueClass(type)) {
|
||||
val mappedType = mappedValueClasses.getOrPut(type.withNullability(false)) {
|
||||
|
||||
val result = valueKlass
|
||||
.declaredMemberProperties
|
||||
.first()
|
||||
.returnType
|
||||
.withNullability(type.isMarkedNullable)
|
||||
|
||||
println("unwrapping value class $type -> $result")
|
||||
|
||||
// recursively unwrap...
|
||||
val transformed = transformPropertyType(result, property, klass)
|
||||
transformed.withNullability(false)
|
||||
}
|
||||
mappedType.withNullability(type.isMarkedNullable)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
private fun isValueClass(type: KType?): Boolean {
|
||||
contract { returnsNotNull() implies (type != null) }
|
||||
return (type?.classifier as? KClass<*>)?.isValue == true
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
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
|
||||
//import org.intellij.lang.annotations.Language
|
||||
//
|
||||
///**
|
||||
// * Convert Kotlin classes to Typescript classes, using [TypeScriptGenerator]
|
||||
// */
|
||||
//fun main(args: Array<String>) {
|
||||
//
|
||||
// val gen = TypeScriptGenerator(
|
||||
// rootClasses = buildSet {
|
||||
//// add(KafkatorioPacket::class)
|
||||
//// addAll(KafkatorioPacket::class.sealedSubclasses)
|
||||
//
|
||||
// add(KafkatorioInstantPacketData::class)
|
||||
// addAll(KafkatorioInstantPacketData::class.sealedSubclasses)
|
||||
//
|
||||
// add(KafkatorioKeyedPacketData::class)
|
||||
// addAll(KafkatorioKeyedPacketData::class.sealedSubclasses)
|
||||
// addAll(KafkatorioKeyedPacketKey::class.sealedSubclasses)
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// when (val filename = args.firstOrNull()) {
|
||||
// null -> println(definitions)
|
||||
// else -> {
|
||||
// val outPath = Path(filename)
|
||||
// outPath.createDirectories()
|
||||
//
|
||||
// splitDefinitions(definitions)
|
||||
// .forEach { (defFile, def) ->
|
||||
// val outFile = outPath.resolve(defFile).toFile()
|
||||
// outFile.printWriter().use {
|
||||
// it.println(kt2tsHeader)
|
||||
// it.print(def)
|
||||
// }
|
||||
// println("Generated file: $outFile")
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
///** Split the TypeScript interfaces into different files */
|
||||
//private fun splitDefinitions(definitions: String): Map<String, String> {
|
||||
// return definitions
|
||||
// .split("\n\n")
|
||||
// .groupBy { def ->
|
||||
//
|
||||
// when {
|
||||
// def isInterface "KafkatorioPacket"
|
||||
// || def extendsAny listOf("KafkatorioPacket")
|
||||
// || def isType "KafkatorioPacketDataType"
|
||||
// -> "kafkatorio-packet.d.ts"
|
||||
//
|
||||
// def isInterface "KafkatorioInstantPacketData"
|
||||
// || def extends "KafkatorioInstantPacketData"
|
||||
// || def isType "KafkatorioInstantPacketDataType"
|
||||
//
|
||||
// || def isInterface "ConfigurationUpdateGameData"
|
||||
// || def isInterface "ConfigurationUpdateModData"
|
||||
// -> "kafkatorio-instant-packet.d.ts"
|
||||
//
|
||||
// def isInterface "KafkatorioKeyedPacket"
|
||||
// || def isInterface "KafkatorioKeyedPacketData"
|
||||
// || def extends "KafkatorioKeyedPacket"
|
||||
// || def isType "KafkatorioKeyedPacketDataType"
|
||||
//
|
||||
// || def isInterface "KafkatorioKeyedPacketKey"
|
||||
// -> "kafkatorio-keyed-packet.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 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,
|
||||
//// property: KProperty<*>,
|
||||
//// klass: KClass<*>
|
||||
//// ): String {
|
||||
//// return camelCaseToSnakeCase(propertyName)
|
||||
//// }
|
||||
////}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package dev.adamko.kafkatorio.events.schema
|
||||
|
||||
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
|
||||
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
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
|
||||
@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(
|
||||
KafkatorioPacket.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Language("TypeScript")
|
||||
val kt2tsHeader = """
|
||||
|// Generated by kxs-ts-gen - do not edit this file manually
|
||||
|
|
||||
""".trimMargin()
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
package dev.adamko.kafkatorio.processor.misc
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE
|
||||
import java.nio.file.StandardWatchEventKinds.ENTRY_DELETE
|
||||
import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY
|
||||
import java.nio.file.WatchEvent
|
||||
import java.nio.file.WatchKey
|
||||
import java.nio.file.WatchService
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.io.path.isReadable
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.cancellable
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapConcat
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.runningFold
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.yield
|
||||
|
||||
|
||||
fun File.fileEventsFlow() = FileEventsFlow(
|
||||
root = this.toPath(),
|
||||
)
|
||||
|
||||
fun Path.fileEventsFlow() = FileEventsFlow(
|
||||
root = this,
|
||||
)
|
||||
|
||||
|
||||
/** [SharedFlow] wrapper for Java's [WatchService] */
|
||||
class FileEventsFlow private constructor(
|
||||
private val root: Path,
|
||||
private val fileEvents: MutableSharedFlow<KWatchEvent>,
|
||||
) : SharedFlow<KWatchEvent> by fileEvents, CoroutineScope {
|
||||
|
||||
constructor(root: Path) : this(root, MutableSharedFlow(0, Int.MAX_VALUE, BufferOverflow.SUSPEND))
|
||||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
CoroutineName("FileEventsFlow") +
|
||||
Dispatchers.Unconfined +
|
||||
CoroutineExceptionHandler { _, e ->
|
||||
println("handling exception in FileEventsFlow")
|
||||
e.printStackTrace()
|
||||
watchService.close()
|
||||
watchKeys.update {
|
||||
it.cancelAll()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val watchService: WatchService = FileSystems.getDefault().newWatchService()
|
||||
|
||||
private val watchKeys: MutableStateFlow<List<WatchKey>> = MutableStateFlow(emptyList())
|
||||
|
||||
|
||||
init {
|
||||
println("launching FileEventsFlow... $root")
|
||||
registerPaths()
|
||||
pollFileEvents()
|
||||
|
||||
launch {
|
||||
fileEvents.emit(KWatchEvent.Initialized)
|
||||
}
|
||||
|
||||
println("launched FileEventsFlow $root")
|
||||
|
||||
root.toFile().walk()
|
||||
// .filter { it.isFile }
|
||||
.onEach {
|
||||
launch {
|
||||
fileEvents.emit(KWatchEvent.Created(it.toPath()))
|
||||
}
|
||||
}
|
||||
.count()
|
||||
.let {
|
||||
println("initializing KWatchEvent.Created $it")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Watch for any file tree changes and re-register the [watchKeys] */
|
||||
private fun registerPaths() = launch {
|
||||
println("launching registerPaths")
|
||||
fileEvents
|
||||
// .onEach { println("file event! $it") }
|
||||
.filter { event ->
|
||||
when (event) {
|
||||
is KWatchEvent.Initialized -> true
|
||||
|
||||
is KWatchEvent.Created -> event.path.isDirectory
|
||||
is KWatchEvent.Deleted -> event.path.isDirectory
|
||||
|
||||
is KWatchEvent.Modified -> false
|
||||
}
|
||||
}.debounce(1.seconds)
|
||||
.runningFold(emptyList<WatchKey>()) { previousKeys, directoryTreeAlteredEvent ->
|
||||
println("re-registering paths $directoryTreeAlteredEvent")
|
||||
|
||||
previousKeys.cancelAll()
|
||||
|
||||
root.toFile().walk()
|
||||
.filter { it.isDirectory }
|
||||
.map {
|
||||
// println("re-registering watch on $it")
|
||||
it.toPath().register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE)
|
||||
}
|
||||
.toList()
|
||||
.also {
|
||||
println("${it.count()} registered watches")
|
||||
}
|
||||
}
|
||||
.cancellable()
|
||||
.collect(watchKeys)
|
||||
}
|
||||
|
||||
|
||||
/** Poll [watchService] for updates while [coroutineContext] is active. */
|
||||
private fun pollFileEvents() = launch {
|
||||
flow<WatchKey> {
|
||||
while (coroutineContext.isActive) {
|
||||
emit(
|
||||
withContext(Dispatchers.IO) { watchService.take() }
|
||||
)
|
||||
println("emitted watchService.take()")
|
||||
yield()
|
||||
}
|
||||
}.mapNotNull { monitorKey ->
|
||||
when (val keyPath = monitorKey.watchable()) {
|
||||
is Path -> monitorKey to keyPath
|
||||
else -> null
|
||||
}
|
||||
}.filter { it.second.isReadable() }
|
||||
.flatMapConcat { (monitorKey: WatchKey, keyPath: Path) ->
|
||||
val events = monitorKey.pollEvents()
|
||||
monitorKey.reset()
|
||||
events
|
||||
.onEach { println("monitorKey event $it") }
|
||||
.filter { it.context() as? Path != null }
|
||||
.mapNotNull { event ->
|
||||
val eventPath = keyPath.resolve(event.context() as Path)
|
||||
|
||||
when (event.kind()) {
|
||||
ENTRY_CREATE -> KWatchEvent.Created(eventPath)
|
||||
ENTRY_DELETE -> KWatchEvent.Deleted(eventPath)
|
||||
ENTRY_MODIFY -> KWatchEvent.Modified(eventPath)
|
||||
else -> null
|
||||
}
|
||||
}.asFlow()
|
||||
}.cancellable()
|
||||
.collect(fileEvents)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val Path.isDirectory: Boolean get() = Files.isDirectory(this)
|
||||
|
||||
private fun List<WatchKey>.cancelAll() = forEach { it.cancel(); it.pollEvents() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrapper around [WatchEvent] that comes with properly resolved absolute path */
|
||||
sealed interface KWatchEvent {
|
||||
|
||||
object Initialized : KWatchEvent
|
||||
|
||||
sealed interface PathEvent : KWatchEvent {
|
||||
/** path of modified file or directory */
|
||||
val path: Path
|
||||
}
|
||||
|
||||
data class Created(override val path: Path) : PathEvent
|
||||
data class Modified(override val path: Path) : PathEvent
|
||||
data class Deleted(override val path: Path) : PathEvent
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,328 @@
|
|||
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.SurfaceIndex
|
||||
import io.kotest.assertions.withClue
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.comparables.shouldBeEqualComparingTo
|
||||
import io.kotest.matchers.equality.shouldBeEqualToComparingFields
|
||||
import io.kotest.matchers.ints.shouldBeExactly
|
||||
import io.kotest.matchers.longs.shouldBeExactly
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.property.Arb
|
||||
import io.kotest.property.arbitrary.boolean
|
||||
import io.kotest.property.arbitrary.booleanArray
|
||||
import io.kotest.property.arbitrary.byte
|
||||
import io.kotest.property.arbitrary.byteArray
|
||||
import io.kotest.property.arbitrary.char
|
||||
import io.kotest.property.arbitrary.charArray
|
||||
import io.kotest.property.arbitrary.double
|
||||
import io.kotest.property.arbitrary.doubleArray
|
||||
import io.kotest.property.arbitrary.float
|
||||
import io.kotest.property.arbitrary.floatArray
|
||||
import io.kotest.property.arbitrary.int
|
||||
import io.kotest.property.arbitrary.intArray
|
||||
import io.kotest.property.arbitrary.long
|
||||
import io.kotest.property.arbitrary.longArray
|
||||
import io.kotest.property.arbitrary.positiveInt
|
||||
import io.kotest.property.arbitrary.short
|
||||
import io.kotest.property.arbitrary.shortArray
|
||||
import io.kotest.property.arbitrary.string
|
||||
import io.kotest.property.arbitrary.uByte
|
||||
import io.kotest.property.arbitrary.uInt
|
||||
import io.kotest.property.arbitrary.uLong
|
||||
import io.kotest.property.arbitrary.uShort
|
||||
import io.kotest.property.checkAll
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
val result = kxsBinary.decodeFromByteArray(ServerMapChunkId.serializer(), ba)
|
||||
|
||||
withClue(ba.toString(Charsets.UTF_8)) {
|
||||
|
||||
result shouldBeEqualToComparingFields ServerMapChunkId(
|
||||
serverId = FactorioServerId("test"),
|
||||
chunkPosition = MapChunkPosition(1, 2),
|
||||
surfaceIndex = SurfaceIndex(1u),
|
||||
chunkSize = ChunkSize.CHUNK_064,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
test("encoding") {
|
||||
val ba = kxsBinary.encodeToByteArray(
|
||||
ServerMapChunkId(
|
||||
serverId = FactorioServerId("test"),
|
||||
chunkPosition = MapChunkPosition(1, 2),
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context("inline value class") {
|
||||
val uuid = "fa858d73-6205-44cf-aeeb-6bbf3148c4a7"
|
||||
//@formatter:off
|
||||
val expectedBytes = byteArrayOf(
|
||||
36, 102, 97, 56, 53, 56, 100, 55, 51, 45, 54, 50, 48, 53, 45, 52, 52, 99,
|
||||
102, 45, 97, 101, 101, 98, 45, 54, 98, 98, 102, 51, 49, 52, 56, 99, 52, 97, 55,
|
||||
)
|
||||
//@formatter:on
|
||||
|
||||
test("encode") {
|
||||
val ba = kxsBinary.encodeToByteArray(FactorioServerId(uuid))
|
||||
|
||||
withClue(ba.toString(Charsets.UTF_8)) {
|
||||
ba shouldBe expectedBytes
|
||||
}
|
||||
}
|
||||
test("decode") {
|
||||
val ba = kxsBinary.encodeToByteArray(FactorioServerId(uuid))
|
||||
val id: FactorioServerId = kxsBinary.decodeFromByteArray(ba)
|
||||
|
||||
id shouldBeEqualToComparingFields FactorioServerId(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
context("ServerMapChunkTiles") {
|
||||
val initial = ServerMapChunkTiles(
|
||||
ServerMapChunkId(
|
||||
FactorioServerId("server-id"),
|
||||
MapChunkPosition(33, 44),
|
||||
SurfaceIndex(4u),
|
||||
ChunkSize.CHUNK_032,
|
||||
),
|
||||
mapOf(
|
||||
MapTilePosition(1, 2) to ColourHex(1u, 2u, 3u, 4u),
|
||||
)
|
||||
)
|
||||
|
||||
test("!json round trip") {
|
||||
val jsonEncoded = jsonMapperKafkatorio.encodeToString(initial)
|
||||
val jsonDecoded =
|
||||
jsonMapperKafkatorio.decodeFromString<ServerMapChunkTiles<ColourHex>>(jsonEncoded)
|
||||
|
||||
jsonDecoded shouldBeEqualToComparingFields initial
|
||||
}
|
||||
|
||||
test("binary round trip") {
|
||||
val binaryEncoded = kxsBinary.encodeToByteArray(initial)
|
||||
val binaryDecoded =
|
||||
kxsBinary.decodeFromByteArray<ServerMapChunkTiles<ColourHex>>(binaryEncoded)
|
||||
|
||||
binaryDecoded shouldBeEqualToComparingFields initial
|
||||
}
|
||||
}
|
||||
|
||||
context("primitives round trip") {
|
||||
test("double") {
|
||||
checkAll(Arb.double()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Double = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("float") {
|
||||
checkAll(Arb.float()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Float = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("int") {
|
||||
checkAll(Arb.int()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Int = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeExactly input
|
||||
}
|
||||
}
|
||||
|
||||
test("long") {
|
||||
checkAll(Arb.long()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Long = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeExactly input
|
||||
}
|
||||
}
|
||||
|
||||
test("short") {
|
||||
checkAll(Arb.short()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Short = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("byte") {
|
||||
checkAll(Arb.byte()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Byte = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("boolean") {
|
||||
checkAll(Arb.boolean()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Boolean = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("char") {
|
||||
checkAll(Arb.char()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: Char = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("string") {
|
||||
checkAll(Arb.string()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: String = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
context("unsigned numbers round trip") {
|
||||
test("uint") {
|
||||
checkAll(Arb.uInt()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(UInt.serializer(), input)
|
||||
val decoded: UInt = kxsBinary.decodeFromByteArray(UInt.serializer(), encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("ulong") {
|
||||
checkAll(Arb.uLong()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(ULong.serializer(), input)
|
||||
val decoded: ULong = kxsBinary.decodeFromByteArray(ULong.serializer(), encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("ushort") {
|
||||
checkAll(Arb.uShort()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(UShort.serializer(), input)
|
||||
val decoded: UShort = kxsBinary.decodeFromByteArray(UShort.serializer(), encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
|
||||
test("ubyte") {
|
||||
checkAll(Arb.uByte()) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(UByte.serializer(), input)
|
||||
val decoded: UByte = kxsBinary.decodeFromByteArray(UByte.serializer(), encoded)
|
||||
decoded shouldBeEqualComparingTo input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("primitive arrays round trip") {
|
||||
test("double array") {
|
||||
checkAll(Arb.doubleArray(Arb.positiveInt(50), Arb.double())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: DoubleArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
|
||||
test("float array") {
|
||||
checkAll(Arb.floatArray(Arb.positiveInt(50), Arb.float())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: FloatArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
|
||||
test("int array") {
|
||||
checkAll(Arb.intArray(Arb.positiveInt(50), Arb.int())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: IntArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
|
||||
test("long array") {
|
||||
checkAll(Arb.longArray(Arb.positiveInt(50), Arb.long())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: LongArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
|
||||
test("short array") {
|
||||
checkAll(Arb.shortArray(Arb.positiveInt(50), Arb.short())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: ShortArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
|
||||
test("byte array") {
|
||||
checkAll(Arb.byteArray(Arb.positiveInt(50), Arb.byte())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: ByteArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
|
||||
test("boolean array") {
|
||||
checkAll(Arb.booleanArray(Arb.positiveInt(50), Arb.boolean())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: BooleanArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
|
||||
test("char array") {
|
||||
checkAll(Arb.charArray(Arb.positiveInt(50), Arb.char())) { input ->
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(input)
|
||||
val decoded: CharArray = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.asList() shouldContainExactly input.asList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
|
||||
@Serializable
|
||||
data class ServerMapChunkTiles<Data>(
|
||||
val chunkId: ServerMapChunkId,
|
||||
val map: Map<MapTilePosition, Data>,
|
||||
) {
|
||||
operator fun plus(other: ServerMapChunkTiles<Data>) = copy(map = map + other.map)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package dev.adamko.kafkatorio.schema.common
|
||||
|
||||
import dev.adamko.kafkatorio.library.kxsBinary
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.equality.shouldBeEqualToComparingFields
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.encodeToByteArray
|
||||
|
||||
class MapTileDictionaryTest : FunSpec({
|
||||
|
||||
context("encode 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("binary round trip") {
|
||||
val encoded: ByteArray = kxsBinary.encodeToByteArray(actual)
|
||||
val decoded: MapTileDictionary = kxsBinary.decodeFromByteArray(encoded)
|
||||
decoded.shouldBeEqualToComparingFields(actual)
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue