summaryrefslogtreecommitdiffstats
path: root/Server/Plugins/APIDump/UsingChunkStays.html
blob: b8cc1bf5f1d78517c9c96d308034e1186be7f066 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
<!DOCTYPE html>
<html>
	<head>
		<title>Cuberite - Using ChunkStays</title>
		<link rel="canonical" href="https://api.cuberite.org/UsingChunkStays.html">
		<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>

		<!-- Piwik -->
		<script type="text/javascript">
		  var _paq = _paq || [];
		  _paq.push(['trackPageView']);
		  _paq.push(['enableLinkTracking']);
		  (function() {
			var u="https://analytics.cuberite.org/";
			_paq.push(['setTrackerUrl', u+'piwik.php']);
			_paq.push(['setSiteId', 5]);
			var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
			g.type='text/javascript'; g.async=true; g.defer=true; g.src='piwik.js'; s.parentNode.insertBefore(g,s);
		  })();
		</script>
		<noscript><p><img src="https://analytics.cuberite.org/piwik.php?idsite=5" style="border:0;" alt="" /></p></noscript>
		<!-- End Piwik Code -->

	</body>
</html>