Using ChunkStays

A plugin may need to manipulate data in arbitrary chunks, and it needs a way to make the server guarantee that the chunks are available in memory.

The problem

Usually when plugins want to manipulate larger areas of world data, they need to make sure that the server has the appropriate chunks loaded in the memory. When the data being manipulated can be further away from the connected players, or the data is being manipulated from a console handler, there is a real chance that the chunks are not loaded.

This gets even more important when using the cBlockArea class for reading and writing. Those functions will fail when any of the required chunks aren't valid. This means that either the block area has incomplete data (Read() failed) or incomplete data has been written to the world (Write() failed). Recovery from this is near impossible - you can't simply read or write again later, because the world may have changed in the meantime.

The solution

The naive solution would be to monitor chunk loads and unloads, and postpone the operations until all the chunks are available. This would be quite ineffective and also very soon it would become very difficult to maintain, if there were multiple code paths requiring this handling.

An alternate approach has been implemented, accessible through a single (somewhat hidden) function call: cWorld:ChunkStay(). All that this call basically does is, it tells the server "Load these chunks for me, and call this callback function once you have them all." And the server does exactly that - it remembers the callback and asks the world loader / generator to provide the chunks. Once the chunks become available, it calls the callback function for the plugin.

There are a few gotcha-s, though. If the code that was requesting the read or write had access to some of the volatile objects, such as cPlayer or cEntity objects, those cannot be accessed by the callback anymore, because they may have become invalid in the meantime - the player may have disconnected, the entity may have despawned. So the callback must use the longer way to access such objects, such as calling cWorld:DoWithEntityByID() or cWorld:DoWithPlayer().

The example

As a simple example, consider a theoretical plugin that allows a player to save the immediate surroundings of the spawn into a schematic file. The player issues a command to initiate the save, and the plugin reads a 50 x 50 x 50 block area around the spawn into a cBlockArea and saves it on the disk as "_spawn.schematic". When it's done with the saving, it wants to send a message to the player to let them know the command has succeeded.

The first attempt shows the naive approach. It simply reads the block area and saves it, then sends the message. I'll repeat once more, this code is the wrong way to do it!

function HandleCommandSaveSpawn(a_Split, a_Player)
	-- Get the coords for the spawn:
	local SpawnX = a_Player:GetWorld():GetSpawnX()
	local SpawnY = a_Player:GetWorld():GetSpawnY()
	local SpawnZ = a_Player:GetWorld():GetSpawnZ()
	local Bounds = cCuboid(SpawnX - 25, SpawnY - 25, SpawnZ - 25, SpawnX + 25, SpawnY + 25, SpawnZ + 25)
	Bounds:ClampY(0, 255)
	
	-- Read the area around spawn into a cBlockArea, save to file:
	local Area = cBlockArea()
	local FileName = a_Player:GetName() .. "_spawn.schematic"
	Area:Read(a_Player:GetWorld(), Bounds, cBlockArea.baTypes + cBlockArea.baMetas)
	Area:SaveToSchematicFile(FileName)
	
	-- Notify the player:
	a_Player:SendMessage(cCompositeChat("The spawn has been saved", mtInfo))
	return true
end

Now if the player goes exploring far and uses the command to save their spawn, the chunks aren't loaded, so the BlockArea reading fails, the BlockArea contains bad data. Note that the plugin fails to do any error checking and if the area isn't read from the world, it happily saves the incomplete data and says "hey, everything's right", althought it has just trashed any previous backup of the spawn schematic with nonsense data.


The following script uses the ChunkStay method to alleviate chunk-related problems. This is the right way of doing it:

function HandleCommandSaveSpawn(a_Split, a_Player)
	-- Get the coords for the spawn:
	local SpawnX = a_Player:GetWorld():GetSpawnX()
	local SpawnY = a_Player:GetWorld():GetSpawnY()
	local SpawnZ = a_Player:GetWorld():GetSpawnZ()
	local Bounds = cCuboid(SpawnX - 25, SpawnY - 25, SpawnZ - 25, SpawnX + 25, SpawnY + 25, SpawnZ + 25)
	Bounds:ClampY(0, 255)
	
	-- Get a list of chunks that we need loaded:
	local MinChunkX = math.floor((SpawnX - 25) / 16)
	local MaxChunkX = math.ceil ((SpawnX + 25) / 16)
	local MinChunkZ = math.floor((SpawnZ - 25) / 16)
	local MaxChunkZ = math.ceil ((SpawnZ + 25) / 16)
	local Chunks = {}
	for x = MinChunkX, MaxChunkX do
		for z = MinChunkZ, MaxChunkZ do
			table.insert(Chunks, {x, z})
		end
	end  -- for x
	
	-- Store the player's name and world to use in the callback, because the a_Player object may no longer be valid:
	local PlayerName = a_Player:GetName()
	local World = a_Player:GetWorld()
	
	-- This is the callback that is executed once all the chunks are loaded:
	local OnAllChunksAvailable = function()
		-- Read the area around spawn into a cBlockArea, save to file:
		local Area = cBlockArea()
		local FileName = PlayerName .. "_spawn.schematic"
		if (Area:Read(World, Bounds, cBlockArea.baTypes + cBlockArea.baMetas)) then
			Area:SaveToSchematicFile(FileName)
			Msg = cCompositeChat("The spawn has been saved", mtInfo)
		else
			Msg = cCompositeChat("Cannot save the spawn", mtFailure)
		end
		
		-- Notify the player:
		-- Note that we cannot use a_Player here, because it may no longer be valid (if the player disconnected before the command completes)
		World:DoWithPlayer(PlayerName,
			function (a_CBPlayer)
				a_CBPlayer:SendMessage(Msg)
			end
		)
	end
	
	-- Ask the server to load our chunks and notify us once it's done:
	World:ChunkStay(Chunks, nil, OnAllChunksAvailable)
	
	-- Note that code here may get executed before the callback is called!
	-- The ChunkStay says "once you have the chunks", not "wait until you have the chunks"
	-- So you can't notify the player here, because the saving needn't have occurred yet.
	
	return true
end

Note that this code does its error checking of the Area:Read() function, and it will not overwrite the previous file unless it actually has the correct data. If you're wondering how the reading could fail when we've got the chunks loaded, there's still the issue of free RAM - if the memory for the area cannot be allocated, it cannot be read even with all the chunks present. So we still do need that check.

The conclusion

Although it makes the code a little bit longer and is a bit more difficult to grasp at first, the ChunkStay is a useful technique to add to your repertoire. It is to be used whenever you need access to chunks that may potentially be inaccessible, and you really need the data.

Possibly the biggest hurdle in using the ChunkStay is the fact that it does its work in the background, thus invalidating all cPlayer and cEntity objects your function may hold, so you need to re-acquire them from their IDs and names. This is the penalty for using multi-threaded code.