diff options
Diffstat (limited to 'MCServer')
-rw-r--r-- | MCServer/Plugins/APIDump/APIDesc.lua | 7 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/UsingChunkStays.html | 167 | ||||
-rw-r--r-- | MCServer/Plugins/APIDump/WebWorldThreads.html | 48 |
3 files changed, 195 insertions, 27 deletions
diff --git a/MCServer/Plugins/APIDump/APIDesc.lua b/MCServer/Plugins/APIDump/APIDesc.lua index 233bdbc46..45e8d9677 100644 --- a/MCServer/Plugins/APIDump/APIDesc.lua +++ b/MCServer/Plugins/APIDump/APIDesc.lua @@ -2929,9 +2929,10 @@ end { -- No sorting is provided for these, they will be output in the same order as defined here { FileName = "Writing-a-MCServer-plugin.html", Title = "Writing a MCServer plugin" }, - { FileName = "SettingUpDecoda.html", Title = "Setting up the Decoda Lua IDE" }, - { FileName = "SettingUpZeroBrane.html", Title = "Setting up the ZeroBrane Studio Lua IDE" }, - { FileName = "WebWorldThreads.html", Title = "Webserver vs World threads" }, + { FileName = "SettingUpDecoda.html", Title = "Setting up the Decoda Lua IDE" }, + { FileName = "SettingUpZeroBrane.html", Title = "Setting up the ZeroBrane Studio Lua IDE" }, + { FileName = "UsingChunkStays.html", Title = "Using ChunkStays" }, + { FileName = "WebWorldThreads.html", Title = "Webserver vs World threads" }, } } ; diff --git a/MCServer/Plugins/APIDump/UsingChunkStays.html b/MCServer/Plugins/APIDump/UsingChunkStays.html new file mode 100644 index 000000000..d3ecc6674 --- /dev/null +++ b/MCServer/Plugins/APIDump/UsingChunkStays.html @@ -0,0 +1,167 @@ +<!DOCTYPE html> +<html> + <head> + <title>MCServer - 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> diff --git a/MCServer/Plugins/APIDump/WebWorldThreads.html b/MCServer/Plugins/APIDump/WebWorldThreads.html index fc80a6178..ee0b172e6 100644 --- a/MCServer/Plugins/APIDump/WebWorldThreads.html +++ b/MCServer/Plugins/APIDump/WebWorldThreads.html @@ -39,31 +39,31 @@ <h2>Example</h2> The Core has the facility to kick players using the web interface. It used the following code for the kicking (inside the webadmin handler): - <pre class="prettyprint lang-lua"> - local KickPlayerName = Request.Params["players-kick"] - local FoundPlayerCallback = function(Player) - if (Player:GetName() == KickPlayerName) then - Player:GetClientHandle():Kick("You were kicked from the game!") - end +<pre class="prettyprint lang-lua"> +local KickPlayerName = Request.Params["players-kick"] +local FoundPlayerCallback = function(Player) + if (Player:GetName() == KickPlayerName) then + Player:GetClientHandle():Kick("You were kicked from the game!") + end +end +cRoot:Get():FindAndDoWithPlayer(KickPlayerName, FoundPlayerCallback) +</pre> +The cRoot:FindAndDoWithPlayer() is unsafe and could have caused a deadlock. The new solution is queue a task; but since we don't know in which world the player is, we need to queue the task to all worlds: +<pre class="prettyprint lang-lua"> +cRoot:Get():ForEachWorld( -- For each world... + function(World) + World:QueueTask( -- ... queue a task... + function(a_World) + a_World:DoWithPlayer(KickPlayerName, -- ... to walk the playerlist... + function (a_Player) + a_Player:GetClientHandle():Kick("You were kicked from the game!") -- ... and kick the player end - cRoot:Get():FindAndDoWithPlayer(KickPlayerName, FoundPlayerCallback) - </pre> - The cRoot:FindAndDoWithPlayer() is unsafe and could have caused a deadlock. The new solution is queue a task; but since we don't know in which world the player is, we need to queue the task to all worlds: - <pre class="prettyprint lang-lua"> - cRoot:Get():ForEachWorld( -- For each world... - function(World) - World:QueueTask( -- ... queue a task... - function(a_World) - a_World:DoWithPlayer(KickPlayerName, -- ... to walk the playerlist... - function (a_Player) - a_Player:GetClientHandle():Kick("You were kicked from the game!") -- ... and kick the player - end - ) - end - ) - end - ) - </pre> + ) + end + ) + end +) +</pre> <script> prettyPrint(); </script> |