Make synchronized block entity use chunk grid to blazingly fast determine potential players to network stuff to

This commit is contained in:
DBotThePony 2022-09-29 23:50:48 +07:00
parent 77aed4871f
commit 177a380bf3
Signed by: DBot
GPG Key ID: DCC23B5715498507
2 changed files with 189 additions and 6 deletions

View File

@ -18,6 +18,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ru.dbotthepony.mc.otm.android.AndroidResearchManager;
import ru.dbotthepony.mc.otm.android.feature.NanobotsArmorFeature;
import ru.dbotthepony.mc.otm.block.entity.SynchronizedBlockEntity;
import ru.dbotthepony.mc.otm.block.entity.blackhole.ExplosionQueue;
import ru.dbotthepony.mc.otm.capability.MatteryCapability;
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability;
@ -135,6 +136,14 @@ public final class OverdriveThatMatters {
EVENT_BUS.addListener(EventPriority.NORMAL, AndroidResearchManager.INSTANCE::reloadEvent);
EVENT_BUS.addListener(EventPriority.NORMAL, AndroidResearchManager.INSTANCE::syncEvent);
EVENT_BUS.addListener(EventPriority.NORMAL, SynchronizedBlockEntity.Companion::onServerStopping);
EVENT_BUS.addListener(EventPriority.NORMAL, SynchronizedBlockEntity.Companion::onServerStarting);
EVENT_BUS.addListener(EventPriority.NORMAL, SynchronizedBlockEntity.Companion::onWatch);
EVENT_BUS.addListener(EventPriority.NORMAL, SynchronizedBlockEntity.Companion::onForget);
EVENT_BUS.addListener(EventPriority.NORMAL, SynchronizedBlockEntity.Companion::playerDisconnected);
EVENT_BUS.addListener(EventPriority.NORMAL, SynchronizedBlockEntity.Companion::playerChangeDimensions);
EVENT_BUS.addListener(EventPriority.NORMAL, SynchronizedBlockEntity.Companion::playerCloneEvent);
MatteryPlayerNetworkChannel.INSTANCE.register();
MenuNetworkChannel.INSTANCE.register();
WeaponNetworkChannel.INSTANCE.register();

View File

@ -1,9 +1,24 @@
package ru.dbotthepony.mc.otm.block.entity
import it.unimi.dsi.fastutil.longs.Long2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import net.minecraft.core.BlockPos
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.event.TickEvent
import net.minecraftforge.event.entity.player.PlayerEvent
import net.minecraftforge.event.entity.player.PlayerEvent.PlayerChangedDimensionEvent
import net.minecraftforge.event.level.ChunkWatchEvent
import net.minecraftforge.event.server.ServerStartingEvent
import net.minecraftforge.event.server.ServerStoppingEvent
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.core.component1
import ru.dbotthepony.mc.otm.core.component2
import ru.dbotthepony.mc.otm.core.component3
@ -11,6 +26,58 @@ import ru.dbotthepony.mc.otm.core.position
import ru.dbotthepony.mc.otm.network.BlockEntitySyncPacket
import ru.dbotthepony.mc.otm.network.FieldSynchronizer
import ru.dbotthepony.mc.otm.network.WorldNetworkChannel
import java.lang.ref.WeakReference
import java.util.LinkedList
import java.util.WeakHashMap
private data class Subscribers(val blockEntities: LinkedList<WeakReference<BlockEntity>> = LinkedList(), val players: LinkedList<ServerPlayer> = LinkedList(), val level: WeakReference<Level>, val chunkPos: Long) {
fun cleanUpBlocks() {
val listIterator = blockEntities.listIterator()
for (block in listIterator) {
if (block.get() == null) {
listIterator.remove()
}
}
}
fun subscribe(blockEntity: BlockEntity) {
for (value in blockEntities) {
if (value.get() === blockEntity) {
return
}
}
blockEntities.add(WeakReference(blockEntity))
}
fun unsubscribe(blockEntity: BlockEntity): Boolean {
val listIterator = blockEntities.listIterator()
for (value in listIterator) {
if (value.get() === blockEntity) {
listIterator.remove()
break
}
}
if (players.isNotEmpty()) {
return false
}
cleanUpBlocks()
return blockEntities.isEmpty()
}
val isEmpty: Boolean get() {
if (!players.isEmpty()) {
return false
}
cleanUpBlocks()
return blockEntities.isEmpty()
}
}
abstract class SynchronizedBlockEntity(p_155228_: BlockEntityType<*>, p_155229_: BlockPos, p_155230_: BlockState) : BlockEntity(p_155228_, p_155229_, p_155230_) {
val synchronizer = FieldSynchronizer()
@ -21,17 +88,46 @@ abstract class SynchronizedBlockEntity(p_155228_: BlockEntityType<*>, p_155229_:
open val synchronizationRadius: Double = 32.0
// TODO: Optimize, if required
override fun setLevel(p_155231_: Level) {
super.setLevel(p_155231_)
unsubscribe()
_subCache = null
}
private var _subCache: Subscribers? = null
private fun unsubscribe() {
val subCache = _subCache ?: return
if (subCache.unsubscribe(this)) {
val level = subCache.level.get()
if (level != null) {
playerMap.get(level)?.remove(subCache.chunkPos)
}
}
}
private fun subscribe(): LinkedList<ServerPlayer> {
unsubscribe()
val subs = playerMap.computeIfAbsent(level) { Long2ObjectAVLTreeMap() }.computeIfAbsent(ChunkPos(blockPos).toLong(), Long2ObjectFunction { Subscribers(level = WeakReference(level), chunkPos = ChunkPos(blockPos).toLong()) })
subs.subscribe(this)
_subCache = subs
return subs.players
}
private val playerSubscribers get() = _subCache?.players ?: subscribe()
fun synchronizeToPlayers() {
if (synchronizationRadius <= 0.0 || synchronizer.isEmpty) {
return
}
check(level is ServerLevel) { "Invalid realm or Level is null" }
synchronizer.observe()
val server = level?.server ?: return
if (server.playerCount <= 0) {
if (playerSubscribers.isEmpty()) {
return
}
@ -42,8 +138,8 @@ abstract class SynchronizedBlockEntity(p_155228_: BlockEntityType<*>, p_155229_:
val double = synchronizationRadius * synchronizationRadius
server.playerList.players.stream().filter {
if (!it.isAlive || it.level != level) {
playerSubscribers.stream().filter {
if (!it.isAlive) {
return@filter false
}
@ -61,4 +157,82 @@ abstract class SynchronizedBlockEntity(p_155228_: BlockEntityType<*>, p_155229_:
}
}
/**
* Why track player-tracked chunks?
*
* because minecraft itself doesn't track them, well,
* in "section partitioning" way.
*
* just look at [net.minecraft.server.level.PlayerMap]
*
* the [net.minecraft.server.level.PlayerMap.getPlayers] straight ignores chunk position
* and just fetches all players
*
* even if they did not ignore that argument, you still have to fetch player *list* though
* [net.minecraft.server.level.ChunkMap.getPlayers], which is not something we want
*/
companion object {
private val LOGGER = LogManager.getLogger()
private val playerMap = WeakHashMap<Level, Long2ObjectAVLTreeMap<Subscribers>>()
fun onServerStopping(event: ServerStoppingEvent) {
playerMap.clear()
}
fun onServerStarting(event: ServerStartingEvent) {
playerMap.clear()
}
fun onWatch(event: ChunkWatchEvent.Watch) {
playerMap.computeIfAbsent(event.level) { Long2ObjectAVLTreeMap() }.computeIfAbsent(event.pos.toLong(), Long2ObjectFunction { Subscribers(level = WeakReference(event.level), chunkPos = event.pos.toLong()) }).let {
val (blocks, players) = it
if (event.player in players) {
LOGGER.error("Player ${event.player} started 'watching' chunk ${event.chunk} more than once.")
} else {
players.add(event.player)
}
}
}
fun onForget(event: ChunkWatchEvent.UnWatch) {
val subs = playerMap.get(event.level)?.get(event.pos.toLong()) ?: return LOGGER.error("Player ${event.player} stopped 'watching' chunk at ${event.pos} before starting to watch it/stopped 'watching' more than once.")
if (subs.players.remove(event.player)) {
if (subs.isEmpty) {
playerMap.get(event.level)?.remove(event.pos.toLong())
}
} else {
LOGGER.error("Player ${event.player} stopped 'watching' chunk at ${event.pos} before starting to watch it/stopped 'watching' more than once.")
}
}
fun playerDisconnected(event: PlayerEvent.PlayerLoggedOutEvent) {
for (tree in playerMap.values) {
val iterator = tree.iterator()
for (entry in iterator) {
val listIterator = entry.value.players.listIterator()
for (value in listIterator) {
if (value === event.entity) {
listIterator.remove()
}
}
if (entry.value.isEmpty) {
iterator.remove()
}
}
}
}
fun playerChangeDimensions(event: PlayerChangedDimensionEvent) {
}
fun playerCloneEvent(event: PlayerEvent.Clone) {
}
}
}