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
}
}
}
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 timeb
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 withif (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.