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 }