Skip to content

Commit

Permalink
Provide a basic implementation of an entity rule manager
Browse files Browse the repository at this point in the history
  • Loading branch information
Aeltumn committed Sep 25, 2024
1 parent 07fbc3c commit 24e584c
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.noxcrew.noxesium.paper.api

import com.noxcrew.noxesium.paper.api.event.NoxesiumPlayerRegisteredEvent
import com.noxcrew.noxesium.paper.api.network.clientbound.ClientboundSetExtraEntityDataPacket
import com.noxcrew.noxesium.paper.api.rule.RemoteServerRule
import io.papermc.paper.event.player.PlayerTrackEntityEvent
import org.bukkit.Bukkit
import org.bukkit.entity.Entity
import org.bukkit.event.EventHandler
import org.bukkit.event.HandlerList
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityRemoveEvent
import java.util.WeakHashMap

/**
* Provides a simple implementation for changing entity rules on entities.
*
* This is not used internally, so you should use it as a reference implementation
* to tweak yourself. Feel free to merge any improvements you make upstream.
*
* For reference, MCC uses a custom implementation of this baked into our own
* fake entity system where we send all entities entirely through packets and manage
* them custom, hence we do not need a system such as this.
*/
public class EntityRuleManager(private val manager: NoxesiumManager) : Listener {

private val entities = WeakHashMap<Entity, RuleHolder>()
private var task: Int = -1

/**
* Registers this manager.
*/
public fun register() {
Bukkit.getPluginManager().registerEvents(this, manager.plugin)

// Send rule updates once a tick in a batch
task = Bukkit.getScheduler().scheduleSyncRepeatingTask(manager.plugin, {
for ((entity, holder) in entities) {
if (!holder.needsUpdate) continue
holder.markAllUpdated()

// Send the packet to all players that can see it
for (player in Bukkit.getOnlinePlayers()) {
if (!player.canSee(entity)) continue
manager.sendPacket(player,
ClientboundSetExtraEntityDataPacket(
entity.entityId,
holder.rules
// Only include rules that need to be updated!
.filter { it.value.changePending }
// Only include rules that are available to this player!
.filter { manager.entityRules.isAvailable(it.key, manager.getProtocolVersion(player) ?: -1) }
.ifEmpty { null }
?.mapValues { (_, rule) ->
{ buffer -> (rule as RemoteServerRule<Any>).write(rule.value, buffer) }
} ?: continue
)
)
}
}
}, 1, 1)
}

/**
* Unregisters this manager.
*/
public fun unregister() {
HandlerList.unregisterAll(this)
Bukkit.getScheduler().cancelTask(task)
}

/** Returns the given [rule] for [entity]. */
public fun <T : Any> getEntityRule(entity: Entity, rule: RuleFunction<T>): RemoteServerRule<T>? =
getEntityRule(entity, rule.index)

/** Returns the given [ruleIndex] for [entity]. */
public fun <T : Any> getEntityRule(entity: Entity, ruleIndex: Int): RemoteServerRule<T>? =
entities.computeIfAbsent(entity) { RuleHolder() }.let { holder ->
manager.entityRules.create(ruleIndex, holder)
}

/**
* Send all entities' data when a player gets registered with Noxesium so
* they can properly see entities that were sent to them already.
*/
@EventHandler
public fun onNoxesiumPlayerRegistered(e: NoxesiumPlayerRegisteredEvent) {
val player = e.player
val protocol = e.protocolVersion

for ((entity, holder) in entities) {
if (!player.canSee(entity)) continue
manager.sendPacket(player,
ClientboundSetExtraEntityDataPacket(
entity.entityId,
holder.rules
// Only include rules that are available to this player!
.filter { manager.entityRules.isAvailable(it.key, protocol) }
.ifEmpty { null }
?.mapValues { (_, rule) ->
{ buffer -> (rule as RemoteServerRule<Any>).write(rule.value, buffer) }
} ?: continue
)
)
}
}

/**
* When an entity starts being shown to a player we
* send its data along as well.
*/
@EventHandler
public fun onEntityShown(e: PlayerTrackEntityEvent) {
val holder = entities[e.entity] ?: return
val protocol = manager.getProtocolVersion(e.player) ?: return

manager.sendPacket(e.player,
ClientboundSetExtraEntityDataPacket(
e.entity.entityId,
holder.rules
// Only include rules that are available to this player!
.filter { manager.entityRules.isAvailable(it.key, protocol) }
.ifEmpty { null }
?.mapValues { (_, rule) ->
{ buffer -> (rule as RemoteServerRule<Any>).write(rule.value, buffer) }
} ?: return
)
)
}

/**
* Remove data stored for an entity when the entity
* gets removed. This also happens indirectly because we
* use a WeakHashMap.
*/
@EventHandler
public fun onEntityRemoved(e: EntityRemoveEvent) {
entities.remove(e.entity)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.noxcrew.noxesium.api.protocol.ClientSettings
import com.noxcrew.noxesium.api.protocol.NoxesiumFeature
import com.noxcrew.noxesium.api.protocol.NoxesiumServerManager
import com.noxcrew.noxesium.api.protocol.ProtocolVersion
import com.noxcrew.noxesium.paper.api.event.NoxesiumPlayerRegisteredEvent
import com.noxcrew.noxesium.paper.api.network.NoxesiumPacket
import com.noxcrew.noxesium.paper.api.network.NoxesiumPackets
import com.noxcrew.noxesium.paper.api.network.clientbound.ClientboundChangeServerRulesPacket
Expand All @@ -21,6 +22,7 @@ import org.bukkit.event.HandlerList
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.plugin.Plugin
import org.bukkit.scheduler.BukkitTask
import org.slf4j.Logger
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
Expand All @@ -33,26 +35,16 @@ public open class NoxesiumManager(
public val logger: Logger,
) : NoxesiumServerManager<Player>, Listener {

/** Stores information sent to a client. */
public data class NoxesiumProfile(
/** All rules sent to this client. */
public val rules: MutableMap<Int, RemoteServerRule<*>> = ConcurrentHashMap(),
) {

/** Whether this profile has pending updates. */
public val needsUpdate: Boolean
get() = rules.values.any { it.changePending }
}

private val players = ConcurrentHashMap<UUID, Int>()
private val settings = ConcurrentHashMap<UUID, ClientSettings>()
private val profiles = ConcurrentHashMap<UUID, NoxesiumProfile>()
private val profiles = ConcurrentHashMap<UUID, RuleHolder>()
private val ready = ConcurrentHashMap.newKeySet<UUID>()
private val pending = ConcurrentHashMap<UUID, Pair<Int, String>>()

private lateinit var v0: BaseNoxesiumListener
private lateinit var v1: BaseNoxesiumListener
private lateinit var v2: BaseNoxesiumListener
private var task: Int = -1

/** Stores all registered server rules. */
public val serverRules: RuleContainer = RuleContainer()
Expand All @@ -72,7 +64,7 @@ public open class NoxesiumManager(
v2 = NoxesiumListenerV2(plugin, logger, this).register()

// Send server rule updates once a tick in a batch
Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, {
task = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, {
for ((player, profile) in profiles) {
if (!profile.needsUpdate) continue
updateServerRules(Bukkit.getPlayer(player) ?: continue, profile)
Expand Down Expand Up @@ -103,6 +95,8 @@ public open class NoxesiumManager(
public fun unregister() {
HandlerList.unregisterAll(this)

Bukkit.getScheduler().cancelTask(task)

v0.unregister()
v1.unregister()
v2.unregister()
Expand Down Expand Up @@ -165,6 +159,9 @@ public open class NoxesiumManager(
// to set default values for the rules
onPlayerRegistered(player)

// Emit an event for hooking into
Bukkit.getPluginManager().callEvent(NoxesiumPlayerRegisteredEvent(player, protocolVersion, version))

// Send updated rules to this player
updateServerRules(player)
}
Expand All @@ -180,7 +177,7 @@ public open class NoxesiumManager(
* Sends updated server rules to [player] based on all rules in [profile]
* that need to be updated.
*/
private fun updateServerRules(player: Player, profile: NoxesiumProfile) {
private fun updateServerRules(player: Player, profile: RuleHolder) {
sendPacket(
player,
ClientboundChangeServerRulesPacket(
Expand Down Expand Up @@ -211,8 +208,8 @@ public open class NoxesiumManager(
getServerRule(player, rule.index)

override fun <T : Any> getServerRule(player: Player, index: Int): RemoteServerRule<T>? =
profiles.computeIfAbsent(player.uniqueId) { NoxesiumProfile() }.let { profile ->
serverRules.create(index, profile.rules, getProtocolVersion(player) ?: -1)
profiles.computeIfAbsent(player.uniqueId) { RuleHolder() }.let { holder ->
serverRules.create(index, holder, getProtocolVersion(player) ?: -1)
}

override fun getClientSettings(player: Player): ClientSettings? =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ public data class RuleFunction<T : Any>(
public val constructor: (Int) -> RemoteServerRule<T>,
)

/** Stores rule objects for an object. */
public data class RuleHolder(
/** All rules sent to this client. */
public val rules: MutableMap<Int, RemoteServerRule<*>> = ConcurrentHashMap(),
) : MutableMap<Int, RemoteServerRule<*>> by rules {

/** Whether this profile has pending updates. */
public val needsUpdate: Boolean
get() = rules.values.any { it.changePending }

/** Marks all rules as having been updated. */
public fun markAllUpdated() {
rules.values.forEach { it.changePending = false }
}
}

/** Provides a container that holds rule types. */
public data class RuleContainer(
private val rules: MutableMap<Int, RuleFunction<*>> = ConcurrentHashMap(),
Expand All @@ -31,9 +47,10 @@ public data class RuleContainer(
}

/** Creates a new rule object, to be stored in the given map. */
public fun <T : Any> create(index: Int, storage: MutableMap<Int, RemoteServerRule<*>>, version: Int): RemoteServerRule<T>? {
public fun <T : Any> create(index: Int, storage: MutableMap<Int, RemoteServerRule<*>>, version: Int? = null): RemoteServerRule<T>? {
// Ensure that this player has the required protocol version, otherwise return `null`.
if (version < (minimumProtocols[index] ?: throw IllegalArgumentException("Cannot find rule with index $index"))) return null
if (version != null && version < (minimumProtocols[index] ?: throw IllegalArgumentException("Cannot find rule with index $index"))) return null

return storage.computeIfAbsent(index) {
val function = rules[index] ?: throw IllegalArgumentException("Cannot find rule with index $index")
function.constructor(index)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.noxcrew.noxesium.paper.api.event

import org.bukkit.entity.Player
import org.bukkit.event.HandlerList
import org.bukkit.event.player.PlayerEvent

/** Emitted by [NoxesiumManager] when it registers a new player. */
public class NoxesiumPlayerRegisteredEvent(
player: Player,
/** The new protocol version of the player. */
public val protocolVersion: Int,
/** The raw version string of the player's installed Noxesium jar. */
public val version: String,
) : PlayerEvent(player) {

public companion object {
@JvmStatic
public val HANDLER_LIST: HandlerList = HandlerList()

@JvmStatic
public fun getHandlerList(): HandlerList = HANDLER_LIST
}

override fun getHandlers(): HandlerList = HANDLER_LIST
}

0 comments on commit 24e584c

Please sign in to comment.