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

Commit d7ff58b

Browse files
committed
feat: implement AFK detection and state management for players
1 parent bcbd7b3 commit d7ff58b

File tree

14 files changed

+180
-5
lines changed

14 files changed

+180
-5
lines changed

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package dev.slne.surf.cloud.api.common.netty.packet
22

3-
import dev.slne.surf.bytebufserializer.Buf
43
import dev.slne.surf.cloud.api.common.meta.PacketCodec
54
import dev.slne.surf.cloud.api.common.meta.SurfNettyPacket
65
import dev.slne.surf.cloud.api.common.netty.network.codec.StreamCodec
76
import dev.slne.surf.cloud.api.common.netty.network.codec.StreamDecoder
87
import dev.slne.surf.cloud.api.common.netty.network.codec.StreamMemberEncoder
98
import dev.slne.surf.cloud.api.common.netty.network.codec.kotlinx.SurfCloudBufSerializer
9+
import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf
1010
import dev.slne.surf.cloud.api.common.util.mutableObject2ObjectMapOf
1111
import io.netty.buffer.ByteBuf
1212
import kotlinx.serialization.InternalSerializationApi
1313
import kotlinx.serialization.KSerializer
14+
import kotlinx.serialization.serializer
1415
import kotlinx.serialization.serializerOrNull
1516
import kotlin.reflect.KClass
1617
import kotlin.reflect.full.companionObject
@@ -66,6 +67,20 @@ private const val DEFAULT_STREAM_CODEC_NAME = "STREAM_CODEC"
6667
private val codecCache = mutableObject2ObjectMapOf<KClass<out NettyPacket>, StreamCodec<*, *>>()
6768

6869

70+
@OptIn(InternalSerializationApi::class)
71+
fun <P : NettyPacket> KClass<out P>.createCodec(): StreamCodec<SurfByteBuf, P> {
72+
val serializer = serializer()
73+
return object : StreamCodec<SurfByteBuf, P> {
74+
override fun decode(buf: SurfByteBuf): P {
75+
return SurfCloudBufSerializer.serializer.decodeFromBuf(buf, serializer)
76+
}
77+
78+
override fun encode(buf: SurfByteBuf, value: P) {
79+
SurfCloudBufSerializer.serializer.encodeToBuf(buf, serializer as KSerializer<P>, value)
80+
}
81+
}
82+
}
83+
6984
/**
7085
* Finds a [StreamCodec] for the specified packet type if available.
7186
*

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ interface CloudPlayer : Audience, OfflineCloudPlayer { // TODO: conversation but
3535
*/
3636
val connected get() = connectedToProxy || connectedToServer
3737

