From 177a380bf318fee902acbff2d7fe2ea85bafe7ab Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 29 Sep 2022 23:50:48 +0700 Subject: [PATCH] Make synchronized block entity use chunk grid to blazingly fast determine potential players to network stuff to --- .../mc/otm/OverdriveThatMatters.java | 9 + .../block/entity/SynchronizedBlockEntity.kt | 186 +++++++++++++++++- 2 files changed, 189 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java b/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java index 01f90c315..7ceb21671 100644 --- a/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java +++ b/src/main/java/ru/dbotthepony/mc/otm/OverdriveThatMatters.java @@ -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(); diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/SynchronizedBlockEntity.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/SynchronizedBlockEntity.kt index 067a8a99c..4ac583892 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/SynchronizedBlockEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/block/entity/SynchronizedBlockEntity.kt @@ -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> = LinkedList(), val players: LinkedList = LinkedList(), val level: WeakReference, 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 { + 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>() + + 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) { + + } + } }