Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit 8c1f707

Browse files
authored
Merge pull request #113 from SLNE-Development/feat/ppdc-view
feat: introduce persistent player data tracking and binary tag handling
2 parents eb0fd80 + 5efba7a commit 8c1f707

File tree

51 files changed

+1819
-499
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1819
-499
lines changed

qodana.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,15 @@ projectJDK: "21" #(Applied in CI/CD pipeline)
2929

3030
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
3131
linter: jetbrains/qodana-jvm:2024.3
32+
33+
exclude:
34+
- name: All
35+
paths:
36+
- surf-cloud-test-plugin
37+
- api
38+
- cert_generation
39+
- cert_new
40+
- Writerside
41+
42+
include:
43+
- name: IncorrectFormatting

settings.gradle.kts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,21 @@ include("surf-cloud-core:surf-cloud-core-client")
2727
findProject(":surf-cloud-core:surf-cloud-core-client")?.name = "surf-cloud-core-client"
2828

2929
include("surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-common")
30-
findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-common")?.name = "surf-cloud-api-client-common"
30+
findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-common")?.name =
31+
"surf-cloud-api-client-common"
3132

3233
include("surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-paper")
33-
findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-paper")?.name = "surf-cloud-api-client-paper"
34+
findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-paper")?.name =
35+
"surf-cloud-api-client-paper"
3436

3537
include("surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-velocity")
36-
findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-velocity")?.name = "surf-cloud-api-client-velocity"
38+
findProject(":surf-cloud-api:surf-cloud-api-client:surf-cloud-api-client-velocity")?.name =
39+
"surf-cloud-api-client-velocity"
3740

3841
include("surf-cloud-bom")
3942

4043
if (!ci) {
4144
include(":surf-cloud-test-plugin:surf-cloud-test-standalone")
42-
}
45+
include(":surf-cloud-test-plugin:surf-cloud-test-core")
46+
include("surf-cloud-test-plugin:surf-cloud-test-paper")
47+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dev.slne.surf.cloud.api.common.internal
2+
3+
import dev.slne.surf.cloud.api.common.util.annotation.InternalApi
4+
import dev.slne.surf.surfapi.core.api.reflection.*
5+
import net.kyori.adventure.nbt.BinaryTag
6+
import net.kyori.adventure.nbt.BinaryTagType
7+
8+
@InternalApi
9+
@SurfProxy(BinaryTagType::class)
10+
internal interface BinaryTagTypeProxy {
11+
12+
@Static
13+
@Field("TYPES", Field.Type.GETTER)
14+
fun getTypes(): List<BinaryTagType<out BinaryTag>>
15+
16+
companion object {
17+
internal val instance = surfReflection.createProxy<BinaryTagTypeProxy>()
18+
}
19+
}

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/network/codec/ByteBufCodecs.kt

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,27 @@ package dev.slne.surf.cloud.api.common.netty.network.codec
33
import dev.slne.surf.cloud.api.common.netty.protocol.buffer.*
44
import dev.slne.surf.cloud.api.common.netty.protocol.buffer.types.Utf8String
55
import dev.slne.surf.cloud.api.common.util.ByIdMap
6+
import dev.slne.surf.cloud.api.common.util.ByIdMap.OutOfBoundsStrategy
67
import dev.slne.surf.cloud.api.common.util.createUnresolvedInetSocketAddress
78
import io.netty.buffer.ByteBuf
9+
import io.netty.buffer.ByteBufInputStream
10+
import io.netty.buffer.ByteBufOutputStream
11+
import io.netty.handler.codec.DecoderException
12+
import io.netty.handler.codec.EncoderException
13+
import it.unimi.dsi.fastutil.io.FastBufferedInputStream
14+
import it.unimi.dsi.fastutil.io.FastBufferedOutputStream
15+
import it.unimi.dsi.fastutil.objects.ObjectArrayList
816
import net.kyori.adventure.key.Key
17+
import net.kyori.adventure.nbt.BinaryTag
918
import net.kyori.adventure.nbt.BinaryTagIO
19+
import net.kyori.adventure.nbt.BinaryTagType
20+
import net.kyori.adventure.nbt.BinaryTagTypes
1021
import net.kyori.adventure.sound.Sound
1122
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer
1223
import java.io.ByteArrayInputStream
1324
import java.io.ByteArrayOutputStream
25+
import java.io.DataInputStream
26+
import java.io.DataOutputStream
1427
import java.math.BigDecimal
1528
import java.math.BigInteger
1629
import java.math.MathContext
@@ -21,10 +34,15 @@ import java.time.Instant
2134
import java.time.ZoneId
2235
import java.time.ZonedDateTime
2336
import java.util.*
37+
import java.util.zip.GZIPInputStream
38+
import java.util.zip.GZIPOutputStream
39+
import kotlin.math.min
2440
import kotlin.time.Duration
2541
import kotlin.time.Duration.Companion.milliseconds
2642

