1
0
Fork 0
cuberite-2a/Server/Plugins/APIDump/UsingChunkStays.html

168 lines
8.1 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Cuberite - Using ChunkStays</title>
<link rel="stylesheet" type="text/css" href="main.css" />
<link rel="stylesheet" type="text/css" href="prettify.css" />
<script src="prettify.js"></script>
<script src="lang-lua.js"></script>
<meta charset="UTF-8">
</head>
<body>
<div id="content">
<h1>Using ChunkStays</h1>
<p>
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.</p>
<h2>The problem</h2>
<p>
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.</p>
<p>
This gets even more important when using the <a href="cBlockArea.html">cBlockArea</a> 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.</p>
<h2>The solution</h2>
<p>
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.</p>
<p>
An alternate approach has been implemented, accessible through a single (somewhat hidden) function
call: <a href="cWorld.html">cWorld:ChunkStay()</a>. 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.</p>
<p>
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 <a href="cPlayer.html">cPlayer</a> or
<a href="cEntity.html">cEntity</a> 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
<a href="cWorld.html">cWorld:DoWithEntityByID()</a> or
<a href="cWorld.html">cWorld:DoWithPlayer()</a>.</p>
<h2>The example</h2>
<p>
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 "<PlayerName>_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.</p>
<p>
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 <b>the wrong way</b> to do it!</p>
<pre class="prettyprint lang-lua">
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
</pre>
<p>
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.</p>
<hr/>
<p>
The following script uses the ChunkStay method to alleviate chunk-related problems. This is <b>the
right way</b> of doing it:</p>
<pre class="prettyprint lang-lua">
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
</pre>
<p>
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.</p>
<h2>The conclusion</h2>
<p>
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.</p>
<p>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.</p>
<script>
prettyPrint();
</script>
</div>
</body>
</html>