From 57aea29da2681a1c09b0d319e2763ebdad26fcba Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 10 Aug 2024 19:20:52 +0700 Subject: [PATCH] Add stream codec --- .../dbotthepony/kstarbound/io/StreamCodec.kt | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt new file mode 100644 index 00000000..3a880570 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StreamCodec.kt @@ -0,0 +1,338 @@ +package ru.dbotthepony.kstarbound.io + +import ru.dbotthepony.kommons.io.readBigDecimal +import ru.dbotthepony.kommons.io.readBinaryString +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.readKOptional +import ru.dbotthepony.kommons.io.readMap +import ru.dbotthepony.kommons.io.readOptional +import ru.dbotthepony.kommons.io.readSignedVarInt +import ru.dbotthepony.kommons.io.readSignedVarLong +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.readVarLong +import ru.dbotthepony.kommons.io.writeBigDecimal +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeKOptional +import ru.dbotthepony.kommons.io.writeMap +import ru.dbotthepony.kommons.io.writeOptional +import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kommons.io.writeSignedVarLong +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kommons.io.writeVarLong +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.KOptional +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.* +import kotlin.reflect.KClass + +/** + * Represents value which can be encoded onto or decoded from stream. + */ +interface StreamCodec { + fun read(stream: DataInputStream): V + fun write(stream: DataOutputStream, value: V) + + /** + * Defensive copy + */ + fun copy(value: V): V + + /** + * Optional custom equality check + */ + fun compare(a: V, b: V): Boolean { + return a == b + } + + class Impl( + private val reader: (stream: DataInputStream) -> V, + private val writer: (stream: DataOutputStream, value: V) -> Unit, + private val copier: ((value: V) -> V) = { it }, + private val comparator: ((a: V, b: V) -> Boolean) = { a, b -> a == b } + ) : StreamCodec { + constructor( + reader: (stream: DataInputStream) -> V, + payloadSize: Long, + writer: (stream: DataOutputStream, value: V) -> Unit, + copier: ((value: V) -> V) = { it }, + comparator: ((a: V, b: V) -> Boolean) = { a, b -> a == b } + ) : this({ stream -> reader.invoke(stream) }, writer, copier, comparator) + + override fun read(stream: DataInputStream): V { + return reader.invoke(stream) + } + + override fun write(stream: DataOutputStream, value: V) { + writer.invoke(stream, value) + } + + override fun copy(value: V): V { + return copier.invoke(value) + } + + override fun compare(a: V, b: V): Boolean { + return comparator.invoke(a, b) + } + } + + class Nullable(val parent: StreamCodec) : StreamCodec { + override fun read(stream: DataInputStream): V? { + return if (stream.read() == 0) null else parent.read(stream) + } + + override fun write(stream: DataOutputStream, value: V?) { + if (value === null) + stream.write(0) + else { + stream.write(1) + parent.write(stream, value) + } + } + + override fun copy(value: V?): V? { + return if (value === null) null else parent.copy(value) + } + + override fun compare(a: V?, b: V?): Boolean { + if (a === null && b === null) return true + if (a === null || b === null) return false + return parent.compare(a, b) + } + } + + class Collection>(val elementCodec: StreamCodec, val collectionFactory: (Int) -> C) : StreamCodec { + override fun read(stream: DataInputStream): C { + val size = stream.readVarInt() + + if (size <= 0) { + return collectionFactory.invoke(0) + } + + val collection = collectionFactory.invoke(size) + + for (i in 0 until size) { + collection.add(elementCodec.read(stream)) + } + + return collection + } + + override fun write(stream: DataOutputStream, value: C) { + stream.writeVarInt(value.size) + value.forEach { elementCodec.write(stream, it) } + } + + override fun copy(value: C): C { + val new = collectionFactory.invoke(value.size) + value.forEach { new.add(elementCodec.copy(it)) } + return new + } + } + + class Map>(val keyCodec: StreamCodec, val valueCodec: StreamCodec, val factory: (Int) -> C): StreamCodec { + override fun read(stream: DataInputStream): C { + return stream.readMap(keyCodec::read, valueCodec::read, factory) + } + + override fun write(stream: DataOutputStream, value: C) { + stream.writeMap(value, keyCodec::write, valueCodec::write) + } + + override fun copy(value: C): C { + val new = factory.invoke(value.size) + + for ((k, v) in value.entries) { + new[keyCodec.copy(k)] = valueCodec.copy(v) + } + + return new + } + } + + class Enum>(clazz: Class) : StreamCodec { + val clazz = searchClass(clazz) + val values: List = listOf(*this.clazz.enumConstants!!) + val valuesMap = values.associateBy { it.name } + + override fun read(stream: DataInputStream): V { + val id = stream.readVarInt() + return values.getOrNull(id) ?: throw NoSuchElementException("No such enum with index $id") + } + + override fun write(stream: DataOutputStream, value: V) { + stream.writeVarInt(value.ordinal) + } + + override fun copy(value: V): V { + return value + } + + override fun compare(a: V, b: V): Boolean { + return a === b + } + + companion object { + /** + * FIXME: enums with abstract methods which get compiled to subclasses, whose DO NOT expose "parent's" enum constants array + * + * is there an already existing solution? + */ + fun > searchClass(clazz: Class): Class { + var search: Class<*> = clazz + + while (search.enumConstants == null && search.superclass != null) { + search = search.superclass + } + + if (search.enumConstants == null) { + throw ClassCastException("$clazz does not represent an enum or enum subclass") + } + + return search as Class + } + } + } + + class KOptional(val codec: StreamCodec) : StreamCodec> { + override fun read(stream: DataInputStream): ru.dbotthepony.kommons.util.KOptional { + return stream.readKOptional(codec::read) + } + + override fun write(stream: DataOutputStream, value: ru.dbotthepony.kommons.util.KOptional) { + stream.writeKOptional(value, codec::write) + } + + override fun copy(value: ru.dbotthepony.kommons.util.KOptional): ru.dbotthepony.kommons.util.KOptional { + return value.map(codec::copy) + } + } + + class Optional(val codec: StreamCodec) : StreamCodec> { + override fun read(stream: DataInputStream): java.util.Optional { + return stream.readOptional(codec::read) + } + + override fun write(stream: DataOutputStream, value: java.util.Optional) { + stream.writeOptional(value, codec::write) + } + + override fun copy(value: java.util.Optional): java.util.Optional { + return value.map(codec::copy) + } + } + + class Mapping(private val codec: StreamCodec, private val from: T.() -> R, private val to: R.() -> T, private val copy: R.() -> R = { this }) : StreamCodec { + override fun read(stream: DataInputStream): R { + return from(codec.read(stream)) + } + + override fun write(stream: DataOutputStream, value: R) { + codec.write(stream, to(value)) + } + + override fun copy(value: R): R { + return copy.invoke(value) + } + } + + class Pair(private val left: StreamCodec, private val right: StreamCodec) : StreamCodec> { + override fun read(stream: DataInputStream): kotlin.Pair { + val left = left.read(stream) + val right = right.read(stream) + return left to right + } + + override fun write(stream: DataOutputStream, value: kotlin.Pair) { + left.write(stream, value.first) + right.write(stream, value.second) + } + + override fun copy(value: kotlin.Pair): kotlin.Pair { + val left = left.copy(value.first) + val right = right.copy(value.second) + + if (left !== this.left && right !== this.right) { + return left to right + } else { + return value + } + } + } +} + +fun StreamCodec.map(from: T.() -> R, to: R.() -> T): StreamCodec { + return StreamCodec.Mapping(this, from, to) +} + +fun StreamCodec.map(from: T.() -> R, to: R.() -> T, copy: R.() -> R): StreamCodec { + return StreamCodec.Mapping(this, from, to, copy) +} + +fun StreamCodec.optional(): StreamCodec> = StreamCodec.Optional(this) +fun StreamCodec.koptional(): StreamCodec> = StreamCodec.KOptional(this) +fun StreamCodec.nullable(): StreamCodec = StreamCodec.Nullable(this) + +val NullValueCodec = StreamCodec.Impl({ _ -> null }, { _, _ -> }) +val BooleanValueCodec = StreamCodec.Impl(DataInputStream::readBoolean, 1L, DataOutputStream::writeBoolean) +val ByteValueCodec = StreamCodec.Impl(DataInputStream::readByte, 1L, { s, v -> s.writeByte(v.toInt()) }) +val ShortValueCodec = StreamCodec.Impl(DataInputStream::readShort, 2L, { s, v -> s.writeShort(v.toInt()) }) +val CharValueCodec = StreamCodec.Impl(DataInputStream::readChar, 2L, { s, v -> s.writeShort(v.code) }) +val IntValueCodec = StreamCodec.Impl(DataInputStream::readInt, 4L, DataOutputStream::writeInt) +val LongValueCodec = StreamCodec.Impl(DataInputStream::readLong, 8L, DataOutputStream::writeLong) +val FloatValueCodec = StreamCodec.Impl(DataInputStream::readFloat, 4L, DataOutputStream::writeFloat) +val DoubleValueCodec = StreamCodec.Impl(DataInputStream::readDouble, 8L, DataOutputStream::writeDouble) +val BigDecimalValueCodec = StreamCodec.Impl(DataInputStream::readBigDecimal, DataOutputStream::writeBigDecimal) +val UUIDValueCodec = StreamCodec.Impl({ s -> UUID(s.readLong(), s.readLong()) }, { s, v -> s.writeLong(v.mostSignificantBits); s.writeLong(v.leastSignificantBits) }) +val VarIntValueCodec = StreamCodec.Impl(DataInputStream::readSignedVarInt, DataOutputStream::writeSignedVarInt) +val VarLongValueCodec = StreamCodec.Impl(DataInputStream::readSignedVarLong, DataOutputStream::writeSignedVarLong) +val BinaryStringCodec = StreamCodec.Impl(DataInputStream::readBinaryString, DataOutputStream::writeBinaryString) + +val UnsignedVarLongCodec = StreamCodec.Impl(DataInputStream::readVarLong, DataOutputStream::writeVarLong) +val UnsignedVarIntCodec = StreamCodec.Impl(DataInputStream::readVarInt, DataOutputStream::writeVarInt) + +val ByteArrayCodec = StreamCodec.Impl(DataInputStream::readByteArray, DataOutputStream::writeByteArray) + +val RGBCodec = StreamCodec.Impl( + { s -> RGBAColor(s.readFloat(), s.readFloat(), s.readFloat()) }, + { s, v -> s.writeFloat(v.red); s.writeFloat(v.green); s.writeFloat(v.blue) }) + +val RGBACodec = StreamCodec.Impl( + { s -> RGBAColor(s.readFloat(), s.readFloat(), s.readFloat(), s.readFloat()) }, + { s, v -> s.writeFloat(v.red); s.writeFloat(v.green); s.writeFloat(v.blue); s.writeFloat(v.alpha) }) + +val OptionalBooleanValueCodec = StreamCodec.Optional(BooleanValueCodec) +val OptionalByteValueCodec = StreamCodec.Optional(ByteValueCodec) +val OptionalShortValueCodec = StreamCodec.Optional(ShortValueCodec) +val OptionalCharValueCodec = StreamCodec.Optional(CharValueCodec) +val OptionalIntValueCodec = StreamCodec.Optional(IntValueCodec) +val OptionalLongValueCodec = StreamCodec.Optional(LongValueCodec) +val OptionalFloatValueCodec = StreamCodec.Optional(FloatValueCodec) +val OptionalDoubleValueCodec = StreamCodec.Optional(DoubleValueCodec) +val OptionalBigDecimalValueCodec = StreamCodec.Optional(BigDecimalValueCodec) +val OptionalUUIDValueCodec = StreamCodec.Optional(UUIDValueCodec) +val OptionalVarIntValueCodec = StreamCodec.Optional(VarIntValueCodec) +val OptionalVarLongValueCodec = StreamCodec.Optional(VarLongValueCodec) +val OptionalBinaryStringCodec = StreamCodec.Optional(BinaryStringCodec) +val OptionalRGBCodec = StreamCodec.Optional(RGBCodec) +val OptionalRGBACodec = StreamCodec.Optional(RGBACodec) + +val KOptionalBooleanValueCodec = StreamCodec.KOptional(BooleanValueCodec) +val KOptionalByteValueCodec = StreamCodec.KOptional(ByteValueCodec) +val KOptionalShortValueCodec = StreamCodec.KOptional(ShortValueCodec) +val KOptionalCharValueCodec = StreamCodec.KOptional(CharValueCodec) +val KOptionalIntValueCodec = StreamCodec.KOptional(IntValueCodec) +val KOptionalLongValueCodec = StreamCodec.KOptional(LongValueCodec) +val KOptionalFloatValueCodec = StreamCodec.KOptional(FloatValueCodec) +val KOptionalDoubleValueCodec = StreamCodec.KOptional(DoubleValueCodec) +val KOptionalBigDecimalValueCodec = StreamCodec.KOptional(BigDecimalValueCodec) +val KOptionalUUIDValueCodec = StreamCodec.KOptional(UUIDValueCodec) +val KOptionalVarIntValueCodec = StreamCodec.KOptional(VarIntValueCodec) +val KOptionalVarLongValueCodec = StreamCodec.KOptional(VarLongValueCodec) +val KOptionalBinaryStringCodec = StreamCodec.KOptional(BinaryStringCodec) +val KOptionalRGBCodec = StreamCodec.KOptional(RGBCodec) +val KOptionalRGBACodec = StreamCodec.KOptional(RGBACodec) + +fun > Class.codec() = StreamCodec.Enum(this) +fun > KClass.codec() = StreamCodec.Enum(this.java)