1 /**
2 Copyright: Copyright (c) 2015-2016 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module chunkmanager;
7 
8 import std.experimental.logger;
9 import std.typecons : Nullable;
10 import server : ChunkWorldPos, ChunkDataSnapshot, Timestamp, BlockId, ChunkFreeList, BlockChange, HashSet;
11 
12 private enum ChunkState {
13 	non_loaded,
14 	added_loaded,
15 	removed_loading,
16 	added_loading,
17 	removed_loaded_saving,
18 	removed_loaded_used,
19 	added_loaded_saving,
20 }
21 
22 private enum traceStateStr = q{
23 	//infof("state @%s %s => %s", cwp, state,
24 	//	chunkStates.get(cwp, ChunkState.non_loaded));
25 };
26 
27 final class ChunkManager {
28 	void delegate(ChunkWorldPos)[] onChunkAddedHandlers;
29 	void delegate(ChunkWorldPos)[] onChunkRemovedHandlers;
30 	void delegate(ChunkWorldPos, ChunkDataSnapshot)[] onChunkLoadedHandlers;
31 	void delegate(ChunkWorldPos, BlockChange[])[] chunkChangesHandlers;
32 	void delegate(ChunkWorldPos cwp, BlockId[] outBuffer) loadChunkHandler;
33 	void delegate(ChunkWorldPos cwp, ChunkDataSnapshot snapshot) saveChunkHandler;
34 
35 	private ChunkFreeList freeList;
36 	private ChunkDataSnapshot[ChunkWorldPos] snapshots;
37 	private ChunkDataSnapshot[Timestamp][ChunkWorldPos] oldSnapshots;
38 	private BlockId[][ChunkWorldPos] writeBuffers;
39 	private BlockChange[][ChunkWorldPos] chunkChanges;
40 	private ChunkState[ChunkWorldPos] chunkStates;
41 	private HashSet!ChunkWorldPos modifiedChunks;
42 	private size_t[ChunkWorldPos] numInternalChunkUsers;
43 	private size_t[ChunkWorldPos] numExternalChunkUsers;
44 
45 
46 	/// Performs save of all modified chunks.
47 	/// Modified chunks
48 	void save() {
49 		foreach(cwp; modifiedChunks.items) {
50 			auto state = chunkStates.get(cwp, ChunkState.non_loaded);
51 			with(ChunkState) final switch(state) {
52 				case non_loaded:
53 					assert(false, "Save should not occur for not added chunks");
54 				case added_loaded:
55 					chunkStates[cwp] = added_loaded_saving;
56 					auto snap = cwp in snapshots;
57 					++snap.numUsers;
58 					saveChunkHandler(cwp, *snap);
59 					break;
60 				case removed_loading:
61 					assert(false, "Save should not occur for not loaded chunks");
62 				case added_loading:
63 					assert(false, "Save should not occur for not loaded chunks");
64 				case removed_loaded_saving:
65 					assert(false, "Save should not occur for not added chunks");
66 				case removed_loaded_used:
67 					assert(false, "Save should not occur for not added chunks");
68 				case added_loaded_saving:
69 					assert(false, "Save should not occur for not for saving chunk");
70 			}
71 			mixin(traceStateStr);
72 		}
73 		modifiedChunks.clear();
74 	}
75 
76 	/// Sets number of users of chunk at cwp.
77 	/// If total chunk users if greater than zero, then chunk is loaded,
78 	/// if equal to zero, chunk will be unloaded.
79 	void setExternalChunkUsers(ChunkWorldPos cwp, size_t numExternalUsers) {
80 		numExternalChunkUsers[cwp] = numExternalUsers;
81 		if (numExternalUsers == 0)
82 			numExternalChunkUsers.remove(cwp);
83 		setChunkTotalObservers(cwp, numInternalChunkUsers.get(cwp, 0) + numExternalUsers);
84 	}
85 
86 	/// returned value isNull if chunk is not loaded/added
87 	Nullable!ChunkDataSnapshot getChunkSnapshot(ChunkWorldPos cwp) {
88 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
89 		if (state == ChunkState.added_loaded || state == ChunkState.added_loaded_saving)
90 			return Nullable!ChunkDataSnapshot(snapshots[cwp]);
91 		else {
92 			return Nullable!ChunkDataSnapshot.init;
93 		}
94 	}
95 
96 	/// Returns writeable copy of current chunk snapshot.
97 	/// Any changes made to it must be reported trough onBlockChanges method.
98 	/// This buffer is valid until commit.
99 	/// After commit this buffer becomes next immutable snapshot.
100 	/// Returns null if chunk is not added and/or not loaded.
101 	BlockId[] getWriteBuffer(ChunkWorldPos cwp) {
102 		auto newData = writeBuffers.get(cwp, null);
103 		if (newData is null) {
104 			newData = createWriteBuffer(cwp);
105 		}
106 		return newData;
107 	}
108 
109 	import std.range : isInputRange, array;
110 	/// Call this whenewer changes to write buffer are done.
111 	/// Those changes will be passed to chunkChangesHandlers to be handled when sendChanges is called.
112 	void onBlockChanges(R)(ChunkWorldPos cwp, R blockChanges)
113 		if (isInputRange!(R))
114 	{
115 		chunkChanges[cwp] = chunkChanges.get(cwp, null) ~ blockChanges.array;
116 	}
117 
118 	/// Returns timestamp of current chunk snapshot.
119 	/// Store this timestamp to use in removeSnapshotUser
120 	Timestamp addCurrentSnapshotUser(ChunkWorldPos cwp) {
121 		auto snap = cwp in snapshots;
122 		assert(snap, "Cannot add chunk user. No such snapshot.");
123 
124 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
125 		assert(state == ChunkState.added_loaded || state == ChunkState.added_loaded_saving,
126 			"To add user chunk must be both added and loaded");
127 
128 		++snap.numUsers;
129 		return snap.timestamp;
130 	}
131 
132 	/// Generic removal of snapshot user. Removes chunk if numUsers == 0.
133 	/// Use this to remove added snapshot user. Use timestamp returned from addCurrentSnapshotUser.
134 	void removeSnapshotUser(ChunkWorldPos cwp, Timestamp timestamp) {
135 		auto snap = cwp in snapshots;
136 		if (snap && snap.timestamp == timestamp) {
137 			auto numUsersLeft = removeCurrentSnapshotUser(cwp);
138 			if (numUsersLeft == 0) {
139 				auto state = chunkStates.get(cwp, ChunkState.non_loaded);
140 				if (state == ChunkState.removed_loaded_used) {
141 					chunkStates[cwp] = ChunkState.non_loaded;
142 					clearChunkData(cwp);
143 				}
144 			}
145 		} else {
146 			auto snapshot = removeOldSnapshotUser(cwp, timestamp);
147 			if (snapshot.numUsers == 0)
148 				recycleSnapshotMemory(snapshot);
149 		}
150 	}
151 
152 	/// Internal. Called by code which loads chunks from storage.
153 	void onSnapshotLoaded(ChunkWorldPos cwp, ChunkDataSnapshot snap) {
154 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
155 		with(ChunkState) final switch(state) {
156 			case non_loaded:
157 				assert(false);
158 			case added_loaded:
159 				assert(false, "On loaded should not occur for already loaded chunk");
160 			case removed_loading:
161 				chunkStates[cwp] = non_loaded;
162 				clearChunkData(cwp);
163 				break;
164 			case added_loading:
165 				chunkStates[cwp] = added_loaded;
166 				snapshots[cwp] = ChunkDataSnapshot(snap.blocks, snap.timestamp);
167 				notifyLoaded(cwp);
168 				break;
169 			case removed_loaded_saving:
170 				assert(false, "On loaded should not occur for already loaded chunk");
171 			case removed_loaded_used:
172 				assert(false, "On loaded should not occur for already loaded chunk");
173 			case added_loaded_saving:
174 				assert(false, "On loaded should not occur for already loaded chunk");
175 		}
176 		mixin(traceStateStr);
177 	}
178 
179 	/// Internal. Called by code which saves chunks to storage.
180 	void onSnapshotSaved(ChunkWorldPos cwp, ChunkDataSnapshot savedSnap) {
181 		auto snap = cwp in snapshots;
182 		if (snap && snap.timestamp == savedSnap.timestamp) {
183 			auto state = chunkStates.get(cwp, ChunkState.non_loaded);
184 			with(ChunkState) final switch(state) {
185 				case non_loaded:
186 					assert(false, "On saved should not occur for not added chunks");
187 				case added_loaded:
188 					assert(false, "On saved should not occur for not saving chunks");
189 				case removed_loading:
190 					assert(false, "On saved should not occur for not loaded chunks");
191 				case added_loading:
192 					assert(false, "On saved should not occur for not loaded chunks");
193 				case removed_loaded_saving:
194 					auto numUsersLeft = removeCurrentSnapshotUser(cwp);
195 					if (numUsersLeft == 0) {
196 						chunkStates[cwp] = non_loaded;
197 						clearChunkData(cwp);
198 					} else {
199 						chunkStates[cwp] = removed_loaded_used;
200 					}
201 					break;
202 				case removed_loaded_used:
203 					assert(false, "On saved should not occur for not saving chunks");
204 				case added_loaded_saving:
205 					chunkStates[cwp] = added_loaded;
206 					removeCurrentSnapshotUser(cwp);
207 					break;
208 			}
209 			mixin(traceStateStr);
210 		} else { // old snapshot saved
211 			auto snapshot = removeOldSnapshotUser(cwp, savedSnap.timestamp);
212 			if (snapshot.numUsers == 0)
213 				recycleSnapshotMemory(snapshot);
214 		}
215 	}
216 
217 	/// called at the end of tick
218 	void commitSnapshots(Timestamp currentTime) {
219 		auto writeBuffersCopy = writeBuffers;
220 		clearWriteBuffers();
221 		foreach(snapshot; writeBuffersCopy.byKeyValue) {
222 			auto cwp = snapshot.key;
223 			auto blocks = snapshot.value;
224 			modifiedChunks.put(cwp);
225 			commitChunkSnapshot(cwp, blocks, currentTime);
226 		}
227 	}
228 
229 	/// Send changes to clients
230 	void sendChanges() {
231 		foreach(changes; chunkChanges.byKeyValue) {
232 			foreach(handler; chunkChangesHandlers)
233 				handler(changes.key, changes.value);
234 		}
235 		clearChunkChanges();
236 	}
237 
238 	//	PPPPPP  RRRRRR  IIIII VV     VV   AAA   TTTTTTT EEEEEEE
239 	//	PP   PP RR   RR  III  VV     VV  AAAAA    TTT   EE
240 	//	PPPPPP  RRRRRR   III   VV   VV  AA   AA   TTT   EEEEE
241 	//	PP      RR  RR   III    VV VV   AAAAAAA   TTT   EE
242 	//	PP      RR   RR IIIII    VVV    AA   AA   TTT   EEEEEEE
243 	//
244 
245 	private void notifyAdded(ChunkWorldPos cwp) {
246 		foreach(handler; onChunkAddedHandlers)
247 			handler(cwp);
248 	}
249 
250 	private void notifyRemoved(ChunkWorldPos cwp) {
251 		foreach(handler; onChunkRemovedHandlers)
252 			handler(cwp);
253 	}
254 
255 	private void notifyLoaded(ChunkWorldPos cwp) {
256 		auto snap = getChunkSnapshot(cwp);
257 		assert(!snap.isNull);
258 		foreach(handler; onChunkLoadedHandlers)
259 			handler(cwp, snap);
260 	}
261 
262 	// Puts chunk in added state requesting load if needed.
263 	// Notifies on add. Notifies on load if loaded.
264 	private void loadChunk(ChunkWorldPos cwp) {
265 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
266 		with(ChunkState) final switch(state) {
267 			case non_loaded:
268 				chunkStates[cwp] = added_loading;
269 				loadChunkHandler(cwp, freeList.allocate());
270 				notifyAdded(cwp);
271 				break;
272 			case added_loaded:
273 				break; // ignore
274 			case removed_loading:
275 				chunkStates[cwp] = added_loading;
276 				notifyAdded(cwp);
277 				break;
278 			case added_loading:
279 				break; // ignore
280 			case removed_loaded_saving:
281 				chunkStates[cwp] = added_loaded_saving;
282 				notifyAdded(cwp);
283 				notifyLoaded(cwp);
284 				break;
285 			case removed_loaded_used:
286 				chunkStates[cwp] = added_loaded;
287 				notifyAdded(cwp);
288 				notifyLoaded(cwp);
289 				break;
290 			case added_loaded_saving:
291 				break; // ignore
292 		}
293 		mixin(traceStateStr);
294 	}
295 
296 	// Puts chunk in removed state requesting save if needed.
297 	// Notifies on remove.
298 	private void unloadChunk(ChunkWorldPos cwp) {
299 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
300 		with(ChunkState) final switch(state) {
301 			case non_loaded:
302 				assert(false, "Unload should not occur when chunk was not yet loaded");
303 			case added_loaded:
304 				assert(cwp !in writeBuffers, "Chunk with write buffer should not be unloaded");
305 				notifyRemoved(cwp);
306 				auto snap = cwp in snapshots;
307 				if(cwp in modifiedChunks) {
308 					chunkStates[cwp] = removed_loaded_saving;
309 					saveChunkHandler(cwp, *snap);
310 					++snap.numUsers;
311 					modifiedChunks.remove(cwp);
312 				} else { // state 0
313 					chunkStates[cwp] = non_loaded;
314 					clearChunkData(cwp);
315 				}
316 				break;
317 			case removed_loading:
318 				assert(false, "Unload should not occur when chunk is already removed");
319 			case added_loading:
320 				notifyRemoved(cwp);
321 				chunkStates[cwp] = removed_loading;
322 				break;
323 			case removed_loaded_saving:
324 				assert(false, "Unload should not occur when chunk is already removed");
325 			case removed_loaded_used:
326 				assert(false, "Unload should not occur when chunk is already removed");
327 			case added_loaded_saving:
328 				notifyRemoved(cwp);
329 				chunkStates[cwp] = removed_loaded_saving;
330 				break;
331 		}
332 		mixin(traceStateStr);
333 	}
334 
335 	// Fully removes chunk
336 	private void clearChunkData(ChunkWorldPos cwp) {
337 		snapshots.remove(cwp);
338 		assert(cwp !in writeBuffers);
339 		assert(cwp !in chunkChanges);
340 		assert(cwp !in modifiedChunks);
341 		chunkStates.remove(cwp);
342 	}
343 
344 	// Creates write buffer for writing changes in it.
345 	// Latest snapshot's data is copied in it.
346 	// On commit stage this is moved into new snapshot and.
347 	// Adds internal user that is removed on commit to prevent unloading with uncommitted changes.
348 	private BlockId[] createWriteBuffer(ChunkWorldPos cwp) {
349 		assert(writeBuffers.get(cwp, null) is null);
350 		auto old = getChunkSnapshot(cwp);
351 		if (old.isNull) {
352 			return null;
353 		}
354 		auto newData = freeList.allocate();
355 		newData[] = old.blocks;
356 		writeBuffers[cwp] = newData;
357 		addInternalUser(cwp); // prevent unload until commit
358 		return newData;
359 	}
360 
361 	// Here comes sum of all internal and external chunk users which results in loading or unloading of specific chunk.
362 	private void setChunkTotalObservers(ChunkWorldPos cwp, size_t totalObservers) {
363 		if (totalObservers > 0) {
364 			loadChunk(cwp);
365 		} else {
366 			unloadChunk(cwp);
367 		}
368 	}
369 
370 	// Used inside chunk manager to add chunk users, to prevent chunk unloading.
371 	private void addInternalUser(ChunkWorldPos cwp) {
372 		numInternalChunkUsers[cwp] = numInternalChunkUsers.get(cwp, 0) + 1;
373 		auto totalUsers = numInternalChunkUsers[cwp] + numExternalChunkUsers.get(cwp, 0);
374 		setChunkTotalObservers(cwp, totalUsers);
375 	}
376 
377 	// Used inside chunk manager to remove chunk users.
378 	private void removeInternalUser(ChunkWorldPos cwp) {
379 		auto numUsers = numInternalChunkUsers.get(cwp, 0);
380 		assert(numUsers > 0, "numInternalChunkUsers is zero when removing internal user");
381 		--numUsers;
382 		if (numUsers == 0)
383 			numInternalChunkUsers.remove(cwp);
384 		else
385 			numInternalChunkUsers[cwp] = numUsers;
386 		auto totalUsers = numUsers + numExternalChunkUsers.get(cwp, 0);
387 		setChunkTotalObservers(cwp, totalUsers);
388 	}
389 
390 	private void clearWriteBuffers() {
391 		writeBuffers = null;
392 	}
393 
394 	private void clearChunkChanges() {
395 		chunkChanges = null;
396 	}
397 
398 	// Returns number of current snapshot users left.
399 	private uint removeCurrentSnapshotUser(ChunkWorldPos cwp) {
400 		auto snap = cwp in snapshots;
401 		assert(snap, "Cannot remove chunk user. No such snapshot.");
402 		assert(snap.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users.");
403 		--snap.numUsers;
404 		return snap.numUsers;
405 	}
406 
407 	// Returns that snapshot with updated numUsers.
408 	// Snapshot is removed from oldSnapshots if numUsers == 0.
409 	private ChunkDataSnapshot removeOldSnapshotUser(ChunkWorldPos cwp, Timestamp timestamp) {
410 		ChunkDataSnapshot[Timestamp]* chunkSnaps = cwp in oldSnapshots;
411 		assert(chunkSnaps, "old snapshot should have waited for releasing user");
412 		ChunkDataSnapshot* snapshot = timestamp in *chunkSnaps;
413 		assert(snapshot, "cannot release snapshot user. No such snapshot");
414 		assert(snapshot.numUsers > 0, "snapshot with 0 users was not released");
415 		--snapshot.numUsers;
416 		if (snapshot.numUsers == 0) {
417 			(*chunkSnaps).remove(timestamp);
418 			if ((*chunkSnaps).length == 0) { // all old snaps of one chunk released
419 				oldSnapshots.remove(cwp);
420 			}
421 		}
422 		return *snapshot;
423 	}
424 
425 	// Commit for single chunk.
426 	private void commitChunkSnapshot(ChunkWorldPos cwp, BlockId[] blocks, Timestamp currentTime) {
427 		auto currentSnapshot = getChunkSnapshot(cwp);
428 		assert(!currentSnapshot.isNull);
429 		if (currentSnapshot.numUsers == 0)
430 			recycleSnapshotMemory(currentSnapshot);
431 		else {
432 			ChunkDataSnapshot[Timestamp] chunkSnaps = oldSnapshots.get(cwp, null);
433 			assert(currentTime !in chunkSnaps);
434 			chunkSnaps[currentTime] = currentSnapshot.get;
435 		}
436 		snapshots[cwp] = ChunkDataSnapshot(blocks, currentTime);
437 
438 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
439 		with(ChunkState) final switch(state) {
440 			case non_loaded:
441 				assert(false, "Commit is not possible for non-loaded chunk");
442 			case added_loaded:
443 				break; // ignore
444 			case removed_loading:
445 				// Write buffer will be never returned when no snapshot is loaded.
446 				assert(false, "Commit is not possible for removed chunk");
447 			case added_loading:
448 				// Write buffer will be never returned when no snapshot is loaded.
449 				assert(false, "Commit is not possible for non-loaded chunk");
450 			case removed_loaded_saving:
451 				// This is guarded by internal user count.
452 				assert(false, "Commit is not possible for removed chunk");
453 			case removed_loaded_used:
454 				// This is guarded by internal user count.
455 				assert(false, "Commit is not possible for removed chunk");
456 			case added_loaded_saving:
457 				// This is now old snapshot with saving state. New one is not used by IO.
458 				chunkStates[cwp] = added_loaded;
459 				break;
460 		}
461 		removeInternalUser(cwp); // remove user added in getWriteBuffer
462 
463 		mixin(traceStateStr);
464 	}
465 
466 	// Called when snapshot data can be recycled.
467 	private void recycleSnapshotMemory(ChunkDataSnapshot snap) {
468 		freeList.deallocate(snap.blocks);
469 	}
470 }
471 
472 //	TTTTTTT EEEEEEE  SSSSS  TTTTTTT  SSSSS
473 //	  TTT   EE      SS        TTT   SS
474 //	  TTT   EEEEE    SSSSS    TTT    SSSSS
475 //	  TTT   EE           SS   TTT        SS
476 //	  TTT   EEEEEEE  SSSSS    TTT    SSSSS
477 //
478 
479 version(unittest) {
480 	private struct Handlers {
481 		void setup(ChunkManager cm) {
482 			cm.onChunkAddedHandlers ~= &onChunkAddedHandler;
483 			cm.onChunkRemovedHandlers ~= &onChunkRemovedHandler;
484 			cm.onChunkLoadedHandlers ~= &onChunkLoadedHandler;
485 			cm.chunkChangesHandlers ~= &chunkChangesHandler;
486 			cm.loadChunkHandler = &loadChunkHandler;
487 			cm.saveChunkHandler = &saveChunkHandler;
488 		}
489 		void onChunkAddedHandler(ChunkWorldPos) {
490 			onChunkAddedHandlerCalled = true;
491 		}
492 		void onChunkRemovedHandler(ChunkWorldPos) {
493 			onChunkRemovedHandlerCalled = true;
494 		}
495 		void onChunkLoadedHandler(ChunkWorldPos, ChunkDataSnapshot) {
496 			onChunkLoadedHandlerCalled = true;
497 		}
498 		void chunkChangesHandler(ChunkWorldPos, BlockChange[]) {
499 			chunkChangesHandlerCalled = true;
500 		}
501 		void loadChunkHandler(ChunkWorldPos cwp, BlockId[] outBuffer) {
502 			loadChunkHandlerCalled = true;
503 		}
504 		void saveChunkHandler(ChunkWorldPos cwp, ChunkDataSnapshot snapshot) {
505 			saveChunkHandlerCalled = true;
506 		}
507 		void assertCalled(size_t flags) {
508 			assert(!(((flags & 0b0000_0001) > 0) ^ onChunkAddedHandlerCalled));
509 			assert(!(((flags & 0b0000_0010) > 0) ^ onChunkRemovedHandlerCalled));
510 			assert(!(((flags & 0b0000_0100) > 0) ^ onChunkLoadedHandlerCalled));
511 			assert(!(((flags & 0b0000_1000) > 0) ^ chunkChangesHandlerCalled));
512 			assert(!(((flags & 0b0001_0000) > 0) ^ loadChunkHandlerCalled));
513 			assert(!(((flags & 0b0010_0000) > 0) ^ saveChunkHandlerCalled));
514 		}
515 
516 		bool onChunkAddedHandlerCalled;
517 		bool onChunkRemovedHandlerCalled;
518 		bool onChunkLoadedHandlerCalled;
519 		bool chunkChangesHandlerCalled;
520 		bool loadChunkHandlerCalled;
521 		bool saveChunkHandlerCalled;
522 	}
523 
524 	private struct FSMTester {
525 		auto cwp = ChunkWorldPos(0);
526 		auto currentState(ref ChunkManager cm) {
527 			return cm.chunkStates.get(ChunkWorldPos(0), ChunkState.non_loaded);
528 		}
529 		void resetChunk(ref ChunkManager cm) {
530 			cm.snapshots.remove(cwp);
531 			cm.oldSnapshots.remove(cwp);
532 			cm.writeBuffers.remove(cwp);
533 			cm.chunkChanges.remove(cwp);
534 			cm.chunkStates.remove(cwp);
535 			cm.modifiedChunks.remove(cwp);
536 			cm.numInternalChunkUsers.remove(cwp);
537 			cm.numExternalChunkUsers.remove(cwp);
538 		}
539 		void gotoState(ref ChunkManager cm, ChunkState state) {
540 			resetChunk(cm);
541 			with(ChunkState) final switch(state) {
542 				case non_loaded:
543 					break;
544 				case added_loaded:
545 					cm.setExternalChunkUsers(cwp, 1);
546 					cm.onSnapshotLoaded(cwp, ChunkDataSnapshot(new BlockId[16]));
547 					break;
548 				case removed_loading:
549 					cm.setExternalChunkUsers(cwp, 1);
550 					cm.setExternalChunkUsers(cwp, 0);
551 					break;
552 				case added_loading:
553 					cm.setExternalChunkUsers(cwp, 1);
554 					break;
555 				case removed_loaded_saving:
556 					gotoState(cm, ChunkState.added_loaded_saving);
557 					cm.setExternalChunkUsers(cwp, 0);
558 					break;
559 				case removed_loaded_used:
560 					gotoState(cm, ChunkState.added_loaded);
561 					cm.getWriteBuffer(cwp);
562 					cm.commitSnapshots(1);
563 					cm.addCurrentSnapshotUser(cwp);
564 					cm.save();
565 					cm.setExternalChunkUsers(cwp, 0);
566 					cm.onSnapshotSaved(cwp, ChunkDataSnapshot(new BlockId[16], Timestamp(1)));
567 					break;
568 				case added_loaded_saving:
569 					gotoState(cm, ChunkState.added_loaded);
570 					cm.getWriteBuffer(cwp);
571 					cm.commitSnapshots(1);
572 					cm.save();
573 					break;
574 			}
575 			import std.string : format;
576 			assert(currentState(cm) == state,
577 				format("Failed to set state %s, got %s", state, currentState(cm)));
578 		}
579 	}
580 }
581 
582 
583 unittest {
584 	import voxelman.log : setupLogger;
585 	setupLogger("snapmantest.log");
586 
587 	Handlers h;
588 	ChunkManager cm;
589 	FSMTester fsmTester;
590 	ChunkWorldPos cwp = ChunkWorldPos(0);
591 
592 	void assertState(ChunkState state) {
593 		import std.string : format;
594 		auto actualState = cm.chunkStates.get(ChunkWorldPos(0), ChunkState.non_loaded);
595 		assert(actualState == state,
596 			format("Got state '%s', while needed '%s'", actualState, state));
597 	}
598 
599 	void resetHandlersState() {
600 		h = Handlers.init;
601 	}
602 	void resetChunkManager() {
603 		cm = new ChunkManager;
604 		h.setup(cm);
605 	}
606 	void reset() {
607 		resetHandlersState();
608 		resetChunkManager();
609 	}
610 
611 	void setupState(ChunkState state) {
612 		fsmTester.gotoState(cm, state);
613 		resetHandlersState();
614 	}
615 
616 	reset();
617 
618 	//--------------------------------------------------------------------------
619 	// non_loaded -> added_loading
620 	cm.setExternalChunkUsers(cwp, 1);
621 	assertState(ChunkState.added_loading);
622 	assert(cm.getChunkSnapshot(ChunkWorldPos(0)).isNull);
623 	h.assertCalled(0b0001_0001); //onChunkAddedHandlerCalled, loadChunkHandlerCalled
624 
625 
626 	//--------------------------------------------------------------------------
627 	setupState(ChunkState.added_loading);
628 	// added_loading -> removed_loading
629 	cm.setExternalChunkUsers(cwp, 0);
630 	assertState(ChunkState.removed_loading);
631 	assert( cm.getChunkSnapshot(ChunkWorldPos(0)).isNull);
632 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
633 
634 
635 	//--------------------------------------------------------------------------
636 	setupState(ChunkState.removed_loading);
637 	// removed_loading -> added_loading
638 	cm.setExternalChunkUsers(cwp, 1);
639 	assertState(ChunkState.added_loading);
640 	assert( cm.getChunkSnapshot(ChunkWorldPos(0)).isNull);
641 	h.assertCalled(0b0000_0001); //onChunkAddedHandlerCalled
642 
643 
644 	//--------------------------------------------------------------------------
645 	setupState(ChunkState.removed_loading);
646 	// removed_loading -> non_loaded
647 	cm.onSnapshotLoaded(ChunkWorldPos(0), ChunkDataSnapshot(new BlockId[16]));
648 	assertState(ChunkState.non_loaded);
649 	assert( cm.getChunkSnapshot(ChunkWorldPos(0)).isNull); // null
650 	h.assertCalled(0b0000_0000);
651 
652 	//--------------------------------------------------------------------------
653 	setupState(ChunkState.added_loading);
654 	// added_loading -> added_loaded
655 	cm.onSnapshotLoaded(ChunkWorldPos(0), ChunkDataSnapshot(new BlockId[16]));
656 	assertState(ChunkState.added_loaded);
657 	assert(!cm.getChunkSnapshot(ChunkWorldPos(0)).isNull); // !null
658 	h.assertCalled(0b0000_0100); //onChunkLoadedHandlerCalled
659 
660 	//--------------------------------------------------------------------------
661 	setupState(ChunkState.added_loaded);
662 	// added_loaded -> non_loaded
663 	cm.setExternalChunkUsers(cwp, 0);
664 	assertState(ChunkState.non_loaded);
665 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
666 
667 	//--------------------------------------------------------------------------
668 	setupState(ChunkState.added_loaded);
669 	// added_loaded -> removed_loaded_saving
670 	cm.getWriteBuffer(cwp);
671 	cm.commitSnapshots(Timestamp(1));
672 	cm.setExternalChunkUsers(cwp, 0);
673 	assertState(ChunkState.removed_loaded_saving);
674 	h.assertCalled(0b0010_0010); //onChunkRemovedHandlerCalled, loadChunkHandlerCalled
675 
676 
677 	//--------------------------------------------------------------------------
678 	setupState(ChunkState.added_loaded);
679 	// added_loaded -> added_loaded_saving
680 	cm.getWriteBuffer(cwp);
681 	cm.commitSnapshots(Timestamp(1));
682 	cm.save();
683 	assertState(ChunkState.added_loaded_saving);
684 	h.assertCalled(0b0010_0000); //loadChunkHandlerCalled
685 
686 
687 	//--------------------------------------------------------------------------
688 	setupState(ChunkState.added_loaded_saving);
689 	// added_loaded_saving -> added_loaded with commit
690 	cm.getWriteBuffer(cwp);
691 	cm.commitSnapshots(Timestamp(2));
692 	assertState(ChunkState.added_loaded);
693 	h.assertCalled(0b0000_0000);
694 
695 
696 	//--------------------------------------------------------------------------
697 	setupState(ChunkState.added_loaded_saving);
698 	// added_loaded_saving -> added_loaded with on_saved
699 	cm.onSnapshotSaved(cwp, ChunkDataSnapshot(new BlockId[16], Timestamp(1)));
700 	assertState(ChunkState.added_loaded);
701 	h.assertCalled(0b0000_0000);
702 
703 
704 	//--------------------------------------------------------------------------
705 	setupState(ChunkState.added_loaded_saving);
706 	// added_loaded_saving -> removed_loaded_saving
707 	cm.setExternalChunkUsers(cwp, 0);
708 	assertState(ChunkState.removed_loaded_saving);
709 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
710 
711 
712 	//--------------------------------------------------------------------------
713 	setupState(ChunkState.removed_loaded_saving);
714 	// removed_loaded_saving -> non_loaded
715 	cm.onSnapshotSaved(cwp, ChunkDataSnapshot(new BlockId[16], Timestamp(1)));
716 	assertState(ChunkState.non_loaded);
717 	h.assertCalled(0b0000_0000);
718 
719 
720 	//--------------------------------------------------------------------------
721 	setupState(ChunkState.added_loaded_saving);
722 	// removed_loaded_saving -> removed_loaded_used
723 	cm.addCurrentSnapshotUser(cwp);
724 	cm.setExternalChunkUsers(cwp, 0);
725 	assertState(ChunkState.removed_loaded_saving);
726 	cm.onSnapshotSaved(cwp, ChunkDataSnapshot(new BlockId[16], Timestamp(1)));
727 	assertState(ChunkState.removed_loaded_used);
728 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
729 
730 
731 	//--------------------------------------------------------------------------
732 	setupState(ChunkState.removed_loaded_saving);
733 	// removed_loaded_saving -> added_loaded_saving
734 	cm.setExternalChunkUsers(cwp, 1);
735 	assertState(ChunkState.added_loaded_saving);
736 	h.assertCalled(0b0000_0101); //onChunkAddedHandlerCalled, onChunkLoadedHandlerCalled
737 
738 
739 	//--------------------------------------------------------------------------
740 	setupState(ChunkState.removed_loaded_used);
741 	// removed_loaded_used -> non_loaded
742 	cm.removeSnapshotUser(cwp, Timestamp(1));
743 	assertState(ChunkState.non_loaded);
744 	h.assertCalled(0b0000_0000);
745 
746 
747 	//--------------------------------------------------------------------------
748 	setupState(ChunkState.removed_loaded_used);
749 	// removed_loaded_used -> added_loaded
750 	cm.setExternalChunkUsers(cwp, 1);
751 	assertState(ChunkState.added_loaded);
752 	h.assertCalled(0b0000_0101); //onChunkAddedHandlerCalled, onChunkLoadedHandlerCalled
753 }