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

Commit 871f3e5

Browse files
committed
feat: introduce persistent player data tracking and binary tag handling
- Added `TrackingPlayerPersistentDataContainerImpl` for player data tracking with patch-based updates. - Implemented binary tag codecs (`BINARY_TAG_CODEC`, `BINARY_TAG_CODEC_COMPRESSED`) for NBT serialization. - Integrated persistent data container updates with `ClientboundUpdatePlayerPersistentDataContainerPacket`. - Enhanced persistent data handling in `StandaloneCloudPlayerImpl` with tracking and broadcast logic. - Updated `PersistentPlayerDataContainerImpl` with utility methods for patch operations (`applyOps`, `getPatchOps`). - Added error handling for failed data fetches in `onNetworkConnect`. - Optimized collection codec constructors in `ByteBufCodecs` for size constraints and performance.
1 parent 0831ea5 commit 871f3e5

File tree

14 files changed

+450
-13
lines changed

14 files changed

+450
-13
lines changed

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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ import dev.slne.surf.cloud.api.common.netty.protocol.buffer.types.Utf8String
55
import dev.slne.surf.cloud.api.common.util.ByIdMap
66
import dev.slne.surf.cloud.api.common.util.createUnresolvedInetSocketAddress
77
import io.netty.buffer.ByteBuf
8+
import io.netty.buffer.ByteBufInputStream
9+
import io.netty.buffer.ByteBufOutputStream
10+
import io.netty.handler.codec.DecoderException
11+
import io.netty.handler.codec.EncoderException
12+
import it.unimi.dsi.fastutil.objects.ObjectArrayList
813
import net.kyori.adventure.key.Key
14+
import net.kyori.adventure.nbt.BinaryTag
915
import net.kyori.adventure.nbt.BinaryTagIO
16+
import net.kyori.adventure.nbt.BinaryTagType
17+
import net.kyori.adventure.nbt.BinaryTagTypes
1018
import net.kyori.adventure.sound.Sound
1119
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer
1220
import java.io.ByteArrayInputStream
@@ -21,10 +29,13 @@ import java.time.Instant
2129
import java.time.ZoneId
2230
import java.time.ZonedDateTime
2331
import java.util.*
32+
import kotlin.math.min
2433
import kotlin.time.Duration
2534
import kotlin.time.Duration.Companion.milliseconds
2635

