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 @@ + + + + MCServer - Using ChunkStays + + + + + + + +
+

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.

+ +
+ + 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 @@

Example

The Core has the facility to kick players using the web interface. It used the following code for the kicking (inside the webadmin handler): -
-			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
+
+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)
+
+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: +
+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)
-			
- 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: -
-			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
-			)
-			
+ ) + end + ) + end +) +