hpfxd / PandaSpigot

Fork of Paper for 1.8.8 focused on improved performance and stability.
GNU General Public License v3.0
229 stars 65 forks source link

0073-Optimize-light-level-comparisons.patch breaks my private plugin #206

Open Ameliorate opened 2 weeks ago

Ameliorate commented 2 weeks ago

I have a plugin that uses some horrible NMS magic to grow crops while they're offline. To do this, the plugin listens to ChunkLoadEvent, and (async) loads the locations of all the growable blocks from a cache. Once it's loaded, it loops through all crops in the chunk on the main thread, and calls net.minecraft.server.Block.b(World, BlockPosition, IBlockData, Random) using a custom implementation of Random that always returns 0 every time it's called, which causes crops to grow one stage for each time b is called.

In PaperSpigot this works fine, however PandaSpigot fails because of the line if (world.isLightLevel(blockposition.up(), 9)) { // PandaSpigot - Use isLightLevel. When I comment it out and replace it with if (world.getLightLevel(blockposition.up()) >= 9) { my plugin works again. Seemingly, isLightLevel will return that the light is insufficient when it actually is.

I don't know if the patch is unsound in other ways, because I didn't do a lot of testing outside what I needed to do to get my plugin working.

Here's the source code to a old version of the plugin that's a lot simpler because it doesn't do any caching or lazy-loading.

class ChunkLoadListener: Listener {
    val CROP_CLASSES = arrayOf(BlockCrops::class, BlockStem::class, BlockReed::class, BlockCocoa::class, BlockNetherWart::class, BlockMushroom::class, BlockSapling::class)

    @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
    fun onChunkLoad(event: ChunkLoadEvent) {
        if (event.isNewChunk) return

        val nmsChunk = (event.chunk as CraftChunk).handle
        val nmsWorld = nmsChunk.world
        val lastSaved = getChunkLastUpdated(event.world, event.chunk.x, event.chunk.z)
        val ticksPassed = (nmsChunk.world.time - lastSaved).toInt()

        for (x in 0..15) {
            for (z in 0..15) {
                for (y in 0..255) {
                    val position = BlockPosition(16 * nmsChunk.locX + x, y, 16 * nmsChunk.locZ + z)
                    val nmsBlock = nmsChunk.getType(position)
                    val nmsBlockData = nmsChunk.getBlockData(position)
                    if (nmsBlock::class in CROP_CLASSES) {
                        simulateTicksOnPlant(nmsChunk, nmsBlock, nmsWorld, position, nmsBlockData, ticksPassed)
                    }
                }
            }
        }
    }

    private fun getChunkLastUpdated(world: org.bukkit.World, x: Int, z: Int): Long {
        // the private `lastSaved` variable on Chunk is a lie.
        // when Spigot loads a Chunk, it does not populate that field with the real `lastSaved`
        // value. however, the LastUpdate really is saved in the NBT file. therefore, we must get
        // the Chunk's NBT file. however, Chunk itself doesn't save a reference to this. instead,
        // we have to manually load the chunk again and get the NBT data (even if it was just loaded).
        // spigot is bad btw
        val worldServer: WorldServer = (world as CraftWorld).handle
        val chunkProviderServer: ChunkProviderServer = worldServer.chunkProviderServer
        val chunkLoader: ChunkRegionLoader = chunkProviderServer.javaClass.getDeclaredField("chunkLoader")
            .also { it.isAccessible = true }
            .get(chunkProviderServer)
                as ChunkRegionLoader
        val loadedChunkData = chunkLoader.loadChunk(worldServer, x, z)
        val chunkNBT = loadedChunkData[1] as NBTTagCompound
        return chunkNBT.getCompound("Level").getLong("LastUpdate")
    }

    private fun simulateTicksOnPlant(chunk: Chunk, block: Block, nmsWorld: World, blockPosition: BlockPosition, blockData: IBlockData, ticksPassed: Int) {
        // first, we get the method that the block uses for its growth tick
        // this method is going to take a Random object, which the block uses to roll whether it should grow.
        // we give it a fake random object, which will send it rigged numbers depending on how many ticks have passed
        val growthTickMethod = block.javaClass.getDeclaredMethod("b", World::class.java, BlockPosition::class.java, IBlockData::class.java, Random::class.java)
        // create the rigged random object
        val sovietRandom = SovietRandom(ticksPassed)
        // and call the random update function, passing the rigged random object
        growthTickMethod.invoke(block, nmsWorld, blockPosition, blockData, sovietRandom)
        // and potentially repeat for multiple growth ticks
        while (sovietRandom.repeat) {
            // IBlockData is immutable. if we don't refetch, we end up repeating the same growth stage over and over
            val currentBlockData: IBlockData = chunk.getBlockData(blockPosition)
            growthTickMethod.invoke(block, nmsWorld, blockPosition, currentBlockData, sovietRandom)
        }
    }
}

private class SovietRandom(val ticksPassed: Int): Random() {
    private var hits = 0
    var repeat = false

    // in vanilla, m is passed as a max, and if the roll is 0, the crop grows. any non'zero return means no growth
    override fun nextInt(m: Int): Int {
        // the 946 number may look magic. the function that we're about to call (`b`) is the random block tick function
        // the random block tick function is called on a few randomly selected blocks in the chunk every tick. on average
        // it's called every 946.03 ticks for a given block, therefore the real amount of rolls we want to simulate is the
        // ticks that have elapsed / 946, not just the raw ticks that have elapsed. if we wanted to be fancy, we would
        // actually get the randomTickSpeed gamerule (which defaults to 3, which is the number for which the result is 946)
        // and adjust this accordingly. i don't actually know how to grab that, though, and Civ+ uses the default one anyway
        val chanceOfGrowth = 1.0 - (1.0 - 1.0 / m).pow(ticksPassed / 946)
        // we roll for growth
        val roll = Math.random()
        if (roll < chanceOfGrowth) {
            // if we are going to grow, we roll again until we miss, to maybe grow even more, or until we've grown 8 times
            if (hits < 8) {
                hits++
                repeat = true
            } else {
                repeat = false
            }
            return 0
        } else {
            return 1
        }
    }
}