2736
object ByteBufCodecs {
37+
const val MAX_INITIAL_COLLECTION_SIZE = 65536
38+
2839
val BOOLEAN_CODEC = streamCodec(ByteBuf::writeBoolean, ByteBuf::readBoolean)
2940
val BYTE_CODEC = streamCodec({ buf, byte -> buf.writeByte(byte.toInt()) }, ByteBuf::readByte)
3041
val SHORT_CODEC =
@@ -138,6 +149,53 @@ object ByteBufCodecs {
138149
BigDecimal(unscaledValue, scale, MathContext(precision))
139150
}
140151

152+
private val TAG_TYPES = arrayOf(
153+
BinaryTagTypes.END,
154+
BinaryTagTypes.BYTE,
155+
BinaryTagTypes.SHORT,
156+
BinaryTagTypes.INT,
157+
BinaryTagTypes.LONG,
158+
BinaryTagTypes.FLOAT,
159+
BinaryTagTypes.DOUBLE,
160+
BinaryTagTypes.BYTE_ARRAY,
161+
BinaryTagTypes.STRING,
162+
BinaryTagTypes.LIST,
163+
BinaryTagTypes.COMPOUND,
164+
BinaryTagTypes.INT_ARRAY,
165+
BinaryTagTypes.LONG_ARRAY,
166+
)
167+
168+
fun getTagType(id: Int): BinaryTagType<*> {
169+
return if (id >= 0 && id < TAG_TYPES.size) TAG_TYPES[id] else error("Unknown tag type id: $id")
170+
}
171+
172+
val BINARY_TAG_CODEC: StreamCodec<ByteBuf, BinaryTag> = streamCodec({ buf, tag ->
173+
val type = tag.type() as BinaryTagType<BinaryTag>
174+
buf.writeByte(type.id().toInt())
175+
ByteBufOutputStream(buf).use {
176+
type.write(tag, it)
177+
}
178+
}, { bytes ->
179+
val typeId = bytes.readByte().toInt()
180+
val type = getTagType(typeId)
181+
ByteBufInputStream(bytes).use {
182+
type.read(it)
183+
}
184+
})
185+
186+
val BINARY_TAG_CODEC_COMPRESSED: StreamCodec<ByteBuf, BinaryTag> = streamCodec({ buf, tag ->
187+
val type = tag.type() as BinaryTagType<BinaryTag>
188+
buf.writeByte(type.id().toInt())
189+
ByteBufOutputStream(buf).use {
190+
type.write(tag, it)
191+
}
192+
}, { bytes ->
193+
val typeId = bytes.readByte().toInt()
194+
val type = getTagType(typeId)
195+
ByteBufInputStream(bytes).use {
196+
type.read(it)
197+
}
198+
})
141199

142200
fun <E : Enum<E>> enumStreamCodec(clazz: Class<E>): StreamCodec<ByteBuf, E> =
143201
streamCodec(ByteBuf::writeEnum) { it.readEnum(clazz) }
@@ -155,4 +213,59 @@ object ByteBufCodecs {
155213
buf.writeVarInt(idGetter(value))
156214
}
157215
}
216+
217+
fun readCount(buffer: ByteBuf, maxSize: Int): Int {
218+
val count = buffer.readVarInt()
219+
if (count > maxSize) {
220+
throw DecoderException("$count elements exceeded max size of: $maxSize")
221+
} else {
222+
return count
223+
}
224+
}
225+
226+
fun writeCount(buffer: ByteBuf, count: Int, maxSize: Int) {
227+
if (count > maxSize) {
228+
throw EncoderException("$count elements exceeded max size of: $maxSize")
229+
} else {
230+
buffer.writeVarInt(count)
231+
}
232+
}
233+
234+
fun <B : ByteBuf, V, C : MutableCollection<V>> collection(
235+
factory: (Int) -> C,
236+
codec: StreamCodec<in B, V>,
237+
maxSize: Int = Int.MAX_VALUE
238+
): StreamCodec<B, C> = object : StreamCodec<B, C> {
239+
override fun decode(buf: B): C {
240+
val count = readCount(buf, maxSize)
241+
val collection = factory(min(count, MAX_INITIAL_COLLECTION_SIZE))
242+
243+
repeat(count) {
244+
collection.add(codec.decode(buf))
245+
}
246+
247+
return collection
248+
}
249+
250+
override fun encode(buf: B, value: C) {
251+
writeCount(buf, value.size, maxSize)
252+
253+
for (element in value) {
254+
codec.encode(buf, element)
255+
}
256+
}
257+
}
258+
259+
fun <B : ByteBuf, V, C : MutableCollection<V>> collection(factory: (Int) -> C): CodecOperation<B, V, C> {
260+
return CodecOperation { size -> collection(factory, size) }
261+
}
262+
263+
264+
fun <B : ByteBuf, V> list(): CodecOperation<B, V, MutableList<V>> {
265+
return CodecOperation { size -> collection(::ObjectArrayList, size) }
266+
}
267+
268+
fun <B : ByteBuf, V> list(maxSize: Int): CodecOperation<B, V, MutableList<V>> {
269+
return CodecOperation { size -> collection(::ObjectArrayList, size, maxSize) }
270+
}
158271
}

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

