diff --git a/qodana.yaml b/qodana.yaml index 962f7de5..4efb37cd 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -29,3 +29,15 @@ projectJDK: "21" #(Applied in CI/CD pipeline) #Specify Qodana linter for analysis (Applied in CI/CD pipeline) linter: jetbrains/qodana-jvm:2024.3 + +exclude: + - name: All + paths: + - surf-cloud-test-plugin + - api + - cert_generation + - cert_new + - Writerside + +include: + - name: IncorrectFormatting \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a55f8551..8cb0404c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,16 +27,21 @@ include("surf-cloud-core:surf-cloud-core-client") findProject(":surf-cloud-core:surf-cloud-core-client")?.name = "surf-cloud-core-client" include("surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-common") -findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-common")?.name = "surf-cloud-api-client-common" +findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-common")?.name = + "surf-cloud-api-client-common" include("surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-paper") -findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-paper")?.name = "surf-cloud-api-client-paper" +findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-paper")?.name = + "surf-cloud-api-client-paper" include("surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-velocity") -findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-velocity")?.name = "surf-cloud-api-client-velocity" +findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-velocity")?.name = + "surf-cloud-api-client-velocity" include("surf-cloud-bom") if (!ci) { include(":surf-cloud-test-plugin:surf-cloud-test-standalone") -} \ No newline at end of file + include(":surf-cloud-test-plugin:surf-cloud-test-core") + include("surf-cloud-test-plugin:surf-cloud-test-paper") +} diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/internal/BinaryTagTypeProxy.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/internal/BinaryTagTypeProxy.kt new file mode 100644 index 00000000..ee9c406c --- /dev/null +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/internal/BinaryTagTypeProxy.kt @@ -0,0 +1,19 @@ +package dev.slne.surf.cloud.api.common.internal + +import dev.slne.surf.cloud.api.common.util.annotation.InternalApi +import dev.slne.surf.surfapi.core.api.reflection.* +import net.kyori.adventure.nbt.BinaryTag +import net.kyori.adventure.nbt.BinaryTagType + +@InternalApi +@SurfProxy(BinaryTagType::class) +internal interface BinaryTagTypeProxy { + + @Static + @Field("TYPES", Field.Type.GETTER) + fun getTypes(): List> + + companion object { + internal val instance = surfReflection.createProxy() + } +} \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/network/codec/ByteBufCodecs.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/network/codec/ByteBufCodecs.kt index 5b6cd3f2..70fa90eb 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/network/codec/ByteBufCodecs.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/network/codec/ByteBufCodecs.kt @@ -3,14 +3,27 @@ package dev.slne.surf.cloud.api.common.netty.network.codec import dev.slne.surf.cloud.api.common.netty.protocol.buffer.* import dev.slne.surf.cloud.api.common.netty.protocol.buffer.types.Utf8String import dev.slne.surf.cloud.api.common.util.ByIdMap +import dev.slne.surf.cloud.api.common.util.ByIdMap.OutOfBoundsStrategy import dev.slne.surf.cloud.api.common.util.createUnresolvedInetSocketAddress import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufInputStream +import io.netty.buffer.ByteBufOutputStream +import io.netty.handler.codec.DecoderException +import io.netty.handler.codec.EncoderException +import it.unimi.dsi.fastutil.io.FastBufferedInputStream +import it.unimi.dsi.fastutil.io.FastBufferedOutputStream +import it.unimi.dsi.fastutil.objects.ObjectArrayList import net.kyori.adventure.key.Key +import net.kyori.adventure.nbt.BinaryTag import net.kyori.adventure.nbt.BinaryTagIO +import net.kyori.adventure.nbt.BinaryTagType +import net.kyori.adventure.nbt.BinaryTagTypes import net.kyori.adventure.sound.Sound import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -21,10 +34,15 @@ import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime import java.util.* +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream +import kotlin.math.min import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds object ByteBufCodecs { + const val MAX_INITIAL_COLLECTION_SIZE = 65536 + val BOOLEAN_CODEC = streamCodec(ByteBuf::writeBoolean, ByteBuf::readBoolean) val BYTE_CODEC = streamCodec({ buf, byte -> buf.writeByte(byte.toInt()) }, ByteBuf::readByte) val SHORT_CODEC = @@ -59,7 +77,7 @@ object ByteBufCodecs { private val SOUND_SOURCE_BY_ID = ByIdMap.continuous( { it.ordinal }, Sound.Source.entries.toTypedArray(), - ByIdMap.OutOfBoundsStrategy.ZERO + OutOfBoundsStrategy.ZERO ) val SOUND_CODEC = streamCodecComposite( KEY_CODEC, @@ -138,6 +156,85 @@ object ByteBufCodecs { BigDecimal(unscaledValue, scale, MathContext(precision)) } + private val TAG_TYPES = arrayOf( + BinaryTagTypes.END, + BinaryTagTypes.BYTE, + BinaryTagTypes.SHORT, + BinaryTagTypes.INT, + BinaryTagTypes.LONG, + BinaryTagTypes.FLOAT, + BinaryTagTypes.DOUBLE, + BinaryTagTypes.BYTE_ARRAY, + BinaryTagTypes.STRING, + BinaryTagTypes.LIST, + BinaryTagTypes.COMPOUND, + BinaryTagTypes.INT_ARRAY, + BinaryTagTypes.LONG_ARRAY, + ) + + private val BINARY_TAG_BY_ID = ByIdMap.continuous( + { it.id().toInt() }, + TAG_TYPES, + OutOfBoundsStrategy.DECODE_ERROR + ) + + val BINARY_TAG_CODEC: StreamCodec = streamCodec({ buf, tag -> + val type = tag.type() as BinaryTagType + buf.writeByte(type.id().toInt()) + + val tmp = buf.alloc().buffer() + try { + DataOutputStream(FastBufferedOutputStream(ByteBufOutputStream(tmp))).use { out -> + type.write(tag, out) + } + val length = tmp.readableBytes() + buf.writeVarInt(length) + buf.writeBytes(tmp, tmp.readerIndex(), length) + } finally { + tmp.release() + } + + }, { buf -> + val type = BINARY_TAG_BY_ID(buf.readByte().toInt()) + val len = buf.readVarInt() + val slice = buf.readRetainedSlice(len) + + try { + DataInputStream(FastBufferedInputStream(ByteBufInputStream(slice))).use { input -> + type.read(input) + } + } finally { + slice.release() + } + }) + + val BINARY_TAG_CODEC_COMPRESSED: StreamCodec = streamCodec({ buf, tag -> + val type = tag.type() as BinaryTagType + buf.writeByte(type.id().toInt()) + val temp = buf.alloc().buffer() + + try { + DataOutputStream(FastBufferedOutputStream(GZIPOutputStream(ByteBufOutputStream(temp)))).use { + type.write(tag, it) + } + buf.writeVarInt(temp.readableBytes()) + buf.writeBytes(temp, temp.readerIndex(), temp.readableBytes()) + } finally { + temp.release() + } + }, { buf -> + val type = BINARY_TAG_BY_ID(buf.readByte().toInt()) + val length = buf.readVarInt() + val slice = buf.readRetainedSlice(length) + + try { + DataInputStream(FastBufferedInputStream(GZIPInputStream(ByteBufInputStream(slice)))).use { + type.read(it) + } + } finally { + slice.release() + } + }) fun > enumStreamCodec(clazz: Class): StreamCodec = streamCodec(ByteBuf::writeEnum) { it.readEnum(clazz) } @@ -155,4 +252,67 @@ object ByteBufCodecs { buf.writeVarInt(idGetter(value)) } } + + fun readCount(buffer: ByteBuf, maxSize: Int): Int { + val count = buffer.readVarInt() + if (count > maxSize) { + throw DecoderException("$count elements exceeded max size of: $maxSize") + } else { + return count + } + } + + fun writeCount(buffer: ByteBuf, count: Int, maxSize: Int) { + if (count > maxSize) { + throw EncoderException("$count elements exceeded max size of: $maxSize") + } else { + buffer.writeVarInt(count) + } + } + + fun > collection( + factory: (Int) -> C, + codec: StreamCodec, + maxSize: Int = Int.MAX_VALUE + ): StreamCodec = object : StreamCodec { + override fun decode(buf: B): C { + val count = readCount(buf, maxSize) + val collection = factory(min(count, MAX_INITIAL_COLLECTION_SIZE)) + + repeat(count) { + collection.add(codec.decode(buf)) + } + + return collection + } + + override fun encode(buf: B, value: C) { + writeCount(buf, value.size, maxSize) + + for (element in value) { + codec.encode(buf, element) + } + } + } + + fun > collection(factory: (Int) -> C): CodecOperation { + return CodecOperation { size -> collection(factory, size) } + } + + + fun list(): CodecOperation> { + return CodecOperation { size -> collection(::ObjectArrayList, size) } + } + + fun list(maxSize: Int): CodecOperation> { + return CodecOperation { size -> collection(::ObjectArrayList, size, maxSize) } + } + + @Suppress("NOTHING_TO_INLINE") + private inline fun decodeError(message: Any): Nothing = + throw DecoderException(message.toString()) + + @Suppress("NOTHING_TO_INLINE") + private inline fun encodeError(message: Any): Nothing = + throw EncoderException(message.toString()) } \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/packet/RespondingNettyPacket.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/packet/RespondingNettyPacket.kt index 166e414b..c94c045d 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/packet/RespondingNettyPacket.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/packet/RespondingNettyPacket.kt @@ -10,7 +10,9 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull +import java.lang.ref.WeakReference import java.util.* +import kotlin.properties.Delegates import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -36,14 +38,18 @@ abstract class RespondingNettyPacket