38+
suspend fun isAfk(): Boolean
39+
3840
/**
3941
* Performs modifications on the player's persistent data container.
4042
*

surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/CloudBukkitInstance.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import com.google.auto.service.AutoService
44
import dev.slne.surf.cloud.api.common.CloudInstance
55
import dev.slne.surf.cloud.bukkit.listener.ListenerManager
66
import dev.slne.surf.cloud.bukkit.netty.BukkitNettyManager
7+
import dev.slne.surf.cloud.bukkit.processor.BukkitListenerProcessor
78
import dev.slne.surf.cloud.core.client.ClientCommonCloudInstance
89
import dev.slne.surf.cloud.core.common.coreCloudInstance
10+
import dev.slne.surf.cloud.core.common.util.bean
911
import dev.slne.surf.cloud.core.common.util.checkInstantiationByServiceLoader
1012

1113
@AutoService(CloudInstance::class)
@@ -17,6 +19,7 @@ class CloudBukkitInstance : ClientCommonCloudInstance(BukkitNettyManager) {
1719
override suspend fun onEnable() {
1820
super.onEnable()
1921

22+
bean<BukkitListenerProcessor>().registerListeners()
2023
ListenerManager.registerListeners()
2124
}
2225

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package dev.slne.surf.cloud.bukkit.listener.player
2+
3+
import dev.slne.surf.cloud.api.client.netty.packet.fireAndForget
4+
import dev.slne.surf.cloud.core.common.netty.network.protocol.running.ServerboundUpdateAFKState
5+
import dev.slne.surf.surfapi.core.api.util.mutableObject2BooleanMapOf
6+
import dev.slne.surf.surfapi.core.api.util.mutableObject2LongMapOf
7+
import org.bukkit.event.EventHandler
8+
import org.bukkit.event.Listener
9+
import org.bukkit.event.player.PlayerJoinEvent
10+
import org.bukkit.event.player.PlayerMoveEvent
11+
import org.bukkit.event.player.PlayerQuitEvent
12+
import org.springframework.scheduling.annotation.Scheduled
13+
import org.springframework.stereotype.Component
14+
import java.util.*
15+
import java.util.concurrent.TimeUnit
16+
import kotlin.time.Duration.Companion.seconds
17+
18+
@Component
19+
class PlayerAfkListener : Listener {
20+
private val afkTime = 10.seconds.inWholeMilliseconds
21+
private val lastMovedTime = mutableObject2LongMapOf<UUID>()
22+
private val currentSentState = mutableObject2BooleanMapOf<UUID>()
23+
24+
@EventHandler
25+
fun onPlayerMove(event: PlayerMoveEvent) {
26+
if (!event.hasChangedOrientation()) return
27+
if (!event.hasChangedPosition()) return
28+
lastMovedTime[event.player.uniqueId] = System.currentTimeMillis()
29+
}
30+
31+
@EventHandler
32+
fun onPlayerJoin(event: PlayerJoinEvent) {
33+
lastMovedTime[event.player.uniqueId] = System.currentTimeMillis()
34+
}
35+
36+
@EventHandler
37+
fun onPlayerQuit(event: PlayerQuitEvent) {
38+
val uuid = event.player.uniqueId
39+
lastMovedTime.removeLong(uuid)
40+
currentSentState.removeBoolean(uuid)
41+
}
42+
43+
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.SECONDS)
44+
fun afkCheckTask() {
45+
val currentTime = System.currentTimeMillis()
46+
47+
lastMovedTime.object2LongEntrySet().fastForEach { entry ->
48+
val uuid = entry.key
49+
val lastMoved = entry.longValue
50+
val timeSinceLastMove = currentTime - lastMoved
51+
val isAfk = timeSinceLastMove >= afkTime
52+
val previousState = currentSentState.put(uuid, isAfk)
53+
if (previousState != isAfk) {
54+
broadcastChange(uuid, isAfk)
55+
}
56+
}
57+
}
58+
59+
private fun broadcastChange(uuid: UUID, isAfk: Boolean) {
60+
ServerboundUpdateAFKState(uuid, isAfk).fireAndForget()
61+
}
62+
}

surf-cloud-bukkit/src/main/kotlin/dev/slne/surf/cloud/bukkit/processor/BukkitListenerProcessor.kt

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dev.slne.surf.cloud.bukkit.processor
22

33
import dev.slne.surf.cloud.api.common.util.isAnnotated
44
import dev.slne.surf.cloud.api.common.util.isCandidateFor
5+
import dev.slne.surf.cloud.api.common.util.mutableObjectListOf
56
import dev.slne.surf.cloud.api.common.util.selectFunctions
67
import dev.slne.surf.cloud.api.common.util.ultimateTargetClass
78
import org.bukkit.Bukkit
@@ -21,6 +22,8 @@ import java.lang.reflect.Method
2122

2223
@Component
2324
class BukkitListenerProcessor : BeanPostProcessor {
25+
private val listeners = mutableObjectListOf<ListenerMetaData>()
26+
2427
override fun postProcessAfterInitialization(bean: Any, beanName: String): Any {
2528
if (bean is AopInfrastructureBean) return bean
2629

@@ -52,7 +55,7 @@ class BukkitListenerProcessor : BeanPostProcessor {
5255
}
5356

5457
val eventParam = params[0]
55-
if (!eventParam.isAssignableFrom(Event::class.java)) {
58+
if (!Event::class.java.isAssignableFrom(eventParam)) {
5659
throw BeanCreationException(
5760
beanName,
5861
"Event handler method parameter must be a subclass of Event"
@@ -77,7 +80,26 @@ class BukkitListenerProcessor : BeanPostProcessor {
7780
throw EventException(e, "Error invoking event handler")
7881
}
7982
}
80-
registerEventHandler(bean, eventClass, eventHandler, eventExecutor)
83+
84+
listeners.add(
85+
ListenerMetaData(
86+
bean,
87+
eventClass,
88+
eventHandler,
89+
eventExecutor
90+
)
91+
)
92+
}
93+
}
94+
95+
fun registerListeners() {
96+
for (listener in listeners) {
97+
registerEventHandler(
98+
listener.bean,
99+
listener.event,
100+
listener.eventHandler,
101+
listener.eventExecutor
102+
)
81103
}
82104
}
83105

@@ -104,4 +126,11 @@ class BukkitListenerProcessor : BeanPostProcessor {
104126
private fun getPluginFromBean(bean: Any): JavaPlugin {
105127
return JavaPlugin.getProvidingPlugin(bean.javaClass)
106128
}
129+
130+
data class ListenerMetaData(
131+
val bean: Any,
132+
val event: Class<out Event>,
133+
val eventHandler: EventHandler,
134+
val eventExecutor: EventExecutor
135+
)
107136
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ abstract class ClientCloudPlayerImpl<PlatformPlayer : Audience>(uuid: UUID) :
4949
override val connectedToProxy get() = proxyServerUid != null
5050

5151
override val connectedToServer get() = serverUid != null
52-
5352
/**
5453
* The audience for this player. If the player is on this server, this will point to
5554
* the bukkit / velocity player. Otherwise packets will be sent to the player via the network.
@@ -74,6 +73,10 @@ abstract class ClientCloudPlayerImpl<PlatformPlayer : Audience>(uuid: UUID) :
7473
return request<FirstSeen>(DataRequestType.FIRST_SEEN).firstSeen
7574
}
7675

76+
override suspend fun isAfk(): Boolean {
77+
return request<IsAFK>(DataRequestType.IS_AFK).isAfk
78+
}
79+
7780
override suspend fun <R> withPersistentData(block: PersistentPlayerDataContainer.() -> R): R {
7881
val response = ServerboundRequestPlayerPersistentDataContainer(uuid).fireAndAwaitOrThrow()
7982

surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/ConnectionImpl.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ class ConnectionImpl(
343343
is TeleportPlayerPacket -> listener.handleTeleportPlayer(msg)
344344
is ServerboundShutdownServerPacket -> listener.handleShutdownServer(msg)
345345
is ServerboundRequestPlayerDataPacket -> listener.handleRequestPlayerData(msg)
346+
is ServerboundUpdateAFKState -> listener.handleUpdateAFKState(msg)
346347

347348
else -> listener.handlePacket(msg) // handle other packets
348349
}

surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningProtocols.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package dev.slne.surf.cloud.core.common.netty.network.protocol.running
22

33
import dev.slne.surf.cloud.api.common.netty.network.ConnectionProtocol
4+
import dev.slne.surf.cloud.api.common.netty.packet.createCodec
5+
import dev.slne.surf.cloud.api.common.netty.packet.findPacketCodec
46
import dev.slne.surf.cloud.api.common.netty.protocol.buffer.SurfByteBuf
57
import dev.slne.surf.cloud.core.common.netty.network.protocol.ProtocolInfoBuilder
68
import dev.slne.surf.cloud.core.common.netty.network.protocol.common.*
@@ -95,6 +97,7 @@ object RunningProtocols {
9597
.addPacket(ServerboundShutdownServerPacket.STREAM_CODEC)
9698
.addPacket(RequestOfflineDisplayNamePacket.STREAM_CODEC)
9799
.addPacket(ServerboundRequestPlayerDataPacket.STREAM_CODEC)
100+
.addPacket(ServerboundUpdateAFKState::class.createCodec())
98101
}
99102

100103
val SERVERBOUND by lazy { SERVERBOUND_TEMPLATE.freeze().bind(::SurfByteBuf) }

surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/RunningServerPacketListener.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ interface RunningServerPacketListener : ServerCommonPacketListener, TickablePack
6363
suspend fun handleShutdownServer(packet: ServerboundShutdownServerPacket)
6464

6565
suspend fun handleRequestPlayerData(packet: ServerboundRequestPlayerDataPacket)
66+
67+
fun handleUpdateAFKState(packet: ServerboundUpdateAFKState)
6668

6769
fun handlePacket(packet: NettyPacket)
6870
}

surf-cloud-core/surf-cloud-core-common/src/main/kotlin/dev/slne/surf/cloud/core/common/netty/network/protocol/running/ServerboundRequestPlayerDataPacket.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ class ServerboundRequestPlayerDataPacket(val uuid: UUID, val type: DataRequestTy
8181
override suspend fun readData(player: OfflineCloudPlayer): DataResponse {
8282
return Playtime(player.playtime())
8383
}
84+
},
85+
IS_AFK(::IsAFK) {
86+
override suspend fun readData(player: OfflineCloudPlayer): DataResponse {
87+
val player= player.player ?: error("Player is not online")
88+
return IsAFK(player.isAfk())
89+
}
8490
};
8591

8692
abstract suspend fun readData(player: OfflineCloudPlayer): DataResponse
@@ -174,6 +180,14 @@ class ServerboundRequestPlayerDataResponse(val data: DataResponse) : ResponseNet
174180
playtime.writeToByteBuf(buf)
175181
}
176182
}
183+
184+
class IsAFK(val isAfk: Boolean) : DataResponse(DataRequestType.IS_AFK) {
185+
constructor(buf: SurfByteBuf) : this(buf.readBoolean())
186+
187+
override fun write(buf: SurfByteBuf) {
188+
buf.writeBoolean(isAfk)
189+
}
190+
}
177191
}
178192

179193
inline fun <reified T> DataResponse.getGenericValue(): T = when (this) {
@@ -185,5 +199,6 @@ inline fun <reified T> DataResponse.getGenericValue(): T = when (this) {
185199
is Name -> check(T::class == String::class) { "Expected String" }.let { name as T }
186200
is NameHistory -> check(T::class == ApiNameHistory::class) { "Expected ApiNameHistory" }.let { history as T }
187201
is Playtime -> check(T::class == ApiPlaytime::class) { "Expected ApiPlaytime" }.let { playtime as T }
202+
is IsAFK -> check(T::class == Boolean::class) { "Expected Boolean" }.let { isAfk as T }
188203
else -> error("Unknown DataResponse type: ${this::class.simpleName}")
189204
}

0 commit comments

Comments
 (0)