2743
object ByteBufCodecs {
44+
const val MAX_INITIAL_COLLECTION_SIZE = 65536
45+
2846
val BOOLEAN_CODEC = streamCodec(ByteBuf::writeBoolean, ByteBuf::readBoolean)
2947
val BYTE_CODEC = streamCodec({ buf, byte -> buf.writeByte(byte.toInt()) }, ByteBuf::readByte)
3048
val SHORT_CODEC =
@@ -59,7 +77,7 @@ object ByteBufCodecs {
5977
private val SOUND_SOURCE_BY_ID = ByIdMap.continuous(
6078
{ it.ordinal },
6179
Sound.Source.entries.toTypedArray(),
62-
ByIdMap.OutOfBoundsStrategy.ZERO
80+
OutOfBoundsStrategy.ZERO
6381
)
6482
val SOUND_CODEC = streamCodecComposite(
6583
KEY_CODEC,
@@ -138,6 +156,85 @@ object ByteBufCodecs {
138156
BigDecimal(unscaledValue, scale, MathContext(precision))
139157
}
140158

159+
private val TAG_TYPES = arrayOf(
160+
BinaryTagTypes.END,
161+
BinaryTagTypes.BYTE,
162+
BinaryTagTypes.SHORT,
163+
BinaryTagTypes.INT,
164+
BinaryTagTypes.LONG,
165+
BinaryTagTypes.FLOAT,
166+
BinaryTagTypes.DOUBLE,
167+
BinaryTagTypes.BYTE_ARRAY,
168+
BinaryTagTypes.STRING,
169+
BinaryTagTypes.LIST,
170+
BinaryTagTypes.COMPOUND,
171+
BinaryTagTypes.INT_ARRAY,
172+
BinaryTagTypes.LONG_ARRAY,
173+
)
174+
175+
private val BINARY_TAG_BY_ID = ByIdMap.continuous(
176+
{ it.id().toInt() },
177+
TAG_TYPES,
178+
OutOfBoundsStrategy.DECODE_ERROR
179+
)
180+
181+
val BINARY_TAG_CODEC: StreamCodec<ByteBuf, BinaryTag> = streamCodec({ buf, tag ->
182+
val type = tag.type() as BinaryTagType<BinaryTag>
183+
buf.writeByte(type.id().toInt())
184+
185+
val tmp = buf.alloc().buffer()
186+
try {
187+
DataOutputStream(FastBufferedOutputStream(ByteBufOutputStream(tmp))).use { out ->
188+
type.write(tag, out)
189+
}
190+
val length = tmp.readableBytes()
191+
buf.writeVarInt(length)
192+
buf.writeBytes(tmp, tmp.readerIndex(), length)
193+
} finally {
194+
tmp.release()
195+
}
196+
197+
}, { buf ->
198+
val type = BINARY_TAG_BY_ID(buf.readByte().toInt())
199+
val len = buf.readVarInt()
200+
val slice = buf.readRetainedSlice(len)
201+
202+
try {
203+
DataInputStream(FastBufferedInputStream(ByteBufInputStream(slice))).use { input ->
204+
type.read(input)
205+
}
206+
} finally {
207+
slice.release()
208+
}
209+
})
210+
211+
val BINARY_TAG_CODEC_COMPRESSED: StreamCodec<ByteBuf, BinaryTag> = streamCodec({ buf, tag ->
212+
val type = tag.type() as BinaryTagType<BinaryTag>
213+
buf.writeByte(type.id().toInt())
214+
val temp = buf.alloc().buffer()
215+
216+
try {
217+
DataOutputStream(FastBufferedOutputStream(GZIPOutputStream(ByteBufOutputStream(temp)))).use {
218+
type.write(tag, it)
219+
}
220+
buf.writeVarInt(temp.readableBytes())
221+
buf.writeBytes(temp, temp.readerIndex(), temp.readableBytes())
222+
} finally {
223+
temp.release()
224+
}
225+
}, { buf ->
226+
val type = BINARY_TAG_BY_ID(buf.readByte().toInt())
227+
val length = buf.readVarInt()
228+
val slice = buf.readRetainedSlice(length)
229+
230+
try {
231+
DataInputStream(FastBufferedInputStream(GZIPInputStream(ByteBufInputStream(slice)))).use {
232+
type.read(it)
233+
}
234+
} finally {
235+
slice.release()
236+
}
237+
})
141238

142239
fun <E : Enum<E>> enumStreamCodec(clazz: Class<E>): StreamCodec<ByteBuf, E> =
143240
streamCodec(ByteBuf::writeEnum) { it.readEnum(clazz) }
@@ -155,4 +252,67 @@ object ByteBufCodecs {
155252
buf.writeVarInt(idGetter(value))
156253
}
157254
}
255+
256+
fun readCount(buffer: ByteBuf, maxSize: Int): Int {
257+
val count = buffer.readVarInt()
258+
if (count > maxSize) {
259+
throw DecoderException("$count elements exceeded max size of: $maxSize")
260+
} else {
261+
return count
262+
}
263+
}
264+
265+
fun writeCount(buffer: ByteBuf, count: Int, maxSize: Int) {
266+
if (count > maxSize) {
267+
throw EncoderException("$count elements exceeded max size of: $maxSize")
268+
} else {
269+
buffer.writeVarInt(count)
270+
}
271+
}
272+
273+
fun <B : ByteBuf, V, C : MutableCollection<V>> collection(
274+
factory: (Int) -> C,
275+
codec: StreamCodec<in B, V>,
276+
maxSize: Int = Int.MAX_VALUE
277+
): StreamCodec<B, C> = object : StreamCodec<B, C> {
278+
override fun decode(buf: B): C {
279+
val count = readCount(buf, maxSize)
280+
val collection = factory(min(count, MAX_INITIAL_COLLECTION_SIZE))
281+
282+
repeat(count) {
283+
collection.add(codec.decode(buf))
284+
}
285+
286+
return collection
287+
}
288+
289+
override fun encode(buf: B, value: C) {
290+
writeCount(buf, value.size, maxSize)
291+
292+
for (element in value) {
293+
codec.encode(buf, element)
294+
}
295+
}
296+
}
297+
298+
fun <B : ByteBuf, V, C : MutableCollection<V>> collection(factory: (Int) -> C): CodecOperation<B, V, C> {
299+
return CodecOperation { size -> collection(factory, size) }
300+
}
301+
302+
303+
fun <B : ByteBuf, V> list(): CodecOperation<B, V, MutableList<V>> {
304+
return CodecOperation { size -> collection(::ObjectArrayList, size) }
305+
}
306+
307+
fun <B : ByteBuf, V> list(maxSize: Int): CodecOperation<B, V, MutableList<V>> {
308+
return CodecOperation { size -> collection(::ObjectArrayList, size, maxSize) }
309+
}
310+
311+
@Suppress("NOTHING_TO_INLINE")
312+
private inline fun decodeError(message: Any): Nothing =
313+
throw DecoderException(message.toString())
314+
315+
@Suppress("NOTHING_TO_INLINE")
316+
private inline fun encodeError(message: Any): Nothing =
317+
throw EncoderException(message.toString())
158318
}

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/netty/packet/RespondingNettyPacket.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import kotlinx.coroutines.CompletableDeferred
1010
import kotlinx.coroutines.TimeoutCancellationException
1111
import kotlinx.coroutines.withTimeout
1212
import kotlinx.coroutines.withTimeoutOrNull
13+
import java.lang.ref.WeakReference
1314
import java.util.*
15+
import kotlin.properties.Delegates
1416
import kotlin.time.Duration
1517
import kotlin.time.Duration.Companion.seconds
1618

@@ -36,14 +38,18 @@ abstract class RespondingNettyPacket<P : ResponseNettyPacket> : NettyPacket() {
3638
* A deferred response object for awaiting the associated response.
3739
*/
3840
@InternalApi
39-
val response = CompletableDeferred<P>()
41+
val response by lazy { CompletableDeferred<P>() }
4042

4143
/**
4244
* The connection through which the response will be sent.
4345
*/
4446
@InternalApi
45-
lateinit var responseConnection: Connection
47+
private var responseConnection by Delegates.notNull<WeakReference<Connection>>()
4648

49+
@InternalApi
50+
fun initResponseConnection(connection: Connection) {
51+
responseConnection = WeakReference(connection)
52+
}
4753

4854
/**
4955
* Fires the packet and awaits its response within the specified timeout.
@@ -109,6 +115,19 @@ abstract class RespondingNettyPacket<P : ResponseNettyPacket> : NettyPacket() {
109115
fun respond(packet: P) {
110116
packet.responseTo = uniqueSessionId
111117
?: error("Responding packet has no session id. Are you sure it was sent?")
118+
119+
val responseConnection = responseConnection.get()
120+
if (responseConnection == null) {
121+
log.atWarning()
122+
.log("Cannot respond to packet ${this::class.simpleName} with session ID $uniqueSessionId: original connection has been garbage collected")
123+
124+
if ((::response.getDelegate() as Lazy<*>).isInitialized()) {
125+
response.completeExceptionally(IllegalStateException("Original connection has been garbage collected"))
126+
}
127+
128+
return
129+
}
130+
112131
responseConnection.send(packet)
113132
}
114133

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/CloudPlayer.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import dev.slne.surf.bytebufserializer.Buf
44
import dev.slne.surf.cloud.api.common.netty.network.codec.kotlinx.cloud.CloudPlayerSerializer
55
import dev.slne.surf.cloud.api.common.netty.network.codec.streamCodec
66
import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainer
7+
import dev.slne.surf.cloud.api.common.player.ppdc.PersistentPlayerDataContainerView
78
import dev.slne.surf.cloud.api.common.player.teleport.TeleportCause
89
import dev.slne.surf.cloud.api.common.player.teleport.TeleportFlag
910
import dev.slne.surf.cloud.api.common.player.teleport.WorldLocation
@@ -29,7 +30,7 @@ import kotlin.time.Duration
2930
* it enables sending messages or components to the player.
3031
*/
3132
@Serializable(with = CloudPlayerSerializer::class)
32-
interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but done correctly?
33+
interface CloudPlayer : Audience, OfflineCloudPlayer {
3334
val name: String
3435

3536
override suspend fun latestIpAddress(): Inet4Address
@@ -55,13 +56,19 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but
5556
fun isAfk(): Boolean
5657
suspend fun currentSessionDuration(): Duration
5758

59+
val persistentData: PersistentPlayerDataContainerView
60+
5861
/**
5962
* Performs modifications on the player's persistent data container.
6063
*
61-
* @param block A suspending block to modify the persistent data container.
64+
* @param block A block to modify the persistent data container.
6265
* @return The result of the block execution.
6366
*/
64-
suspend fun <R> withPersistentData(block: PersistentPlayerDataContainer.() -> R): R
67+
fun <R> editPdc(block: PersistentPlayerDataContainer.() -> R): R
68+
69+
@Deprecated("Use non-suspending editPdc method instead", ReplaceWith("editPdc(block)"))
70+
suspend fun <R> withPersistentData(block: PersistentPlayerDataContainer.() -> R): R =
71+
editPdc(block)
6572

6673
/**
6774
* Connects the player to a specified server.

surf-cloud-api/surf-cloud-api-common/src/main/kotlin/dev/slne/surf/cloud/api/common/player/ppdc/ListPersistentPlayerDataType.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ package dev.slne.surf.cloud.api.common.player.ppdc
66
* @param P The primitive type of the list elements.
77
* @param C The complex type of the list elements.
88
*/
9-
interface ListPersistentPlayerDataType<P : Any, C>: PersistentPlayerDataType<List<P>, C> {
9+
interface ListPersistentPlayerDataType<P : Any, C>: PersistentPlayerDataType<List<P>, List<C>> {
1010

1111
/**
1212
* The data type of the elements in the list.

0 commit comments

Comments
 (0)