: NettyPacket() { * A deferred response object for awaiting the associated response. */ @InternalApi - val response = CompletableDeferred

() + val response by lazy { CompletableDeferred

() } /** * The connection through which the response will be sent. */ @InternalApi - lateinit var responseConnection: Connection + private var responseConnection by Delegates.notNull>() + @InternalApi + fun initResponseConnection(connection: Connection) { + responseConnection = WeakReference(connection) + } /** * Fires the packet and awaits its response within the specified timeout. @@ -109,6 +115,19 @@ abstract class RespondingNettyPacket

: NettyPacket() { fun respond(packet: P) { packet.responseTo = uniqueSessionId ?: error("Responding packet has no session id. Are you sure it was sent?") + + val responseConnection = responseConnection.get() + if (responseConnection == null) { + log.atWarning() + .log("Cannot respond to packet ${this::class.simpleName} with session ID $uniqueSessionId: original connection has been garbage collected") + + if ((::response.getDelegate() as Lazy<*>).isInitialized()) { + response.completeExceptionally(IllegalStateException("Original connection has been garbage collected")) + } + + return + } + responseConnection.send(packet) } diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt index 52bdaaba..8a5ef2fc 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt @@ -4,6 +4,7 @@ import dev.slne.surf.bytebufserializer.Buf import dev.slne.surf.cloud.api.common.netty.network.codec.kotlinx.cloud.CloudPlayerSerializer import dev.slne.surf.cloud.api.common.netty.network.codec.streamCodec import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainer +import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainerView import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag import dev.slne.surf.cloud.api.common.player.teleport.WorldLocation @@ -29,7 +30,7 @@ import kotlin.time.Duration * it enables sending messages or components to the player. */ @Serializable(with = CloudPlayerSerializer::class) -interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but done correctly? +interface CloudPlayer : Audience, OfflineCloudPlayer { val name: String override suspend fun latestIpAddress(): Inet4Address @@ -55,13 +56,19 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but fun isAfk(): Boolean suspend fun currentSessionDuration(): Duration + val persistentData: PersistentPlayerDataContainerView + /** * Performs modifications on the player's persistent data container. * - * @param block A suspending block to modify the persistent data container. + * @param block A block to modify the persistent data container. * @return The result of the block execution. */ - suspend fun withPersistentData(block: PersistentPlayerDataContainer.() -> R): R + fun editPdc(block: PersistentPlayerDataContainer.() -> R): R + + @Deprecated("Use non-suspending editPdc method instead", ReplaceWith("editPdc(block)")) + suspend fun withPersistentData(block: PersistentPlayerDataContainer.() -> R): R = + editPdc(block) /** * Connects the player to a specified server. diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataType.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataType.kt index 9fcd1f5d..811e296e 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataType.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataType.kt @@ -6,7 +6,7 @@ package dev.slne.surf.cloud.api.common.player.ppdc * @param P The primitive type of the list elements. * @param C The complex type of the list elements. */ -interface ListPersistentPlayerDataType

: PersistentPlayerDataType, C> { +interface ListPersistentPlayerDataType

: PersistentPlayerDataType, List> { /** * The data type of the elements in the list. diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataTypeProvider.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataTypeProvider.kt new file mode 100644 index 00000000..856d558e --- /dev/null +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataTypeProvider.kt @@ -0,0 +1,70 @@ +package dev.slne.surf.cloud.api.common.player.ppdc + +import com.google.common.collect.Lists + +class ListPersistentPlayerDataTypeProvider { + companion object { + private val BYTE = ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.BYTE) + private val SHORT = ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.SHORT) + private val INT = ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.INT) + private val LONG = ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.LONG) + private val FLOAT = ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.FLOAT) + private val DOUBLE = + ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.DOUBLE) + private val BOOLEAN = + ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.BOOLEAN) + private val STRING = + ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.STRING) + private val BYTE_ARRAY = + ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.BYTE_ARRAY) + private val INT_ARRAY = + ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.INT_ARRAY) + private val LONG_ARRAY = + ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.LONG_ARRAY) + private val DATA_CONTAINER = + ListPersistentPlayerDataTypeImpl.create(PersistentPlayerDataType.TAG_CONTAINER) + } + + + fun bytes() = BYTE + fun shorts() = SHORT + fun ints() = INT + fun longs() = LONG + fun floats() = FLOAT + fun doubles() = DOUBLE + fun booleans() = BOOLEAN + fun strings() = STRING + fun byteArrays() = BYTE_ARRAY + fun intArrays() = INT_ARRAY + fun longArrays() = LONG_ARRAY + fun dataContainers() = DATA_CONTAINER + fun

listTypeFrom(elementType: PersistentPlayerDataType) = + ListPersistentPlayerDataTypeImpl.create(elementType) + + private class ListPersistentPlayerDataTypeImpl

( + override val elementType: PersistentPlayerDataType + ) : ListPersistentPlayerDataType { + @Suppress("UNCHECKED_CAST") + override val primitiveType = List::class.java as Class> + + override fun fromPrimitive( + primitive: List

, + context: PersistentPlayerDataAdapterContext + ): List { + return Lists.transform(primitive) { elementType.fromPrimitive(it, context) } + } + + override fun toPrimitive( + complex: List, + context: PersistentPlayerDataAdapterContext + ): List

{ + return Lists.transform(complex) { elementType.toPrimitive(it, context) } + } + + companion object { + fun

create(elementType: PersistentPlayerDataType): ListPersistentPlayerDataType { + return ListPersistentPlayerDataTypeImpl(elementType) + } + } + } +} \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainer.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainer.kt index 4c5288e9..69ace49c 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainer.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainer.kt @@ -24,32 +24,17 @@ interface PersistentPlayerDataContainer: PersistentPlayerDataContainerView { fun

set(key: Key, type: PersistentPlayerDataType, value: C) // region Primitive-specific set methods. - fun setBoolean(key: Key, value: Boolean) - fun setByte(key: Key, value: Byte) - fun setShort(key: Key, value: Short) - fun setInt(key: Key, value: Int) - fun setLong(key: Key, value: Long) - fun setFloat(key: Key, value: Float) - fun setDouble(key: Key, value: Double) - fun setString(key: Key, value: String) - fun setByteArray(key: Key, value: ByteArray) - fun setIntArray(key: Key, value: IntArray) - fun setLongArray(key: Key, value: LongArray) - // endregion - - // region Primitive-specific get methods. - fun getBoolean(key: Key): Boolean? - fun getNumber(key: Key): Number? - fun getByte(key: Key): Byte? - fun getShort(key: Key): Short? - fun getInt(key: Key): Int? - fun getLong(key: Key): Long? - fun getFloat(key: Key): Float? - fun getDouble(key: Key): Double? - fun getString(key: Key): String? - fun getByteArray(key: Key): ByteArray? - fun getIntArray(key: Key): IntArray? - fun getLongArray(key: Key): LongArray? + fun setBoolean(key: Key, value: Boolean?) + fun setByte(key: Key, value: Byte?) + fun setShort(key: Key, value: Short?) + fun setInt(key: Key, value: Int?) + fun setLong(key: Key, value: Long?) + fun setFloat(key: Key, value: Float?) + fun setDouble(key: Key, value: Double?) + fun setString(key: Key, value: String?) + fun setByteArray(key: Key, value: ByteArray?) + fun setIntArray(key: Key, value: IntArray?) + fun setLongArray(key: Key, value: LongArray?) // endregion /** @@ -65,4 +50,6 @@ interface PersistentPlayerDataContainer: PersistentPlayerDataContainerView { * @param buf The buffer to read from. */ fun readFromBuf(buf: SurfByteBuf) + + override fun snapshot(): PersistentPlayerDataContainer } \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainerView.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainerView.kt index 5cad2370..6dee1a38 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainerView.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataContainerView.kt @@ -43,6 +43,21 @@ interface PersistentPlayerDataContainerView { */ fun

get(key: Key, type: PersistentPlayerDataType): C? + // region Primitive-specific get methods. + fun getBoolean(key: Key): Boolean? + fun getNumber(key: Key): Number? + fun getByte(key: Key): Byte? + fun getShort(key: Key): Short? + fun getInt(key: Key): Int? + fun getLong(key: Key): Long? + fun getFloat(key: Key): Float? + fun getDouble(key: Key): Double? + fun getString(key: Key): String? + fun getByteArray(key: Key): ByteArray? + fun getIntArray(key: Key): IntArray? + fun getLongArray(key: Key): LongArray? + // endregion + /** * Retrieves all keys present in this container. * @@ -70,4 +85,37 @@ interface PersistentPlayerDataContainerView { * @param buf The buffer to write to. */ fun writeToBuf(buf: SurfByteBuf) + + fun snapshot(): PersistentPlayerDataContainerView + + companion object { + /** + * Maximum nesting depth for compound tags. + * This limit prevents memory exhaustion from extremely large nested structures. + * Set to a reasonable limit that should handle most legitimate use cases while + * protecting against pathological inputs. + */ + const val MAX_NESTING_DEPTH = 512 + + /** + * Ensures that the nesting depth does not exceed the maximum allowed limit. + * + * This function validates that the provided depth is within acceptable bounds, + * preventing memory exhaustion from extremely deep nested structures. + * + * @param depth The current nesting depth to validate. + * @param exceptionFactory A factory function that creates the exception to throw + * when the depth exceeds the limit. Defaults to [IllegalStateException]. + * @throws Throwable When the depth exceeds [MAX_NESTING_DEPTH]. The specific exception + * type is determined by the [exceptionFactory] parameter. + */ + inline fun ensureValidNestingDepth( + depth: Int, + exceptionFactory: (message: String) -> Throwable = ::IllegalStateException + ) { + if (depth > MAX_NESTING_DEPTH) { + throw exceptionFactory("Exceeded maximum allowed nesting depth of $MAX_NESTING_DEPTH. This likely indicates a corrupted or maliciously crafted data structure.") + } + } + } } \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataType.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataType.kt index 027ae4ba..dc4873f3 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataType.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/PersistentPlayerDataType.kt @@ -1,7 +1,5 @@ package dev.slne.surf.cloud.api.common.player.ppdc -import kotlin.reflect.KClass - /** * Represents a data type that can be serialized and deserialized in a persistent player data container. * @@ -12,7 +10,7 @@ interface PersistentPlayerDataType

{ /** * The primitive type used for serialization. */ - val primitiveType: KClass

+ val primitiveType: Class

/** * Converts a primitive value to its complex representation. @@ -31,4 +29,60 @@ interface PersistentPlayerDataType

{ * @return The primitive representation of the complex value. */ fun toPrimitive(complex: C, context: PersistentPlayerDataAdapterContext): P + + companion object { + val BYTE = PrimitivePersistentPlayerDataType() + val SHORT = PrimitivePersistentPlayerDataType() + val INT = PrimitivePersistentPlayerDataType() + val LONG = PrimitivePersistentPlayerDataType() + val FLOAT = PrimitivePersistentPlayerDataType() + val DOUBLE = PrimitivePersistentPlayerDataType() + val BOOLEAN = BooleanPersistentPlayerDataType.create() + val STRING = PrimitivePersistentPlayerDataType() + val BYTE_ARRAY = PrimitivePersistentPlayerDataType() + val INT_ARRAY = PrimitivePersistentPlayerDataType() + val LONG_ARRAY = PrimitivePersistentPlayerDataType() + val TAG_CONTAINER = PrimitivePersistentPlayerDataType() + val LIST by lazy { ListPersistentPlayerDataTypeProvider() } + + + internal class PrimitivePersistentPlayerDataType

(override val primitiveType: Class

) : + PersistentPlayerDataType { + override fun fromPrimitive( + primitive: P, + context: PersistentPlayerDataAdapterContext + ): P = primitive + + override fun toPrimitive( + complex: P, + context: PersistentPlayerDataAdapterContext + ): P = complex + + companion object { + internal inline operator fun invoke(): PersistentPlayerDataType { + return PrimitivePersistentPlayerDataType(P::class.java) + } + } + } + + internal class BooleanPersistentPlayerDataType : + PersistentPlayerDataType { + override val primitiveType: Class = Byte::class.java + + override fun fromPrimitive( + primitive: Byte, + context: PersistentPlayerDataAdapterContext + ): Boolean = primitive != 0.toByte() + + override fun toPrimitive( + complex: Boolean, + context: PersistentPlayerDataAdapterContext + ): Byte = if (complex) 1 else 0 + + companion object { + fun create(): PersistentPlayerDataType = + BooleanPersistentPlayerDataType() + } + } + } } \ No newline at end of file diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/task/PrePlayerJoinTask.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/task/PrePlayerJoinTask.kt index 97e8219f..96ac7915 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/task/PrePlayerJoinTask.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/task/PrePlayerJoinTask.kt @@ -1,25 +1,64 @@ package dev.slne.surf.cloud.api.common.player.task +import dev.slne.surf.cloud.api.common.netty.network.codec.ByteBufCodecs +import dev.slne.surf.cloud.api.common.netty.network.codec.StreamCodec +import dev.slne.surf.cloud.api.common.netty.network.codec.streamCodecUnitSimple import dev.slne.surf.cloud.api.common.player.OfflineCloudPlayer import dev.slne.surf.cloud.api.common.util.annotation.InternalApi +import dev.slne.surf.cloud.api.common.util.int2ObjectMapOf +import io.netty.handler.codec.DecoderException import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable import net.kyori.adventure.text.Component interface PrePlayerJoinTask { suspend fun preJoin(player: OfflineCloudPlayer): Result - @Serializable sealed interface Result { + val type: Type - @Serializable - data object ALLOWED: Result + data object ALLOWED : Result { + val TYPE = Type(0) + val STREAM_CODEC = streamCodecUnitSimple(this) + override val type = TYPE + } - @Serializable - data class DENIED(val reason: @Contextual Component): Result + data class DENIED(val reason: @Contextual Component) : Result { + override val type = TYPE - @Serializable - data object ERROR: Result + companion object { + val TYPE = Type(1) + val STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.COMPONENT_CODEC, + DENIED::reason, + ::DENIED + ) + } + } + + data object ERROR : Result { + val TYPE = Type(2) + val STREAM_CODEC = streamCodecUnitSimple(this) + override val type = TYPE + } + + @JvmInline + value class Type(val id: Int) + + companion object { + private val TYPES = int2ObjectMapOf( + ALLOWED.TYPE.id to ALLOWED.STREAM_CODEC, + DENIED.TYPE.id to DENIED.STREAM_CODEC, + ERROR.TYPE.id to ERROR.STREAM_CODEC + ) + + val STREAM_CODEC = ByteBufCodecs.VAR_INT_CODEC.dispatch( + { it.type.id }, + { + TYPES.get(it) + ?: throw DecoderException("Unknown PrePlayerJoinTask.Result type id: $it") + } + ) + } } @InternalApi diff --git a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/ByIdMap.kt b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/ByIdMap.kt index 549d5cd2..547e12cd 100644 --- a/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/ByIdMap.kt +++ b/surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/util/ByIdMap.kt @@ -1,5 +1,7 @@ package dev.slne.surf.cloud.api.common.util +import dev.slne.surf.cloud.api.common.util.ByIdMap.OutOfBoundsStrategy.* +import io.netty.handler.codec.DecoderException import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import org.spongepowered.math.GenericMath @@ -58,36 +60,47 @@ object ByIdMap { keyExtractor: (T) -> Int, values: Array, outOfBoundsStrategy: OutOfBoundsStrategy - ): (Int) -> T { + ): (id: Int) -> T { val objects = createSortedArray(keyExtractor, values) val size = objects.size return when (outOfBoundsStrategy) { - OutOfBoundsStrategy.ZERO -> { + ZERO -> { val first = objects[0] { key -> if (key >= 0 && key < size) objects[key] else first } } - OutOfBoundsStrategy.WRAP -> { key -> + WRAP -> { key -> objects[Math.floorMod( key, size )] } - OutOfBoundsStrategy.CLAMP -> { key -> + CLAMP -> { key -> objects[GenericMath.clamp( key, 0, size - 1 )] } + + DECODE_ERROR -> { key -> + if (key >= 0 && key < size) objects[key] else throw DecoderException("Invalid key index: $key") + } + + is ERROR -> { key -> + if (key in 0.. Throwable) : OutOfBoundsStrategy() } } diff --git a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/netty/network/ClientRunningPacketListenerImpl.kt b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/netty/network/ClientRunningPacketListenerImpl.kt index 5cc9e776..c75dde7c 100644 --- a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/netty/network/ClientRunningPacketListenerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/netty/network/ClientRunningPacketListenerImpl.kt @@ -46,14 +46,16 @@ class ClientRunningPacketListenerImpl( private val log = logger() override suspend fun handlePlayerConnectedToServer(packet: PlayerConnectedToServerPacket) { - playerManagerImpl.updateOrCreatePlayer( + commonPlayerManagerImpl.updateOrCreatePlayer( packet.uuid, packet.name, packet.proxy, packet.playerIp, packet.serverName, false - ) + ) { + overwritePpdc(packet.pdc) + } } override suspend fun handlePlayerDisconnectFromServer(packet: PlayerDisconnectFromServerPacket) { @@ -209,7 +211,9 @@ class ClientRunningPacketListenerImpl( } override suspend fun handleAddPlayerToServer(packet: ClientboundAddPlayerToServerPacket) { - (serverManagerImpl.retrieveServerByName(packet.serverName)?.users as? UserListImpl)?.add(packet.playerUuid) + (serverManagerImpl.retrieveServerByName(packet.serverName)?.users as? UserListImpl)?.add( + packet.playerUuid + ) } override suspend fun handleRemovePlayerFromServer(packet: ClientboundRemovePlayerFromServerPacket) { @@ -371,6 +375,11 @@ class ClientRunningPacketListenerImpl( platformExtension.sendToast(packet.uuid, packet.toast) } + override fun handleUpdatePlayerPersistentDataContainer(packet: UpdatePlayerPersistentDataContainerPacket) { + val player = commonPlayerManagerImpl.getPlayer(packet.uuid) ?: return + player.applyPpdcPatch(packet.patch) + } + override fun handlePacket(packet: NettyPacket) { val listeners = NettyListenerRegistry.getListeners(packet.javaClass) ?: return if (listeners.isEmpty()) return diff --git a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt index 4aca20ea..428e2c8e 100644 --- a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt @@ -23,15 +23,15 @@ import dev.slne.surf.cloud.core.common.netty.network.protocol.running.* import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerDataPacket.DataRequestType import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerDataResponse.* import dev.slne.surf.cloud.core.common.player.CommonCloudPlayerImpl -import dev.slne.surf.cloud.core.common.player.ppdc.PersistentPlayerDataContainerImpl +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcPatch import dev.slne.surf.cloud.core.common.util.hasPermissionPlattform import dev.slne.surf.surfapi.core.api.messages.adventure.getPointer -import dev.slne.surf.surfapi.core.api.nbt.fast import net.kyori.adventure.audience.Audience import net.kyori.adventure.audience.MessageType import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.identity.Identity import net.kyori.adventure.inventory.Book +import net.kyori.adventure.nbt.CompoundBinaryTag import net.kyori.adventure.resource.ResourcePackRequest import net.kyori.adventure.sound.Sound import net.kyori.adventure.sound.Sound.Emitter @@ -46,6 +46,7 @@ import java.net.Inet4Address import java.time.ZonedDateTime import java.util.* import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.write import kotlin.time.Duration import kotlin.time.Duration.Companion.days import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerDataResponse.NameHistory as NameHistoryResponse @@ -65,6 +66,7 @@ abstract class ClientCloudPlayerImpl( override val connectedToProxy get() = proxyServerName != null override val connectedToServer get() = serverName != null + /** * The audience for this player. If the player is on this server, this will point to * the bukkit / velocity player. Otherwise packets will be sent to the player via the network. @@ -73,6 +75,27 @@ abstract class ClientCloudPlayerImpl( protected abstract val platformClass: Class + fun applyPpdcPatch(patch: PdcPatch) { + ppdcReentrantLock.write { + ppdc.applyOps(ppdc.tag, patch) + } + } + + fun overwritePpdc(tag: CompoundBinaryTag) { + ppdcReentrantLock.write { + ppdc.fromTagCompound(tag) + } + } + + override fun editPdc(block: PersistentPlayerDataContainer.() -> R): R { + val (result, patch) = editPdc0(true, block) + if (!patch.empty) { + UpdatePlayerPersistentDataContainerPacket(uuid, patch).fireAndForget() + } + + return result + } + override suspend fun latestIpAddress(): Inet4Address { return request(DataRequestType.LATEST_IP_ADDRESS).ip ?: error("Failed to get IP address") @@ -121,22 +144,6 @@ abstract class ClientCloudPlayerImpl( ) == true } - override suspend fun withPersistentData(block: PersistentPlayerDataContainer.() -> R): R { - val response = ServerboundRequestPlayerPersistentDataContainer(uuid).fireAndAwaitOrThrow() - - val nbt = response.nbt - val container = PersistentPlayerDataContainerImpl(nbt.fast(synchronize = true)) - val result = container.block() - - ServerboundPlayerPersistentDataContainerUpdatePacket( - uuid, - response.verificationId, - container.toTagCompound() - ).fireAndForget() - - return result - } - override suspend fun displayName(): Component { val localName = audience?.getPointer(Identity.DISPLAY_NAME) if (localName != null) { diff --git a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/CommonClientCloudPlayerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/CommonClientCloudPlayerManagerImpl.kt index 5798b8cd..5278c811 100644 --- a/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/CommonClientCloudPlayerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/CommonClientCloudPlayerManagerImpl.kt @@ -1,13 +1,22 @@ package dev.slne.surf.cloud.core.client.player +import dev.slne.surf.cloud.api.client.netty.packet.fireAndAwaitOrThrow +import dev.slne.surf.cloud.api.common.player.task.PrePlayerJoinTask +import dev.slne.surf.cloud.core.common.messages.MessageManager +import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerPersistentDataContainer import dev.slne.surf.cloud.core.common.player.CloudPlayerManagerImpl import dev.slne.surf.cloud.core.common.player.CommonOfflineCloudPlayerImpl import dev.slne.surf.cloud.core.common.player.playerManagerImpl +import dev.slne.surf.surfapi.core.api.nbt.fast +import dev.slne.surf.surfapi.core.api.util.logger import net.kyori.adventure.audience.Audience import java.util.* +import java.util.concurrent.TimeUnit abstract class CommonClientCloudPlayerManagerImpl> : CloudPlayerManagerImpl

() { + private val log = logger() + override suspend fun updateProxyServer( player: P, serverName: String @@ -44,10 +53,32 @@ abstract class CommonClientCloudPlayerManagerImpl listener.handlePlayerPersistentDataContainerUpdate( - msg - ) - is ServerboundConnectPlayerToServerPacket -> listener.handleConnectPlayerToServer( msg ) @@ -449,6 +445,9 @@ class ConnectionImpl( ) is SendToastPacket -> listener.handleSendToast(msg) + is UpdatePlayerPersistentDataContainerPacket -> listener.handleUpdatePlayerPersistentDataContainer( + msg + ) else -> listener.handlePacket(msg) // handle other packets } @@ -629,6 +628,9 @@ class ConnectionImpl( is ClientboundCacheDeltaPacket -> listener.handleCacheDelta(msg) is ClientboundCacheErrorPacket -> listener.handleCacheError(msg) is SendToastPacket -> listener.handleSendToast(msg) + is UpdatePlayerPersistentDataContainerPacket -> listener.handleUpdatePlayerPersistentDataContainer( + msg + ) else -> listener.handlePacket(msg) } diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/RespondingPacketSendHandler.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/RespondingPacketSendHandler.kt index 45464596..dd6faa8e 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/RespondingPacketSendHandler.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/RespondingPacketSendHandler.kt @@ -32,7 +32,7 @@ class RespondingPacketSendHandler : UnifiedReadOnlyChannelHandler() } if (msg is RespondingNettyPacket<*>) { - msg.responseConnection = ctx.channel().attr(ConnectionImpl.CHANNEL_ATTRIBUTE_KEY).get() + msg.initResponseConnection(ctx.channel().attr(ConnectionImpl.CHANNEL_ATTRIBUTE_KEY).get()) } if (msg is ResponseNettyPacket) { diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/handshake/ServerboundHandshakePacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/handshake/ServerboundHandshakePacket.kt index fe02e45b..e5490211 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/handshake/ServerboundHandshakePacket.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/handshake/ServerboundHandshakePacket.kt @@ -8,7 +8,7 @@ import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket import dev.slne.surf.cloud.api.common.netty.packet.packetCodec import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf -const val PROTOCOL_VERSION = 1 +const val PROTOCOL_VERSION = 2 /** * First packet sent by the client to the cloud server. It is used to establish a connection to the cloud server. diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ClientboundRunPrePlayerJoinTasksPacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ClientboundRunPrePlayerJoinTasksPacket.kt index ded22a3a..a7172290 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ClientboundRunPrePlayerJoinTasksPacket.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ClientboundRunPrePlayerJoinTasksPacket.kt @@ -1,6 +1,7 @@ package dev.slne.surf.cloud.core.common.netty.network.protocol.running import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket +import dev.slne.surf.cloud.api.common.netty.network.codec.StreamCodec import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow import dev.slne.surf.cloud.api.common.netty.packet.RespondingNettyPacket import dev.slne.surf.cloud.api.common.netty.packet.ResponseNettyPacket @@ -15,5 +16,13 @@ class ClientboundRunPrePlayerJoinTasksPacket(val uuid: @Contextual UUID) : RespondingNettyPacket() @SurfNettyPacket("cloud:response:run_pre_player_join_tasks", PacketFlow.SERVERBOUND) -@Serializable -class RunPrePlayerJoinTasksResultPacket(val result: PrePlayerJoinTask.Result) : ResponseNettyPacket() \ No newline at end of file +class RunPrePlayerJoinTasksResultPacket(val result: PrePlayerJoinTask.Result) : + ResponseNettyPacket() { + companion object { + val STREAM_CODEC = StreamCodec.composite( + PrePlayerJoinTask.Result.STREAM_CODEC, + RunPrePlayerJoinTasksResultPacket::result, + ::RunPrePlayerJoinTasksResultPacket + ) + } +} \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/PlayerConnectToServerPacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/PlayerConnectToServerPacket.kt index a145d09b..6d610f6f 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/PlayerConnectToServerPacket.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/PlayerConnectToServerPacket.kt @@ -2,13 +2,15 @@ package dev.slne.surf.cloud.core.common.netty.network.protocol.running import dev.slne.surf.cloud.api.common.meta.DefaultIds import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket +import dev.slne.surf.cloud.api.common.netty.network.codec.ByteBufCodecs +import dev.slne.surf.cloud.api.common.netty.network.codec.StreamCodec +import dev.slne.surf.cloud.api.common.netty.network.codec.composite import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket import dev.slne.surf.cloud.api.common.netty.packet.RespondingNettyPacket import dev.slne.surf.cloud.api.common.netty.packet.ResponseNettyPacket import dev.slne.surf.cloud.api.common.player.task.PrePlayerJoinTask -import kotlinx.serialization.Contextual -import kotlinx.serialization.Serializable +import net.kyori.adventure.nbt.CompoundBinaryTag import java.net.Inet4Address import java.util.* @@ -23,25 +25,66 @@ import java.util.* * @param proxy If the server is a proxy */ @SurfNettyPacket(DefaultIds.PLAYER_CONNECT_TO_SERVER_PACKET, PacketFlow.SERVERBOUND) -@Serializable data class PlayerConnectToServerPacket( - val uuid: @Contextual UUID, + val uuid: UUID, val name: String, val serverName: String, val proxy: Boolean, - val playerIp: @Contextual Inet4Address, -) : RespondingNettyPacket() + val playerIp: Inet4Address +) : RespondingNettyPacket() { + companion object { + val STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.UUID_CODEC, + PlayerConnectToServerPacket::uuid, + ByteBufCodecs.STRING_CODEC, + PlayerConnectToServerPacket::name, + ByteBufCodecs.STRING_CODEC, + PlayerConnectToServerPacket::serverName, + ByteBufCodecs.BOOLEAN_CODEC, + PlayerConnectToServerPacket::proxy, + ByteBufCodecs.INET_4_ADDRESS_CODEC, + PlayerConnectToServerPacket::playerIp, + ::PlayerConnectToServerPacket + ) + } +} @SurfNettyPacket(DefaultIds.PLAYER_CONNECTED_TO_SERVER_PACKET, PacketFlow.CLIENTBOUND) -@Serializable data class PlayerConnectedToServerPacket( - val uuid: @Contextual UUID, + val uuid: UUID, val name: String, val serverName: String, val proxy: Boolean, - val playerIp: @Contextual Inet4Address, -) : NettyPacket() + val playerIp: Inet4Address, + val pdc: CompoundBinaryTag +) : NettyPacket() { + companion object { + val STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.UUID_CODEC, + PlayerConnectedToServerPacket::uuid, + ByteBufCodecs.STRING_CODEC, + PlayerConnectedToServerPacket::name, + ByteBufCodecs.STRING_CODEC, + PlayerConnectedToServerPacket::serverName, + ByteBufCodecs.BOOLEAN_CODEC, + PlayerConnectedToServerPacket::proxy, + ByteBufCodecs.INET_4_ADDRESS_CODEC, + PlayerConnectedToServerPacket::playerIp, + ByteBufCodecs.COMPOUND_TAG_CODEC, + PlayerConnectedToServerPacket::pdc, + ::PlayerConnectedToServerPacket + ) + } +} @SurfNettyPacket("cloud:response:connect_to_server", PacketFlow.BIDIRECTIONAL) -@Serializable -class PlayerConnectToServerResponsePacket(val result: PrePlayerJoinTask.Result) : ResponseNettyPacket() \ No newline at end of file +class PlayerConnectToServerResponsePacket(val result: PrePlayerJoinTask.Result) : + ResponseNettyPacket() { + companion object { + val STREAM_CODEC = StreamCodec.composite( + PrePlayerJoinTask.Result.STREAM_CODEC, + PlayerConnectToServerResponsePacket::result, + ::PlayerConnectToServerResponsePacket + ) + } +} \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt index 02c277ab..68bc2c34 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningClientPacketListener.kt @@ -97,5 +97,7 @@ interface RunningClientPacketListener : ClientCommonPacketListener { fun handleSendToast(packet: SendToastPacket) + fun handleUpdatePlayerPersistentDataContainer(packet: UpdatePlayerPersistentDataContainerPacket) + fun handlePacket(packet: NettyPacket) } \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt index c4e50eb4..b60d7248 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt @@ -31,8 +31,8 @@ object RunningProtocols { .addPacket(ClientboundSendResourcePacksPacket.STREAM_CODEC) .addPacket(ClientboundRemoveResourcePacksPacket.STREAM_CODEC) .addPacket(ClientboundClearResourcePacksPacket.STREAM_CODEC) - .addPacket(PlayerConnectedToServerPacket::class.createCodec()) - .addPacket(PlayerConnectToServerResponsePacket::class.createCodec()) + .addPacket(PlayerConnectedToServerPacket.STREAM_CODEC) + .addPacket(PlayerConnectToServerResponsePacket.STREAM_CODEC) .addPacket(PlayerDisconnectFromServerPacket.STREAM_CODEC) .addPacket(ClientboundRequestDisplayNamePacket.STREAM_CODEC) .addPacket(ResponseDisplayNamePacketRequestPacket.STREAM_CODEC) @@ -72,6 +72,7 @@ object RunningProtocols { .addPacket(WhitelistStatusResponsePacket::class.createCodec()) .addPacket(WhitelistResponsePacket::class.createCodec()) .addPacket(SendToastPacket.STREAM_CODEC) + .addPacket(UpdatePlayerPersistentDataContainerPacket.STREAM_CODEC) } val CLIENTBOUND by lazy { CLIENTBOUND_TEMPLATE.freeze().bind(::SurfByteBuf) } @@ -101,15 +102,14 @@ object RunningProtocols { .addPacket(ServerboundBroadcastPacket.STREAM_CODEC) .addPacket(ServerboundClientInformationPacket.STREAM_CODEC) .addPacket(ServerboundPingRequestPacket.STREAM_CODEC) - .addPacket(PlayerConnectToServerPacket::class.createCodec()) - .addPacket(PlayerConnectToServerResponsePacket::class.createCodec()) + .addPacket(PlayerConnectToServerPacket.STREAM_CODEC) + .addPacket(PlayerConnectToServerResponsePacket.STREAM_CODEC) .addPacket(PlayerDisconnectFromServerPacket.STREAM_CODEC) .addPacket(ServerboundRequestDisplayNamePacket.STREAM_CODEC) .addPacket(ResponseDisplayNamePacketRequestPacket.STREAM_CODEC) .addPacket(RequestLuckpermsMetaDataPacket.STREAM_CODEC) .addPacket(LuckpermsMetaDataResponsePacket.STREAM_CODEC) .addPacket(ServerboundRequestPlayerPersistentDataContainer.STREAM_CODEC) - .addPacket(ServerboundPlayerPersistentDataContainerUpdatePacket.STREAM_CODEC) .addPacket(ServerboundConnectPlayerToServerPacket::class.createCodec()) .addPacket(DisconnectPlayerPacket.STREAM_CODEC) .addPacket(TeleportPlayerPacket.STREAM_CODEC) @@ -121,7 +121,7 @@ object RunningProtocols { .addPacket(ServerboundPullPlayersToGroupPacket::class.createCodec()) .addPacket(SilentDisconnectPlayerPacket::class.createCodec()) .addPacket(TeleportPlayerToPlayerPacket::class.createCodec()) - .addPacket(RunPrePlayerJoinTasksResultPacket::class.createCodec()) + .addPacket(RunPrePlayerJoinTasksResultPacket.STREAM_CODEC) .addPacket(ServerboundGeneratePunishmentIdPacket::class.createCodec()) .addPacket(ServerboundCreateKickPacket::class.createCodec()) .addPacket(ServerboundCreateWarnPacket::class.createCodec()) @@ -150,6 +150,7 @@ object RunningProtocols { .addPacket(ServerboundUpdateWhitelistPacket::class.createCodec()) .addPacket(ServerboundRefreshWhitelistPacket.STREAM_CODEC) .addPacket(SendToastPacket.STREAM_CODEC) + .addPacket(UpdatePlayerPersistentDataContainerPacket.STREAM_CODEC) } val SERVERBOUND by lazy { SERVERBOUND_TEMPLATE.freeze().bind(::SurfByteBuf) } diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt index aad47510..35ca1080 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt @@ -53,8 +53,6 @@ interface RunningServerPacketListener : ServerCommonPacketListener, TickablePack suspend fun handleRequestPlayerPersistentDataContainer(packet: ServerboundRequestPlayerPersistentDataContainer) - suspend fun handlePlayerPersistentDataContainerUpdate(packet: ServerboundPlayerPersistentDataContainerUpdatePacket) - suspend fun handleConnectPlayerToServer(packet: ServerboundConnectPlayerToServerPacket) suspend fun handleQueuePlayerToGroup(packet: ServerboundQueuePlayerToGroupPacket) @@ -128,5 +126,7 @@ interface RunningServerPacketListener : ServerCommonPacketListener, TickablePack fun handleSendToast(packet: SendToastPacket) + fun handleUpdatePlayerPersistentDataContainer(packet: UpdatePlayerPersistentDataContainerPacket) + fun handlePacket(packet: NettyPacket) } \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundPlayerPersistentDataContainerUpdatePacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundPlayerPersistentDataContainerUpdatePacket.kt deleted file mode 100644 index a4c384c4..00000000 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundPlayerPersistentDataContainerUpdatePacket.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.slne.surf.cloud.core.common.netty.network.protocol.running - -import dev.slne.surf.cloud.api.common.meta.DefaultIds -import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket -import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow -import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket -import dev.slne.surf.cloud.api.common.netty.packet.packetCodec -import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf -import net.kyori.adventure.nbt.CompoundBinaryTag -import java.util.* - -@SurfNettyPacket( - DefaultIds.SERVERBOUND_PLAYER_PERSISTENT_DATA_CONTAINER_UPDATE, - PacketFlow.SERVERBOUND -) -class ServerboundPlayerPersistentDataContainerUpdatePacket : NettyPacket { - companion object { - val STREAM_CODEC = packetCodec( - ServerboundPlayerPersistentDataContainerUpdatePacket::write, - ::ServerboundPlayerPersistentDataContainerUpdatePacket - ) - } - - val uuid: UUID - val verificationId: Int - val nbt: CompoundBinaryTag - - constructor(uuid: UUID, verificationId: Int, nbt: CompoundBinaryTag) { - this.uuid = uuid - this.verificationId = verificationId - this.nbt = nbt - } - - private constructor(buf: SurfByteBuf) { - uuid = buf.readUuid() - verificationId = buf.readInt() - nbt = buf.readCompoundTag() - } - - private fun write(buf: SurfByteBuf) { - buf.writeUuid(uuid) - buf.writeInt(verificationId) - buf.writeCompoundTag(nbt) - } -} \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/UpdatePlayerPersistentDataContainerPacket.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/UpdatePlayerPersistentDataContainerPacket.kt new file mode 100644 index 00000000..7ea5d7bb --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/UpdatePlayerPersistentDataContainerPacket.kt @@ -0,0 +1,25 @@ +package dev.slne.surf.cloud.core.common.netty.network.protocol.running + +import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket +import dev.slne.surf.cloud.api.common.netty.network.codec.ByteBufCodecs +import dev.slne.surf.cloud.api.common.netty.network.codec.StreamCodec +import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow +import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcPatch +import java.util.* + +@SurfNettyPacket("cloud:bidirectional:player_pdc/update", PacketFlow.BIDIRECTIONAL) +class UpdatePlayerPersistentDataContainerPacket( + val uuid: UUID, + val patch: PdcPatch +) : NettyPacket() { + companion object { + val STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.UUID_CODEC, + UpdatePlayerPersistentDataContainerPacket::uuid, + PdcPatch.STREAM_CODEC, + UpdatePlayerPersistentDataContainerPacket::patch, + ::UpdatePlayerPersistentDataContainerPacket + ) + } +} \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt index dc22a98e..ba335585 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt @@ -77,8 +77,9 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa proxy: Boolean, ip: Inet4Address, serverName: String, - runPreJoinTasks: Boolean - ): PrePlayerJoinTask.Result { + runPreJoinTasks: Boolean, + extraInit: P.() -> Unit = {} + ): PlayerUpdateOrCreateResult { val existing = playerCache.getOrNull(uuid) if (existing != null) { if (proxy) { @@ -87,12 +88,18 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa updateServer(existing, serverName) } onServerConnect(uuid, existing, serverName) - return PrePlayerJoinTask.Result.ALLOWED + return PlayerUpdateOrCreateResult( + player = existing, + preJoinResult = PrePlayerJoinTask.Result.ALLOWED, + created = false, + preJoinAllowed = true + ) } return try { - playerCache.get(uuid) { + val player = playerCache.get(uuid) { val newPlayer = createPlayer(uuid, name, proxy, ip, serverName) + newPlayer.extraInit() if (runPreJoinTasks) { val pre = preJoin(newPlayer) @@ -102,9 +109,19 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa onServerConnect(uuid, newPlayer, serverName) newPlayer } - PrePlayerJoinTask.Result.ALLOWED + PlayerUpdateOrCreateResult( + player = player, + preJoinResult = PrePlayerJoinTask.Result.ALLOWED, + created = true, + preJoinAllowed = true + ) } catch (e: PreJoinDenied) { - e.result + PlayerUpdateOrCreateResult( + player = null, + preJoinResult = e.result, + created = true, + preJoinAllowed = false + ) } @@ -192,6 +209,13 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa // return PrePlayerJoinTask.Result.ALLOWED } + inner class PlayerUpdateOrCreateResult( + val player: P?, + val preJoinResult: PrePlayerJoinTask.Result, + val created: Boolean, + val preJoinAllowed: Boolean = preJoinResult is PrePlayerJoinTask.Result.ALLOWED + ) + // private suspend fun getOrCreatePlayerAtomically( // uuid: UUID, // name: String, @@ -276,7 +300,12 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa } @MustBeInvokedByOverriders - open suspend fun onNetworkDisconnect(uuid: UUID, player: P, oldProxy: String?, oldServer: String?) { + open suspend fun onNetworkDisconnect( + uuid: UUID, + player: P, + oldProxy: String?, + oldServer: String? + ) { try { CloudPlayerDisconnectFromNetworkEvent(this, player).post() } catch (e: Throwable) { @@ -297,10 +326,11 @@ abstract class CloudPlayerManagerImpl

: CloudPlayerMa } } - open fun terminate() {} + open suspend fun terminate() { + } - private class PreJoinDenied(val result: PrePlayerJoinTask.Result) : RuntimeException() { + class PreJoinDenied(val result: PrePlayerJoinTask.Result) : RuntimeException() { companion object { @Serial private const val serialVersionUID: Long = -5043277924406776272L diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt index 9a4f38e8..1a2d531f 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CommonCloudPlayerImpl.kt @@ -2,14 +2,56 @@ package dev.slne.surf.cloud.core.common.player import dev.slne.surf.cloud.api.common.player.CloudPlayer import dev.slne.surf.cloud.api.common.player.ConnectionResultEnum +import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainer import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.CloudServerManager +import dev.slne.surf.cloud.core.common.player.ppdc.PersistentPlayerDataContainerViewImpl +import dev.slne.surf.cloud.core.common.player.ppdc.TrackingPlayerPersistentDataContainerImpl +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcPatch import java.time.ZonedDateTime import java.util.* +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write abstract class CommonCloudPlayerImpl(uuid: UUID, override val name: String) : CommonOfflineCloudPlayerImpl(uuid), CloudPlayer { + protected val ppdc = TrackingPlayerPersistentDataContainerImpl() + protected val ppdcReentrantLock = ReentrantReadWriteLock(true) + protected val persistentDataView = object : PersistentPlayerDataContainerViewImpl() { + override fun toTagCompound() = ppdcReentrantLock.read { ppdc.tag } + override fun getTag(key: String) = ppdcReentrantLock.read { ppdc.getTag(key) } + override fun snapshotTag() = ppdcReentrantLock.read { super.snapshotTag() } + } + + override val persistentData get() = persistentDataView + + private inline fun runWithTracking( + target: TrackingPlayerPersistentDataContainerImpl, + block: TrackingPlayerPersistentDataContainerImpl.() -> R + ): Pair = try { + target.startTracking() + val result = target.block() + val patch = target.getPatchOps() + result to patch + } finally { + target.clearTracking() + } + + fun editPdc0( + snapshot: Boolean, + block: PersistentPlayerDataContainer.() -> R + ): Pair = if (snapshot) { + val snapshotPpdc = ppdcReentrantLock.read { ppdc.snapshot() } + runWithTracking(snapshotPpdc, block) + } else { + ppdcReentrantLock.write { + runWithTracking(ppdc, block) + } + } + + override suspend fun connectToServer( group: String, server: String diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerImpl.kt index 0c4bca76..e4b5b3f7 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerImpl.kt @@ -3,15 +3,21 @@ package dev.slne.surf.cloud.core.common.player.ppdc import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainer import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataType +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcOp +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcPatch import dev.slne.surf.surfapi.core.api.nbt.FastCompoundBinaryTag import dev.slne.surf.surfapi.core.api.nbt.fast import net.kyori.adventure.key.Key -import net.kyori.adventure.nbt.* +import net.kyori.adventure.nbt.BinaryTag +import net.kyori.adventure.nbt.CompoundBinaryTag +import java.util.* -class PersistentPlayerDataContainerImpl( - @Volatile - private var tag: FastCompoundBinaryTag = CompoundBinaryTag.empty().fast(synchronize = true) +open class PersistentPlayerDataContainerImpl( + tag: FastCompoundBinaryTag = CompoundBinaryTag.empty().fast() ) : PersistentPlayerDataContainerViewImpl(), PersistentPlayerDataContainer { + var tag: FastCompoundBinaryTag = tag + private set + override fun getTag(key: String) = tag.get(key) private inline fun getTag(key: Key): T? { @@ -36,114 +42,200 @@ class PersistentPlayerDataContainerImpl( tag.put(key, value) } - override fun setBoolean(key: Key, value: Boolean) { - tag.putBoolean(key.asString(), value) - } - - override fun setByte(key: Key, value: Byte) { - tag.putByte(key.asString(), value) - } - - override fun setShort(key: Key, value: Short) { - tag.putShort(key.asString(), value) - } - - override fun setInt(key: Key, value: Int) { - tag.putInt(key.asString(), value) + override fun setBoolean(key: Key, value: Boolean?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putBoolean(key.asString(), value) + } } - override fun setLong(key: Key, value: Long) { - tag.putLong(key.asString(), value) + override fun setByte(key: Key, value: Byte?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putByte(key.asString(), value) + } } - override fun setFloat(key: Key, value: Float) { - tag.putFloat(key.asString(), value) + override fun setShort(key: Key, value: Short?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putShort(key.asString(), value) + } } - override fun setDouble(key: Key, value: Double) { - tag.putDouble(key.asString(), value) + override fun setInt(key: Key, value: Int?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putInt(key.asString(), value) + } } - override fun setString(key: Key, value: String) { - tag.putString(key.asString(), value) + override fun setLong(key: Key, value: Long?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putLong(key.asString(), value) + } } - override fun setByteArray(key: Key, value: ByteArray) { - tag.putByteArray(key.asString(), value) + override fun setFloat(key: Key, value: Float?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putFloat(key.asString(), value) + } } - override fun setIntArray(key: Key, value: IntArray) { - tag.putIntArray(key.asString(), value) + override fun setDouble(key: Key, value: Double?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putDouble(key.asString(), value) + } } - override fun setLongArray(key: Key, value: LongArray) { - tag.putLongArray(key.asString(), value) + override fun setString(key: Key, value: String?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putString(key.asString(), value) + } } - override fun getBoolean(key: Key): Boolean? { - val tag = getTag(key) ?: return null - return tag.value() != 0.toByte() + override fun setByteArray(key: Key, value: ByteArray?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putByteArray(key.asString(), value) + } } - override fun getNumber(key: Key): Number? { - return getTag(key)?.numberValue() + override fun setIntArray(key: Key, value: IntArray?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putIntArray(key.asString(), value) + } } - override fun getByte(key: Key): Byte? { - return getTag(key)?.value() + override fun setLongArray(key: Key, value: LongArray?) { + if (value == null) { + tag.remove(key.asString()) + } else { + tag.putLongArray(key.asString(), value) + } } - override fun getShort(key: Key): Short? { - return getTag(key)?.value() - } - - override fun getInt(key: Key): Int? { - return getTag(key)?.value() + override fun remove(key: Key) { + tag.remove(key.asString()) } - override fun getLong(key: Key): Long? { - return getTag(key)?.value() - } + override val empty: Boolean + get() = tag.size() == 0 - override fun getFloat(key: Key): Float? { - return getTag(key)?.value() - } + override fun toTagCompound(): CompoundBinaryTag = tag.fast() - override fun getDouble(key: Key): Double? { - return getTag(key)?.value() + override fun readFromBuf(buf: SurfByteBuf) { + tag = buf.readCompoundTag().fast() } - override fun getString(key: Key): String? { - return getTag(key)?.value() + override fun snapshot(): PersistentPlayerDataContainerImpl { + return PersistentPlayerDataContainerImpl(snapshotTag().fast()) } - override fun getByteArray(key: Key): ByteArray? { - return getTag(key)?.value() + fun fromTagCompound(tag: CompoundBinaryTag) { + this.tag = tag.fast() } - override fun getIntArray(key: Key): IntArray? { - return getTag(key)?.value() + fun applyOps(root: FastCompoundBinaryTag, patch: PdcPatch) { + for (op in patch.ops) { + when (op) { + is PdcOp.Remove -> removeAtPath(root, op.path) + is PdcOp.Put -> putAtPath(root, op.path, op.value) + } + } } - override fun getLongArray(key: Key): LongArray? { - return getTag(key)?.value() + private fun putAtPath(root: FastCompoundBinaryTag, path: List, value: BinaryTag) { + if (path.isEmpty()) { + require(value is CompoundBinaryTag) { "root replace expects CompoundBinaryTag, but was ${value::class.simpleName}" } + root.clear() + root.put(value) + } else { + val parent = traverseCompoundPath(root, path.dropLast(1), createIfMissing = true)!! + val key = path.last() + + if (value is CompoundBinaryTag && value.isEmpty) { + parent.remove(key) + pruneEmptyAncestors(root, path.dropLast(1)) + } else { + parent.put(key, value) + } + } } - override fun remove(key: Key) { - tag.remove(key.asString()) + private fun removeAtPath(root: FastCompoundBinaryTag, path: List) { + if (path.isEmpty()) { + root.clear() + } else { + val parent = + traverseCompoundPath(root, path.dropLast(1), createIfMissing = false) ?: return + parent.remove(path.last()) + pruneEmptyAncestors(root, path.dropLast(1)) + } } - override val empty: Boolean - get() = tag.size() == 0 - - override fun toTagCompound(): CompoundBinaryTag = tag.fast() - - override fun readFromBuf(buf: SurfByteBuf) { - tag = buf.readCompoundTag().fast(synchronize = true) + private fun traverseCompoundPath( + root: FastCompoundBinaryTag, + path: List, + createIfMissing: Boolean = true + ): FastCompoundBinaryTag? { + var current = root + for (segment in path) { + val existing = current.getCompound(segment, null) + val child = when { + existing != null -> existing as? FastCompoundBinaryTag ?: existing.fast() + createIfMissing -> CompoundBinaryTag.empty().fast() + else -> return null + } + + current.put(segment, child) + current = child + } + return current } - fun fromTagCompound(tag: CompoundBinaryTag) { - this.tag = tag.fast(synchronize = true) + private fun pruneEmptyAncestors( + root: FastCompoundBinaryTag, + pathToDeepestParent: List + ) { + if (pathToDeepestParent.isEmpty()) return + + val stack = ArrayDeque>() + var current: FastCompoundBinaryTag = root + + for (segment in pathToDeepestParent) { + val childTag = current.getCompound(segment, null) ?: return + val childFast = childTag as? FastCompoundBinaryTag ?: childTag.fast() + + current.put(segment, childFast) + stack.addLast(current to segment) + current = childFast + } + + while (stack.isNotEmpty()) { + val (parent, key) = stack.removeLast() + val child = parent.getCompound(key, null) ?: continue + if (child.size() == 0) { + parent.remove(key) + } else { + break + } + } } override fun equals(other: Any?): Boolean { @@ -159,4 +251,7 @@ class PersistentPlayerDataContainerImpl( return tag.hashCode() } + override fun toString(): String { + return "PersistentPlayerDataContainerImpl(tag=$tag)" + } } \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerViewImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerViewImpl.kt index ebecaccb..2ecf043d 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerViewImpl.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataContainerViewImpl.kt @@ -5,11 +5,12 @@ import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataAdapterCon import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainerView import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataType import dev.slne.surf.cloud.api.common.util.toObjectSet +import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectSet import net.kyori.adventure.key.Key -import net.kyori.adventure.nbt.BinaryTag -import net.kyori.adventure.nbt.CompoundBinaryTag +import net.kyori.adventure.nbt.* import org.jetbrains.annotations.Unmodifiable +import java.util.* abstract class PersistentPlayerDataContainerViewImpl : PersistentPlayerDataContainerView { @@ -34,16 +35,65 @@ abstract class PersistentPlayerDataContainerViewImpl : PersistentPlayerDataConta ): C? { val value = getTag(key.asString()) ?: return null - if (!type.primitiveType.isInstance(value)) { - error("Value under key ${key.asString()} is not of type ${type.primitiveType.simpleName}") - } - return type.fromPrimitive( PersistentPlayerDataTypeRegistry.extract(type, value), PersistentPlayerDataAdapterContextImpl ) } + private inline fun getTag(key: Key): T? { + return getTag(key.asString()) as? T + } + + override fun getBoolean(key: Key): Boolean? { + val tag = getTag(key) ?: return null + return tag.value() != 0.toByte() + } + + override fun getNumber(key: Key): Number? { + return getTag(key)?.numberValue() + } + + override fun getByte(key: Key): Byte? { + return getTag(key)?.value() + } + + override fun getShort(key: Key): Short? { + return getTag(key)?.value() + } + + override fun getInt(key: Key): Int? { + return getTag(key)?.value() + } + + override fun getLong(key: Key): Long? { + return getTag(key)?.value() + } + + override fun getFloat(key: Key): Float? { + return getTag(key)?.value() + } + + override fun getDouble(key: Key): Double? { + return getTag(key)?.value() + } + + override fun getString(key: Key): String? { + return getTag(key)?.value() + } + + override fun getByteArray(key: Key): ByteArray? { + return getTag(key)?.value() + } + + override fun getIntArray(key: Key): IntArray? { + return getTag(key)?.value() + } + + override fun getLongArray(key: Key): LongArray? { + return getTag(key)?.value() + } + override val keys: @Unmodifiable ObjectSet get() = toTagCompound().keySet().asSequence() .map { it.split(":", limit = 2) } @@ -62,4 +112,112 @@ abstract class PersistentPlayerDataContainerViewImpl : PersistentPlayerDataConta val root = toTagCompound() buf.writeCompoundTag(root) } + + override fun snapshot(): PersistentPlayerDataContainerViewImpl { + val tagCopy = snapshotTag() + + return object : PersistentPlayerDataContainerViewImpl() { + override fun toTagCompound() = tagCopy + override fun getTag(key: String) = tagCopy.get(key) + override fun snapshotTag() = tagCopy + } + } + + /** + * Creates a snapshot of the tag compound. + * Subclasses should override this method to ensure the snapshot is created + * while holding appropriate locks to prevent concurrent modifications. + */ + protected open fun snapshotTag(): CompoundBinaryTag { + val tag = deepCopy(toTagCompound()) + return CompoundBinaryTag.builder() + .put(tag) + .build() + } + + + /** + * Creates a deep copy of the provided `CompoundBinaryTag` without using recursion. + * + * This function iterates through the tree structure of the `CompoundBinaryTag` in a non-recursive manner + * to build a complete copy. It avoids stack overflow issues that can occur with deeply nested structures + * when using a recursive approach. + * + * Uses `ArrayDeque` instead of `Stack` for better performance characteristics: + * - `ArrayDeque` is not synchronized, making it faster for single-threaded use + * - `Stack` extends `Vector`, which has legacy synchronization overhead + * - `ArrayDeque` is the recommended implementation for stack operations in modern Java/Kotlin + * + * @param root The root `CompoundBinaryTag` to be deep copied. + * @return A deep copy of the specified `CompoundBinaryTag`. + * @throws IllegalStateException if the structure is too deeply nested (exceeds [MAX_NESTING_DEPTH]) + */ + private fun deepCopy(root: CompoundBinaryTag): CompoundBinaryTag { + data class Frame( + val entries: List>, + var idx: Int, + val builder: CompoundBinaryTag.Builder, + val parent: Frame?, + val parentKey: String? + ) + + fun entriesOf(tag: CompoundBinaryTag): List> { + val list = ObjectArrayList>(tag.size()) + tag.forEach { (k, v) -> list.add(k to v) } + return list + } + + val stack = ArrayDeque() + stack.addLast( + Frame( + entries = entriesOf(root), + idx = 0, + builder = CompoundBinaryTag.builder(), + parent = null, + parentKey = null + ) + ) + + var result: CompoundBinaryTag? = null + + while (stack.isNotEmpty()) { + val top = stack.removeLast() + + while (top.idx < top.entries.size) { + val (key, value) = top.entries[top.idx++] + + if (value is CompoundBinaryTag) { + PersistentPlayerDataContainerView.ensureValidNestingDepth(stack.size) + + stack.addLast(top) + stack.addLast( + Frame( + entries = entriesOf(value), + idx = 0, + builder = CompoundBinaryTag.builder(), + parent = top, + parentKey = key + ) + ) + + break + } else { + top.builder.put(key, value) + } + } + + + if (top.idx >= top.entries.size) { + val built = top.builder.build() + if (top.parent == null) { + result = built + } else { + val parent = top.parent + parent.builder.put(requireNotNull(top.parentKey), built) + } + } + } + + return requireNotNull(result) + } } \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataTypeRegistry.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataTypeRegistry.kt index 04b32122..5e75cee9 100644 --- a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataTypeRegistry.kt +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/PersistentPlayerDataTypeRegistry.kt @@ -4,239 +4,200 @@ import com.google.common.primitives.Primitives import dev.slne.surf.cloud.api.common.player.ppdc.ListPersistentPlayerDataType import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainer import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataType -import dev.slne.surf.cloud.api.common.util.mutableObject2ObjectMapOf import dev.slne.surf.cloud.api.common.util.mutableObjectListOf -import dev.slne.surf.cloud.api.common.util.synchronize import net.kyori.adventure.nbt.* -import kotlin.reflect.KClass -import kotlin.reflect.cast +import java.util.concurrent.ConcurrentHashMap -private typealias AdapterCreator = (KClass<*>) -> PersistentPlayerDataTypeRegistry.TagAdapter<*, *> +private typealias AdapterCreator = (Class<*>) -> PersistentPlayerDataTypeRegistry.TagAdapter<*, *> +@Suppress("UNCHECKED_CAST") object PersistentPlayerDataTypeRegistry { - private val adapters = mutableObject2ObjectMapOf, TagAdapter<*, *>>().synchronize() + private val adapters = ConcurrentHashMap, TagAdapter<*, *>>() private val createAdapter: AdapterCreator = { createAdapter(it) } - private fun createAdapter(type: KClass): TagAdapter<*, *> { + private fun createAdapter(type: Class): TagAdapter<*, *> { var type = type - if (!Primitives.isWrapperType(type.java)) { - type = - Primitives.wrap(type.java).kotlin //Make sure we will always "switch" over the wrapper types + if (!Primitives.isWrapperType(type)) { + type = Primitives.wrap(type) //Make sure we will always "switch" over the wrapper types } // region Primitives - when (type) { - Byte::class -> { - return createAdapter( - Byte::class, - BinaryTagTypes.BYTE, - ByteBinaryTag::byteBinaryTag - ) { it.value() } - } - - Short::class -> { - return createAdapter( - Short::class, - BinaryTagTypes.SHORT, - ShortBinaryTag::shortBinaryTag - ) { it.value() } - } - - Int::class -> { - return createAdapter( - Int::class, - BinaryTagTypes.INT, - IntBinaryTag::intBinaryTag - ) { it.value() } - } - - Long::class -> { - return createAdapter( - Long::class, - BinaryTagTypes.LONG, - LongBinaryTag::longBinaryTag - ) { it.value() } - } - - Float::class -> { - return createAdapter( - Float::class, - BinaryTagTypes.FLOAT, - FloatBinaryTag::floatBinaryTag - ) { it.value() } - } + return when (type) { + Byte::class.java -> createAdapter( + Byte::class.java, + ByteBinaryTag::class.java, + BinaryTagTypes.BYTE, + ByteBinaryTag::byteBinaryTag + ) { it.value() } + + Short::class.java -> createAdapter( + Short::class.java, + ShortBinaryTag::class.java, + BinaryTagTypes.SHORT, + ShortBinaryTag::shortBinaryTag + ) { it.value() } + + Int::class.java -> createAdapter( + Int::class.java, + IntBinaryTag::class.java, + BinaryTagTypes.INT, + IntBinaryTag::intBinaryTag + ) { it.value() } + + Long::class.java -> createAdapter( + Long::class.java, + LongBinaryTag::class.java, + BinaryTagTypes.LONG, + LongBinaryTag::longBinaryTag + ) { it.value() } + + Float::class.java -> createAdapter( + Float::class.java, + FloatBinaryTag::class.java, + BinaryTagTypes.FLOAT, + FloatBinaryTag::floatBinaryTag + ) { it.value() } + + Double::class.java -> createAdapter( + Double::class.java, + DoubleBinaryTag::class.java, + BinaryTagTypes.DOUBLE, + DoubleBinaryTag::doubleBinaryTag + ) { it.value() } + + Boolean::class.java -> createAdapter( + Boolean::class.java, + ByteBinaryTag::class.java, + BinaryTagTypes.BYTE, + { if (it) ByteBinaryTag.ONE else ByteBinaryTag.ZERO } + ) { it.value() != 0.toByte() } + + Char::class.java -> createAdapter( + Char::class.java, + IntBinaryTag::class.java, + BinaryTagTypes.INT, + { IntBinaryTag.intBinaryTag(it.code) } + ) { it.value().toChar() } + + String::class.java -> createAdapter( + String::class.java, + StringBinaryTag::class.java, + BinaryTagTypes.STRING, + StringBinaryTag::stringBinaryTag + ) { it.value() } - Double::class -> { - return createAdapter( - Double::class, - BinaryTagTypes.DOUBLE, - DoubleBinaryTag::doubleBinaryTag - ) { it.value() } - } - - Boolean::class -> { - return createAdapter( - Boolean::class, - BinaryTagTypes.BYTE, - { if (it) ByteBinaryTag.ONE else ByteBinaryTag.ZERO } - ) { it.value() != 0.toByte() } - } - - Char::class -> { - return createAdapter( - Char::class, - BinaryTagTypes.INT, - { IntBinaryTag.intBinaryTag(it.code) } - ) { it.value().toChar() } - } - - String::class -> { - return createAdapter( - String::class, - BinaryTagTypes.STRING, - StringBinaryTag::stringBinaryTag - ) { it.value() } - } // endregion // region Primitive non-list arrays - ByteArray::class -> { - return createAdapter( - ByteArray::class, - BinaryTagTypes.BYTE_ARRAY, - { ByteArrayBinaryTag.byteArrayBinaryTag(*it.copyOf()) }, - { it.value().copyOf() } - ) - } + ByteArray::class.java -> createAdapter( + ByteArray::class.java, + ByteArrayBinaryTag::class.java, + BinaryTagTypes.BYTE_ARRAY, + ByteArrayBinaryTag::byteArrayBinaryTag, + ByteArrayBinaryTag::value + ) - IntArray::class -> { - return createAdapter( - IntArray::class, - BinaryTagTypes.INT_ARRAY, - { IntArrayBinaryTag.intArrayBinaryTag(*it.copyOf()) }, - { it.value().copyOf() } - ) - } - LongArray::class -> { - return createAdapter( - LongArray::class, - BinaryTagTypes.LONG_ARRAY, - { LongArrayBinaryTag.longArrayBinaryTag(*it.copyOf()) }, - { it.value().copyOf() } - ) - } + IntArray::class.java -> createAdapter( + IntArray::class.java, + IntArrayBinaryTag::class.java, + BinaryTagTypes.INT_ARRAY, + IntArrayBinaryTag::intArrayBinaryTag, + IntArrayBinaryTag::value + ) - BooleanArray::class -> { - return createAdapter( - BooleanArray::class, - BinaryTagTypes.BYTE_ARRAY, - { bytes -> - ByteArrayBinaryTag.byteArrayBinaryTag(*bytes.map { if (it) 1.toByte() else 0.toByte() } - .toByteArray()) - }, - { tag -> tag.value().map { it == 1.toByte() }.toBooleanArray() } - ) - } + LongArray::class.java -> createAdapter( + LongArray::class.java, + LongArrayBinaryTag::class.java, + BinaryTagTypes.LONG_ARRAY, + LongArrayBinaryTag::longArrayBinaryTag, + LongArrayBinaryTag::value + ) - CharArray::class -> { - return createAdapter( - CharArray::class, - BinaryTagTypes.INT_ARRAY, - { ints -> - IntArrayBinaryTag.intArrayBinaryTag(*ints.map { it.code }.toIntArray()) - }, - { tag -> tag.value().map { it.toChar() }.toCharArray() } - ) - } // endregion - Array::class -> { - return createAdapter( - Array::class, - BinaryTagTypes.LIST, - { pdcs -> - val builder = ListBinaryTag.builder(BinaryTagTypes.COMPOUND) - for (pdc in pdcs) { - require(pdc is PersistentPlayerDataContainerImpl) { "The PDC must be an instance of PersistentPlayerDataContainerImpl" } - builder.add(pdc.toTagCompound()) - } - builder.build() - }, { - it.mapIndexed { index, _ -> - val container = PersistentPlayerDataContainerImpl() - val compound = it.getCompound(index) - compound.forEach { (key, value) -> container.put(key, value) } - container - }.toTypedArray() + Array::class.java -> createAdapter( + Array::class.java, + ListBinaryTag::class.java, + BinaryTagTypes.LIST, + { containerArray -> + val builder = ListBinaryTag.builder(BinaryTagTypes.COMPOUND) + for (pdc in containerArray) { + require(pdc is PersistentPlayerDataContainerImpl) { "The PDC must be an instance of PersistentPlayerDataContainerImpl" } + builder.add(pdc.toTagCompound()) } - ) - } + builder.build() + }, { tag -> + tag.mapIndexed { index, _ -> + val container = PersistentPlayerDataContainerImpl() + val compound = tag.getCompound(index) + compound.forEach { (key, value) -> container.put(key, value) } + container + }.toTypedArray() + } + ) + - PersistentPlayerDataContainer::class -> { - return createAdapter( - PersistentPlayerDataContainerImpl::class, - BinaryTagTypes.COMPOUND, - { it.toTagCompound() }, - { - PersistentPlayerDataContainerImpl().apply { - it.forEach { (key, value) -> - put(key, value) - } - } + PersistentPlayerDataContainer::class.java -> createAdapter( + PersistentPlayerDataContainerImpl::class.java, + CompoundBinaryTag::class.java, + BinaryTagTypes.COMPOUND, + PersistentPlayerDataContainerImpl::toTagCompound + ) { + PersistentPlayerDataContainerImpl().apply { + it.forEach { (key, value) -> + put(key, value) } - ) + } } - List::class -> { - @Suppress("UNCHECKED_CAST") - return createAdapter( - List::class, - BinaryTagTypes.LIST, - { type, value -> - constructList( - type as PersistentPlayerDataType, *>, - value as List - ) - }, - this::extractList, - this::matchesListTag - ) - } - } + List::class.java -> createAdapter( + List::class.java, + ListBinaryTag::class.java, + BinaryTagTypes.LIST, + { type, value -> + constructList( + type as PersistentPlayerDataType, *>, + value as List + ) + }, + this::extractList, + this::matchesListTag + ) - error("Could not find a valid TagAdapter implementation for the requested type ${type.simpleName}") + else -> error("Could not find a valid TagAdapter implementation for the requested type ${type.simpleName}") + } } private fun createAdapter( - primitiveType: KClass, + primitiveType: Class, + tagType: Class, nbtBaseType: BinaryTagType, builder: (T) -> Z, extractor: (Z) -> T - ): TagAdapter { - return createAdapter( - primitiveType, - nbtBaseType, - { _, value -> builder(value) }, - { _, value -> extractor(value) }, - { _, tag -> nbtBaseType.test(tag.type()) } - ) - } + ): TagAdapter = createAdapter( + primitiveType, + tagType, + nbtBaseType, + { _, value -> builder(value) }, + { _, value -> extractor(value) }, + { _, tag -> nbtBaseType.test(tag.type()) } + ) private fun createAdapter( - primitiveType: KClass, + primitiveType: Class, + tagType: Class, nbtBaseType: BinaryTagType, builder: (PersistentPlayerDataType, T) -> Z, extractor: (PersistentPlayerDataType, Z) -> T, matcher: (PersistentPlayerDataType, BinaryTag) -> Boolean - ): TagAdapter { - return TagAdapter(primitiveType, nbtBaseType, builder, extractor, matcher) - } + ): TagAdapter = + TagAdapter(primitiveType, tagType, nbtBaseType, builder, extractor, matcher) + fun

isInstanceOf(type: PersistentPlayerDataType, tag: BinaryTag): Boolean { return getOrCreateAdapter(type).isInstance(type, tag) } - @Suppress("UNCHECKED_CAST") private fun getOrCreateAdapter(type: PersistentPlayerDataType): TagAdapter { return adapters.computeIfAbsent(type.primitiveType, createAdapter) as TagAdapter } @@ -260,31 +221,30 @@ object PersistentPlayerDataTypeRegistry { return primitiveType.cast(foundValue) } - @Suppress("UNCHECKED_CAST") - private fun

> constructList( - type: PersistentPlayerDataType, + private fun

constructList( + type: PersistentPlayerDataType, *>, list: List

): ListBinaryTag { check(type is ListPersistentPlayerDataType<*, *>) { "The passed list cannot be written to the PDC with a ${type::class.simpleName} (expected a list data type)" } - val type = type as ListPersistentPlayerDataType - val elementType = type.elementType + val listType = type as ListPersistentPlayerDataType + val elementType = listType.elementType + val elementAdapter = getOrCreateAdapter(elementType) - getOrCreateAdapter(elementType) - val values = list.map { wrap(elementType, it) } + val builder = ListBinaryTag.builder(elementAdapter.nbtBaseType, list.size) + for (element in list) { + builder.add(wrap(elementType, element)) + } - return ListBinaryTag.heterogeneousListBinaryTag() - .add(values) - .build() + return builder.build() } - @Suppress("UNCHECKED_CAST") private fun

extractList( type: PersistentPlayerDataType, listTag: ListBinaryTag ): List

{ check(type is ListPersistentPlayerDataType<*, *>) { "The found list tag cannot be read with a ${type::class.simpleName} (expected a list data type)" } - val type = type as ListPersistentPlayerDataType - val elementType = type.elementType + val listType = type as ListPersistentPlayerDataType + val elementType = listType.elementType val output = mutableObjectListOf

(listTag.size()) for (tag in listTag) { @@ -314,16 +274,16 @@ object PersistentPlayerDataTypeRegistry { } internal data class TagAdapter

( - val primitiveType: KClass

, + val primitiveType: Class

, + val tagType: Class, val nbtBaseType: BinaryTagType, val builder: (PersistentPlayerDataType, P) -> T, val extractor: (PersistentPlayerDataType, T) -> P, val matcher: (PersistentPlayerDataType, BinaryTag) -> Boolean ) { - @Suppress("UNCHECKED_CAST") fun extract(dataType: PersistentPlayerDataType, base: BinaryTag): P { require(nbtBaseType.test(base.type())) { "The provided NBTBase was of the type ${base.type()}. Expected type $nbtBaseType" } - return extractor(dataType, base as T) + return extractor(dataType, tagType.cast(base)) } fun build(dataType: PersistentPlayerDataType, value: Any): T { diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/TrackingPlayerPersistentDataContainerImpl.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/TrackingPlayerPersistentDataContainerImpl.kt new file mode 100644 index 00000000..27eedd3b --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/TrackingPlayerPersistentDataContainerImpl.kt @@ -0,0 +1,82 @@ +package dev.slne.surf.cloud.core.common.player.ppdc + +import dev.slne.surf.cloud.api.common.util.mutableObjectListOf +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcOp +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcPatch +import dev.slne.surf.surfapi.core.api.nbt.FastCompoundBinaryTag +import dev.slne.surf.surfapi.core.api.nbt.fast +import net.kyori.adventure.nbt.CompoundBinaryTag + +open class TrackingPlayerPersistentDataContainerImpl( + tag: FastCompoundBinaryTag = CompoundBinaryTag.empty().fast() +) : PersistentPlayerDataContainerImpl(tag) { + @Volatile + private var baseSnapshotReference: CompoundBinaryTag? = null + + fun startTracking() { + check(baseSnapshotReference == null) { "Tracking already started" } + + baseSnapshotReference = toTagCompound() + } + + fun clearTracking() { + baseSnapshotReference = null + } + + fun getPatchOps(): PdcPatch { + val base = baseSnapshotReference + val curr = toTagCompound() + val ops = diffToOps(base, curr) + + return PdcPatch(ops) + } + + private fun diffToOps( + base: CompoundBinaryTag?, + curr: CompoundBinaryTag?, + prefix: List = emptyList() + ): MutableList { + val ops = mutableObjectListOf() + + if (base == null && curr != null) { + ops += PdcOp.Put(prefix, curr) + return ops + } + + if (base != null && curr == null) { + ops += PdcOp.Remove(prefix) + return ops + } + + if (base == null && curr == null) return ops + + val baseKeys = base!!.keySet() + val currKeys = curr!!.keySet() + + for (k in baseKeys - currKeys) { + ops += PdcOp.Remove(prefix + k) + } + + for (k in currKeys) { + val b = base.get(k) + val c = curr.get(k) + + if (b == c) continue + + when { + b is CompoundBinaryTag && c is CompoundBinaryTag -> { + ops += diffToOps(b, c, prefix + k) + } + + else -> { + ops += PdcOp.Put(prefix + k, c!!) + } + } + } + return ops + } + + override fun snapshot(): TrackingPlayerPersistentDataContainerImpl { + return TrackingPlayerPersistentDataContainerImpl(snapshotTag().fast()) + } +} \ No newline at end of file diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/network/PdcOp.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/network/PdcOp.kt new file mode 100644 index 00000000..d22177a2 --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/network/PdcOp.kt @@ -0,0 +1,82 @@ +package dev.slne.surf.cloud.core.common.player.ppdc.network + +import dev.slne.surf.cloud.api.common.netty.network.codec.ByteBufCodecs +import dev.slne.surf.cloud.api.common.netty.network.codec.StreamCodec +import dev.slne.surf.cloud.api.common.netty.protocol.buffer.readList +import dev.slne.surf.cloud.api.common.netty.protocol.buffer.readUtf +import dev.slne.surf.cloud.api.common.netty.protocol.buffer.writeCollection +import dev.slne.surf.cloud.api.common.netty.protocol.buffer.writeUtf +import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainerView +import dev.slne.surf.cloud.api.common.util.ByIdMap +import io.netty.buffer.ByteBuf +import net.kyori.adventure.nbt.BinaryTag + +sealed interface PdcOp { + val path: List + val type: Type + + data class Put( + override val path: List, + val value: BinaryTag + ) : PdcOp { + override val type = Type.PUT + + init { + PersistentPlayerDataContainerView.ensureValidNestingDepth(path.size) + } + } + + data class Remove( + override val path: List + ) : PdcOp { + override val type = Type.REMOVE + + init { + PersistentPlayerDataContainerView.ensureValidNestingDepth(path.size) + } + } + + companion object { + val STREAM_CODEC = StreamCodec.of(::write, ::read) + + private fun write(buf: ByteBuf, op: PdcOp) { + Type.STREAM_CODEC.encode(buf, op.type) + buf.writeCollection(op.path) { buf, segment -> + buf.writeUtf(segment) + } + + when (op) { + is Put -> ByteBufCodecs.BINARY_TAG_CODEC_COMPRESSED.encode(buf, op.value) + is Remove -> Unit + } + } + + private fun read(buf: ByteBuf): PdcOp { + val type = Type.STREAM_CODEC.decode(buf) + val path = buf.readList { it.readUtf() } + return when (type) { + Type.PUT -> { + val value = ByteBufCodecs.BINARY_TAG_CODEC_COMPRESSED.decode(buf) + Put(path, value) + } + + Type.REMOVE -> Remove(path) + } + } + } + + enum class Type(val id: Int) { + PUT(0), + REMOVE(1); + + companion object { + val BY_ID = ByIdMap.continuous( + Type::id, + entries.toTypedArray(), + ByIdMap.OutOfBoundsStrategy.DECODE_ERROR + ) + + val STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Type::id) + } + } +} diff --git a/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/network/PdcPatch.kt b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/network/PdcPatch.kt new file mode 100644 index 00000000..afd85c83 --- /dev/null +++ b/surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/ppdc/network/PdcPatch.kt @@ -0,0 +1,13 @@ +package dev.slne.surf.cloud.core.common.player.ppdc.network + +import dev.slne.surf.cloud.api.common.netty.network.codec.ByteBufCodecs + +data class PdcPatch(val ops: MutableList) { + val empty get() = ops.isEmpty() + + companion object { + val STREAM_CODEC = PdcOp.STREAM_CODEC + .apply(ByteBufCodecs.list()) + .map(::PdcPatch, PdcPatch::ops) + } +} \ No newline at end of file diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt index 95d1f048..0662818e 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/netty/server/network/ServerRunningPacketListenerImpl.kt @@ -11,7 +11,6 @@ import dev.slne.surf.cloud.api.common.player.whitelist.WhitelistSettings import dev.slne.surf.cloud.api.common.player.whitelist.WhitelistStatus import dev.slne.surf.cloud.api.common.server.CloudServer import dev.slne.surf.cloud.api.common.server.CloudServerManager -import dev.slne.surf.cloud.api.common.util.mutableIntSetOf import dev.slne.surf.cloud.core.common.coroutines.PacketHandlerScope import dev.slne.surf.cloud.core.common.coroutines.PunishmentHandlerScope import dev.slne.surf.cloud.core.common.coroutines.QueueConnectionScope @@ -26,6 +25,7 @@ import dev.slne.surf.cloud.core.common.util.bean import dev.slne.surf.cloud.standalone.netty.server.NettyServerImpl import dev.slne.surf.cloud.standalone.netty.server.ServerClientImpl import dev.slne.surf.cloud.standalone.player.StandaloneCloudPlayerImpl +import dev.slne.surf.cloud.standalone.player.standalonePlayerManagerImpl import dev.slne.surf.cloud.standalone.server.StandaloneCloudServerImpl import dev.slne.surf.cloud.standalone.server.StandaloneProxyCloudServerImpl import dev.slne.surf.cloud.standalone.server.serverManagerImpl @@ -51,7 +51,7 @@ class ServerRunningPacketListenerImpl( private val log = logger() override suspend fun handlePlayerConnectToServer(packet: PlayerConnectToServerPacket) { - val result = playerManagerImpl.updateOrCreatePlayer( + val result = standalonePlayerManagerImpl.updateOrCreatePlayer( packet.uuid, packet.name, packet.proxy, @@ -60,7 +60,8 @@ class ServerRunningPacketListenerImpl( true ) - packet.respond(PlayerConnectToServerResponsePacket(result)) + packet.respond(PlayerConnectToServerResponsePacket(result.preJoinResult)) + if (!result.preJoinAllowed) return broadcast( PlayerConnectedToServerPacket( @@ -68,9 +69,11 @@ class ServerRunningPacketListenerImpl( packet.name, packet.serverName, packet.proxy, - packet.playerIp + packet.playerIp, + result.player!!.ppdcTagSnapshot() ) ) + serverManagerImpl.getCommonStandaloneServerByName(packet.serverName) ?.handlePlayerConnect(packet.uuid) } @@ -234,39 +237,20 @@ class ServerRunningPacketListenerImpl( packet.respond(LuckpermsMetaDataResponsePacket(data)) } - private val pendingVerificationIds = mutableIntSetOf() override suspend fun handleRequestPlayerPersistentDataContainer(packet: ServerboundRequestPlayerPersistentDataContainer) { withPlayer(packet.uuid) { - val data = getPersistentData() + val data = ppdcTagSnapshot() val id = random.nextInt() - pendingVerificationIds.add(id) packet.respond(ClientboundPlayerPersistentDataContainerResponse(id, data)) } } - override suspend fun handlePlayerPersistentDataContainerUpdate(packet: ServerboundPlayerPersistentDataContainerUpdatePacket) { - if (!pendingVerificationIds.remove(packet.verificationId)) { - log.atWarning() - .log( - "Received invalid persistent data container update id %s", - packet.verificationId - ) - return - } - - withPlayer(packet.uuid) { - log.atInfo() - .log("Updating persistent data for %s with data %s", packet.uuid, packet.nbt) - updatePersistentData(packet.nbt) - } - } - override suspend fun handleConnectPlayerToServer(packet: ServerboundConnectPlayerToServerPacket) { val (uuid, serverName, queue, sendQueuedMessage) = packet withPlayer(uuid) { val result = when (val server = CloudServerManager.retrieveServerByName(serverName)) { - null -> ConnectionResultEnum.SERVER_NOT_FOUND(serverName.toString()) + null -> ConnectionResultEnum.SERVER_NOT_FOUND(serverName) !is CloudServer -> ConnectionResultEnum.CANNOT_CONNECT_TO_PROXY else -> withContext(QueueConnectionScope.context) { if (queue) connectToServerOrQueue(server, sendQueuedMessage) @@ -609,6 +593,12 @@ class ServerRunningPacketListenerImpl( withPlayer(packet.uuid) { sendToast(packet.toast) } } + override fun handleUpdatePlayerPersistentDataContainer(packet: UpdatePlayerPersistentDataContainerPacket) { + withPlayer(packet.uuid) { + applyPpdcPatch(packet.patch) + } + } + override fun handlePacket(packet: NettyPacket) { val listeners = NettyListenerRegistry.getListeners(packet.javaClass) ?: return if (listeners.isEmpty()) return diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt index 5f4894d7..6749b953 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerImpl.kt @@ -14,13 +14,14 @@ import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag import dev.slne.surf.cloud.api.common.player.teleport.WorldLocation import dev.slne.surf.cloud.api.common.player.toast.NetworkToast import dev.slne.surf.cloud.api.common.server.CloudServer +import dev.slne.surf.cloud.api.server.netty.packet.broadcast import dev.slne.surf.cloud.api.server.server.ServerCommonCloudServer import dev.slne.surf.cloud.core.common.netty.network.protocol.running.* import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundTransferPlayerPacketResponse.Status import dev.slne.surf.cloud.core.common.player.CommonCloudPlayerImpl import dev.slne.surf.cloud.core.common.player.playtime.PlaytimeEntry import dev.slne.surf.cloud.core.common.player.playtime.PlaytimeImpl -import dev.slne.surf.cloud.core.common.player.ppdc.PersistentPlayerDataContainerImpl +import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcPatch import dev.slne.surf.cloud.core.common.util.bean import dev.slne.surf.cloud.standalone.netty.server.NettyServerImpl import dev.slne.surf.cloud.standalone.player.db.exposed.CloudPlayerService @@ -32,8 +33,6 @@ import dev.slne.surf.surfapi.core.api.util.logger import dev.slne.surf.surfapi.core.api.util.mutableObjectListOf import dev.slne.surf.surfapi.core.api.util.toObjectList import it.unimi.dsi.fastutil.objects.ObjectList -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import net.kyori.adventure.audience.MessageType import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.identity.Identity @@ -54,6 +53,8 @@ import java.time.temporal.ChronoUnit import java.util.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.read +import kotlin.concurrent.write import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -86,9 +87,7 @@ class StandaloneCloudPlayerImpl(uuid: UUID, name: String, val ip: Inet4Address) @Volatile var connectingToServer: StandaloneCloudServerImpl? = null - private val ppdc = PersistentPlayerDataContainerImpl() - private val ppdcMutex = Mutex() private var firstSeenCache: ZonedDateTime? = null private val afk = AtomicBoolean(false) @@ -96,14 +95,18 @@ class StandaloneCloudPlayerImpl(uuid: UUID, name: String, val ip: Inet4Address) var sessionStartTime: ZonedDateTime = ZonedDateTime.now() fun savePlayerData(tag: CompoundBinaryTag.Builder) { - if (!ppdc.empty) { - tag.put("ppdc", ppdc.toTagCompound()) + ppdcReentrantLock.read { + if (!ppdc.empty) { + tag.put("ppdc", ppdc.toTagCompound()) + } } } fun readPlayerData(tag: CompoundBinaryTag) { - val ppdcTag = tag.get("ppdc") as? CompoundBinaryTag ?: return - ppdc.fromTagCompound(ppdcTag) + ppdcReentrantLock.write { + val ppdcTag = tag.getCompound("ppdc") + ppdc.fromTagCompound(ppdcTag) + } } override fun isAfk(): Boolean { @@ -132,11 +135,33 @@ class StandaloneCloudPlayerImpl(uuid: UUID, name: String, val ip: Inet4Address) } } - override suspend fun withPersistentData(block: PersistentPlayerDataContainer.() -> R): R = - ppdcMutex.withLock { - ppdc.block() + override fun editPdc(block: PersistentPlayerDataContainer.() -> R): R { + val (result, patch) = editPdc0(false, block) + + if (patch.empty) { + return result } + val packet = UpdatePlayerPersistentDataContainerPacket(uuid, patch) + packet.broadcast() + + return result + } + + fun applyPpdcPatch(patch: PdcPatch) { + if (patch.empty) return + + ppdcReentrantLock.write { + ppdc.applyOps(ppdc.tag, patch) + } + + UpdatePlayerPersistentDataContainerPacket(uuid, patch).broadcast() + } + + fun ppdcTagSnapshot(): CompoundBinaryTag { + return ppdcReentrantLock.read { ppdc.snapshot() }.toTagCompound() + } + override fun disconnect(reason: Component) { val connection = proxyServer?.connection ?: server?.connection connection?.send(DisconnectPlayerPacket(uuid, reason)) @@ -146,10 +171,6 @@ class StandaloneCloudPlayerImpl(uuid: UUID, name: String, val ip: Inet4Address) server?.connection?.send(SilentDisconnectPlayerPacket(uuid)) } - suspend fun getPersistentData() = ppdcMutex.withLock { ppdc.toTagCompound() } - suspend fun updatePersistentData(tag: CompoundBinaryTag) = - ppdcMutex.withLock { ppdc.fromTagCompound(tag) } - override suspend fun latestIpAddress(): Inet4Address { return ip } diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerManagerImpl.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerManagerImpl.kt index 498c6a19..189d5306 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerManagerImpl.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/player/StandaloneCloudPlayerManagerImpl.kt @@ -22,11 +22,13 @@ import dev.slne.surf.cloud.standalone.server.StandaloneCloudServerImpl import dev.slne.surf.cloud.standalone.server.StandaloneProxyCloudServerImpl import dev.slne.surf.cloud.standalone.server.serverManagerImpl import dev.slne.surf.surfapi.core.api.util.logger +import dev.slne.surf.surfapi.core.api.util.mutableObject2LongMapOf import kotlinx.coroutines.* import java.net.Inet4Address import java.time.ZonedDateTime import java.util.* import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds @AutoService(CloudPlayerManager::class) class StandaloneCloudPlayerManagerImpl : CloudPlayerManagerImpl() { @@ -158,7 +160,9 @@ class StandaloneCloudPlayerManagerImpl : CloudPlayerManagerImpl() + + while (!playerCache.underlying().asMap().isEmpty()) { // TODO: enhance + forEachPlayer { + val lastSent = + sentDisconnect.computeIfAbsent(it.uuid) { System.currentTimeMillis() } + + if (System.currentTimeMillis() - lastSent > 15.seconds.inWholeMilliseconds) { + it.disconnect(MessageManager.networkShutdown) + sentDisconnect.put(it.uuid, System.currentTimeMillis()) + } + } + delay(5.seconds) + } + } + private fun logServerNotFound(name: String, player: StandaloneCloudPlayerImpl) { log.atWarning() .log("Could not find server '$name' for player ${player.uuid}") diff --git a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/plugin/spring/PluginSpringConfig.kt b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/plugin/spring/PluginSpringConfig.kt index 2c0e78bf..815ee758 100644 --- a/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/plugin/spring/PluginSpringConfig.kt +++ b/surf-cloud-standalone/src/main/kotlin/dev/slne/surf/cloud/standalone/plugin/spring/PluginSpringConfig.kt @@ -4,6 +4,7 @@ import dev.slne.surf.cloud.api.server.plugin.PluginConfig import dev.slne.surf.cloud.api.server.plugin.configuration.PluginMeta import dev.slne.surf.cloud.api.server.plugin.provider.classloader.SpringPluginClassloader import dev.slne.surf.cloud.core.common.spring.CloudChildSpringApplicationConfiguration +import dev.slne.surf.cloud.standalone.commands.ConsoleCommandProcessor import dev.slne.surf.cloud.standalone.plugin.entrypoint.classloader.SpringPluginClassloaderImpl import dev.slne.surf.cloud.standalone.plugin.spring.config.DatabaseConfigConfiguration import dev.slne.surf.cloud.standalone.plugin.spring.config.PluginDatasourceConfiguration @@ -16,7 +17,7 @@ import org.springframework.boot.builder.SpringApplicationBuilder import org.springframework.stereotype.Component @Component -class PluginSpringConfig() : +class PluginSpringConfig(private val consoleCommandProcessor: ConsoleCommandProcessor) : CloudChildSpringApplicationConfiguration { override fun configureChildApplication( @@ -35,6 +36,10 @@ class PluginSpringConfig() : "pluginClassloader", RootBeanDefinition(SpringPluginClassloader::class.java) { classLoader } ) + context.registerBeanDefinition( + "pluginConsoleCommandProcessor", + RootBeanDefinition(ConsoleCommandProcessor::class.java) { consoleCommandProcessor } + ) classLoader.context = context }) diff --git a/surf-cloud-test-plugin/surf-cloud-test-core/build.gradle.kts b/surf-cloud-test-plugin/surf-cloud-test-core/build.gradle.kts new file mode 100644 index 00000000..21c5aa4f --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-core/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("dev.slne.surf.surfapi.gradle.core") +} + +dependencies { + compileOnly(project(":surf-cloud-api:surf-cloud-api-common")) +} \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/TestSpringApplication.kt b/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/TestSpringApplication.kt new file mode 100644 index 00000000..67d51950 --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/TestSpringApplication.kt @@ -0,0 +1,20 @@ +package dev.slne.surf.cloudtest + +import dev.slne.surf.cloud.api.common.CloudInstance +import dev.slne.surf.cloud.api.common.SurfCloudApplication +import dev.slne.surf.cloud.api.common.startSpringApplication +import org.springframework.context.ConfigurableApplicationContext + +@SurfCloudApplication +class TestSpringApplication { + companion object { + lateinit var context: ConfigurableApplicationContext + fun run() { + context = CloudInstance.startSpringApplication(TestSpringApplication::class) + } + + fun stop() { + context.stop() + } + } +} \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/core/test/ppdc/PpdcTest.kt b/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/core/test/ppdc/PpdcTest.kt new file mode 100644 index 00000000..61e12b6f --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/core/test/ppdc/PpdcTest.kt @@ -0,0 +1,99 @@ +package dev.slne.surf.cloudtest.core.test.ppdc + +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainer +import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainerView +import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataType +import dev.slne.surf.surfapi.core.api.messages.Colors +import dev.slne.surf.surfapi.core.api.messages.adventure.buildText +import dev.slne.surf.surfapi.core.api.messages.adventure.key +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.ComponentLike +import org.springframework.stereotype.Component as SpringComponent + +@SpringComponent +class PpdcTest { + + fun readTestData(player: CloudPlayer): TestData? { + val ppdc = player.persistentData.get(TEST_DATA_KEY, PersistentPlayerDataType.TAG_CONTAINER) + ?: return null + + return TestData.fromPpdc(ppdc) + } + + fun saveTestData(player: CloudPlayer, testData: TestData) { + player.editPdc { + val ppdc = get(TEST_DATA_KEY, PersistentPlayerDataType.TAG_CONTAINER) + ?: adapterContext.newPersistentDataContainer() + testData.saveToPdc(ppdc) + set(TEST_DATA_KEY, PersistentPlayerDataType.TAG_CONTAINER, ppdc) + } + } + + data class TestData( + val test: String, + val testInt: Int? = null, + val testBoolean: Boolean? = null, + val testList: List? = null, + ): ComponentLike { + + fun saveToPdc(ppdc: PersistentPlayerDataContainer) { + ppdc.setString(TEST_KEY, test) + ppdc.setInt(TEST_KEY_INT, testInt) + ppdc.setBoolean(TEST_KEY_BOOLEAN, testBoolean) + if (testList == null) { + ppdc.remove(TEST_KEY_LIST) + } else { + ppdc.set(TEST_KEY_LIST, PersistentPlayerDataType.LIST.strings(), testList) + } + } + + override fun asComponent() = buildText { + appendMap( + mapOf( + TEST_KEY.asString() to test, + TEST_KEY_INT.asString() to testInt?.toString(), + TEST_KEY_BOOLEAN.asString() to testBoolean?.toString(), + TEST_KEY_LIST.asString() to testList?.joinToString() + ), + { + Component.text(it, Colors.VARIABLE_KEY) + }, + { + if (it == null) { + Component.text("N/A", Colors.VARIABLE_VALUE) + } else { + Component.text(it, Colors.VARIABLE_VALUE) + } + }, + Component.empty() + ) + } + + companion object { + val TEST_KEY = key("test", "test_key") + val TEST_KEY_INT = key("test", "test_key_int") + val TEST_KEY_BOOLEAN = key("test", "test_key_boolean") + val TEST_KEY_LIST = key("test", "test_key_list") + + + fun fromPpdc(ppdc: PersistentPlayerDataContainerView): TestData? { + val test = ppdc.getString(TEST_KEY) ?: return null + val testInt = ppdc.getInt(TEST_KEY_INT) + val testBoolean = ppdc.getBoolean(TEST_KEY_BOOLEAN) + val testKeyList = ppdc.get(TEST_KEY_LIST, PersistentPlayerDataType.LIST.strings()) + + return TestData( + test = test, + testInt = testInt, + testBoolean = testBoolean, + testList = testKeyList + ) + } + } + } + + companion object { + val TEST_DATA_KEY = key("test", "test_data") + } +} \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/core/test/ppdc/PpdcTestExecutor.kt b/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/core/test/ppdc/PpdcTestExecutor.kt new file mode 100644 index 00000000..86326b8e --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-core/src/main/kotlin/dev/slne/surf/cloudtest/core/test/ppdc/PpdcTestExecutor.kt @@ -0,0 +1,41 @@ +package dev.slne.surf.cloudtest.core.test.ppdc + +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.surfapi.core.api.messages.adventure.sendText +import net.kyori.adventure.audience.Audience +import org.springframework.stereotype.Component + +@Component +class PpdcTestExecutor(private val ppdcTest: PpdcTest) { + + suspend fun showPpdcTestData(sender: Audience, player: CloudPlayer) { + sender.sendText { + info("Test data for ") + append(player.displayName()) + info(": ") + val data = ppdcTest.readTestData(player) + if (data == null) { + error("No data found") + } else { + append(data) + } + } + } + + suspend fun setRandomPpdcTestData(sender: Audience, player: CloudPlayer) { + val testData = PpdcTest.TestData( + test = "TestString_${(1000..9999).random()}", + testInt = (0..100).random(), + testBoolean = listOf(true, false).random(), + testList = List((1..5).random()) { "Item${(1..100).random()}" } + ) + ppdcTest.saveTestData(player, testData) + + sender.sendText { + info("Test data for ") + append(player.displayName()) + info(" set to: ") + append(testData) + } + } +} \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-paper/build.gradle.kts b/surf-cloud-test-plugin/surf-cloud-test-paper/build.gradle.kts new file mode 100644 index 00000000..64368e5c --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-paper/build.gradle.kts @@ -0,0 +1,23 @@ +import dev.slne.surf.surfapi.gradle.util.registerRequired + +plugins { + id("dev.slne.surf.surfapi.gradle.paper-plugin") +} + +dependencies { + api(project(":surf-cloud-test-plugin:surf-cloud-test-core")) + compileOnly(project(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-paper")) +} + +surfPaperPluginApi { + mainClass("dev.slne.surf.cloudtest.paper.PaperMain") + bootstrapper("dev.slne.surf.cloudtest.paper.PaperBootstrapper") + + bootstrapDependencies { + registerRequired("surf-cloud-bukkit") + } + + serverDependencies { + registerRequired("surf-cloud-bukkit") + } +} \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/PaperBootstrapper.kt b/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/PaperBootstrapper.kt new file mode 100644 index 00000000..cbac3524 --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/PaperBootstrapper.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.cloudtest.paper + +import dev.slne.surf.cloudtest.TestSpringApplication +import io.papermc.paper.plugin.bootstrap.BootstrapContext +import io.papermc.paper.plugin.bootstrap.PluginBootstrap + +class PaperBootstrapper : PluginBootstrap { + override fun bootstrap(context: BootstrapContext) { + TestSpringApplication.run() + } +} \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/PaperMain.kt b/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/PaperMain.kt new file mode 100644 index 00000000..0fdaa5d9 --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/PaperMain.kt @@ -0,0 +1,16 @@ +package dev.slne.surf.cloudtest.paper + +import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin +import dev.slne.surf.cloudtest.TestSpringApplication +import dev.slne.surf.cloudtest.paper.test.command.testPpdcCommand +import org.bukkit.plugin.java.JavaPlugin +import org.springframework.beans.factory.getBean + +class PaperMain : SuspendingJavaPlugin() { + override suspend fun onLoadAsync() { + testPpdcCommand() + } +} + +val plugin get() = JavaPlugin.getPlugin(PaperMain::class.java) +inline fun bean() = TestSpringApplication.context.getBean() \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/test/command/TestPpdcCommand.kt b/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/test/command/TestPpdcCommand.kt new file mode 100644 index 00000000..61d958f8 --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-paper/src/main/kotlin/dev/slne/surf/cloudtest/paper/test/command/TestPpdcCommand.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.cloudtest.paper.test.command + +import com.github.shynixn.mccoroutine.folia.launch +import dev.jorel.commandapi.CommandPermission +import dev.jorel.commandapi.kotlindsl.anyExecutor +import dev.jorel.commandapi.kotlindsl.commandTree +import dev.jorel.commandapi.kotlindsl.getValue +import dev.jorel.commandapi.kotlindsl.literalArgument +import dev.slne.surf.cloud.api.client.paper.command.args.onlineCloudPlayerArgument +import dev.slne.surf.cloud.api.common.player.CloudPlayer +import dev.slne.surf.cloudtest.core.test.ppdc.PpdcTestExecutor +import dev.slne.surf.cloudtest.paper.bean +import dev.slne.surf.cloudtest.paper.plugin + +fun testPpdcCommand() = commandTree("test-ppdc") { + withPermission(CommandPermission.OP) + + onlineCloudPlayerArgument("player") { + literalArgument("set") { + anyExecutor { sender, args -> + val player: CloudPlayer by args + plugin.launch { + bean().setRandomPpdcTestData(sender, player) + } + } + } + literalArgument("get") { + anyExecutor { sender, args -> + val player: CloudPlayer by args + plugin.launch { + bean().showPpdcTestData(sender, player) + } + } + } + } +} \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-standalone/build.gradle.kts b/surf-cloud-test-plugin/surf-cloud-test-standalone/build.gradle.kts index cc671d2d..7b3d6e20 100644 --- a/surf-cloud-test-plugin/surf-cloud-test-standalone/build.gradle.kts +++ b/surf-cloud-test-plugin/surf-cloud-test-standalone/build.gradle.kts @@ -3,6 +3,7 @@ plugins { } dependencies { + api(project(":surf-cloud-test-plugin:surf-cloud-test-core")) compileOnly(project(":surf-cloud-api:surf-cloud-api-server")) // https://mvnrepository.com/artifact/commons-io/commons-io diff --git a/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/TestStandaloneBootstrap.kt b/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/TestStandaloneBootstrap.kt index f618093d..27f05938 100644 --- a/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/TestStandaloneBootstrap.kt +++ b/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/TestStandaloneBootstrap.kt @@ -1,12 +1,11 @@ package dev.slne.surf.cloudtest.standalone.test -import dev.slne.surf.cloud.api.common.CloudInstance -import dev.slne.surf.cloud.api.common.startSpringApplication import dev.slne.surf.cloud.api.server.plugin.bootstrap.BootstrapContext import dev.slne.surf.cloud.api.server.plugin.bootstrap.StandalonePluginBootstrap +import dev.slne.surf.cloudtest.TestSpringApplication class TestStandaloneBootstrap : StandalonePluginBootstrap { override suspend fun bootstrap(context: BootstrapContext) { - CloudInstance.startSpringApplication(TestStandaloneSpringApplication::class) + TestSpringApplication.run() } } \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/TestStandaloneSpringApplication.kt b/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/TestStandaloneSpringApplication.kt deleted file mode 100644 index a119c44b..00000000 --- a/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/TestStandaloneSpringApplication.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.slne.surf.cloudtest.standalone.test - -import dev.slne.surf.cloud.api.common.SurfCloudApplication -import dev.slne.surf.cloud.api.server.plugin.AdditionalStandaloneConfiguration - -@SurfCloudApplication -@AdditionalStandaloneConfiguration -class TestStandaloneSpringApplication \ No newline at end of file diff --git a/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/command/TestPpdcCommand.kt b/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/command/TestPpdcCommand.kt new file mode 100644 index 00000000..565f213a --- /dev/null +++ b/surf-cloud-test-plugin/surf-cloud-test-standalone/src/main/kotlin/dev/slne/surf/cloudtest/standalone/test/command/TestPpdcCommand.kt @@ -0,0 +1,34 @@ +package dev.slne.surf.cloudtest.standalone.test.command + +import com.mojang.brigadier.CommandDispatcher +import dev.slne.surf.cloud.api.server.command.* +import dev.slne.surf.cloud.api.server.command.argument.CloudPlayerArgumentType +import dev.slne.surf.cloudtest.core.test.ppdc.PpdcTestExecutor +import dev.slne.surf.cloudtest.standalone.test.plugin + +@ConsoleCommand +class TestPpdcCommand(private val ppdcTestExecutor: PpdcTestExecutor) : AbstractConsoleCommand() { + override fun register(dispatcher: CommandDispatcher) { + dispatcher.register(literal("test-ppdc") { + then("player", CloudPlayerArgumentType.player()) { + then("set") { + execute { ctx -> + val player = CloudPlayerArgumentType.getPlayer(ctx, "player") + plugin.launch { + ppdcTestExecutor.setRandomPpdcTestData(ctx.source, player) + } + } + } + + then("get") { + execute { ctx -> + val player = CloudPlayerArgumentType.getPlayer(ctx, "player") + plugin.launch { + ppdcTestExecutor.showPpdcTestData(ctx.source, player) + } + } + } + } + }) + } +} \ No newline at end of file