1 /**
2 Copyright: Copyright (c) 2015-2017 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.world.storage.chunkmanager;
7 
8 import voxelman.log;
9 import std.typecons : Nullable;
10 import std..string : format;
11 public import std.typecons : Flag, Yes, No;
12 
13 import voxelman.world.block;
14 import voxelman.core.config;
15 import voxelman.world.storage.chunk;
16 import voxelman.world.storage.coordinates : ChunkWorldPos, adjacentPositions;
17 import voxelman.world.storage.utils;
18 import voxelman.container.hashset;
19 import voxelman.world.storage.chunkprovider;
20 
21 
22 private enum ChunkState {
23 	non_loaded,
24 	added_loaded,
25 	added_loading,
26 	removed_loaded_used
27 }
28 
29 private enum traceStateStr = q{
30 	//infof("state @%s %s => %s", cwp, state,
31 	//	chunkStates.get(cwp, ChunkState.non_loaded));
32 };
33 
34 
35 struct AdjChunkPositions27 {
36 	this(ChunkWorldPos cwp) {
37 		central = cwp;
38 		adjacentPositions!26(cwp, adjacent26);
39 	}
40 	union {
41 		ChunkWorldPos[27] all;
42 		ChunkWorldPos[26] adjacent26;
43 		struct {
44 			ChunkWorldPos[6] adjacent6;
45 			ChunkWorldPos[20] adjacent20;
46 			ChunkWorldPos central;
47 		}
48 	}
49 }
50 
51 struct AdjChunkLayers27 {
52 	union {
53 		Nullable!ChunkLayerSnap[27] all;
54 		Nullable!ChunkLayerSnap[26] adjacent26;
55 		struct {
56 			Nullable!ChunkLayerSnap[6] adjacent6;
57 			Nullable!ChunkLayerSnap[20] adjacent20;
58 			Nullable!ChunkLayerSnap central;
59 		}
60 	}
61 }
62 
63 Nullable!ChunkLayerSnap[len] getChunkSnapshots
64 	(size_t len)
65 	(ChunkManager cm,
66 	ChunkWorldPos[len] positions,
67 	ubyte layer,
68 	Flag!"Uncompress" uncompress = No.Uncompress)
69 {
70 	typeof(return) result;
71 
72 	foreach(i, cwp; positions)
73 	{
74 		result[i] = cm.getChunkSnapshot(cwp, layer, uncompress);
75 	}
76 
77 	return result;
78 }
79 
80 //version = TRACE_SNAP_USERS;
81 //version = DBG_OUT;
82 //version = DBG_COMPR;
83 
84 enum WriteBufferPolicy
85 {
86 	createUniform,
87 	copySnapshotArray,
88 }
89 
90 struct ChunkLayerInfo
91 {
92 	/// Defines the size of
93 	LayerDataLenType uniformExpansionType;
94 }
95 
96 enum MAX_CHUNK_LAYERS = 8;
97 
98 final class ChunkManager {
99 	void delegate(ChunkWorldPos)[] onChunkAddedHandlers;
100 	void delegate(ChunkWorldPos)[] onChunkRemovedHandlers;
101 	void delegate(ChunkWorldPos) onChunkLoadedHandler;
102 
103 	void delegate(ChunkWorldPos) loadChunkHandler;
104 	void delegate(ChunkWorldPos) cancelLoadChunkHandler;
105 
106 	// Used on server only
107 	size_t delegate() startChunkSave;
108 	void delegate(ChunkLayerItem layer) pushLayer;
109 	void delegate(size_t headerPos, ChunkHeaderItem header) endChunkSave;
110 
111 	bool isLoadCancelingEnabled = false; /// Set to true on client to cancel load on unload
112 	bool isChunkSavingEnabled = true;
113 	long totalLayerDataBytes; // debug
114 
115 	private ChunkLayerSnap[ChunkWorldPos][] snapshots;
116 	private ChunkLayerSnap[TimestampType][ChunkWorldPos][] oldSnapshots;
117 	private WriteBuffer[ChunkWorldPos][] writeBuffers;
118 	private ChunkState[ChunkWorldPos] chunkStates;
119 	private HashSet!ChunkWorldPos modifiedChunks;
120 	private size_t[ChunkWorldPos] numInternalChunkObservers;
121 	private size_t[ChunkWorldPos] numExternalChunkObservers;
122 	// total number of snapshot users of all 'snapshots'
123 	// used to change state from added_loaded to removed_loaded_used
124 	private size_t[ChunkWorldPos] totalSnapshotUsers;
125 
126 	ubyte numLayers;
127 	private ChunkLayerInfo[] layerInfos;
128 
129 	void setup(ubyte _numLayers) {
130 		numLayers = _numLayers;
131 		snapshots.length = numLayers;
132 		oldSnapshots.length = numLayers;
133 		writeBuffers.length = numLayers;
134 		layerInfos.length = numLayers;
135 	}
136 
137 	void setLayerInfo(ChunkLayerInfo info, ubyte layer) {
138 		layerInfos[layer] = info;
139 	}
140 
141 	/// Performs save of all modified chunks.
142 	/// Modified chunks are those that were committed.
143 	/// Perform save right after commit.
144 	void save() {
145 		foreach(cwp; modifiedChunks) {
146 			saveChunk(cwp);
147 		}
148 		clearModifiedChunks();
149 	}
150 
151 	/// Used on client to clear modified chunks instead of saving them.
152 	void clearModifiedChunks()
153 	{
154 		modifiedChunks.clear();
155 	}
156 
157 	HashSet!ChunkWorldPos getModifiedChunks()
158 	{
159 		return modifiedChunks;
160 	}
161 
162 	bool areChunksLoaded(ChunkWorldPos[] positions) {
163 		foreach(pos; positions)
164 			if (!isChunkLoaded(pos))
165 				return false;
166 		return true;
167 	}
168 
169 	bool isChunkLoaded(ChunkWorldPos cwp) {
170 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
171 		return state == ChunkState.added_loaded;
172 	}
173 
174 	bool isChunkAdded(ChunkWorldPos cwp) {
175 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
176 		with(ChunkState) {
177 			return state == added_loaded || state == added_loading;
178 		}
179 	}
180 
181 	bool hasSnapshot(ChunkWorldPos cwp, ubyte layer) {
182 		return (cwp in snapshots[layer]) !is null;
183 	}
184 
185 	/// Sets number of users of chunk at cwp.
186 	/// If total chunk users if greater than zero, then chunk is loaded,
187 	/// if equal to zero, chunk will be unloaded.
188 	void setExternalChunkObservers(ChunkWorldPos cwp, size_t numExternalObservers) {
189 		numExternalChunkObservers[cwp] = numExternalObservers;
190 		if (numExternalObservers == 0)
191 			numExternalChunkObservers.remove(cwp);
192 		setChunkTotalObservers(cwp, numInternalChunkObservers.get(cwp, 0) + numExternalObservers);
193 	}
194 
195 	/// returned value isNull if chunk is not loaded/added
196 	/// If uncompress is Yes then tries to convert snapshot to uncompressed.
197 	/// If has users, then uncompressed snapshot copy is returned. Original will not be uncompressed.
198 	Nullable!ChunkLayerSnap getChunkSnapshot(ChunkWorldPos cwp, ubyte layer, Flag!"Uncompress" uncompress = No.Uncompress) {
199 		if (isChunkLoaded(cwp))
200 		{
201 			auto snap = cwp in snapshots[layer];
202 			if (snap)
203 			{
204 				if (snap.type == StorageType.compressedArray && uncompress)
205 				{
206 					ubyte[] decompressedData = decompressLayerData((*snap).getArray!ubyte);
207 					if (snap.numUsers == 0) {
208 						recycleSnapshotMemory(*snap);
209 						snap.dataPtr = decompressedData.ptr;
210 						snap.dataLength = cast(LayerDataLenType)decompressedData.length;
211 						totalLayerDataBytes += snap.dataLength;
212 						snap.type = StorageType.fullArray;
213 					}
214 					else
215 					{
216 						// BUG: memory leak
217 						ChunkLayerSnap res = *snap;
218 						res.dataPtr = decompressedData.ptr;
219 						res.dataLength = cast(LayerDataLenType)decompressedData.length;
220 						res.type = StorageType.fullArray;
221 						return Nullable!ChunkLayerSnap(res);
222 					}
223 				}
224 				auto res = Nullable!ChunkLayerSnap(*snap);
225 				return res;
226 			}
227 			else
228 			{
229 				auto res = ChunkLayerSnap.init;
230 				res.dataLength = layerInfos[layer].uniformExpansionType;
231 				return Nullable!ChunkLayerSnap(res);
232 			}
233 		}
234 
235 		auto res = Nullable!ChunkLayerSnap.init;
236 		return res;
237 	}
238 
239 	/// Returns writeable copy of current chunk snapshot.
240 	/// This buffer is valid until commit.
241 	/// After commit this buffer becomes next immutable snapshot.
242 	/// Returns null if chunk is not added and/or not loaded.
243 	/// If write buffer was not yet created then it is created based on policy.
244 	///
245 	/// If allowNonLoaded is enabled, then will create write buffer even if chunk is in non_loaded state.
246 	///   Useful for offline generation and conversion tools that write directly to chunk manager.
247 	///   You can write all chunk at once and then commit. Internal user will prevent write buffers from unloading.
248 	///   And on commit a save will be performed automatically.
249 	///
250 	/// BUG: returned pointer points inside hash table.
251 	///      If new write buffer is added hash table can reallocate.
252 	///      Do not use more than one write buffer at a time.
253 	///      Reallocation can prevent changes to buffers obtained earlier than reallocation to be invisible.
254 	WriteBuffer* getOrCreateWriteBuffer(ChunkWorldPos cwp, ubyte layer,
255 		WriteBufferPolicy policy = WriteBufferPolicy.createUniform,
256 		bool allowNonLoaded = false)
257 	{
258 		if (!isChunkLoaded(cwp) && !allowNonLoaded) return null;
259 		auto writeBuffer = cwp in writeBuffers[layer];
260 		if (writeBuffer is null) {
261 			writeBuffer = createWriteBuffer(cwp, layer);
262 			if (writeBuffer && policy == WriteBufferPolicy.copySnapshotArray) {
263 				auto old = getChunkSnapshot(cwp, layer);
264 				if (!old.isNull) {
265 					applyLayer(old, writeBuffer.layer);
266 				}
267 			}
268 		}
269 		return writeBuffer;
270 	}
271 
272 	/// Returns all write buffers. You can modify data in write buffers, but not
273 	/// hashmap itself.
274 	/// Can be used for metadata update before commit.
275 	WriteBuffer[ChunkWorldPos] getWriteBuffers(ubyte layer) {
276 		return writeBuffers[layer];
277 	}
278 
279 	/// Returns timestamp of current chunk snapshot.
280 	/// Store this timestamp to use in removeSnapshotUser
281 	/// Adding a user to non-existent snapshot of loaded chunk is allowed,
282 	/// since a ChunkLayerSnap.init is returned for such layer.
283 	/// Special TimestampType.max is returned.
284 	TimestampType addCurrentSnapshotUser(ChunkWorldPos cwp, ubyte layer) {
285 		auto snap = cwp in snapshots[layer];
286 		if (!snap)
287 		{
288 			return TimestampType.max;
289 		}
290 
291 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
292 		assert(state == ChunkState.added_loaded,
293 			format("To add user chunk must be both added and loaded, not %s", state));
294 
295 		totalSnapshotUsers[cwp] = totalSnapshotUsers.get(cwp, 0) + 1;
296 
297 		++snap.numUsers;
298 		version(TRACE_SNAP_USERS) tracef("#%s:%s (add cur:+1) %s/%s @%s", cwp, layer, snap.numUsers, totalSnapshotUsers[cwp], snap.timestamp);
299 		return snap.timestamp;
300 	}
301 
302 	/// Generic removal of snapshot user. Removes chunk if numUsers == 0.
303 	/// Use this to remove added snapshot user. Use timestamp returned from addCurrentSnapshotUser.
304 	/// Removing TimestampType.max is no-op.
305 	void removeSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp, ubyte layer) {
306 		if (timestamp == TimestampType.max) return;
307 		auto snap = cwp in snapshots[layer];
308 		if (snap && snap.timestamp == timestamp)
309 		{
310 			auto totalUsersLeft = removeCurrentSnapshotUser(cwp, layer);
311 			if (totalUsersLeft == 0)
312 			{
313 				auto state = chunkStates.get(cwp, ChunkState.non_loaded);
314 				assert(state == ChunkState.added_loaded || state == ChunkState.removed_loaded_used);
315 				if (state == ChunkState.removed_loaded_used)
316 				{
317 					chunkStates[cwp] = ChunkState.non_loaded;
318 					clearChunkData(cwp);
319 				}
320 			}
321 		}
322 		else
323 		{
324 			removeOldSnapshotUser(cwp, timestamp, layer);
325 		}
326 	}
327 
328 	/// Internal. Called by code which loads chunks from storage.
329 	/// LoadedChunk is a type that has following members:
330 	///   ChunkHeaderItem getHeader()
331 	///   ChunkLayerItem getLayer()
332 	void onSnapshotLoaded(ChunkWorldPos cwp, ChunkLayerItem[] layers, bool needsSave) {
333 		version(DBG_OUT)infof("res loaded %s", cwp);
334 
335 		foreach(layer; layers)
336 		{
337 			totalLayerDataBytes += getLayerDataBytes(layer);
338 
339 			snapshots[layer.layerId][cwp] = ChunkLayerSnap(layer);
340 			version(DBG_COMPR)if (layer.type == StorageType.compressedArray)
341 				infof("CM Loaded %s %s %s\n(%(%02x%))", cwp, layer.dataPtr, layer.dataLength, layer.getArray!ubyte);
342 		}
343 
344 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
345 
346 		with(ChunkState) final switch(state)
347 		{
348 			case non_loaded:
349 				if (isLoadCancelingEnabled) {
350 					clearChunkData(cwp);
351 				} else {
352 					assert(false, "On loaded should not occur for non-loading chunk");
353 				}
354 				break;
355 			case added_loaded:
356 				assert(false, "On loaded should not occur for already loaded chunk");
357 			case added_loading:
358 				chunkStates[cwp] = added_loaded;
359 				if (needsSave) modifiedChunks.put(cwp);
360 				notifyLoaded(cwp);
361 				break;
362 			case removed_loaded_used:
363 				assert(false, "On loaded should not occur for already loaded chunk");
364 		}
365 		mixin(traceStateStr);
366 	}
367 
368 	/// Internal. Called by code which saves chunks to storage.
369 	/// SavedChunk is a type that has following memeber:
370 	///   ChunkHeaderItem getHeader()
371 	///   ChunkLayerTimestampItem getLayerTimestamp()
372 	void onSnapshotSaved(ChunkWorldPos cwp, ChunkLayerTimestampItem[] timestamps) {
373 		version(DBG_OUT)infof("res saved %s", cwp);
374 
375 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
376 		foreach(layer; timestamps)
377 		{
378 			// will delete current chunk when totalUsersLeft becomes 0 and is removed
379 			removeSnapshotUser(cwp, layer.timestamp, layer.layerId);
380 		}
381 		mixin(traceStateStr);
382 	}
383 
384 	/// called at the end of tick
385 	void commitSnapshots(TimestampType currentTime) {
386 		foreach(ubyte layer; 0..numLayers)
387 		{
388 			auto writeBuffersCopy = writeBuffers[layer];
389 			// Clear it here because commit can unload chunk.
390 			// And unload asserts that chunk is not in writeBuffers.
391 			writeBuffers[layer] = null;
392 			foreach(snapshot; writeBuffersCopy.byKeyValue)
393 			{
394 				auto cwp = snapshot.key;
395 				WriteBuffer writeBuffer = snapshot.value;
396 				if (writeBuffer.isModified)
397 				{
398 					modifiedChunks.put(cwp);
399 					commitLayerSnapshot(cwp, writeBuffer, currentTime, layer);
400 					if (!isChunkLoaded(cwp)) {
401 						chunkStates[cwp] = ChunkState.added_loaded;
402 						notifyLoaded(cwp);
403 					}
404 				}
405 				else
406 				{
407 					freeLayerArray(writeBuffer.layer);
408 				}
409 				removeInternalObserver(cwp); // remove user added in createWriteBuffer
410 			}
411 		}
412 	}
413 
414 	//	PPPPPP  RRRRRR  IIIII VV     VV   AAA   TTTTTTT EEEEEEE
415 	//	PP   PP RR   RR  III  VV     VV  AAAAA    TTT   EE
416 	//	PPPPPP  RRRRRR   III   VV   VV  AA   AA   TTT   EEEEE
417 	//	PP      RR  RR   III    VV VV   AAAAAAA   TTT   EE
418 	//	PP      RR   RR IIIII    VVV    AA   AA   TTT   EEEEEEE
419 	//
420 
421 	private void notifyAdded(ChunkWorldPos cwp) {
422 		foreach(handler; onChunkAddedHandlers)
423 			handler(cwp);
424 	}
425 
426 	private void notifyRemoved(ChunkWorldPos cwp) {
427 		foreach(handler; onChunkRemovedHandlers)
428 			handler(cwp);
429 	}
430 
431 	private void notifyLoaded(ChunkWorldPos cwp) {
432 		if (onChunkLoadedHandler) onChunkLoadedHandler(cwp);
433 	}
434 
435 	private void saveChunk(ChunkWorldPos cwp)
436 	{
437 		assert(startChunkSave, "startChunkSave is null");
438 		assert(pushLayer, "pushLayer is null");
439 		assert(endChunkSave, "endChunkSave is null");
440 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
441 		assert(state == ChunkState.added_loaded, "Save should only occur for added_loaded chunks");
442 
443 		size_t headerPos = startChunkSave();
444 		// code lower does work of addCurrentSnapshotUsers too
445 		ubyte numChunkLayers;
446 		foreach(ubyte layerId; 0..numLayers)
447 		{
448 			if (auto snap = cwp in snapshots[layerId])
449 			{
450 				++numChunkLayers;
451 				++snap.numUsers; // in case new snapshot replaces current one, we need to keep it while it is saved
452 				pushLayer(ChunkLayerItem(*snap, layerId));
453 			}
454 		}
455 		totalSnapshotUsers[cwp] = totalSnapshotUsers.get(cwp, 0) + numChunkLayers;
456 
457 		endChunkSave(headerPos, ChunkHeaderItem(cwp, numChunkLayers, 0));
458 	}
459 
460 	// Puts chunk in added state requesting load if needed.
461 	// Notifies on add. Notifies on load if loaded.
462 	private void loadChunk(ChunkWorldPos cwp) {
463 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
464 		with(ChunkState) final switch(state) {
465 			case non_loaded:
466 				chunkStates[cwp] = added_loading;
467 				loadChunkHandler(cwp);
468 				notifyAdded(cwp);
469 				break;
470 			case added_loaded:
471 				break; // ignore
472 			case added_loading:
473 				break; // ignore
474 			case removed_loaded_used:
475 				chunkStates[cwp] = added_loaded;
476 				notifyAdded(cwp);
477 				notifyLoaded(cwp);
478 				break;
479 		}
480 		mixin(traceStateStr);
481 	}
482 
483 	// Puts chunk in removed state requesting save if needed.
484 	// Notifies on remove.
485 	private void unloadChunk(ChunkWorldPos cwp) {
486 		auto state = chunkStates.get(cwp, ChunkState.non_loaded);
487 		notifyRemoved(cwp);
488 		with(ChunkState) final switch(state) {
489 			case non_loaded:
490 				assert(false, "Unload should not occur when chunk was not yet loaded");
491 			case added_loaded:
492 				if(cwp in modifiedChunks)
493 				{
494 					modifiedChunks.remove(cwp);
495 					if (isChunkSavingEnabled)
496 					{
497 						saveChunk(cwp);
498 						chunkStates[cwp] = removed_loaded_used;
499 					}
500 				}
501 				else
502 				{ // state 0
503 					auto totalUsersLeft = totalSnapshotUsers.get(cwp, 0);
504 					if (totalUsersLeft == 0)
505 					{
506 						chunkStates[cwp] = non_loaded;
507 						modifiedChunks.remove(cwp);
508 						clearChunkData(cwp);
509 					}
510 					else
511 					{
512 						chunkStates[cwp] = removed_loaded_used;
513 					}
514 				}
515 				break;
516 			case added_loading:
517 				cancelLoadChunkHandler(cwp);
518 				clearChunkData(cwp);
519 				break;
520 			case removed_loaded_used:
521 				assert(false, "Unload should not occur when chunk is already removed");
522 		}
523 		mixin(traceStateStr);
524 	}
525 
526 	// Fully removes chunk
527 	private void clearChunkData(ChunkWorldPos cwp) {
528 		foreach(layer; 0..numLayers)
529 		{
530 			if (auto snap = cwp in snapshots[layer])
531 			{
532 				recycleSnapshotMemory(*snap);
533 				snapshots[layer].remove(cwp);
534 				assert(cwp !in writeBuffers[layer]);
535 			}
536 			assert(cwp !in totalSnapshotUsers);
537 		}
538 		assert(cwp !in modifiedChunks);
539 		chunkStates.remove(cwp);
540 	}
541 
542 	// Creates write buffer for writing changes in it.
543 	// Latest snapshot's data is not copied in it.
544 	// Write buffer is then avaliable through getWriteBuffer/getOrCreateWriteBuffer.
545 	// On commit stage WB is moved into new snapshot if write buffer was modified.
546 	// Adds internal user that is removed on commit to prevent chunk from unloading with uncommitted changes.
547 	// Returns pointer to created write buffer.
548 	private WriteBuffer* createWriteBuffer(ChunkWorldPos cwp, ubyte layer) {
549 		assert(cwp !in writeBuffers[layer]);
550 		auto wb = WriteBuffer.init;
551 		wb.layer.layerId = layer;
552 		wb.layer.dataLength = layerInfos[layer].uniformExpansionType;
553 		writeBuffers[layer][cwp] = wb;
554 		addInternalObserver(cwp); // prevent unload until commit
555 		return cwp in writeBuffers[layer];
556 	}
557 
558 	// Here comes sum of all internal and external chunk users which results in loading or unloading of specific chunk.
559 	private void setChunkTotalObservers(ChunkWorldPos cwp, size_t totalObservers) {
560 		if (totalObservers > 0) {
561 			loadChunk(cwp);
562 		} else {
563 			unloadChunk(cwp);
564 		}
565 	}
566 
567 	// Used inside chunk manager to add chunk users, to prevent chunk unloading.
568 	private void addInternalObserver(ChunkWorldPos cwp) {
569 		numInternalChunkObservers[cwp] = numInternalChunkObservers.get(cwp, 0) + 1;
570 		auto totalObservers = numInternalChunkObservers[cwp] + numExternalChunkObservers.get(cwp, 0);
571 		setChunkTotalObservers(cwp, totalObservers);
572 	}
573 
574 	// Used inside chunk manager to remove chunk users.
575 	private void removeInternalObserver(ChunkWorldPos cwp) {
576 		auto numObservers = numInternalChunkObservers.get(cwp, 0);
577 		assert(numObservers > 0, "numInternalChunkObservers is zero when removing internal user");
578 		--numObservers;
579 		if (numObservers == 0)
580 			numInternalChunkObservers.remove(cwp);
581 		else
582 			numInternalChunkObservers[cwp] = numObservers;
583 		auto totalObservers = numObservers + numExternalChunkObservers.get(cwp, 0);
584 		setChunkTotalObservers(cwp, totalObservers);
585 	}
586 
587 	// Returns number of current snapshot users left.
588 	private size_t removeCurrentSnapshotUser(ChunkWorldPos cwp, ubyte layer) {
589 		auto snap = cwp in snapshots[layer];
590 		assert(snap && snap.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users");
591 
592 		auto totalUsers = cwp in totalSnapshotUsers;
593 		assert(totalUsers && (*totalUsers) > 0, "cannot remove chunk user. Snapshot has 0 users");
594 
595 		--snap.numUsers;
596 		--(*totalUsers);
597 		version(TRACE_SNAP_USERS) tracef("#%s:%s (rem cur:-1) %s/%s @%s", cwp, layer, snap.numUsers, totalSnapshotUsers.get(cwp, 0), snap.timestamp);
598 
599 		if ((*totalUsers) == 0) {
600 			totalSnapshotUsers.remove(cwp);
601 			return 0;
602 		}
603 
604 		return (*totalUsers);
605 	}
606 
607 	/// Snapshot is removed from oldSnapshots if numUsers == 0.
608 	private void removeOldSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp, ubyte layer) {
609 		ChunkLayerSnap[TimestampType]* chunkSnaps = cwp in oldSnapshots[layer];
610 		version(TRACE_SNAP_USERS) tracef("#%s:%s (rem old) x/%s @%s", cwp, layer, totalSnapshotUsers.get(cwp, 0), timestamp);
611 		assert(chunkSnaps, "old snapshot should have waited for releasing user");
612 		ChunkLayerSnap* snapshot = timestamp in *chunkSnaps;
613 		assert(snapshot, "cannot release snapshot user. No such snapshot");
614 		assert(snapshot.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users");
615 		--snapshot.numUsers;
616 		version(TRACE_SNAP_USERS) tracef("#%s:%s (rem old:-1) %s/%s @%s", cwp, layer, snapshot.numUsers, totalSnapshotUsers.get(cwp, 0), timestamp);
617 		if (snapshot.numUsers == 0) {
618 			(*chunkSnaps).remove(timestamp);
619 			if ((*chunkSnaps).length == 0) { // all old snaps of one chunk released
620 				oldSnapshots[layer].remove(cwp);
621 			}
622 			recycleSnapshotMemory(*snapshot);
623 		}
624 	}
625 
626 	// Commit for single chunk.
627 	private void commitLayerSnapshot(ChunkWorldPos cwp, WriteBuffer writeBuffer, TimestampType currentTime, ubyte layer) {
628 		auto currentSnapshot = getChunkSnapshot(cwp, layer);
629 		if (!currentSnapshot.isNull) handleCurrentSnapCommit(cwp, layer, currentSnapshot.get());
630 
631 		assert(writeBuffer.isModified);
632 
633 		if (writeBuffer.removeSnapshot)
634 		{
635 			freeLayerArray(writeBuffer.layer);
636 			snapshots[layer].remove(cwp);
637 			return;
638 		}
639 
640 		writeBuffer.layer.timestamp = currentTime;
641 		snapshots[layer][cwp] = ChunkLayerSnap(writeBuffer.layer);
642 		totalLayerDataBytes += getLayerDataBytes(writeBuffer.layer);
643 	}
644 
645 	private void handleCurrentSnapCommit(ChunkWorldPos cwp, ubyte layer, ChunkLayerSnap currentSnapshot)
646 	{
647 		if (currentSnapshot.numUsers == 0) {
648 			version(TRACE_SNAP_USERS) tracef("#%s:%s (commit:%s) %s/%s @%s", cwp, layer, currentSnapshot.numUsers, 0, totalSnapshotUsers.get(cwp, 0), currentTime);
649 			recycleSnapshotMemory(currentSnapshot);
650 		} else {
651 			// transfer users from current layer snapshot into old snapshot
652 			auto totalUsers = cwp in totalSnapshotUsers;
653 			assert(totalUsers && (*totalUsers) >= currentSnapshot.numUsers, "layer has not enough users");
654 			(*totalUsers) -= currentSnapshot.numUsers;
655 			if ((*totalUsers) == 0) {
656 				totalSnapshotUsers.remove(cwp);
657 			}
658 
659 			if (auto layerSnaps = cwp in oldSnapshots[layer]) {
660 				version(TRACE_SNAP_USERS) tracef("#%s:%s (commit add:%s) %s/%s @%s", cwp, layer,
661 					currentSnapshot.numUsers, 0, totalSnapshotUsers.get(cwp, 0), currentTime);
662 				assert(currentSnapshot.timestamp !in *layerSnaps);
663 				(*layerSnaps)[currentSnapshot.timestamp] = currentSnapshot;
664 			} else {
665 				version(TRACE_SNAP_USERS) tracef("#%s:%s (commit new:%s) %s/%s @%s", cwp, layer,
666 					currentSnapshot.numUsers, 0, totalSnapshotUsers.get(cwp, 0), currentTime);
667 				oldSnapshots[layer][cwp] = [currentSnapshot.timestamp : currentSnapshot];
668 				version(TRACE_SNAP_USERS) tracef("oldSnapshots[%s][%s] == %s", layer, cwp, oldSnapshots[layer][cwp]);
669 			}
670 		}
671 	}
672 
673 	// Called when snapshot data can be recycled.
674 	private void recycleSnapshotMemory(ref ChunkLayerSnap snap) {
675 		totalLayerDataBytes -= getLayerDataBytes(snap);
676 		freeLayerArray(snap);
677 	}
678 }
679 
680 
681 //	TTTTTTT EEEEEEE  SSSSS  TTTTTTT  SSSSS
682 //	  TTT   EE      SS        TTT   SS
683 //	  TTT   EEEEE    SSSSS    TTT    SSSSS
684 //	  TTT   EE           SS   TTT        SS
685 //	  TTT   EEEEEEE  SSSSS    TTT    SSSSS
686 //
687 
688 version(unittest) {
689 	enum ZERO_CWP = ChunkWorldPos(0, 0, 0, 0);
690 
691 	private struct Handlers {
692 		void setup(ChunkManager cm) {
693 			cm.onChunkAddedHandlers ~= &onChunkAddedHandler;
694 			cm.onChunkRemovedHandlers ~= &onChunkRemovedHandler;
695 			cm.onChunkLoadedHandler = &onChunkLoadedHandler;
696 			cm.loadChunkHandler = &loadChunkHandler;
697 			cm.cancelLoadChunkHandler = &cancelLoadChunkHandler;
698 
699 			cm.startChunkSave = &startChunkSave;
700 			cm.pushLayer = &pushLayer;
701 			cm.endChunkSave = &endChunkSave;
702 		}
703 		void onChunkAddedHandler(ChunkWorldPos) {
704 			onChunkAddedHandlerCalled = true;
705 		}
706 		void onChunkRemovedHandler(ChunkWorldPos) {
707 			onChunkRemovedHandlerCalled = true;
708 		}
709 		void onChunkLoadedHandler(ChunkWorldPos) {
710 			onChunkLoadedHandlerCalled = true;
711 		}
712 		void loadChunkHandler(ChunkWorldPos cwp) {
713 			loadChunkHandlerCalled = true;
714 		}
715 		void cancelLoadChunkHandler(ChunkWorldPos cwp) {
716 			cancelLoadChunkHandlerCalled = true;
717 		}
718 		size_t startChunkSave() {
719 			saveChunkHandlerCalled = true;
720 			return 0;
721 		}
722 		void pushLayer(ChunkLayerItem layer) {}
723 		void endChunkSave(size_t headerPos, ChunkHeaderItem header) {}
724 		void assertCalled(size_t flags) {
725 			assert(!(((flags & 0b0000_0001) > 0) ^ onChunkAddedHandlerCalled));
726 			assert(!(((flags & 0b0000_0010) > 0) ^ onChunkRemovedHandlerCalled));
727 			assert(!(((flags & 0b0000_0100) > 0) ^ onChunkLoadedHandlerCalled));
728 			assert(!(((flags & 0b0001_0000) > 0) ^ loadChunkHandlerCalled));
729 			assert(!(((flags & 0b0010_0000) > 0) ^ saveChunkHandlerCalled));
730 		}
731 
732 		bool onChunkAddedHandlerCalled;
733 		bool onChunkRemovedHandlerCalled;
734 		bool onChunkLoadedHandlerCalled;
735 		bool loadChunkHandlerCalled;
736 		bool cancelLoadChunkHandlerCalled;
737 		bool saveChunkHandlerCalled;
738 	}
739 
740 	private auto test_LoadedLayers = [ChunkLayerItem()];
741 
742 	private struct FSMTester {
743 		auto ZERO_CWP = ChunkWorldPos(0, 0, 0, 0);
744 		auto currentState(ChunkManager cm) {
745 			return cm.chunkStates.get(ZERO_CWP, ChunkState.non_loaded);
746 		}
747 		void resetChunk(ChunkManager cm) {
748 			foreach(layer; 0..cm.numLayers) {
749 				cm.snapshots[layer].remove(ZERO_CWP);
750 				cm.oldSnapshots[layer].remove(ZERO_CWP);
751 				cm.writeBuffers[layer].remove(ZERO_CWP);
752 			}
753 			cm.chunkStates.remove(ZERO_CWP);
754 			cm.modifiedChunks.remove(ZERO_CWP);
755 			cm.numInternalChunkObservers.remove(ZERO_CWP);
756 			cm.numExternalChunkObservers.remove(ZERO_CWP);
757 			cm.totalSnapshotUsers.remove(ZERO_CWP);
758 		}
759 		void gotoState(ChunkManager cm, ChunkState state) {
760 			resetChunk(cm);
761 			with(ChunkState) final switch(state) {
762 				case non_loaded:
763 					break;
764 				case added_loaded:
765 					cm.setExternalChunkObservers(ZERO_CWP, 1);
766 					cm.onSnapshotLoaded(ZERO_CWP, test_LoadedLayers, false);
767 					break;
768 				case added_loading:
769 					cm.setExternalChunkObservers(ZERO_CWP, 1);
770 					break;
771 				case removed_loaded_used:
772 					gotoState(cm, ChunkState.added_loaded);
773 					cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
774 					cm.commitSnapshots(1);
775 					TimestampType timestamp = cm.addCurrentSnapshotUser(ZERO_CWP, BLOCK_LAYER);
776 					cm.save();
777 					cm.setExternalChunkObservers(ZERO_CWP, 0);
778 					cm.onSnapshotSaved(ZERO_CWP, [ChunkLayerTimestampItem(timestamp, 0)]);
779 					break;
780 			}
781 			import std..string : format;
782 			assert(currentState(cm) == state,
783 				format("Failed to set state %s, got %s", state, currentState(cm)));
784 		}
785 
786 		void gotoStateSaving(ChunkManager cm, ChunkState state)
787 		{
788 			if (state == ChunkState.added_loaded)
789 			{
790 				gotoState(cm, ChunkState.added_loaded);
791 				cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
792 				cm.commitSnapshots(1);
793 				cm.save();
794 			}
795 			else if (state == ChunkState.removed_loaded_used)
796 			{
797 				gotoStateSaving(cm, ChunkState.added_loaded);
798 				cm.setExternalChunkObservers(ZERO_CWP, 0);
799 			}
800 			assert(currentState(cm) == state,
801 				format("Failed to set state %s, got %s", state, currentState(cm)));
802 		}
803 	}
804 }
805 
806 
807 unittest {
808 	import voxelman.log : setupLogger;
809 	setupLogger("test.log");
810 
811 	Handlers h;
812 	ChunkManager cm;
813 	FSMTester fsmTester;
814 
815 	void assertState(ChunkState state) {
816 		import std..string : format;
817 		auto actualState = fsmTester.currentState(cm);
818 		assert(actualState == state,
819 			format("Got state '%s', while needed '%s'", actualState, state));
820 	}
821 
822 	void assertHasOldSnapshot(TimestampType timestamp) {
823 		assert(timestamp in cm.oldSnapshots[BLOCK_LAYER][ZERO_CWP]);
824 	}
825 
826 	void assertNoOldSnapshots() {
827 		assert(ZERO_CWP !in cm.oldSnapshots[BLOCK_LAYER]);
828 	}
829 
830 	void assertHasSnapshot() {
831 		assert(!cm.getChunkSnapshot(ZERO_CWP, BLOCK_LAYER).isNull);
832 	}
833 
834 	void assertHasNoSnapshot() {
835 		assert( cm.getChunkSnapshot(ZERO_CWP, BLOCK_LAYER).isNull);
836 	}
837 
838 	void resetHandlersState() {
839 		h = Handlers.init;
840 	}
841 	void resetChunkManager() {
842 		cm = new ChunkManager;
843 		ubyte numLayers = 1;
844 		cm.setup(numLayers);
845 		h.setup(cm);
846 	}
847 	void reset() {
848 		resetHandlersState();
849 		resetChunkManager();
850 	}
851 
852 	void setupState(ChunkState state) {
853 		fsmTester.gotoState(cm, state);
854 		resetHandlersState();
855 		assertState(state);
856 	}
857 
858 	void setupStateSaving(ChunkState state) {
859 		fsmTester.gotoStateSaving(cm, state);
860 		resetHandlersState();
861 		assertState(state);
862 	}
863 
864 	reset();
865 
866 	//--------------------------------------------------------------------------
867 	// non_loaded -> added_loading
868 	cm.setExternalChunkObservers(ZERO_CWP, 1);
869 	assertState(ChunkState.added_loading);
870 	assertHasNoSnapshot();
871 	h.assertCalled(0b0001_0001); //onChunkAddedHandlerCalled, loadChunkHandlerCalled
872 
873 
874 	//--------------------------------------------------------------------------
875 	setupState(ChunkState.added_loading);
876 	// added_loading -> non_loaded
877 	cm.setExternalChunkObservers(ZERO_CWP, 0);
878 	assertState(ChunkState.non_loaded);
879 	assertHasNoSnapshot();
880 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
881 
882 
883 	//--------------------------------------------------------------------------
884 	setupState(ChunkState.added_loading);
885 	// added_loading -> added_loaded + modified
886 	cm.onSnapshotLoaded(ZERO_CWP, test_LoadedLayers, true);
887 	assertState(ChunkState.added_loaded);
888 	assertHasSnapshot();
889 	h.assertCalled(0b0000_0100); //onChunkLoadedHandlerCalled
890 	assert(ZERO_CWP in cm.modifiedChunks);
891 
892 	//--------------------------------------------------------------------------
893 	setupState(ChunkState.added_loading);
894 	// added_loading -> added_loaded
895 	cm.onSnapshotLoaded(ZERO_CWP, test_LoadedLayers, false);
896 	assertState(ChunkState.added_loaded);
897 	assertHasSnapshot();
898 	h.assertCalled(0b0000_0100); //onChunkLoadedHandlerCalled
899 	assert(ZERO_CWP !in cm.modifiedChunks);
900 
901 	//--------------------------------------------------------------------------
902 	setupState(ChunkState.added_loaded);
903 	// added_loaded -> non_loaded
904 	cm.setExternalChunkObservers(ZERO_CWP, 0);
905 	assertState(ChunkState.non_loaded);
906 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
907 
908 	//--------------------------------------------------------------------------
909 	setupState(ChunkState.added_loaded);
910 	// added_loaded -> removed_loaded_used
911 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
912 	cm.commitSnapshots(TimestampType(1));
913 	cm.setExternalChunkObservers(ZERO_CWP, 0);
914 	assertState(ChunkState.removed_loaded_used);
915 	h.assertCalled(0b0010_0010); //onChunkRemovedHandlerCalled, loadChunkHandlerCalled
916 
917 	//--------------------------------------------------------------------------
918 	setupState(ChunkState.added_loaded);
919 	// added_loaded -> added_loaded
920 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
921 	cm.commitSnapshots(TimestampType(1));
922 	cm.save();
923 	assertState(ChunkState.added_loaded);
924 	h.assertCalled(0b0010_0000); //loadChunkHandlerCalled
925 
926 	//--------------------------------------------------------------------------
927 	setupState(ChunkState.added_loaded);
928 	// added_loaded with user -> added_loaded no user after commit
929 	cm.addCurrentSnapshotUser(ZERO_CWP, BLOCK_LAYER);
930 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
931 	cm.commitSnapshots(TimestampType(1));
932 	assertState(ChunkState.added_loaded);
933 	h.assertCalled(0b0000_0000);
934 	assertHasOldSnapshot(TimestampType(0));
935 
936 	//--------------------------------------------------------------------------
937 	setupStateSaving(ChunkState.added_loaded);
938 	// added_loaded saving -> added_loaded after on_saved
939 	cm.onSnapshotSaved(ZERO_CWP, [ChunkLayerTimestampItem(1)]);
940 	assertState(ChunkState.added_loaded);
941 	h.assertCalled(0b0000_0000);
942 
943 	//--------------------------------------------------------------------------
944 	setupStateSaving(ChunkState.added_loaded);
945 	// added_loaded saving -> removed_loaded saving
946 	cm.setExternalChunkObservers(ZERO_CWP, 0);
947 	assertState(ChunkState.removed_loaded_used);
948 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
949 
950 
951 	//--------------------------------------------------------------------------
952 	setupStateSaving(ChunkState.removed_loaded_used);
953 	// removed_loaded_used saving -> non_loaded
954 	cm.onSnapshotSaved(ZERO_CWP, [ChunkLayerTimestampItem(1)]);
955 	assertState(ChunkState.non_loaded);
956 	h.assertCalled(0b0000_0000);
957 
958 	//--------------------------------------------------------------------------
959 	setupStateSaving(ChunkState.added_loaded);
960 	// removed_loaded_used saving -> removed_loaded_used
961 	cm.addCurrentSnapshotUser(ZERO_CWP, BLOCK_LAYER);
962 	cm.setExternalChunkObservers(ZERO_CWP, 0);
963 	assertState(ChunkState.removed_loaded_used); // & saving
964 	cm.onSnapshotSaved(ZERO_CWP, [ChunkLayerTimestampItem(1)]);
965 	assertState(ChunkState.removed_loaded_used);
966 	h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled
967 
968 	//--------------------------------------------------------------------------
969 	setupStateSaving(ChunkState.removed_loaded_used);
970 	// removed_loaded_used saving -> added_loaded saving
971 	cm.setExternalChunkObservers(ZERO_CWP, 1);
972 	assertState(ChunkState.added_loaded);
973 	h.assertCalled(0b0000_0101); //onChunkAddedHandlerCalled, onChunkLoadedHandlerCalled
974 
975 	//--------------------------------------------------------------------------
976 	setupState(ChunkState.removed_loaded_used);
977 	// removed_loaded_used -> non_loaded
978 	cm.removeSnapshotUser(ZERO_CWP, TimestampType(1), BLOCK_LAYER);
979 	assertState(ChunkState.non_loaded);
980 	h.assertCalled(0b0000_0000);
981 
982 
983 	//--------------------------------------------------------------------------
984 	setupState(ChunkState.removed_loaded_used);
985 	// removed_loaded_used -> added_loaded
986 	cm.setExternalChunkObservers(ZERO_CWP, 1);
987 	assertState(ChunkState.added_loaded);
988 	h.assertCalled(0b0000_0101); //onChunkAddedHandlerCalled, onChunkLoadedHandlerCalled
989 
990 
991 	//--------------------------------------------------------------------------
992 	// test unload of old chunk when it has users. No prev snapshots for given pos.
993 	setupState(ChunkState.added_loaded);
994 	TimestampType timestamp = cm.addCurrentSnapshotUser(ZERO_CWP, BLOCK_LAYER);
995 	assert(timestamp == TimestampType(0));
996 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
997 	cm.commitSnapshots(1);
998 	assert(timestamp in cm.oldSnapshots[BLOCK_LAYER][ZERO_CWP]);
999 	cm.removeSnapshotUser(ZERO_CWP, timestamp, BLOCK_LAYER);
1000 	assertNoOldSnapshots();
1001 
1002 	//--------------------------------------------------------------------------
1003 	// test unload of old chunk when it has users. Already has snapshot for earlier timestamp.
1004 	setupState(ChunkState.added_loaded);
1005 
1006 	TimestampType timestamp0 = cm.addCurrentSnapshotUser(ZERO_CWP, BLOCK_LAYER);
1007 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
1008 	cm.commitSnapshots(1); // commit adds timestamp 0 to oldSnapshots
1009 	assert(timestamp0 in cm.oldSnapshots[BLOCK_LAYER][ZERO_CWP]);
1010 
1011 	TimestampType timestamp1 = cm.addCurrentSnapshotUser(ZERO_CWP, BLOCK_LAYER);
1012 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
1013 	cm.commitSnapshots(2); // commit adds timestamp 1 to oldSnapshots
1014 	assert(timestamp1 in cm.oldSnapshots[BLOCK_LAYER][ZERO_CWP]);
1015 
1016 	cm.removeSnapshotUser(ZERO_CWP, timestamp0, BLOCK_LAYER);
1017 	cm.removeSnapshotUser(ZERO_CWP, timestamp1, BLOCK_LAYER);
1018 	assertNoOldSnapshots();
1019 
1020 	//--------------------------------------------------------------------------
1021 	// test case where old snapshot was saved and current snapshot is added_loaded
1022 	setupState(ChunkState.added_loaded);
1023 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
1024 	cm.commitSnapshots(1);
1025 	cm.save();
1026 	cm.getOrCreateWriteBuffer(ZERO_CWP, BLOCK_LAYER, WriteBufferPolicy.copySnapshotArray);
1027 	cm.commitSnapshots(2); // now, snap that is saved is old.
1028 	cm.onSnapshotSaved(ZERO_CWP, [ChunkLayerTimestampItem(1)]);
1029 	assertNoOldSnapshots();
1030 }