Lines changed: 4 additions & 1 deletion
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
@@ -28,7 +29,7 @@ import kotlin.time.Duration
2829
* it enables sending messages or components to the player.
2930
*/
3031
@Serializable(with = CloudPlayerSerializer::class)
31-
interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but done correctly?
32+
interface CloudPlayer : Audience, OfflineCloudPlayer {
3233
val name: String
3334

3435
override suspend fun latestIpAddress(): Inet4Address
@@ -54,6 +55,8 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but
5455
fun isAfk(): Boolean
5556
suspend fun currentSessionDuration(): Duration
5657

58+
val persistentData: PersistentPlayerDataContainerView
59+
5760
/**
5861
* Performs modifications on the player's persistent data container.
5962
*

surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/ClientCloudPlayerImpl.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ import dev.slne.surf.cloud.core.common.netty.network.protocol.running.Serverboun
2323
import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerDataResponse.*
2424
import dev.slne.surf.cloud.core.common.player.CommonCloudPlayerImpl
2525
import dev.slne.surf.cloud.core.common.player.ppdc.PersistentPlayerDataContainerImpl
26+
import dev.slne.surf.cloud.core.common.player.ppdc.PersistentPlayerDataContainerViewImpl
2627
import dev.slne.surf.cloud.core.common.util.hasPermissionPlattform
2728
import dev.slne.surf.surfapi.core.api.messages.adventure.getPointer
29+
import dev.slne.surf.surfapi.core.api.nbt.FastCompoundBinaryTag
2830
import dev.slne.surf.surfapi.core.api.nbt.fast
2931
import net.kyori.adventure.audience.Audience
3032
import net.kyori.adventure.audience.MessageType
3133
import net.kyori.adventure.bossbar.BossBar
3234
import net.kyori.adventure.identity.Identity
3335
import net.kyori.adventure.inventory.Book
36+
import net.kyori.adventure.nbt.CompoundBinaryTag
3437
import net.kyori.adventure.resource.ResourcePackRequest
3538
import net.kyori.adventure.sound.Sound
3639
import net.kyori.adventure.sound.Sound.Emitter
@@ -64,6 +67,12 @@ abstract class ClientCloudPlayerImpl<PlatformPlayer : Audience>(
6467
override val connectedToProxy get() = proxyServerName != null
6568
override val connectedToServer get() = serverName != null
6669

70+
var ppdcData: FastCompoundBinaryTag = CompoundBinaryTag.empty().fast()
71+
override val persistentData = object : PersistentPlayerDataContainerViewImpl() {
72+
override fun toTagCompound() = ppdcData
73+
override fun getTag(key: String) = ppdcData.get(key)
74+
}
75+
6776
/**
6877
* The audience for this player. If the player is on this server, this will point to
6978
* the bukkit / velocity player. Otherwise packets will be sent to the player via the network.

surf-cloud-core/surf-cloud-core-client/src/main/kotlin/dev/slne/surf/cloud/core/client/player/CommonClientCloudPlayerManagerImpl.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package dev.slne.surf.cloud.core.client.player
22

3+
import dev.slne.surf.cloud.api.client.netty.packet.fireAndAwaitOrThrow
4+
import dev.slne.surf.cloud.api.common.player.task.PrePlayerJoinTask
5+
import dev.slne.surf.cloud.core.common.messages.MessageManager
6+
import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundRequestPlayerPersistentDataContainer
37
import dev.slne.surf.cloud.core.common.player.CloudPlayerManagerImpl
48
import dev.slne.surf.cloud.core.common.player.CommonOfflineCloudPlayerImpl
59
import dev.slne.surf.cloud.core.common.player.playerManagerImpl
10+
import dev.slne.surf.surfapi.core.api.nbt.fast
11+
import dev.slne.surf.surfapi.core.api.util.logger
612
import net.kyori.adventure.audience.Audience
713
import java.util.*
14+
import java.util.concurrent.TimeUnit
815

916
abstract class CommonClientCloudPlayerManagerImpl<Platform : Audience, P : ClientCloudPlayerImpl<Platform>> :
1017
CloudPlayerManagerImpl<P>() {
18+
private val log = logger()
19+
1120
override suspend fun updateProxyServer(
1221
player: P,
1322
serverName: String
@@ -44,10 +53,32 @@ abstract class CommonClientCloudPlayerManagerImpl<Platform : Audience, P : Clien
4453
return player.serverName
4554
}
4655

47-
override fun getOfflinePlayer(uuid: UUID, createIfNotExists: Boolean): CommonOfflineCloudPlayerImpl {
56+
override fun getOfflinePlayer(
57+
uuid: UUID,
58+
createIfNotExists: Boolean
59+
): CommonOfflineCloudPlayerImpl {
4860
return OfflineCloudPlayerImpl(uuid, createIfNotExists)
4961
}
5062

63+
override suspend fun onNetworkConnect(uuid: UUID, player: P) {
64+
try {
65+
val ppdcData = ServerboundRequestPlayerPersistentDataContainer(uuid)
66+
.fireAndAwaitOrThrow()
67+
.nbt
68+
69+
player.ppdcData = ppdcData.fast()
70+
} catch (e: Exception) {
71+
log.atWarning()
72+
.withCause(e)
73+
.atMostEvery(5, TimeUnit.SECONDS)
74+
.log("Could not fetch persistent player data container for player $uuid, denying join")
75+
76+
throw PreJoinDenied(PrePlayerJoinTask.Result.DENIED(MessageManager.couldNotFetchPlayerData))
77+
}
78+
79+
super.onNetworkConnect(uuid, player)
80+
}
81+
5182
abstract fun getAudience(uuid: UUID): Audience?
5283
}
5384

surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/messages/MessageManager.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@ object MessageManager { // TODO: Add more messages
6969
)
7070
}
7171

72+
val couldNotFetchPlayerData = buildText {
73+
CommonComponents.renderDisconnectMessage(
74+
this,
75+
"FEHLER BEIM LADEN DEINER SPIELERDATEN",
76+
{
77+
error("Beim Laden deiner Spielerdaten ist ein Fehler aufgetreten.")
78+
appendNewline()
79+
error("Bitte versuche es erneut.")
80+
},
81+
true
82+
)
83+
}
84+
7285
fun formatZonedDateTime(time: ZonedDateTime?) = buildText {
7386
if (time == null) {
7487
variableValue("N/A")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package dev.slne.surf.cloud.core.common.netty.network.protocol.running
2+
3+
import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket
4+
import dev.slne.surf.cloud.api.common.netty.network.codec.ByteBufCodecs
5+
import dev.slne.surf.cloud.api.common.netty.network.codec.streamCodecComposite
6+
import dev.slne.surf.cloud.api.common.netty.network.protocol.PacketFlow
7+
import dev.slne.surf.cloud.api.common.netty.packet.NettyPacket
8+
import dev.slne.surf.cloud.core.common.player.ppdc.network.PdcPatch
9+
import java.util.*
10+
11+
@SurfNettyPacket("cloud:clientbound:player_pdc/update", PacketFlow.CLIENTBOUND)
12+
class ClientboundUpdatePlayerPersistentDataContainerPacket(
13+
val uuid: UUID,
14+
val patch: PdcPatch
15+
) : NettyPacket() {
16+
companion object {
17+
val STREAM_CODEC = streamCodecComposite(
18+
ByteBufCodecs.UUID_CODEC,
19+
ClientboundUpdatePlayerPersistentDataContainerPacket::uuid,
20+
PdcPatch.STREAM_CODEC,
21+
ClientboundUpdatePlayerPersistentDataContainerPacket::patch,
22+
::ClientboundUpdatePlayerPersistentDataContainerPacket
23+
)
24+
}
25+
}

surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/player/CloudPlayerManagerImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ abstract class CloudPlayerManagerImpl<P : CommonCloudPlayerImpl> : CloudPlayerMa
300300
open fun terminate() {}
301301

302302

303-
private class PreJoinDenied(val result: PrePlayerJoinTask.Result) : RuntimeException() {
303+
class PreJoinDenied(val result: PrePlayerJoinTask.Result) : RuntimeException() {
304304
companion object {
305305
@Serial
306306
private const val serialVersionUID: Long = -5043277924406776272L

0 commit comments

Comments
 (0)