diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/Patcher.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/Patcher.kt index faef9942b5..2950f14f3b 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/Patcher.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/Patcher.kt @@ -20,7 +20,7 @@ import xyz.xenondevs.nova.transformer.adapter.LcsWrapperAdapter import xyz.xenondevs.nova.transformer.patch.FieldFilterPatch import xyz.xenondevs.nova.transformer.patch.block.BlockUpdatesPatch import xyz.xenondevs.nova.transformer.patch.bossbar.BossBarOriginPatch -import xyz.xenondevs.nova.transformer.patch.chunk.ChunkLoadSchedulingPatch +import xyz.xenondevs.nova.transformer.patch.chunk.ChunkSchedulingPatch import xyz.xenondevs.nova.transformer.patch.item.AnvilResultPatch import xyz.xenondevs.nova.transformer.patch.item.AttributePatch import xyz.xenondevs.nova.transformer.patch.item.DamageablePatches @@ -65,7 +65,7 @@ internal object Patcher { WrapperBlockPatch, MappedRegistryPatch, FuelPatches, RemainingItemPatches, FireResistancePatches, SoundPatches, BroadcastPacketPatch, CBFCompoundTagPatch, EventPreventionPatch, LegacyConversionPatch, WearablePatch, NovaRuleTestPatch, BossBarOriginPatch, ThreadingDetectorPatch, FakePlayerLastHurtPatch, BlockUpdatesPatch, - ChunkLoadSchedulingPatch + ChunkSchedulingPatch ).filter(Transformer::shouldTransform).toSet() } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/chunk/ChunkLoadSchedulingPatch.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/chunk/ChunkLoadSchedulingPatch.kt deleted file mode 100644 index b6158e4470..0000000000 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/chunk/ChunkLoadSchedulingPatch.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.xenondevs.nova.transformer.patch.chunk - -import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor -import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler -import net.minecraft.world.level.chunk.ChunkStatus -import xyz.xenondevs.bytebase.asm.buildInsnList -import xyz.xenondevs.nova.transformer.MethodTransformer -import xyz.xenondevs.nova.util.reflection.ReflectionUtils -import xyz.xenondevs.nova.world.ChunkPos -import xyz.xenondevs.nova.world.format.WorldDataManager -import java.util.function.Consumer - -private val CHUNK_TASK_SCHEDULER_SCHEDULE_CHUNK_LOAD_METHOD = ReflectionUtils.getMethod( - ChunkTaskScheduler::class, - false, - "scheduleChunkLoad", - Int::class, Int::class, ChunkStatus::class, Boolean::class, PrioritisedExecutor.Priority::class, Consumer::class -) - -internal object ChunkLoadSchedulingPatch : MethodTransformer(CHUNK_TASK_SCHEDULER_SCHEDULE_CHUNK_LOAD_METHOD) { - - override fun transform() { - methodNode.instructions.insert(buildInsnList { - addLabel() - aLoad(0) - iLoad(1) - iLoad(2) - invokeStatic(::handleScheduleChunkLoad) - }) - } - - @JvmStatic - fun handleScheduleChunkLoad(scheduler: ChunkTaskScheduler, x: Int, z: Int) { - WorldDataManager.loadAsync(ChunkPos(scheduler.world.uuid, x, z)) - } - -} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/chunk/ChunkSchedulingPatch.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/chunk/ChunkSchedulingPatch.kt new file mode 100644 index 0000000000..5dbb4b9e4c --- /dev/null +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/transformer/patch/chunk/ChunkSchedulingPatch.kt @@ -0,0 +1,71 @@ +package xyz.xenondevs.nova.transformer.patch.chunk + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor +import io.papermc.paper.chunk.system.ChunkSystem +import io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler +import net.minecraft.world.level.chunk.ChunkStatus +import net.minecraft.world.level.chunk.LevelChunk +import xyz.xenondevs.bytebase.asm.buildInsnList +import xyz.xenondevs.bytebase.jvm.VirtualClassPath +import xyz.xenondevs.nova.transformer.MultiTransformer +import xyz.xenondevs.nova.util.reflection.ReflectionUtils +import xyz.xenondevs.nova.world.ChunkPos +import xyz.xenondevs.nova.world.format.WorldDataManager +import java.util.function.Consumer +import kotlin.reflect.KFunction + +private val CHUNK_TASK_SCHEDULER_SCHEDULE_CHUNK_LOAD_METHOD = ReflectionUtils.getMethod( + ChunkTaskScheduler::class, + false, + "scheduleChunkLoad", + Int::class, Int::class, ChunkStatus::class, Boolean::class, PrioritisedExecutor.Priority::class, Consumer::class +) + +internal object ChunkSchedulingPatch : MultiTransformer(ChunkTaskScheduler::class, ChunkSystem::class) { + + override fun transform() { + VirtualClassPath[CHUNK_TASK_SCHEDULER_SCHEDULE_CHUNK_LOAD_METHOD] + .instructions.insert(buildInsnList { + addLabel() + aLoad(0) + iLoad(1) + iLoad(2) + invokeStatic(::handleScheduleChunkLoad) + }) + + fun insertEnableTicking(fn: KFunction<*>) = + VirtualClassPath[fn].instructions.insert(buildInsnList { + addLabel() + aLoad(0) + invokeStatic(::enableChunkTicking) + }) + + fun insertDisableTicking(fn: KFunction<*>) = + VirtualClassPath[fn].instructions.insert(buildInsnList { + addLabel() + aLoad(0) + invokeStatic(::disableChunkTicking) + }) + + insertEnableTicking(ChunkSystem::onChunkEntityTicking) + insertEnableTicking(ChunkSystem::onChunkTicking) + insertDisableTicking(ChunkSystem::onChunkNotTicking) + insertDisableTicking(ChunkSystem::onChunkNotEntityTicking) + } + + @JvmStatic + fun handleScheduleChunkLoad(scheduler: ChunkTaskScheduler, x: Int, z: Int) { + WorldDataManager.loadAsync(ChunkPos(scheduler.world.uuid, x, z)) + } + + @JvmStatic + fun enableChunkTicking(chunk: LevelChunk) { + WorldDataManager.startTicking(ChunkPos(chunk.level.uuid, chunk.locX, chunk.locZ)) + } + + @JvmStatic + fun disableChunkTicking(chunk: LevelChunk) { + WorldDataManager.stopTicking(ChunkPos(chunk.level.uuid, chunk.locX, chunk.locZ)) + } + +} \ No newline at end of file diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/WorldDataManager.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/WorldDataManager.kt index 893c10f61d..1ceee21e91 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/WorldDataManager.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/WorldDataManager.kt @@ -81,6 +81,14 @@ object WorldDataManager : Listener { GlobalScope.launch { worlds.values.forEach { it.loadAsync(pos) } } } + internal fun startTicking(pos: ChunkPos) { + runBlocking { getOrLoadChunk(pos).startTicking() } + } + + internal fun stopTicking(pos: ChunkPos) { + runBlocking { getOrLoadChunk(pos).stopTicking() } + } + fun getBlockState(pos: BlockPos): NovaBlockState? = runBlocking { getOrLoadChunk(pos.chunkPos).getBlockState(pos) } diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunk.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunk.kt index de60cbc980..ae8780fd01 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunk.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunk.kt @@ -34,10 +34,9 @@ import xyz.xenondevs.nova.world.format.BlockStateIdResolver import java.io.ByteArrayOutputStream import java.util.* import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.locks.ReentrantReadWriteLock +import java.util.concurrent.locks.ReentrantLock import java.util.logging.Level -import kotlin.concurrent.read -import kotlin.concurrent.write +import kotlin.concurrent.withLock import kotlin.coroutines.CoroutineContext import kotlin.math.roundToLong import kotlin.random.Random @@ -49,12 +48,15 @@ internal class RegionChunk( private val tileEntityData: MutableMap = HashMap(), ) : RegionizedChunk { - private val lock = ReentrantReadWriteLock(true) + private val lock = ReentrantLock() @Volatile var isEnabled = false private set + private var shouldTick = false + private var isTicking = false + private val world = pos.world!! private val level = world.serverLevel private val minHeight = world.minHeight @@ -64,6 +66,7 @@ internal class RegionChunk( private val vanillaTileEntities: MutableMap = HashMap() private val tileEntities: MutableMap = ConcurrentHashMap() // concurrent to allow modifications during ticking + private var sectionsEmpty = sections.all { it.isEmpty() } private var tick = 0 private var asyncTickerSupervisor: Job? = null private var tileEntityAsyncTickers: HashMap? = null @@ -119,36 +122,54 @@ internal class RegionChunk( /** * Gets the [NovaBlockState] at the given [pos]. */ - fun getBlockState(pos: BlockPos): NovaBlockState? = - getSection(pos.y)[pos.x and 0xF, pos.y and 0xF, pos.z and 0xF] + fun getBlockState(pos: BlockPos): NovaBlockState? = lock.withLock { + return getSection(pos.y)[pos.x and 0xF, pos.y and 0xF, pos.z and 0xF] + } /** * Sets the [BlockState][state] at the given [pos]. */ - fun setBlockState(pos: BlockPos, state: NovaBlockState?) { - getSection(pos.y)[pos.x and 0xF, pos.y and 0xF, pos.z and 0xF] = state - + fun setBlockState(pos: BlockPos, state: NovaBlockState?) = lock.withLock { + val section = getSection(pos.y) + section[pos.x and 0xF, pos.y and 0xF, pos.z and 0xF] = state if (state != null) { - lock.read { tileEntities[pos]?.blockState = state } + tileEntities[pos]?.blockState = state + + // if this is the first block in the chunk, we may need to start ticking it + if (sectionsEmpty) { + sectionsEmpty = false + if (shouldTick && !isTicking) { + startTicking() + } + } + } else if (!sectionsEmpty && section.isEmpty()) { + // this section is now empty, so we check if the whole chunk is empty + sectionsEmpty = sections.all { it.isEmpty() } + if (sectionsEmpty && isTicking) { + stopTicking() + shouldTick = true + } } } /** * Gets the [VanillaTileEntity] at the given [pos]. */ - fun getVanillaTileEntity(pos: BlockPos): VanillaTileEntity? = - lock.read { vanillaTileEntities[pos] } + fun getVanillaTileEntity(pos: BlockPos): VanillaTileEntity? = lock.withLock { + return vanillaTileEntities[pos] + } /** * Gets a snapshot of all [VanillaTileEntities][VanillaTileEntity] in this chunk. */ - fun getVanillaTileEntities(): List = - lock.read { ArrayList(vanillaTileEntities.values) } + fun getVanillaTileEntities(): List = lock.withLock { + return ArrayList(vanillaTileEntities.values) + } /** * Sets the [VanillaTileEntity][vte] at the given [pos]. */ - fun setVanillaTileEntity(pos: BlockPos, vte: VanillaTileEntity?): VanillaTileEntity? = lock.write { + fun setVanillaTileEntity(pos: BlockPos, vte: VanillaTileEntity?): VanillaTileEntity? = lock.withLock { val previous: VanillaTileEntity? if (vte == null) { previous = vanillaTileEntities.remove(pos) @@ -170,19 +191,21 @@ internal class RegionChunk( /** * Gets the [TileEntity] at the given [pos]. */ - fun getTileEntity(pos: BlockPos): TileEntity? = - lock.read { tileEntities[pos] } + fun getTileEntity(pos: BlockPos): TileEntity? = lock.withLock { + return tileEntities[pos] + } /** * Gets a snapshot of all [TileEntities][TileEntity] in this chunk. */ - fun getTileEntities(): List = - lock.read { tileEntities.values.toList() } + fun getTileEntities(): List = lock.withLock { + return tileEntities.values.toList() + } /** * Sets the [tileEntity] at the given [pos]. */ - fun setTileEntity(pos: BlockPos, tileEntity: TileEntity?): TileEntity? = lock.write { + fun setTileEntity(pos: BlockPos, tileEntity: TileEntity?): TileEntity? = lock.withLock { val previous: TileEntity? if (tileEntity == null) { previous = tileEntities.remove(pos) @@ -216,25 +239,16 @@ internal class RegionChunk( /** * Enables this RegionChunk. * - * Enabling a RegionChunk activates synchronous and asynchronous tile-entity ticking, - * as well as random ticks and also calls [TileEntity.handleEnable]. + * This loads all models and calls [TileEntity.handleEnable], [VanillaTileEntity.handleEnable]. + * + * May only be called from the server thread. */ fun enable() { checkServerThread() - lock.write { + lock.withLock { if (isEnabled) return - // enable sync ticking - syncTicker = runTaskTimer(0, 1, ::tick) - - // enable async ticking - val supervisor = SupervisorJob(AsyncExecutor.SUPERVISOR) - asyncTickerSupervisor = supervisor - tileEntityAsyncTickers = tileEntities.values.asSequence() - .filter { it.block.asyncTickrate > 0 } - .associateWithTo(HashMap()) { launchAsyncTicker(supervisor, it) } - // load models for ((pos, tileEntity) in tileEntities) tileEntity.blockState.modelProvider.load(pos) @@ -256,25 +270,18 @@ internal class RegionChunk( } /** - * Disables this RegionChunk. + * Disables this [RegionChunk]. * - * Disabling a RegionChunk deactivates synchronous and asynchronous tile-entity ticking, as well as random ticks and - * also calls [TileEntity.handleDisable]. + * This unloads all models and calls [TileEntity.handleDisable], [VanillaTileEntity.handleDisable]. + * + * May only be called from the server thread. */ fun disable() { checkServerThread() - lock.write { + lock.withLock { if (!isEnabled) return - // disable sync ticking - syncTicker?.cancel() - syncTicker = null - - // disable async ticking - asyncTickerSupervisor?.cancel("Chunk ticking disabled") - tileEntityAsyncTickers = null - // unload models for ((pos, tileEntity) in tileEntities) tileEntity.blockState.modelProvider.unload(pos) @@ -286,7 +293,51 @@ internal class RegionChunk( } } - private fun tick() = lock.read { + /** + * Starts ticking this [RegionChunk]. + */ + fun startTicking(): Unit = lock.withLock { + if (isTicking) + return + + shouldTick = true + + if (sectionsEmpty) + return + + // enable sync ticking + syncTicker = runTaskTimer(0, 1, ::tick) + + // enable async ticking + val supervisor = SupervisorJob(AsyncExecutor.SUPERVISOR) + asyncTickerSupervisor = supervisor + tileEntityAsyncTickers = tileEntities.values.asSequence() + .filter { it.block.asyncTickrate > 0 } + .associateWithTo(HashMap()) { launchAsyncTicker(supervisor, it) } + + isTicking = true + } + + /** + * Stops ticking this [RegionChunk]. + */ + fun stopTicking(): Unit = lock.withLock { + if (!isTicking) + return + + // disable sync ticking + syncTicker?.cancel() + syncTicker = null + + // disable async ticking + asyncTickerSupervisor?.cancel("Chunk ticking disabled") + tileEntityAsyncTickers = null + + shouldTick = false + isTicking = false + } + + private fun tick() = lock.withLock { tick++ // tile-entity ticks @@ -310,20 +361,18 @@ internal class RegionChunk( val randomTickSpeed = level.gameRules.getInt(GameRules.RULE_RANDOMTICKING) if (randomTickSpeed > 0) { for (section in sections) { - section.lock.read { - if (!section.isEmpty()) { - repeat(randomTickSpeed) { - val x = Random.nextInt(0, 16) - val y = Random.nextInt(0, 16) - val z = Random.nextInt(0, 16) - val blockState = section[x, y, z] - if (blockState != null) { - val pos = BlockPos(world, pos.x + x, minHeight + y, pos.z + z) - try { - blockState.block.handleRandomTick(pos, blockState) - } catch (t: Throwable) { - LOGGER.log(Level.SEVERE, "An exception occurred while ticking block $blockState at $pos", t) - } + if (!section.isEmpty()) { + repeat(randomTickSpeed) { + val x = Random.nextInt(0, 16) + val y = Random.nextInt(0, 16) + val z = Random.nextInt(0, 16) + val blockState = section[x, y, z] + if (blockState != null) { + val pos = BlockPos(world, pos.x + x, minHeight + y, pos.z + z) + try { + blockState.block.handleRandomTick(pos, blockState) + } catch (t: Throwable) { + LOGGER.log(Level.SEVERE, "An exception occurred while ticking block $blockState at $pos", t) } } } @@ -359,38 +408,30 @@ internal class RegionChunk( /** * Writes this chunk to the given [writer]. */ - override fun write(writer: ByteWriter): Boolean = lock.read { - // acquire read locks for all sections - for (section in sections) section.lock.readLock().lock() + override fun write(writer: ByteWriter): Boolean = lock.withLock { + if (vanillaTileEntityData.isEmpty() && tileEntityData.isEmpty() && sections.all { it.isEmpty() }) + return false - try { - if (vanillaTileEntityData.isEmpty() && tileEntityData.isEmpty() && sections.all { it.isEmpty() }) - return false - - vanillaTileEntities.values.forEach(VanillaTileEntity::saveData) - tileEntities.values.forEach(TileEntity::saveData) - - // TODO: don't serialize pos as BlockPos - CBF.write(vanillaTileEntityData.takeUnlessEmpty(), writer) - CBF.write(tileEntityData.takeUnlessEmpty(), writer) - - val sectionBitmask = BitSet(sectionCount) - val sectionsBuffer = ByteArrayOutputStream() - val sectionsWriter = ByteWriter.fromStream(sectionsBuffer) - - for ((sectionIdx, section) in sections.withIndex()) { - sectionBitmask.set(sectionIdx, section.write(sectionsWriter)) - } - - writer.writeInt(sectionCount) - writer.writeBytes(Arrays.copyOf(sectionBitmask.toByteArray(), sectionCount.ceilDiv(8))) - writer.writeBytes(sectionsBuffer.toByteArray()) - - return true - } finally { - // release read locks for all sections - for (section in sections) section.lock.readLock().unlock() + vanillaTileEntities.values.forEach(VanillaTileEntity::saveData) + tileEntities.values.forEach(TileEntity::saveData) + + // TODO: don't serialize pos as BlockPos + CBF.write(vanillaTileEntityData.takeUnlessEmpty(), writer) + CBF.write(tileEntityData.takeUnlessEmpty(), writer) + + val sectionBitmask = BitSet(sectionCount) + val sectionsBuffer = ByteArrayOutputStream() + val sectionsWriter = ByteWriter.fromStream(sectionsBuffer) + + for ((sectionIdx, section) in sections.withIndex()) { + sectionBitmask.set(sectionIdx, section.write(sectionsWriter)) } + + writer.writeInt(sectionCount) + writer.writeBytes(Arrays.copyOf(sectionBitmask.toByteArray(), sectionCount.ceilDiv(8))) + writer.writeBytes(sectionsBuffer.toByteArray()) + + return true } companion object : RegionizedChunkReader() { diff --git a/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunkSection.kt b/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunkSection.kt index 040982186d..cc57a32a9e 100644 --- a/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunkSection.kt +++ b/nova/src/main/kotlin/xyz/xenondevs/nova/world/format/chunk/RegionChunkSection.kt @@ -8,9 +8,6 @@ import xyz.xenondevs.nova.world.format.chunk.container.ArraySectionDataContainer import xyz.xenondevs.nova.world.format.chunk.container.MapSectionDataContainer import xyz.xenondevs.nova.world.format.chunk.container.SectionDataContainer import xyz.xenondevs.nova.world.format.chunk.container.SingleValueSectionDataContainer -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.read -import kotlin.concurrent.write /** * A 16x16x16 section of a [RegionChunk]. @@ -25,24 +22,22 @@ internal class RegionChunkSection( constructor(idResolver: IdResolver) : this(idResolver, MapSectionDataContainer(idResolver)) - val lock = ReentrantReadWriteLock(true) - /** * Returns true if this section is empty. */ fun isEmpty(): Boolean = - lock.read { container.nonEmptyBlockCount == 0 } + container.nonEmptyBlockCount == 0 /** * Retrieves the [NovaBlockState] at the given [x], [y] and [z] section coordinates. */ operator fun get(x: Int, y: Int, z: Int): T? = - lock.read { container[x, y, z] } + container[x, y, z] /** * Sets the [NovaBlockState] at the given [x], [y] and [z] section coordinates. */ - operator fun set(x: Int, y: Int, z: Int, state: T?) = lock.write { + operator fun set(x: Int, y: Int, z: Int, state: T?) { val current = get(x, y, z) if (current == state) return @@ -54,7 +49,7 @@ internal class RegionChunkSection( /** * Migrates the container to another type if necessary. */ - private fun checkMigrateContainer(state: T?) = lock.write { + private fun checkMigrateContainer(state: T?) { val container = container when { // convert from single value container if a state has changed @@ -77,7 +72,7 @@ internal class RegionChunkSection( * Writes this section to the given [writer]. * Returns true if the section was written, false if it was empty. */ - fun write(writer: ByteWriter): Boolean = lock.read { + fun write(writer: ByteWriter): Boolean { optimizeContainer() if (container.nonEmptyBlockCount == 0) @@ -90,7 +85,7 @@ internal class RegionChunkSection( /** * Converts the container to a more efficient type if possible. */ - private fun optimizeContainer() = lock.write { + private fun optimizeContainer() { val container = container // single value container cannot be optimized further