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 }