PocketMine-MP 5.27.2 git-d86943fa8c6384be3e2c1901ebf94f584b27e784
Loading...
Searching...
No Matches
World.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
27namespace pocketmine\world;
28
61use pocketmine\item\ItemUseResult;
94use pocketmine\world\format\LightArray;
109use function abs;
110use function array_filter;
111use function array_key_exists;
112use function array_keys;
113use function array_map;
114use function array_merge;
115use function array_sum;
116use function array_values;
117use function assert;
118use function cos;
119use function count;
120use function floor;
121use function get_class;
122use function gettype;
123use function is_a;
124use function is_object;
125use function max;
126use function microtime;
127use function min;
128use function morton2d_decode;
129use function morton2d_encode;
130use function morton3d_decode;
131use function morton3d_encode;
132use function mt_rand;
133use function preg_match;
134use function spl_object_id;
135use function strtolower;
136use function trim;
137use const M_PI;
138use const PHP_INT_MAX;
139use const PHP_INT_MIN;
140
141#include <rules/World.h>
142
148class World implements ChunkManager{
149
150 private static int $worldIdCounter = 1;
151
152 public const Y_MAX = 320;
153 public const Y_MIN = -64;
154
155 public const TIME_DAY = 1000;
156 public const TIME_NOON = 6000;
157 public const TIME_SUNSET = 12000;
158 public const TIME_NIGHT = 13000;
159 public const TIME_MIDNIGHT = 18000;
160 public const TIME_SUNRISE = 23000;
161
162 public const TIME_FULL = 24000;
163
164 public const DIFFICULTY_PEACEFUL = 0;
165 public const DIFFICULTY_EASY = 1;
166 public const DIFFICULTY_NORMAL = 2;
167 public const DIFFICULTY_HARD = 3;
168
169 public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
170
171 //TODO: this could probably do with being a lot bigger
172 private const BLOCK_CACHE_SIZE_CAP = 2048;
173
178 private array $players = [];
179
184 private array $entities = [];
189 private array $entityLastKnownPositions = [];
190
195 private array $entitiesByChunk = [];
196
201 public array $updateEntities = [];
202
203 private bool $inDynamicStateRecalculation = false;
208 private array $blockCache = [];
209 private int $blockCacheSize = 0;
214 private array $blockCollisionBoxCache = [];
215
216 private int $sendTimeTicker = 0;
217
218 private int $worldId;
219
220 private int $providerGarbageCollectionTicker = 0;
221
222 private int $minY;
223 private int $maxY;
224
229 private array $registeredTickingChunks = [];
230
237 private array $validTickingChunks = [];
238
244 private array $recheckTickingChunks = [];
245
250 private array $chunkLoaders = [];
251
256 private array $chunkListeners = [];
261 private array $playerChunkListeners = [];
262
267 private array $packetBuffersByChunk = [];
268
273 private array $unloadQueue = [];
274
275 private int $time;
276 public bool $stopTime = false;
277
278 private float $sunAnglePercentage = 0.0;
279 private int $skyLightReduction = 0;
280
281 private string $folderName;
282 private string $displayName;
283
288 private array $chunks = [];
289
294 private array $changedBlocks = [];
295
297 private ReversePriorityQueue $scheduledBlockUpdateQueue;
302 private array $scheduledBlockUpdateQueueIndex = [];
303
305 private \SplQueue $neighbourBlockUpdateQueue;
310 private array $neighbourBlockUpdateQueueIndex = [];
311
316 private array $activeChunkPopulationTasks = [];
321 private array $chunkLock = [];
322 private int $maxConcurrentChunkPopulationTasks = 2;
327 private array $chunkPopulationRequestMap = [];
332 private \SplQueue $chunkPopulationRequestQueue;
337 private array $chunkPopulationRequestQueueIndex = [];
338
343 private array $generatorRegisteredWorkers = [];
344
345 private bool $autoSave = true;
346
347 private int $sleepTicks = 0;
348
349 private int $chunkTickRadius;
350 private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK;
355 private array $randomTickBlocks = [];
356
357 public WorldTimings $timings;
358
359 public float $tickRateTime = 0;
360
361 private bool $doingTick = false;
362
364 private string $generator;
365
366 private bool $unloaded = false;
371 private array $unloadCallbacks = [];
372
373 private ?BlockLightUpdate $blockLightUpdate = null;
374 private ?SkyLightUpdate $skyLightUpdate = null;
375
376 private \Logger $logger;
377
378 private RuntimeBlockStateRegistry $blockStateRegistry;
379
383 public static function chunkHash(int $x, int $z) : int{
384 return morton2d_encode($x, $z);
385 }
386
387 private const MORTON3D_BIT_SIZE = 21;
388 private const BLOCKHASH_Y_BITS = 9;
389 private const BLOCKHASH_Y_PADDING = 64; //size (in blocks) of padding after both boundaries of the Y axis
390 private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
391 private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
392 private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
393 private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
394 private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
395 private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
396 private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
397 private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS;
398
402 public static function blockHash(int $x, int $y, int $z) : int{
403 $shiftedY = $y + self::BLOCKHASH_Y_OFFSET;
404 if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){
405 throw new \InvalidArgumentException("Y coordinate $y is out of range!");
406 }
407 //morton3d gives us 21 bits on each axis, but the Y axis only requires 9
408 //so we use the extra space on Y (12 bits) and add 6 extra bits from X and Z instead.
409 //if we ever need more space for Y (e.g. due to expansion), take bits from X/Z to compensate.
410 return morton3d_encode(
411 $x & self::BLOCKHASH_XZ_MASK,
412 ($shiftedY /* & self::BLOCKHASH_Y_MASK */) |
413 ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) |
414 ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT),
415 $z & self::BLOCKHASH_XZ_MASK
416 );
417 }
418
422 public static function chunkBlockHash(int $x, int $y, int $z) : int{
423 return morton3d_encode($x, $y, $z);
424 }
425
432 public static function getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z) : void{
433 [$baseX, $baseY, $baseZ] = morton3d_decode($hash);
434
435 $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
436 $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
437
438 $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
439 $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET;
440 $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
441 }
442
448 public static function getXZ(int $hash, ?int &$x, ?int &$z) : void{
449 [$x, $z] = morton2d_decode($hash);
450 }
451
452 public static function getDifficultyFromString(string $str) : int{
453 switch(strtolower(trim($str))){
454 case "0":
455 case "peaceful":
456 case "p":
457 return World::DIFFICULTY_PEACEFUL;
458
459 case "1":
460 case "easy":
461 case "e":
462 return World::DIFFICULTY_EASY;
463
464 case "2":
465 case "normal":
466 case "n":
467 return World::DIFFICULTY_NORMAL;
468
469 case "3":
470 case "hard":
471 case "h":
472 return World::DIFFICULTY_HARD;
473 }
474
475 return -1;
476 }
477
481 public function __construct(
482 private Server $server,
483 string $name, //TODO: this should be folderName (named arguments BC break)
484 private WritableWorldProvider $provider,
485 private AsyncPool $workerPool
486 ){
487 $this->folderName = $name;
488 $this->worldId = self::$worldIdCounter++;
489
490 $this->displayName = $this->provider->getWorldData()->getName();
491 $this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName");
492
493 $this->blockStateRegistry = RuntimeBlockStateRegistry::getInstance();
494 $this->minY = $this->provider->getWorldMinY();
495 $this->maxY = $this->provider->getWorldMaxY();
496
497 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
498 $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
499 throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
500 $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
501 $this->generator = $generator->getGeneratorClass();
502 $this->chunkPopulationRequestQueue = new \SplQueue();
503 $this->addOnUnloadCallback(function() : void{
504 $this->logger->debug("Cancelling unfulfilled generation requests");
505
506 foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){
507 $promise->reject();
508 unset($this->chunkPopulationRequestMap[$chunkHash]);
509 }
510 if(count($this->chunkPopulationRequestMap) !== 0){
511 //TODO: this might actually get hit because generation rejection callbacks might try to schedule new
512 //requests, and we can't prevent that right now because there's no way to detect "unloading" state
513 throw new AssumptionFailedError("New generation requests scheduled during unload");
514 }
515 });
516
517 $this->scheduledBlockUpdateQueue = new ReversePriorityQueue();
518 $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
519
520 $this->neighbourBlockUpdateQueue = new \SplQueue();
521
522 $this->time = $this->provider->getWorldData()->getTime();
523
524 $cfg = $this->server->getConfigGroup();
525 $this->chunkTickRadius = min($this->server->getViewDistance(), max(0, $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_TICK_RADIUS, 4)));
526 if($cfg->getPropertyInt("chunk-ticking.per-tick", 40) <= 0){
527 //TODO: this needs l10n
528 $this->logger->warning("\"chunk-ticking.per-tick\" setting is deprecated, but you've used it to disable chunk ticking. Set \"chunk-ticking.tick-radius\" to 0 in \"pocketmine.yml\" instead.");
529 $this->chunkTickRadius = 0;
530 }
531 $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_BLOCKS_PER_SUBCHUNK_PER_TICK, self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
532 $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2);
533
534 $this->initRandomTickBlocksFromConfig($cfg);
535
536 $this->timings = new WorldTimings($this);
537
538 $this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{
539 if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
540 $this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
541 unset($this->generatorRegisteredWorkers[$workerId]);
542 }
543 });
544 $workerPool = $this->workerPool;
545 $this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{
546 $workerPool->removeWorkerStartHook($workerStartHook);
547 });
548 }
549
550 private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
551 $dontTickBlocks = [];
552 $parser = StringToItemParser::getInstance();
553 foreach($cfg->getProperty(YmlServerProperties::CHUNK_TICKING_DISABLE_BLOCK_TICKING, []) as $name){
554 $name = (string) $name;
555 $item = $parser->parse($name);
556 if($item !== null){
557 $block = $item->getBlock();
558 }elseif(preg_match("/^-?\d+$/", $name) === 1){
559 //TODO: this is a really sketchy hack - remove this as soon as possible
560 try{
561 $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((int) $name, 0);
562 }catch(BlockStateDeserializeException){
563 continue;
564 }
565 $block = $this->blockStateRegistry->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
566 }else{
567 //TODO: we probably ought to log an error here
568 continue;
569 }
570
571 if($block->getTypeId() !== BlockTypeIds::AIR){
572 $dontTickBlocks[$block->getTypeId()] = $name;
573 }
574 }
575
576 foreach($this->blockStateRegistry->getAllKnownStates() as $state){
577 $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
578 if($dontTickName === null && $state->ticksRandomly()){
579 $this->randomTickBlocks[$state->getStateId()] = true;
580 }
581 }
582 }
583
584 public function getTickRateTime() : float{
585 return $this->tickRateTime;
586 }
587
588 public function registerGeneratorToWorker(int $worker) : void{
589 $this->logger->debug("Registering generator on worker $worker");
590 $this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker);
591 $this->generatorRegisteredWorkers[$worker] = true;
592 }
593
594 public function unregisterGenerator() : void{
595 foreach($this->workerPool->getRunningWorkers() as $i){
596 if(isset($this->generatorRegisteredWorkers[$i])){
597 $this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i);
598 }
599 }
600 $this->generatorRegisteredWorkers = [];
601 }
602
603 public function getServer() : Server{
604 return $this->server;
605 }
606
607 public function getLogger() : \Logger{
608 return $this->logger;
609 }
610
611 final public function getProvider() : WritableWorldProvider{
612 return $this->provider;
613 }
614
618 final public function getId() : int{
619 return $this->worldId;
620 }
621
622 public function isLoaded() : bool{
623 return !$this->unloaded;
624 }
625
629 public function onUnload() : void{
630 if($this->unloaded){
631 throw new \LogicException("Tried to close a world which is already closed");
632 }
633
634 foreach($this->unloadCallbacks as $callback){
635 $callback();
636 }
637 $this->unloadCallbacks = [];
638
639 foreach($this->chunks as $chunkHash => $chunk){
640 self::getXZ($chunkHash, $chunkX, $chunkZ);
641 $this->unloadChunk($chunkX, $chunkZ, false);
642 }
643 foreach($this->entitiesByChunk as $chunkHash => $entities){
644 self::getXZ($chunkHash, $chunkX, $chunkZ);
645
646 $leakedEntities = 0;
647 foreach($entities as $entity){
648 if(!$entity->isFlaggedForDespawn()){
649 $leakedEntities++;
650 }
651 $entity->close();
652 }
653 if($leakedEntities !== 0){
654 $this->logger->warning("$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!");
655 }
656 }
657
658 $this->save();
659
660 $this->unregisterGenerator();
661
662 $this->provider->close();
663 $this->blockCache = [];
664 $this->blockCacheSize = 0;
665 $this->blockCollisionBoxCache = [];
666
667 $this->unloaded = true;
668 }
669
671 public function addOnUnloadCallback(\Closure $callback) : void{
672 $this->unloadCallbacks[spl_object_id($callback)] = $callback;
673 }
674
676 public function removeOnUnloadCallback(\Closure $callback) : void{
677 unset($this->unloadCallbacks[spl_object_id($callback)]);
678 }
679
688 private function filterViewersForPosition(Vector3 $pos, array $allowed) : array{
689 $candidates = $this->getViewersForPosition($pos);
690 $filtered = [];
691 foreach($allowed as $player){
692 $k = spl_object_id($player);
693 if(isset($candidates[$k])){
694 $filtered[$k] = $candidates[$k];
695 }
696 }
697
698 return $filtered;
699 }
700
704 public function addSound(Vector3 $pos, Sound $sound, ?array $players = null) : void{
705 $players ??= $this->getViewersForPosition($pos);
706
707 if(WorldSoundEvent::hasHandlers()){
708 $ev = new WorldSoundEvent($this, $sound, $pos, $players);
709 $ev->call();
710 if($ev->isCancelled()){
711 return;
712 }
713
714 $sound = $ev->getSound();
715 $players = $ev->getRecipients();
716 }
717
718 $pk = $sound->encode($pos);
719 if(count($pk) > 0){
720 if($players === $this->getViewersForPosition($pos)){
721 foreach($pk as $e){
722 $this->broadcastPacketToViewers($pos, $e);
723 }
724 }else{
725 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
726 }
727 }
728 }
729
733 public function addParticle(Vector3 $pos, Particle $particle, ?array $players = null) : void{
734 $players ??= $this->getViewersForPosition($pos);
735
736 if(WorldParticleEvent::hasHandlers()){
737 $ev = new WorldParticleEvent($this, $particle, $pos, $players);
738 $ev->call();
739 if($ev->isCancelled()){
740 return;
741 }
742
743 $particle = $ev->getParticle();
744 $players = $ev->getRecipients();
745 }
746
747 $pk = $particle->encode($pos);
748 if(count($pk) > 0){
749 if($players === $this->getViewersForPosition($pos)){
750 foreach($pk as $e){
751 $this->broadcastPacketToViewers($pos, $e);
752 }
753 }else{
754 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
755 }
756 }
757 }
758
759 public function getAutoSave() : bool{
760 return $this->autoSave;
761 }
762
763 public function setAutoSave(bool $value) : void{
764 $this->autoSave = $value;
765 }
766
776 public function getChunkPlayers(int $chunkX, int $chunkZ) : array{
777 return $this->playerChunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
778 }
779
786 public function getChunkLoaders(int $chunkX, int $chunkZ) : array{
787 return $this->chunkLoaders[World::chunkHash($chunkX, $chunkZ)] ?? [];
788 }
789
796 public function getViewersForPosition(Vector3 $pos) : array{
797 return $this->getChunkPlayers($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
798 }
799
803 public function broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet) : void{
804 $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE, $packet);
805 }
806
807 private function broadcastPacketToPlayersUsingChunk(int $chunkX, int $chunkZ, ClientboundPacket $packet) : void{
808 if(!isset($this->packetBuffersByChunk[$index = World::chunkHash($chunkX, $chunkZ)])){
809 $this->packetBuffersByChunk[$index] = [$packet];
810 }else{
811 $this->packetBuffersByChunk[$index][] = $packet;
812 }
813 }
814
815 public function registerChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ, bool $autoLoad = true) : void{
816 $loaderId = spl_object_id($loader);
817
818 if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){
819 $this->chunkLoaders[$chunkHash] = [];
820 }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){
821 return;
822 }
823
824 $this->chunkLoaders[$chunkHash][$loaderId] = $loader;
825
826 $this->cancelUnloadChunkRequest($chunkX, $chunkZ);
827
828 if($autoLoad){
829 $this->loadChunk($chunkX, $chunkZ);
830 }
831 }
832
833 public function unregisterChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ) : void{
834 $chunkHash = World::chunkHash($chunkX, $chunkZ);
835 $loaderId = spl_object_id($loader);
836 if(isset($this->chunkLoaders[$chunkHash][$loaderId])){
837 if(count($this->chunkLoaders[$chunkHash]) === 1){
838 unset($this->chunkLoaders[$chunkHash]);
839 $this->unloadChunkRequest($chunkX, $chunkZ, true);
840 if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){
841 $this->chunkPopulationRequestMap[$chunkHash]->reject();
842 unset($this->chunkPopulationRequestMap[$chunkHash]);
843 }
844 }else{
845 unset($this->chunkLoaders[$chunkHash][$loaderId]);
846 }
847 }
848 }
849
853 public function registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
854 $hash = World::chunkHash($chunkX, $chunkZ);
855 if(isset($this->chunkListeners[$hash])){
856 $this->chunkListeners[$hash][spl_object_id($listener)] = $listener;
857 }else{
858 $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener];
859 }
860 if($listener instanceof Player){
861 $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener;
862 }
863 }
864
870 public function unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
871 $hash = World::chunkHash($chunkX, $chunkZ);
872 if(isset($this->chunkListeners[$hash])){
873 if(count($this->chunkListeners[$hash]) === 1){
874 unset($this->chunkListeners[$hash]);
875 unset($this->playerChunkListeners[$hash]);
876 }else{
877 unset($this->chunkListeners[$hash][spl_object_id($listener)]);
878 unset($this->playerChunkListeners[$hash][spl_object_id($listener)]);
879 }
880 }
881 }
882
886 public function unregisterChunkListenerFromAll(ChunkListener $listener) : void{
887 foreach($this->chunkListeners as $hash => $listeners){
888 World::getXZ($hash, $chunkX, $chunkZ);
889 $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
890 }
891 }
892
899 public function getChunkListeners(int $chunkX, int $chunkZ) : array{
900 return $this->chunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
901 }
902
906 public function sendTime(Player ...$targets) : void{
907 if(count($targets) === 0){
908 $targets = $this->players;
909 }
910 foreach($targets as $player){
911 $player->getNetworkSession()->syncWorldTime($this->time);
912 }
913 }
914
915 public function isDoingTick() : bool{
916 return $this->doingTick;
917 }
918
922 public function doTick(int $currentTick) : void{
923 if($this->unloaded){
924 throw new \LogicException("Attempted to tick a world which has been closed");
925 }
926
927 $this->timings->doTick->startTiming();
928 $this->doingTick = true;
929 try{
930 $this->actuallyDoTick($currentTick);
931 }finally{
932 $this->doingTick = false;
933 $this->timings->doTick->stopTiming();
934 }
935 }
936
937 protected function actuallyDoTick(int $currentTick) : void{
938 if(!$this->stopTime){
939 //this simulates an overflow, as would happen in any language which doesn't do stupid things to var types
940 if($this->time === PHP_INT_MAX){
941 $this->time = PHP_INT_MIN;
942 }else{
943 $this->time++;
944 }
945 }
946
947 $this->sunAnglePercentage = $this->computeSunAnglePercentage(); //Sun angle depends on the current time
948 $this->skyLightReduction = $this->computeSkyLightReduction(); //Sky light reduction depends on the sun angle
949
950 if(++$this->sendTimeTicker === 200){
951 $this->sendTime();
952 $this->sendTimeTicker = 0;
953 }
954
955 $this->unloadChunks();
956 if(++$this->providerGarbageCollectionTicker >= 6000){
957 $this->provider->doGarbageCollection();
958 $this->providerGarbageCollectionTicker = 0;
959 }
960
961 $this->timings->scheduledBlockUpdates->startTiming();
962 //Delayed updates
963 while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){
965 $vec = $this->scheduledBlockUpdateQueue->extract()["data"];
966 unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
967 if(!$this->isInLoadedTerrain($vec)){
968 continue;
969 }
970 $block = $this->getBlock($vec);
971 $block->onScheduledUpdate();
972 }
973 $this->timings->scheduledBlockUpdates->stopTiming();
974
975 $this->timings->neighbourBlockUpdates->startTiming();
976 //Normal updates
977 while($this->neighbourBlockUpdateQueue->count() > 0){
978 $index = $this->neighbourBlockUpdateQueue->dequeue();
979 unset($this->neighbourBlockUpdateQueueIndex[$index]);
980 World::getBlockXYZ($index, $x, $y, $z);
981 if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){
982 continue;
983 }
984
985 $block = $this->getBlockAt($x, $y, $z);
986
987 if(BlockUpdateEvent::hasHandlers()){
988 $ev = new BlockUpdateEvent($block);
989 $ev->call();
990 if($ev->isCancelled()){
991 continue;
992 }
993 }
994 foreach($this->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z)) as $entity){
995 $entity->onNearbyBlockChange();
996 }
997 $block->onNearbyBlockChange();
998 }
999
1000 $this->timings->neighbourBlockUpdates->stopTiming();
1001
1002 $this->timings->entityTick->startTiming();
1003 //Update entities that need update
1004 foreach($this->updateEntities as $id => $entity){
1005 if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
1006 unset($this->updateEntities[$id]);
1007 }
1008 if($entity->isFlaggedForDespawn()){
1009 $entity->close();
1010 }
1011 }
1012 $this->timings->entityTick->stopTiming();
1013
1014 $this->timings->randomChunkUpdates->startTiming();
1015 $this->tickChunks();
1016 $this->timings->randomChunkUpdates->stopTiming();
1017
1018 $this->executeQueuedLightUpdates();
1019
1020 if(count($this->changedBlocks) > 0){
1021 if(count($this->players) > 0){
1022 foreach($this->changedBlocks as $index => $blocks){
1023 if(count($blocks) === 0){ //blocks can be set normally and then later re-set with direct send
1024 continue;
1025 }
1026 World::getXZ($index, $chunkX, $chunkZ);
1027 if(!$this->isChunkLoaded($chunkX, $chunkZ)){
1028 //a previous chunk may have caused this one to be unloaded by a ChunkListener
1029 continue;
1030 }
1031 if(count($blocks) > 512){
1032 $chunk = $this->getChunk($chunkX, $chunkZ) ?? throw new AssumptionFailedError("We already checked that the chunk is loaded");
1033 foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){
1034 $p->onChunkChanged($chunkX, $chunkZ, $chunk);
1035 }
1036 }else{
1037 foreach($this->createBlockUpdatePackets($blocks) as $packet){
1038 $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet);
1039 }
1040 }
1041 }
1042 }
1043
1044 $this->changedBlocks = [];
1045
1046 }
1047
1048 if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
1049 $this->checkSleep();
1050 }
1051
1052 foreach($this->packetBuffersByChunk as $index => $entries){
1053 World::getXZ($index, $chunkX, $chunkZ);
1054 $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ);
1055 if(count($chunkPlayers) > 0){
1056 NetworkBroadcastUtils::broadcastPackets($chunkPlayers, $entries);
1057 }
1058 }
1059
1060 $this->packetBuffersByChunk = [];
1061 }
1062
1063 public function checkSleep() : void{
1064 if(count($this->players) === 0){
1065 return;
1066 }
1067
1068 $resetTime = true;
1069 foreach($this->getPlayers() as $p){
1070 if(!$p->isSleeping()){
1071 $resetTime = false;
1072 break;
1073 }
1074 }
1075
1076 if($resetTime){
1077 $time = $this->getTimeOfDay();
1078
1079 if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
1080 $this->setTime($this->getTime() + World::TIME_FULL - $time);
1081
1082 foreach($this->getPlayers() as $p){
1083 $p->stopSleep();
1084 }
1085 }
1086 }
1087 }
1088
1089 public function setSleepTicks(int $ticks) : void{
1090 $this->sleepTicks = $ticks;
1091 }
1092
1099 public function createBlockUpdatePackets(array $blocks) : array{
1100 $packets = [];
1101
1102 $blockTranslator = TypeConverter::getInstance()->getBlockTranslator();
1103
1104 foreach($blocks as $b){
1105 if(!($b instanceof Vector3)){
1106 throw new \TypeError("Expected Vector3 in blocks array, got " . (is_object($b) ? get_class($b) : gettype($b)));
1107 }
1108
1109 $fullBlock = $this->getBlockAt($b->x, $b->y, $b->z);
1110 $blockPosition = BlockPosition::fromVector3($b);
1111
1112 $tile = $this->getTileAt($b->x, $b->y, $b->z);
1113 if($tile instanceof Spawnable){
1114 $expectedClass = $fullBlock->getIdInfo()->getTileClass();
1115 if($expectedClass !== null && $tile instanceof $expectedClass && count($fakeStateProperties = $tile->getRenderUpdateBugWorkaroundStateProperties($fullBlock)) > 0){
1116 $originalStateData = $blockTranslator->internalIdToNetworkStateData($fullBlock->getStateId());
1117 $fakeStateData = new BlockStateData(
1118 $originalStateData->getName(),
1119 array_merge($originalStateData->getStates(), $fakeStateProperties),
1120 $originalStateData->getVersion()
1121 );
1122 $packets[] = UpdateBlockPacket::create(
1123 $blockPosition,
1124 $blockTranslator->getBlockStateDictionary()->lookupStateIdFromData($fakeStateData) ?? throw new AssumptionFailedError("Unmapped fake blockstate data: " . $fakeStateData->toNbt()),
1125 UpdateBlockPacket::FLAG_NETWORK,
1126 UpdateBlockPacket::DATA_LAYER_NORMAL
1127 );
1128 }
1129 }
1130 $packets[] = UpdateBlockPacket::create(
1131 $blockPosition,
1132 $blockTranslator->internalIdToNetworkId($fullBlock->getStateId()),
1133 UpdateBlockPacket::FLAG_NETWORK,
1134 UpdateBlockPacket::DATA_LAYER_NORMAL
1135 );
1136
1137 if($tile instanceof Spawnable){
1138 $packets[] = BlockActorDataPacket::create($blockPosition, $tile->getSerializedSpawnCompound());
1139 }
1140 }
1141
1142 return $packets;
1143 }
1144
1145 public function clearCache(bool $force = false) : void{
1146 if($force){
1147 $this->blockCache = [];
1148 $this->blockCacheSize = 0;
1149 $this->blockCollisionBoxCache = [];
1150 }else{
1151 //Recalculate this when we're asked - blockCacheSize may be higher than the real size
1152 $this->blockCacheSize = 0;
1153 foreach($this->blockCache as $list){
1154 $this->blockCacheSize += count($list);
1155 if($this->blockCacheSize > self::BLOCK_CACHE_SIZE_CAP){
1156 $this->blockCache = [];
1157 $this->blockCacheSize = 0;
1158 break;
1159 }
1160 }
1161
1162 $count = 0;
1163 foreach($this->blockCollisionBoxCache as $list){
1164 $count += count($list);
1165 if($count > self::BLOCK_CACHE_SIZE_CAP){
1166 //TODO: Is this really the best logic?
1167 $this->blockCollisionBoxCache = [];
1168 break;
1169 }
1170 }
1171 }
1172 }
1173
1174 private function trimBlockCache() : void{
1175 $before = $this->blockCacheSize;
1176 //Since PHP maintains key order, earliest in foreach should be the oldest entries
1177 //Older entries are less likely to be hot, so destroying these should usually have the lowest impact on performance
1178 foreach($this->blockCache as $chunkHash => $blocks){
1179 unset($this->blockCache[$chunkHash]);
1180 $this->blockCacheSize -= count($blocks);
1181 if($this->blockCacheSize < self::BLOCK_CACHE_SIZE_CAP){
1182 break;
1183 }
1184 }
1185 }
1186
1191 public function getRandomTickedBlocks() : array{
1192 return $this->randomTickBlocks;
1193 }
1194
1195 public function addRandomTickedBlock(Block $block) : void{
1196 if($block instanceof UnknownBlock){
1197 throw new \InvalidArgumentException("Cannot do random-tick on unknown block");
1198 }
1199 $this->randomTickBlocks[$block->getStateId()] = true;
1200 }
1201
1202 public function removeRandomTickedBlock(Block $block) : void{
1203 unset($this->randomTickBlocks[$block->getStateId()]);
1204 }
1205
1210 public function getChunkTickRadius() : int{
1211 return $this->chunkTickRadius;
1212 }
1213
1218 public function setChunkTickRadius(int $radius) : void{
1219 $this->chunkTickRadius = $radius;
1220 }
1221
1229 public function getTickingChunks() : array{
1230 return array_keys($this->validTickingChunks);
1231 }
1232
1237 public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1238 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
1239 $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
1240 $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
1241 }
1242
1247 public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1248 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1249 $tickerId = spl_object_id($ticker);
1250 if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
1251 if(count($this->registeredTickingChunks[$chunkHash]) === 1){
1252 unset(
1253 $this->registeredTickingChunks[$chunkHash],
1254 $this->recheckTickingChunks[$chunkHash],
1255 $this->validTickingChunks[$chunkHash]
1256 );
1257 }else{
1258 unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
1259 }
1260 }
1261 }
1262
1263 private function tickChunks() : void{
1264 if($this->chunkTickRadius <= 0 || count($this->registeredTickingChunks) === 0){
1265 return;
1266 }
1267
1268 if(count($this->recheckTickingChunks) > 0){
1269 $this->timings->randomChunkUpdatesChunkSelection->startTiming();
1270
1271 $chunkTickableCache = [];
1272
1273 foreach($this->recheckTickingChunks as $hash => $_){
1274 World::getXZ($hash, $chunkX, $chunkZ);
1275 if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
1276 $this->validTickingChunks[$hash] = $hash;
1277 }
1278 }
1279 $this->recheckTickingChunks = [];
1280
1281 $this->timings->randomChunkUpdatesChunkSelection->stopTiming();
1282 }
1283
1284 foreach($this->validTickingChunks as $index => $_){
1285 World::getXZ($index, $chunkX, $chunkZ);
1286
1287 $this->tickChunk($chunkX, $chunkZ);
1288 }
1289 }
1290
1297 private function isChunkTickable(int $chunkX, int $chunkZ, array &$cache) : bool{
1298 for($cx = -1; $cx <= 1; ++$cx){
1299 for($cz = -1; $cz <= 1; ++$cz){
1300 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1301 if(isset($cache[$chunkHash])){
1302 if(!$cache[$chunkHash]){
1303 return false;
1304 }
1305 continue;
1306 }
1307 if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
1308 $cache[$chunkHash] = false;
1309 return false;
1310 }
1311 $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
1312 if($adjacentChunk === null || !$adjacentChunk->isPopulated()){
1313 $cache[$chunkHash] = false;
1314 return false;
1315 }
1316 $lightPopulatedState = $adjacentChunk->isLightPopulated();
1317 if($lightPopulatedState !== true){
1318 if($lightPopulatedState === false){
1319 $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
1320 }
1321 $cache[$chunkHash] = false;
1322 return false;
1323 }
1324
1325 $cache[$chunkHash] = true;
1326 }
1327 }
1328
1329 return true;
1330 }
1331
1341 private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{
1342 for($cx = -1; $cx <= 1; ++$cx){
1343 for($cz = -1; $cz <= 1; ++$cz){
1344 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1345 unset($this->validTickingChunks[$chunkHash]);
1346 if(isset($this->registeredTickingChunks[$chunkHash])){
1347 $this->recheckTickingChunks[$chunkHash] = $chunkHash;
1348 }else{
1349 unset($this->recheckTickingChunks[$chunkHash]);
1350 }
1351 }
1352 }
1353 }
1354
1355 private function orderLightPopulation(int $chunkX, int $chunkZ) : void{
1356 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1357 $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
1358 if($lightPopulatedState === false){
1359 $this->chunks[$chunkHash]->setLightPopulated(null);
1360 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1361
1362 $this->workerPool->submitTask(new LightPopulationTask(
1363 $this->chunks[$chunkHash],
1364 function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : void{
1371 if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){
1372 return;
1373 }
1374 //TODO: calculated light information might not be valid if the terrain changed during light calculation
1375
1376 $chunk->setHeightMapArray($heightMap);
1377 foreach($blockLight as $y => $lightArray){
1378 $chunk->getSubChunk($y)->setBlockLightArray($lightArray);
1379 }
1380 foreach($skyLight as $y => $lightArray){
1381 $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
1382 }
1383 $chunk->setLightPopulated(true);
1384 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1385 }
1386 ));
1387 }
1388 }
1389
1390 private function tickChunk(int $chunkX, int $chunkZ) : void{
1391 $chunk = $this->getChunk($chunkX, $chunkZ);
1392 if($chunk === null){
1393 //the chunk may have been unloaded during a previous chunk's update (e.g. during BlockGrowEvent)
1394 return;
1395 }
1396 foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
1397 $entity->onRandomUpdate();
1398 }
1399
1400 $blockFactory = $this->blockStateRegistry;
1401 foreach($chunk->getSubChunks() as $Y => $subChunk){
1402 if(!$subChunk->isEmptyFast()){
1403 $k = 0;
1404 for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){
1405 if(($i % 5) === 0){
1406 //60 bits will be used by 5 blocks (12 bits each)
1407 $k = mt_rand(0, (1 << 60) - 1);
1408 }
1409 $x = $k & SubChunk::COORD_MASK;
1410 $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK;
1411 $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK;
1412 $k >>= (SubChunk::COORD_BIT_SIZE * 3);
1413
1414 $state = $subChunk->getBlockStateId($x, $y, $z);
1415
1416 if(isset($this->randomTickBlocks[$state])){
1417 $block = $blockFactory->fromStateId($state);
1418 $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
1419 $block->onRandomTick();
1420 }
1421 }
1422 }
1423 }
1424 }
1425
1429 public function __debugInfo() : array{
1430 return [];
1431 }
1432
1433 public function save(bool $force = false) : bool{
1434
1435 if(!$this->getAutoSave() && !$force){
1436 return false;
1437 }
1438
1439 (new WorldSaveEvent($this))->call();
1440
1441 $timings = $this->timings->syncDataSave;
1442 $timings->startTiming();
1443
1444 $this->provider->getWorldData()->setTime($this->time);
1445 $this->saveChunks();
1446 $this->provider->getWorldData()->save();
1447
1448 $timings->stopTiming();
1449
1450 return true;
1451 }
1452
1453 public function saveChunks() : void{
1454 $this->timings->syncChunkSave->startTiming();
1455 try{
1456 foreach($this->chunks as $chunkHash => $chunk){
1457 self::getXZ($chunkHash, $chunkX, $chunkZ);
1458 $this->provider->saveChunk($chunkX, $chunkZ, new ChunkData(
1459 $chunk->getSubChunks(),
1460 $chunk->isPopulated(),
1461 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk()))),
1462 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
1463 ), $chunk->getTerrainDirtyFlags());
1464 $chunk->clearTerrainDirtyFlags();
1465 }
1466 }finally{
1467 $this->timings->syncChunkSave->stopTiming();
1468 }
1469 }
1470
1475 public function scheduleDelayedBlockUpdate(Vector3 $pos, int $delay) : void{
1476 if(
1477 !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
1478 (isset($this->scheduledBlockUpdateQueueIndex[$index = World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
1479 ){
1480 return;
1481 }
1482 $this->scheduledBlockUpdateQueueIndex[$index] = $delay;
1483 $this->scheduledBlockUpdateQueue->insert(new Vector3((int) $pos->x, (int) $pos->y, (int) $pos->z), $delay + $this->server->getTick());
1484 }
1485
1486 private function tryAddToNeighbourUpdateQueue(int $x, int $y, int $z) : void{
1487 if($this->isInWorld($x, $y, $z)){
1488 $hash = World::blockHash($x, $y, $z);
1489 if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){
1490 $this->neighbourBlockUpdateQueue->enqueue($hash);
1491 $this->neighbourBlockUpdateQueueIndex[$hash] = true;
1492 }
1493 }
1494 }
1495
1502 private function internalNotifyNeighbourBlockUpdate(int $x, int $y, int $z) : void{
1503 $this->tryAddToNeighbourUpdateQueue($x, $y, $z);
1504 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1505 $this->tryAddToNeighbourUpdateQueue($x + $dx, $y + $dy, $z + $dz);
1506 }
1507 }
1508
1516 public function notifyNeighbourBlockUpdate(Vector3 $pos) : void{
1517 $this->internalNotifyNeighbourBlockUpdate($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ());
1518 }
1519
1524 public function getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst = false) : array{
1525 $minX = (int) floor($bb->minX - 1);
1526 $minY = (int) floor($bb->minY - 1);
1527 $minZ = (int) floor($bb->minZ - 1);
1528 $maxX = (int) floor($bb->maxX + 1);
1529 $maxY = (int) floor($bb->maxY + 1);
1530 $maxZ = (int) floor($bb->maxZ + 1);
1531
1532 $collides = [];
1533
1534 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1535 if($targetFirst){
1536 for($z = $minZ; $z <= $maxZ; ++$z){
1537 $zOverflow = $z === $minZ || $z === $maxZ;
1538 for($x = $minX; $x <= $maxX; ++$x){
1539 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1540 for($y = $minY; $y <= $maxY; ++$y){
1541 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1542
1543 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1544 if($overflow ?
1545 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1546 match ($stateCollisionInfo) {
1547 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1548 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1549 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1550 }
1551 ){
1552 return [$this->getBlockAt($x, $y, $z)];
1553 }
1554 }
1555 }
1556 }
1557 }else{
1558 //TODO: duplicated code :( this way is better for performance though
1559 for($z = $minZ; $z <= $maxZ; ++$z){
1560 $zOverflow = $z === $minZ || $z === $maxZ;
1561 for($x = $minX; $x <= $maxX; ++$x){
1562 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1563 for($y = $minY; $y <= $maxY; ++$y){
1564 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1565
1566 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1567 if($overflow ?
1568 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1569 match ($stateCollisionInfo) {
1570 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1571 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1572 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1573 }
1574 ){
1575 $collides[] = $this->getBlockAt($x, $y, $z);
1576 }
1577 }
1578 }
1579 }
1580 }
1581
1582 return $collides;
1583 }
1584
1589 private function getBlockCollisionInfo(int $x, int $y, int $z, array $collisionInfo) : int{
1590 if(!$this->isInWorld($x, $y, $z)){
1591 return RuntimeBlockStateRegistry::COLLISION_NONE;
1592 }
1593 $chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1594 if($chunk === null){
1595 return RuntimeBlockStateRegistry::COLLISION_NONE;
1596 }
1597 $stateId = $chunk
1598 ->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)
1599 ->getBlockStateId(
1600 $x & SubChunk::COORD_MASK,
1601 $y & SubChunk::COORD_MASK,
1602 $z & SubChunk::COORD_MASK
1603 );
1604 return $collisionInfo[$stateId];
1605 }
1606
1618 private function getBlockCollisionBoxesForCell(int $x, int $y, int $z, array $collisionInfo) : array{
1619 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1620 $boxes = match($stateCollisionInfo){
1621 RuntimeBlockStateRegistry::COLLISION_NONE => [],
1622 RuntimeBlockStateRegistry::COLLISION_CUBE => [AxisAlignedBB::one()->offset($x, $y, $z)],
1623 default => $this->getBlockAt($x, $y, $z)->getCollisionBoxes()
1624 };
1625
1626 //overlapping AABBs can't make any difference if this is a cube, so we can save some CPU cycles in this common case
1627 if($stateCollisionInfo !== RuntimeBlockStateRegistry::COLLISION_CUBE){
1628 $cellBB = null;
1629 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1630 $offsetX = $x + $dx;
1631 $offsetY = $y + $dy;
1632 $offsetZ = $z + $dz;
1633 $stateCollisionInfo = $this->getBlockCollisionInfo($offsetX, $offsetY, $offsetZ, $collisionInfo);
1634 if($stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW){
1635 //avoid allocating this unless it's needed
1636 $cellBB ??= AxisAlignedBB::one()->offset($x, $y, $z);
1637 $extraBoxes = $this->getBlockAt($offsetX, $offsetY, $offsetZ)->getCollisionBoxes();
1638 foreach($extraBoxes as $extraBox){
1639 if($extraBox->intersectsWith($cellBB)){
1640 $boxes[] = $extraBox;
1641 }
1642 }
1643 }
1644 }
1645 }
1646
1647 return $boxes;
1648 }
1649
1654 public function getBlockCollisionBoxes(AxisAlignedBB $bb) : array{
1655 $minX = (int) floor($bb->minX);
1656 $minY = (int) floor($bb->minY);
1657 $minZ = (int) floor($bb->minZ);
1658 $maxX = (int) floor($bb->maxX);
1659 $maxY = (int) floor($bb->maxY);
1660 $maxZ = (int) floor($bb->maxZ);
1661
1662 $collides = [];
1663
1664 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1665
1666 for($z = $minZ; $z <= $maxZ; ++$z){
1667 for($x = $minX; $x <= $maxX; ++$x){
1668 $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1669 for($y = $minY; $y <= $maxY; ++$y){
1670 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1671
1672 $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z, $collisionInfo);
1673
1674 foreach($boxes as $blockBB){
1675 if($blockBB->intersectsWith($bb)){
1676 $collides[] = $blockBB;
1677 }
1678 }
1679 }
1680 }
1681 }
1682
1683 return $collides;
1684 }
1685
1690 public function computeSunAnglePercentage() : float{
1691 $timeProgress = ($this->time % self::TIME_FULL) / self::TIME_FULL;
1692
1693 //0.0 needs to be high noon, not dusk
1694 $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25);
1695
1696 //Offset the sun progress to be above the horizon longer at dusk and dawn
1697 //this is roughly an inverted sine curve, which pushes the sun progress back at dusk and forwards at dawn
1698 $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3);
1699
1700 return $sunProgress + $diff;
1701 }
1702
1706 public function getSunAnglePercentage() : float{
1707 return $this->sunAnglePercentage;
1708 }
1709
1713 public function getSunAngleRadians() : float{
1714 return $this->sunAnglePercentage * 2 * M_PI;
1715 }
1716
1720 public function getSunAngleDegrees() : float{
1721 return $this->sunAnglePercentage * 360.0;
1722 }
1723
1728 public function computeSkyLightReduction() : int{
1729 $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5)));
1730
1731 //TODO: check rain and thunder level
1732
1733 return (int) ($percentage * 11);
1734 }
1735
1739 public function getSkyLightReduction() : int{
1740 return $this->skyLightReduction;
1741 }
1742
1747 public function getFullLight(Vector3 $pos) : int{
1748 $floorX = $pos->getFloorX();
1749 $floorY = $pos->getFloorY();
1750 $floorZ = $pos->getFloorZ();
1751 return $this->getFullLightAt($floorX, $floorY, $floorZ);
1752 }
1753
1758 public function getFullLightAt(int $x, int $y, int $z) : int{
1759 $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z);
1760 if($skyLight < 15){
1761 return max($skyLight, $this->getBlockLightAt($x, $y, $z));
1762 }else{
1763 return $skyLight;
1764 }
1765 }
1766
1771 public function getHighestAdjacentFullLightAt(int $x, int $y, int $z) : int{
1772 return $this->getHighestAdjacentLight($x, $y, $z, $this->getFullLightAt(...));
1773 }
1774
1779 public function getPotentialLight(Vector3 $pos) : int{
1780 $floorX = $pos->getFloorX();
1781 $floorY = $pos->getFloorY();
1782 $floorZ = $pos->getFloorZ();
1783 return $this->getPotentialLightAt($floorX, $floorY, $floorZ);
1784 }
1785
1790 public function getPotentialLightAt(int $x, int $y, int $z) : int{
1791 return max($this->getPotentialBlockSkyLightAt($x, $y, $z), $this->getBlockLightAt($x, $y, $z));
1792 }
1793
1798 public function getHighestAdjacentPotentialLightAt(int $x, int $y, int $z) : int{
1799 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialLightAt(...));
1800 }
1801
1808 public function getPotentialBlockSkyLightAt(int $x, int $y, int $z) : int{
1809 if(!$this->isInWorld($x, $y, $z)){
1810 return $y >= self::Y_MAX ? 15 : 0;
1811 }
1812 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1813 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1814 }
1815 return 0; //TODO: this should probably throw instead (light not calculated yet)
1816 }
1817
1823 public function getRealBlockSkyLightAt(int $x, int $y, int $z) : int{
1824 $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction;
1825 return $light < 0 ? 0 : $light;
1826 }
1827
1833 public function getBlockLightAt(int $x, int $y, int $z) : int{
1834 if(!$this->isInWorld($x, $y, $z)){
1835 return 0;
1836 }
1837 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1838 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1839 }
1840 return 0; //TODO: this should probably throw instead (light not calculated yet)
1841 }
1842
1843 public function updateAllLight(int $x, int $y, int $z) : void{
1844 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
1845 return;
1846 }
1847
1848 $blockFactory = $this->blockStateRegistry;
1849 $this->timings->doBlockSkyLightUpdates->startTiming();
1850 if($this->skyLightUpdate === null){
1851 $this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
1852 }
1853 $this->skyLightUpdate->recalculateNode($x, $y, $z);
1854 $this->timings->doBlockSkyLightUpdates->stopTiming();
1855
1856 $this->timings->doBlockLightUpdates->startTiming();
1857 if($this->blockLightUpdate === null){
1858 $this->blockLightUpdate = new BlockLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light);
1859 }
1860 $this->blockLightUpdate->recalculateNode($x, $y, $z);
1861 $this->timings->doBlockLightUpdates->stopTiming();
1862 }
1863
1867 private function getHighestAdjacentLight(int $x, int $y, int $z, \Closure $lightGetter) : int{
1868 $max = 0;
1869 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1870 $x1 = $x + $offsetX;
1871 $y1 = $y + $offsetY;
1872 $z1 = $z + $offsetZ;
1873 if(
1874 !$this->isInWorld($x1, $y1, $z1) ||
1875 ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) === null ||
1876 $chunk->isLightPopulated() !== true
1877 ){
1878 continue;
1879 }
1880 $max = max($max, $lightGetter($x1, $y1, $z1));
1881 }
1882 return $max;
1883 }
1884
1888 public function getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z) : int{
1889 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialBlockSkyLightAt(...));
1890 }
1891
1896 public function getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z) : int{
1897 return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction;
1898 }
1899
1903 public function getHighestAdjacentBlockLight(int $x, int $y, int $z) : int{
1904 return $this->getHighestAdjacentLight($x, $y, $z, $this->getBlockLightAt(...));
1905 }
1906
1907 private function executeQueuedLightUpdates() : void{
1908 if($this->blockLightUpdate !== null){
1909 $this->timings->doBlockLightUpdates->startTiming();
1910 $this->blockLightUpdate->execute();
1911 $this->blockLightUpdate = null;
1912 $this->timings->doBlockLightUpdates->stopTiming();
1913 }
1914
1915 if($this->skyLightUpdate !== null){
1916 $this->timings->doBlockSkyLightUpdates->startTiming();
1917 $this->skyLightUpdate->execute();
1918 $this->skyLightUpdate = null;
1919 $this->timings->doBlockSkyLightUpdates->stopTiming();
1920 }
1921 }
1922
1923 public function isInWorld(int $x, int $y, int $z) : bool{
1924 return (
1925 $x <= Limits::INT32_MAX && $x >= Limits::INT32_MIN &&
1926 $y < $this->maxY && $y >= $this->minY &&
1927 $z <= Limits::INT32_MAX && $z >= Limits::INT32_MIN
1928 );
1929 }
1930
1941 public function getBlock(Vector3 $pos, bool $cached = true, bool $addToCache = true) : Block{
1942 return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache);
1943 }
1944
1954 public function getBlockAt(int $x, int $y, int $z, bool $cached = true, bool $addToCache = true) : Block{
1955 $relativeBlockHash = null;
1956 $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1957
1958 if($this->isInWorld($x, $y, $z)){
1959 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1960
1961 if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
1962 return $this->blockCache[$chunkHash][$relativeBlockHash];
1963 }
1964
1965 $chunk = $this->chunks[$chunkHash] ?? null;
1966 if($chunk !== null){
1967 $block = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
1968 }else{
1969 $addToCache = false;
1970 $block = VanillaBlocks::AIR();
1971 }
1972 }else{
1973 $block = VanillaBlocks::AIR();
1974 }
1975
1976 $block->position($this, $x, $y, $z);
1977
1978 if($this->inDynamicStateRecalculation){
1979 //this call was generated by a parent getBlock() call calculating dynamic stateinfo
1980 //don't calculate dynamic state and don't add to block cache (since it won't have dynamic state calculated).
1981 //this ensures that it's impossible for dynamic state properties to recursively depend on each other.
1982 $addToCache = false;
1983 }else{
1984 $this->inDynamicStateRecalculation = true;
1985 $replacement = $block->readStateFromWorld();
1986 if($replacement !== $block){
1987 $replacement->position($this, $x, $y, $z);
1988 $block = $replacement;
1989 }
1990 $this->inDynamicStateRecalculation = false;
1991 }
1992
1993 if($addToCache && $relativeBlockHash !== null){
1994 $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
1995
1996 if(++$this->blockCacheSize >= self::BLOCK_CACHE_SIZE_CAP){
1997 $this->trimBlockCache();
1998 }
1999 }
2000
2001 return $block;
2002 }
2003
2009 public function setBlock(Vector3 $pos, Block $block, bool $update = true) : void{
2010 $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update);
2011 }
2012
2021 public function setBlockAt(int $x, int $y, int $z, Block $block, bool $update = true) : void{
2022 if(!$this->isInWorld($x, $y, $z)){
2023 throw new \InvalidArgumentException("Pos x=$x,y=$y,z=$z is outside of the world bounds");
2024 }
2025 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2026 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2027 if($this->loadChunk($chunkX, $chunkZ) === null){ //current expected behaviour is to try to load the terrain synchronously
2028 throw new WorldException("Cannot set a block in un-generated terrain");
2029 }
2030
2031 //TODO: this computes state ID twice (we do it again in writeStateToWorld()). Not great for performance :(
2032 $stateId = $block->getStateId();
2033 if(!$this->blockStateRegistry->hasStateId($stateId)){
2034 throw new \LogicException("Block state ID not known to RuntimeBlockStateRegistry (probably not registered)");
2035 }
2036 if(!GlobalBlockStateHandlers::getSerializer()->isRegistered($block)){
2037 throw new \LogicException("Block not registered with GlobalBlockStateHandlers serializer");
2038 }
2039
2040 $this->timings->setBlock->startTiming();
2041
2042 $this->unlockChunk($chunkX, $chunkZ, null);
2043
2044 $block = clone $block;
2045
2046 $block->position($this, $x, $y, $z);
2047 $block->writeStateToWorld();
2048 $pos = new Vector3($x, $y, $z);
2049
2050 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2051 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
2052
2053 unset($this->blockCache[$chunkHash][$relativeBlockHash]);
2054 $this->blockCacheSize--;
2055 unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
2056 //blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the
2057 //caches for those blocks as well
2058 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
2059 $sideChunkPosHash = World::chunkHash(($x + $offsetX) >> Chunk::COORD_BIT_SIZE, ($z + $offsetZ) >> Chunk::COORD_BIT_SIZE);
2060 $sideChunkBlockHash = World::chunkBlockHash($x + $offsetX, $y + $offsetY, $z + $offsetZ);
2061 unset($this->blockCollisionBoxCache[$sideChunkPosHash][$sideChunkBlockHash]);
2062 }
2063
2064 if(!isset($this->changedBlocks[$chunkHash])){
2065 $this->changedBlocks[$chunkHash] = [];
2066 }
2067 $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
2068
2069 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2070 $listener->onBlockChanged($pos);
2071 }
2072
2073 if($update){
2074 $this->updateAllLight($x, $y, $z);
2075 $this->internalNotifyNeighbourBlockUpdate($x, $y, $z);
2076 }
2077
2078 $this->timings->setBlock->stopTiming();
2079 }
2080
2081 public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion = null, int $delay = 10) : ?ItemEntity{
2082 if($item->isNull()){
2083 return null;
2084 }
2085
2086 $itemEntity = new ItemEntity(Location::fromObject($source, $this, Utils::getRandomFloat() * 360, 0), $item);
2087
2088 $itemEntity->setPickupDelay($delay);
2089 $itemEntity->setMotion($motion ?? new Vector3(Utils::getRandomFloat() * 0.2 - 0.1, 0.2, Utils::getRandomFloat() * 0.2 - 0.1));
2090 $itemEntity->spawnToAll();
2091
2092 return $itemEntity;
2093 }
2094
2101 public function dropExperience(Vector3 $pos, int $amount) : array{
2102 $orbs = [];
2103
2104 foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){
2105 $orb = new ExperienceOrb(Location::fromObject($pos, $this, Utils::getRandomFloat() * 360, 0), $split);
2106
2107 $orb->setMotion(new Vector3((Utils::getRandomFloat() * 0.2 - 0.1) * 2, Utils::getRandomFloat() * 0.4, (Utils::getRandomFloat() * 0.2 - 0.1) * 2));
2108 $orb->spawnToAll();
2109
2110 $orbs[] = $orb;
2111 }
2112
2113 return $orbs;
2114 }
2115
2124 public function useBreakOn(Vector3 $vector, ?Item &$item = null, ?Player $player = null, bool $createParticles = false, array &$returnedItems = []) : bool{
2125 $vector = $vector->floor();
2126
2127 $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE;
2128 $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2129 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2130 return false;
2131 }
2132
2133 $target = $this->getBlock($vector);
2134 $affectedBlocks = $target->getAffectedBlocks();
2135
2136 if($item === null){
2137 $item = VanillaItems::AIR();
2138 }
2139
2140 $drops = [];
2141 if($player === null || $player->hasFiniteResources()){
2142 $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
2143 }
2144
2145 $xpDrop = 0;
2146 if($player !== null && $player->hasFiniteResources()){
2147 $xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
2148 }
2149
2150 if($player !== null){
2151 $ev = new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
2152
2153 if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
2154 $ev->cancel();
2155 }
2156
2157 if($player->isAdventure(true) && !$ev->isCancelled()){
2158 $canBreak = false;
2159 $itemParser = LegacyStringToItemParser::getInstance();
2160 foreach($item->getCanDestroy() as $v){
2161 $entry = $itemParser->parse($v);
2162 if($entry->getBlock()->hasSameTypeId($target)){
2163 $canBreak = true;
2164 break;
2165 }
2166 }
2167
2168 if(!$canBreak){
2169 $ev->cancel();
2170 }
2171 }
2172
2173 $ev->call();
2174 if($ev->isCancelled()){
2175 return false;
2176 }
2177
2178 $drops = $ev->getDrops();
2179 $xpDrop = $ev->getXpDropAmount();
2180
2181 }elseif(!$target->getBreakInfo()->isBreakable()){
2182 return false;
2183 }
2184
2185 foreach($affectedBlocks as $t){
2186 $this->destroyBlockInternal($t, $item, $player, $createParticles, $returnedItems);
2187 }
2188
2189 $item->onDestroyBlock($target, $returnedItems);
2190
2191 if(count($drops) > 0){
2192 $dropPos = $vector->add(0.5, 0.5, 0.5);
2193 foreach($drops as $drop){
2194 if(!$drop->isNull()){
2195 $this->dropItem($dropPos, $drop);
2196 }
2197 }
2198 }
2199
2200 if($xpDrop > 0){
2201 $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop);
2202 }
2203
2204 return true;
2205 }
2206
2210 private function destroyBlockInternal(Block $target, Item $item, ?Player $player, bool $createParticles, array &$returnedItems) : void{
2211 if($createParticles){
2212 $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5), new BlockBreakParticle($target));
2213 }
2214
2215 $target->onBreak($item, $player, $returnedItems);
2216
2217 $tile = $this->getTile($target->getPosition());
2218 if($tile !== null){
2219 $tile->onBlockDestroyed();
2220 }
2221 }
2222
2230 public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{
2231 $blockClicked = $this->getBlock($vector);
2232 $blockReplace = $blockClicked->getSide($face);
2233
2234 if($clickVector === null){
2235 $clickVector = new Vector3(0.0, 0.0, 0.0);
2236 }else{
2237 $clickVector = new Vector3(
2238 min(1.0, max(0.0, $clickVector->x)),
2239 min(1.0, max(0.0, $clickVector->y)),
2240 min(1.0, max(0.0, $clickVector->z))
2241 );
2242 }
2243
2244 if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){
2245 //TODO: build height limit messages for custom world heights and mcregion cap
2246 return false;
2247 }
2248 $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE;
2249 $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2250 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2251 return false;
2252 }
2253
2254 if($blockClicked->getTypeId() === BlockTypeIds::AIR){
2255 return false;
2256 }
2257
2258 if($player !== null){
2259 $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
2260 if($player->isSneaking()){
2261 $ev->setUseItem(false);
2262 $ev->setUseBlock($item->isNull()); //opening doors is still possible when sneaking if using an empty hand
2263 }
2264 if($player->isSpectator()){
2265 $ev->cancel(); //set it to cancelled so plugins can bypass this
2266 }
2267
2268 $ev->call();
2269 if(!$ev->isCancelled()){
2270 if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2271 return true;
2272 }
2273
2274 if($ev->useItem()){
2275 $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
2276 if($result !== ItemUseResult::NONE){
2277 return $result === ItemUseResult::SUCCESS;
2278 }
2279 }
2280 }else{
2281 return false;
2282 }
2283 }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2284 return true;
2285 }
2286
2287 if($item->isNull() || !$item->canBePlaced()){
2288 return false;
2289 }
2290 $hand = $item->getBlock($face);
2291 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2292
2293 if($hand->canBePlacedAt($blockClicked, $clickVector, $face, true)){
2294 $blockReplace = $blockClicked;
2295 //TODO: while this mimics the vanilla behaviour with replaceable blocks, we should really pass some other
2296 //value like NULL and let place() deal with it. This will look like a bug to anyone who doesn't know about
2297 //the vanilla behaviour.
2298 $face = Facing::UP;
2299 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2300 }elseif(!$hand->canBePlacedAt($blockReplace, $clickVector, $face, false)){
2301 return false;
2302 }
2303
2304 $tx = new BlockTransaction($this);
2305 if(!$hand->place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player)){
2306 return false;
2307 }
2308
2309 foreach($tx->getBlocks() as [$x, $y, $z, $block]){
2310 $block->position($this, $x, $y, $z);
2311 foreach($block->getCollisionBoxes() as $collisionBox){
2312 if(count($this->getCollidingEntities($collisionBox)) > 0){
2313 return false; //Entity in block
2314 }
2315 }
2316 }
2317
2318 if($player !== null){
2319 $ev = new BlockPlaceEvent($player, $tx, $blockClicked, $item);
2320 if($player->isSpectator()){
2321 $ev->cancel();
2322 }
2323
2324 if($player->isAdventure(true) && !$ev->isCancelled()){
2325 $canPlace = false;
2326 $itemParser = LegacyStringToItemParser::getInstance();
2327 foreach($item->getCanPlaceOn() as $v){
2328 $entry = $itemParser->parse($v);
2329 if($entry->getBlock()->hasSameTypeId($blockClicked)){
2330 $canPlace = true;
2331 break;
2332 }
2333 }
2334
2335 if(!$canPlace){
2336 $ev->cancel();
2337 }
2338 }
2339
2340 $ev->call();
2341 if($ev->isCancelled()){
2342 return false;
2343 }
2344 }
2345
2346 if(!$tx->apply()){
2347 return false;
2348 }
2349 foreach($tx->getBlocks() as [$x, $y, $z, $_]){
2350 $tile = $this->getTileAt($x, $y, $z);
2351 if($tile !== null){
2352 //TODO: seal this up inside block placement
2353 $tile->copyDataFromItem($item);
2354 }
2355
2356 $this->getBlockAt($x, $y, $z)->onPostPlace();
2357 }
2358
2359 if($playSound){
2360 $this->addSound($hand->getPosition(), new BlockPlaceSound($hand));
2361 }
2362
2363 $item->pop();
2364
2365 return true;
2366 }
2367
2368 public function getEntity(int $entityId) : ?Entity{
2369 return $this->entities[$entityId] ?? null;
2370 }
2371
2378 public function getEntities() : array{
2379 return $this->entities;
2380 }
2381
2392 public function getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2393 $nearby = [];
2394
2395 foreach($this->getNearbyEntities($bb, $entity) as $ent){
2396 if($ent->canBeCollidedWith() && ($entity === null || $entity->canCollideWith($ent))){
2397 $nearby[] = $ent;
2398 }
2399 }
2400
2401 return $nearby;
2402 }
2403
2410 public function getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2411 $nearby = [];
2412
2413 $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE;
2414 $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE;
2415 $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE;
2416 $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE;
2417
2418 for($x = $minX; $x <= $maxX; ++$x){
2419 for($z = $minZ; $z <= $maxZ; ++$z){
2420 foreach($this->getChunkEntities($x, $z) as $ent){
2421 if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
2422 $nearby[] = $ent;
2423 }
2424 }
2425 }
2426 }
2427
2428 return $nearby;
2429 }
2430
2442 public function getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType = Entity::class, bool $includeDead = false) : ?Entity{
2443 assert(is_a($entityType, Entity::class, true));
2444
2445 $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2446 $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2447 $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2448 $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2449
2450 $currentTargetDistSq = $maxDistance ** 2;
2451
2456 $currentTarget = null;
2457
2458 for($x = $minX; $x <= $maxX; ++$x){
2459 for($z = $minZ; $z <= $maxZ; ++$z){
2460 foreach($this->getChunkEntities($x, $z) as $entity){
2461 if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
2462 continue;
2463 }
2464 $distSq = $entity->getPosition()->distanceSquared($pos);
2465 if($distSq < $currentTargetDistSq){
2466 $currentTargetDistSq = $distSq;
2467 $currentTarget = $entity;
2468 }
2469 }
2470 }
2471 }
2472
2473 return $currentTarget;
2474 }
2475
2482 public function getPlayers() : array{
2483 return $this->players;
2484 }
2485
2492 public function getTile(Vector3 $pos) : ?Tile{
2493 return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z));
2494 }
2495
2499 public function getTileAt(int $x, int $y, int $z) : ?Tile{
2500 return ($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null ? $chunk->getTile($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK) : null;
2501 }
2502
2503 public function getBiomeId(int $x, int $y, int $z) : int{
2504 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2505 return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2506 }
2507 return BiomeIds::OCEAN; //TODO: this should probably throw instead (terrain not generated yet)
2508 }
2509
2510 public function getBiome(int $x, int $y, int $z) : Biome{
2511 return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
2512 }
2513
2514 public function setBiomeId(int $x, int $y, int $z, int $biomeId) : void{
2515 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2516 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2517 $this->unlockChunk($chunkX, $chunkZ, null);
2518 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
2519 $chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
2520 }else{
2521 //if we allowed this, the modifications would be lost when the chunk is created
2522 throw new WorldException("Cannot set biome in a non-generated chunk");
2523 }
2524 }
2525
2530 public function getLoadedChunks() : array{
2531 return $this->chunks;
2532 }
2533
2534 public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{
2535 return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
2536 }
2537
2542 public function getChunkEntities(int $chunkX, int $chunkZ) : array{
2543 return $this->entitiesByChunk[World::chunkHash($chunkX, $chunkZ)] ?? [];
2544 }
2545
2549 public function getOrLoadChunkAtPosition(Vector3 $pos) : ?Chunk{
2550 return $this->loadChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2551 }
2552
2559 public function getAdjacentChunks(int $x, int $z) : array{
2560 $result = [];
2561 for($xx = -1; $xx <= 1; ++$xx){
2562 for($zz = -1; $zz <= 1; ++$zz){
2563 if($xx === 0 && $zz === 0){
2564 continue; //center chunk
2565 }
2566 $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
2567 }
2568 }
2569
2570 return $result;
2571 }
2572
2587 public function lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId) : void{
2588 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2589 if(isset($this->chunkLock[$chunkHash])){
2590 throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked");
2591 }
2592 $this->chunkLock[$chunkHash] = $lockId;
2593 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2594 }
2595
2604 public function unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId) : bool{
2605 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2606 if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){
2607 unset($this->chunkLock[$chunkHash]);
2608 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2609 return true;
2610 }
2611 return false;
2612 }
2613
2619 public function isChunkLocked(int $chunkX, int $chunkZ) : bool{
2620 return isset($this->chunkLock[World::chunkHash($chunkX, $chunkZ)]);
2621 }
2622
2623 public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
2624 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2625 $oldChunk = $this->loadChunk($chunkX, $chunkZ);
2626 if($oldChunk !== null && $oldChunk !== $chunk){
2627 $deletedTiles = 0;
2628 $transferredTiles = 0;
2629 foreach($oldChunk->getTiles() as $oldTile){
2630 $tilePosition = $oldTile->getPosition();
2631 $localX = $tilePosition->getFloorX() & Chunk::COORD_MASK;
2632 $localY = $tilePosition->getFloorY();
2633 $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
2634
2635 $newBlock = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ));
2636 $expectedTileClass = $newBlock->getIdInfo()->getTileClass();
2637 if(
2638 $expectedTileClass === null || //new block doesn't expect a tile
2639 !($oldTile instanceof $expectedTileClass) || //new block expects a different tile
2640 (($newTile = $chunk->getTile($localX, $localY, $localZ)) !== null && $newTile !== $oldTile) //new chunk already has a different tile
2641 ){
2642 $oldTile->close();
2643 $deletedTiles++;
2644 }else{
2645 $transferredTiles++;
2646 $chunk->addTile($oldTile);
2647 $oldChunk->removeTile($oldTile);
2648 }
2649 }
2650 if($deletedTiles > 0 || $transferredTiles > 0){
2651 $this->logger->debug("Replacement of chunk $chunkX $chunkZ caused deletion of $deletedTiles obsolete/conflicted tiles, and transfer of $transferredTiles");
2652 }
2653 }
2654
2655 $this->chunks[$chunkHash] = $chunk;
2656
2657 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2658 unset($this->blockCache[$chunkHash]);
2659 unset($this->blockCollisionBoxCache[$chunkHash]);
2660 unset($this->changedBlocks[$chunkHash]);
2661 $chunk->setTerrainDirty();
2662 $this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking
2663
2664 if(!$this->isChunkInUse($chunkX, $chunkZ)){
2665 $this->unloadChunkRequest($chunkX, $chunkZ);
2666 }
2667
2668 if($oldChunk === null){
2669 if(ChunkLoadEvent::hasHandlers()){
2670 (new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk, true))->call();
2671 }
2672
2673 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2674 $listener->onChunkLoaded($chunkX, $chunkZ, $chunk);
2675 }
2676 }else{
2677 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2678 $listener->onChunkChanged($chunkX, $chunkZ, $chunk);
2679 }
2680 }
2681
2682 for($cX = -1; $cX <= 1; ++$cX){
2683 for($cZ = -1; $cZ <= 1; ++$cZ){
2684 foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
2685 $entity->onNearbyBlockChange();
2686 }
2687 }
2688 }
2689 }
2690
2697 public function getHighestBlockAt(int $x, int $z) : ?int{
2698 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2699 return $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2700 }
2701 throw new WorldException("Cannot get highest block in an ungenerated chunk");
2702 }
2703
2707 public function isInLoadedTerrain(Vector3 $pos) : bool{
2708 return $this->isChunkLoaded($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2709 }
2710
2711 public function isChunkLoaded(int $x, int $z) : bool{
2712 return isset($this->chunks[World::chunkHash($x, $z)]);
2713 }
2714
2715 public function isChunkGenerated(int $x, int $z) : bool{
2716 return $this->loadChunk($x, $z) !== null;
2717 }
2718
2719 public function isChunkPopulated(int $x, int $z) : bool{
2720 $chunk = $this->loadChunk($x, $z);
2721 return $chunk !== null && $chunk->isPopulated();
2722 }
2723
2727 public function getSpawnLocation() : Position{
2728 return Position::fromObject($this->provider->getWorldData()->getSpawn(), $this);
2729 }
2730
2734 public function setSpawnLocation(Vector3 $pos) : void{
2735 $previousSpawn = $this->getSpawnLocation();
2736 $this->provider->getWorldData()->setSpawn($pos);
2737 (new SpawnChangeEvent($this, $previousSpawn))->call();
2738
2739 $location = Position::fromObject($pos, $this);
2740 foreach($this->players as $player){
2741 $player->getNetworkSession()->syncWorldSpawnPoint($location);
2742 }
2743 }
2744
2748 public function addEntity(Entity $entity) : void{
2749 if($entity->isClosed()){
2750 throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world");
2751 }
2752 if($entity->getWorld() !== $this){
2753 throw new \InvalidArgumentException("Invalid Entity world");
2754 }
2755 if(array_key_exists($entity->getId(), $this->entities)){
2756 if($this->entities[$entity->getId()] === $entity){
2757 throw new \InvalidArgumentException("Entity " . $entity->getId() . " has already been added to this world");
2758 }else{
2759 throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId());
2760 }
2761 }
2762 if(!EntityFactory::getInstance()->isRegistered($entity::class)){
2763 //canSaveWithChunk is mutable, so that means it could be toggled after adding the entity and cause a crash
2764 //later on. Better we just force all entities to have a save ID, even if it might not be needed.
2765 throw new \LogicException("Entity " . $entity::class . " is not registered for a save ID in EntityFactory");
2766 }
2767 $pos = $entity->getPosition()->asVector3();
2768 $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
2769 $this->entityLastKnownPositions[$entity->getId()] = $pos;
2770
2771 if($entity instanceof Player){
2772 $this->players[$entity->getId()] = $entity;
2773 }
2774 $this->entities[$entity->getId()] = $entity;
2775 }
2776
2782 public function removeEntity(Entity $entity) : void{
2783 if($entity->getWorld() !== $this){
2784 throw new \InvalidArgumentException("Invalid Entity world");
2785 }
2786 if(!array_key_exists($entity->getId(), $this->entities)){
2787 throw new \InvalidArgumentException("Entity is not tracked by this world (possibly already removed?)");
2788 }
2789 $pos = $this->entityLastKnownPositions[$entity->getId()];
2790 $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2791 if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
2792 if(count($this->entitiesByChunk[$chunkHash]) === 1){
2793 unset($this->entitiesByChunk[$chunkHash]);
2794 }else{
2795 unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
2796 }
2797 }
2798 unset($this->entityLastKnownPositions[$entity->getId()]);
2799
2800 if($entity instanceof Player){
2801 unset($this->players[$entity->getId()]);
2802 $this->checkSleep();
2803 }
2804
2805 unset($this->entities[$entity->getId()]);
2806 unset($this->updateEntities[$entity->getId()]);
2807 }
2808
2812 public function onEntityMoved(Entity $entity) : void{
2813 if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){
2814 //this can happen if the entity was teleported before addEntity() was called
2815 return;
2816 }
2817 $oldPosition = $this->entityLastKnownPositions[$entity->getId()];
2818 $newPosition = $entity->getPosition();
2819
2820 $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2821 $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2822 $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2823 $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2824
2825 if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
2826 $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
2827 if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
2828 if(count($this->entitiesByChunk[$oldChunkHash]) === 1){
2829 unset($this->entitiesByChunk[$oldChunkHash]);
2830 }else{
2831 unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
2832 }
2833 }
2834
2835 $newViewers = $this->getViewersForPosition($newPosition);
2836 foreach($entity->getViewers() as $player){
2837 if(!isset($newViewers[spl_object_id($player)])){
2838 $entity->despawnFrom($player);
2839 }else{
2840 unset($newViewers[spl_object_id($player)]);
2841 }
2842 }
2843 foreach($newViewers as $player){
2844 $entity->spawnTo($player);
2845 }
2846
2847 $newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
2848 $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
2849 }
2850 $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
2851 }
2852
2857 public function addTile(Tile $tile) : void{
2858 if($tile->isClosed()){
2859 throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world");
2860 }
2861 $pos = $tile->getPosition();
2862 if(!$pos->isValid() || $pos->getWorld() !== $this){
2863 throw new \InvalidArgumentException("Invalid Tile world");
2864 }
2865 if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
2866 throw new \InvalidArgumentException("Tile position is outside the world bounds");
2867 }
2868 if(!TileFactory::getInstance()->isRegistered($tile::class)){
2869 throw new \LogicException("Tile " . $tile::class . " is not registered for a save ID in TileFactory");
2870 }
2871
2872 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2873 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2874
2875 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2876 $this->chunks[$hash]->addTile($tile);
2877 }else{
2878 throw new \InvalidArgumentException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ");
2879 }
2880
2881 //delegate tile ticking to the corresponding block
2882 $this->scheduleDelayedBlockUpdate($pos->asVector3(), 1);
2883 }
2884
2889 public function removeTile(Tile $tile) : void{
2890 $pos = $tile->getPosition();
2891 if(!$pos->isValid() || $pos->getWorld() !== $this){
2892 throw new \InvalidArgumentException("Invalid Tile world");
2893 }
2894
2895 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2896 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2897
2898 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2899 $this->chunks[$hash]->removeTile($tile);
2900 }
2901 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2902 $listener->onBlockChanged($pos->asVector3());
2903 }
2904 }
2905
2906 public function isChunkInUse(int $x, int $z) : bool{
2907 return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
2908 }
2909
2916 public function loadChunk(int $x, int $z) : ?Chunk{
2917 if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){
2918 return $this->chunks[$chunkHash];
2919 }
2920
2921 $this->timings->syncChunkLoad->startTiming();
2922
2923 $this->cancelUnloadChunkRequest($x, $z);
2924
2925 $this->timings->syncChunkLoadData->startTiming();
2926
2927 $loadedChunkData = null;
2928
2929 try{
2930 $loadedChunkData = $this->provider->loadChunk($x, $z);
2931 }catch(CorruptedChunkException $e){
2932 $this->logger->critical("Failed to load chunk x=$x z=$z: " . $e->getMessage());
2933 }
2934
2935 $this->timings->syncChunkLoadData->stopTiming();
2936
2937 if($loadedChunkData === null){
2938 $this->timings->syncChunkLoad->stopTiming();
2939 return null;
2940 }
2941
2942 $chunkData = $loadedChunkData->getData();
2943 $chunk = new Chunk($chunkData->getSubChunks(), $chunkData->isPopulated());
2944 if(!$loadedChunkData->isUpgraded()){
2945 $chunk->clearTerrainDirtyFlags();
2946 }else{
2947 $this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
2948 }
2949 $this->chunks[$chunkHash] = $chunk;
2950
2951 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2952 unset($this->blockCache[$chunkHash]);
2953 unset($this->blockCollisionBoxCache[$chunkHash]);
2954
2955 $this->initChunk($x, $z, $chunkData);
2956
2957 if(ChunkLoadEvent::hasHandlers()){
2958 (new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], false))->call();
2959 }
2960
2961 if(!$this->isChunkInUse($x, $z)){
2962 $this->logger->debug("Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity");
2963 $this->unloadChunkRequest($x, $z);
2964 }
2965 foreach($this->getChunkListeners($x, $z) as $listener){
2966 $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
2967 }
2968 $this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded
2969
2970 $this->timings->syncChunkLoad->stopTiming();
2971
2972 return $this->chunks[$chunkHash];
2973 }
2974
2975 private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{
2976 $logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ");
2977
2978 if(count($chunkData->getEntityNBT()) !== 0){
2979 $this->timings->syncChunkLoadEntities->startTiming();
2980 $entityFactory = EntityFactory::getInstance();
2981 foreach($chunkData->getEntityNBT() as $k => $nbt){
2982 try{
2983 $entity = $entityFactory->createFromData($this, $nbt);
2984 }catch(SavedDataLoadingException $e){
2985 $logger->error("Bad entity data at list position $k: " . $e->getMessage());
2986 $logger->logException($e);
2987 continue;
2988 }
2989 if($entity === null){
2990 $saveIdTag = $nbt->getTag("identifier") ?? $nbt->getTag("id");
2991 $saveId = "<unknown>";
2992 if($saveIdTag instanceof StringTag){
2993 $saveId = $saveIdTag->getValue();
2994 }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format
2995 $saveId = "legacy(" . $saveIdTag->getValue() . ")";
2996 }
2997 $logger->warning("Deleted unknown entity type $saveId");
2998 }
2999 //TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place
3000 //here, because entities currently add themselves to the world
3001 }
3002
3003 $this->timings->syncChunkLoadEntities->stopTiming();
3004 }
3005
3006 if(count($chunkData->getTileNBT()) !== 0){
3007 $this->timings->syncChunkLoadTileEntities->startTiming();
3008 $tileFactory = TileFactory::getInstance();
3009 foreach($chunkData->getTileNBT() as $k => $nbt){
3010 try{
3011 $tile = $tileFactory->createFromData($this, $nbt);
3012 }catch(SavedDataLoadingException $e){
3013 $logger->error("Bad tile entity data at list position $k: " . $e->getMessage());
3014 $logger->logException($e);
3015 continue;
3016 }
3017 if($tile === null){
3018 $logger->warning("Deleted unknown tile entity type " . $nbt->getString("id", "<unknown>"));
3019 continue;
3020 }
3021
3022 $tilePosition = $tile->getPosition();
3023 if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
3024 $logger->error("Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
3025 }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
3026 $logger->error("Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
3027 }elseif($this->getTile($tilePosition) !== null){
3028 $logger->error("Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
3029 }else{
3030 $this->addTile($tile);
3031 }
3032 }
3033
3034 $this->timings->syncChunkLoadTileEntities->stopTiming();
3035 }
3036 }
3037
3038 private function queueUnloadChunk(int $x, int $z) : void{
3039 $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true);
3040 }
3041
3042 public function unloadChunkRequest(int $x, int $z, bool $safe = true) : bool{
3043 if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
3044 return false;
3045 }
3046
3047 $this->queueUnloadChunk($x, $z);
3048
3049 return true;
3050 }
3051
3052 public function cancelUnloadChunkRequest(int $x, int $z) : void{
3053 unset($this->unloadQueue[World::chunkHash($x, $z)]);
3054 }
3055
3056 public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{
3057 if($safe && $this->isChunkInUse($x, $z)){
3058 return false;
3059 }
3060
3061 if(!$this->isChunkLoaded($x, $z)){
3062 return true;
3063 }
3064
3065 $this->timings->doChunkUnload->startTiming();
3066
3067 $chunkHash = World::chunkHash($x, $z);
3068
3069 $chunk = $this->chunks[$chunkHash] ?? null;
3070
3071 if($chunk !== null){
3072 if(ChunkUnloadEvent::hasHandlers()){
3073 $ev = new ChunkUnloadEvent($this, $x, $z, $chunk);
3074 $ev->call();
3075 if($ev->isCancelled()){
3076 $this->timings->doChunkUnload->stopTiming();
3077
3078 return false;
3079 }
3080 }
3081
3082 if($trySave && $this->getAutoSave()){
3083 $this->timings->syncChunkSave->startTiming();
3084 try{
3085 $this->provider->saveChunk($x, $z, new ChunkData(
3086 $chunk->getSubChunks(),
3087 $chunk->isPopulated(),
3088 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk()))),
3089 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
3090 ), $chunk->getTerrainDirtyFlags());
3091 }finally{
3092 $this->timings->syncChunkSave->stopTiming();
3093 }
3094 }
3095
3096 foreach($this->getChunkListeners($x, $z) as $listener){
3097 $listener->onChunkUnloaded($x, $z, $chunk);
3098 }
3099
3100 foreach($this->getChunkEntities($x, $z) as $entity){
3101 if($entity instanceof Player){
3102 continue;
3103 }
3104 $entity->close();
3105 }
3106
3107 $chunk->onUnload();
3108 }
3109
3110 unset($this->chunks[$chunkHash]);
3111 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
3112 unset($this->blockCache[$chunkHash]);
3113 unset($this->blockCollisionBoxCache[$chunkHash]);
3114 unset($this->changedBlocks[$chunkHash]);
3115 unset($this->registeredTickingChunks[$chunkHash]);
3116 $this->markTickingChunkForRecheck($x, $z);
3117
3118 if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
3119 $this->logger->debug("Rejecting population promise for chunk $x $z");
3120 $this->chunkPopulationRequestMap[$chunkHash]->reject();
3121 unset($this->chunkPopulationRequestMap[$chunkHash]);
3122 if(isset($this->activeChunkPopulationTasks[$chunkHash])){
3123 $this->logger->debug("Marking population task for chunk $x $z as orphaned");
3124 $this->activeChunkPopulationTasks[$chunkHash] = false;
3125 }
3126 }
3127
3128 $this->timings->doChunkUnload->stopTiming();
3129
3130 return true;
3131 }
3132
3136 public function isSpawnChunk(int $X, int $Z) : bool{
3137 $spawn = $this->getSpawnLocation();
3138 $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
3139 $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
3140
3141 return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
3142 }
3143
3151 public function requestSafeSpawn(?Vector3 $spawn = null) : Promise{
3153 $resolver = new PromiseResolver();
3154 $spawn ??= $this->getSpawnLocation();
3155 /*
3156 * TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the provided
3157 * position, which is currently OK, but might be a problem in the future.
3158 */
3159 $this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
3160 function() use ($spawn, $resolver) : void{
3161 $spawn = $this->getSafeSpawn($spawn);
3162 $resolver->resolve($spawn);
3163 },
3164 function() use ($resolver) : void{
3165 $resolver->reject();
3166 }
3167 );
3168
3169 return $resolver->getPromise();
3170 }
3171
3178 public function getSafeSpawn(?Vector3 $spawn = null) : Position{
3179 if(!($spawn instanceof Vector3) || $spawn->y <= $this->minY){
3180 $spawn = $this->getSpawnLocation();
3181 }
3182
3183 $max = $this->maxY;
3184 $v = $spawn->floor();
3185 $chunk = $this->getOrLoadChunkAtPosition($v);
3186 if($chunk === null){
3187 throw new WorldException("Cannot find a safe spawn point in non-generated terrain");
3188 }
3189 $x = (int) $v->x;
3190 $z = (int) $v->z;
3191 $y = (int) min($max - 2, $v->y);
3192 $wasAir = $this->getBlockAt($x, $y - 1, $z)->getTypeId() === BlockTypeIds::AIR; //TODO: bad hack, clean up
3193 for(; $y > $this->minY; --$y){
3194 if($this->getBlockAt($x, $y, $z)->isFullCube()){
3195 if($wasAir){
3196 $y++;
3197 }
3198 break;
3199 }else{
3200 $wasAir = true;
3201 }
3202 }
3203
3204 for(; $y >= $this->minY && $y < $max; ++$y){
3205 if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
3206 if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
3207 return new Position($spawn->x, $y === (int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
3208 }
3209 }else{
3210 ++$y;
3211 }
3212 }
3213
3214 return new Position($spawn->x, $y, $spawn->z, $this);
3215 }
3216
3220 public function getTime() : int{
3221 return $this->time;
3222 }
3223
3227 public function getTimeOfDay() : int{
3228 return $this->time % self::TIME_FULL;
3229 }
3230
3235 public function getDisplayName() : string{
3236 return $this->displayName;
3237 }
3238
3242 public function setDisplayName(string $name) : void{
3243 (new WorldDisplayNameChangeEvent($this, $this->displayName, $name))->call();
3244
3245 $this->displayName = $name;
3246 $this->provider->getWorldData()->setName($name);
3247 }
3248
3252 public function getFolderName() : string{
3253 return $this->folderName;
3254 }
3255
3259 public function setTime(int $time) : void{
3260 $this->time = $time;
3261 $this->sendTime();
3262 }
3263
3267 public function stopTime() : void{
3268 $this->stopTime = true;
3269 $this->sendTime();
3270 }
3271
3275 public function startTime() : void{
3276 $this->stopTime = false;
3277 $this->sendTime();
3278 }
3279
3283 public function getSeed() : int{
3284 return $this->provider->getWorldData()->getSeed();
3285 }
3286
3287 public function getMinY() : int{
3288 return $this->minY;
3289 }
3290
3291 public function getMaxY() : int{
3292 return $this->maxY;
3293 }
3294
3295 public function getDifficulty() : int{
3296 return $this->provider->getWorldData()->getDifficulty();
3297 }
3298
3299 public function setDifficulty(int $difficulty) : void{
3300 if($difficulty < 0 || $difficulty > 3){
3301 throw new \InvalidArgumentException("Invalid difficulty level $difficulty");
3302 }
3303 (new WorldDifficultyChangeEvent($this, $this->getDifficulty(), $difficulty))->call();
3304 $this->provider->getWorldData()->setDifficulty($difficulty);
3305
3306 foreach($this->players as $player){
3307 $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty());
3308 }
3309 }
3310
3311 private function addChunkHashToPopulationRequestQueue(int $chunkHash) : void{
3312 if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
3313 $this->chunkPopulationRequestQueue->enqueue($chunkHash);
3314 $this->chunkPopulationRequestQueueIndex[$chunkHash] = true;
3315 }
3316 }
3317
3321 private function enqueuePopulationRequest(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3322 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3323 $this->addChunkHashToPopulationRequestQueue($chunkHash);
3325 $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver();
3326 if($associatedChunkLoader === null){
3327 $temporaryLoader = new class implements ChunkLoader{};
3328 $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
3329 $resolver->getPromise()->onCompletion(
3330 fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
3331 static function() : void{}
3332 );
3333 }
3334 return $resolver->getPromise();
3335 }
3336
3337 private function drainPopulationRequestQueue() : void{
3338 $failed = [];
3339 while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
3340 $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
3341 unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
3342 World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
3343 if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
3344 assert(!($this->activeChunkPopulationTasks[$nextChunkHash] ?? false), "Population for chunk $nextChunkX $nextChunkZ already running");
3345 if(
3346 !$this->orderChunkPopulation($nextChunkX, $nextChunkZ, null)->isResolved() &&
3347 !isset($this->activeChunkPopulationTasks[$nextChunkHash])
3348 ){
3349 $failed[] = $nextChunkHash;
3350 }
3351 }
3352 }
3353
3354 //these requests failed even though they weren't rate limited; we can't directly re-add them to the back of the
3355 //queue because it would result in an infinite loop
3356 foreach($failed as $hash){
3357 $this->addChunkHashToPopulationRequestQueue($hash);
3358 }
3359 }
3360
3366 private function checkChunkPopulationPreconditions(int $chunkX, int $chunkZ) : array{
3367 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3368 $resolver = $this->chunkPopulationRequestMap[$chunkHash] ?? null;
3369 if($resolver !== null && isset($this->activeChunkPopulationTasks[$chunkHash])){
3370 //generation is already running
3371 return [$resolver, false];
3372 }
3373
3374 $temporaryChunkLoader = new class implements ChunkLoader{};
3375 $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3376 $chunk = $this->loadChunk($chunkX, $chunkZ);
3377 $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3378 if($chunk !== null && $chunk->isPopulated()){
3379 //chunk is already populated; return a pre-resolved promise that will directly fire callbacks assigned
3380 $resolver ??= new PromiseResolver();
3381 unset($this->chunkPopulationRequestMap[$chunkHash]);
3382 $resolver->resolve($chunk);
3383 return [$resolver, false];
3384 }
3385 return [$resolver, true];
3386 }
3387
3399 public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3400 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3401 if(!$proceedWithPopulation){
3402 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3403 }
3404
3405 if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
3406 //too many chunks are already generating; delay resolution of the request until later
3407 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3408 }
3409 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3410 }
3411
3422 public function orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3423 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3424 if(!$proceedWithPopulation){
3425 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3426 }
3427
3428 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3429 }
3430
3435 private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{
3436 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3437
3438 $timings = $this->timings->chunkPopulationOrder;
3439 $timings->startTiming();
3440
3441 try{
3442 for($xx = -1; $xx <= 1; ++$xx){
3443 for($zz = -1; $zz <= 1; ++$zz){
3444 if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
3445 //chunk is already in use by another generation request; queue the request for later
3446 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3447 }
3448 }
3449 }
3450
3451 $this->activeChunkPopulationTasks[$chunkHash] = true;
3452 if($resolver === null){
3453 $resolver = new PromiseResolver();
3454 $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
3455 }
3456
3457 $chunkPopulationLockId = new ChunkLockId();
3458
3459 $temporaryChunkLoader = new class implements ChunkLoader{
3460 };
3461 for($xx = -1; $xx <= 1; ++$xx){
3462 for($zz = -1; $zz <= 1; ++$zz){
3463 $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
3464 $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
3465 }
3466 }
3467
3468 $centerChunk = $this->loadChunk($chunkX, $chunkZ);
3469 $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
3470 $task = new PopulationTask(
3471 $this->worldId,
3472 $chunkX,
3473 $chunkZ,
3474 $centerChunk,
3475 $adjacentChunks,
3476 function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) : void{
3477 if(!$this->isLoaded()){
3478 return;
3479 }
3480
3481 $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
3482 }
3483 );
3484 $workerId = $this->workerPool->selectWorker();
3485 if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
3486 $this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
3487 unset($this->generatorRegisteredWorkers[$workerId]);
3488 }
3489 if(!isset($this->generatorRegisteredWorkers[$workerId])){
3490 $this->registerGeneratorToWorker($workerId);
3491 }
3492 $this->workerPool->submitTaskToWorker($task, $workerId);
3493
3494 return $resolver->getPromise();
3495 }finally{
3496 $timings->stopTiming();
3497 }
3498 }
3499
3504 private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
3505 $timings = $this->timings->chunkPopulationCompletion;
3506 $timings->startTiming();
3507
3508 $dirtyChunks = 0;
3509 for($xx = -1; $xx <= 1; ++$xx){
3510 for($zz = -1; $zz <= 1; ++$zz){
3511 $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
3512 if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
3513 $dirtyChunks++;
3514 }
3515 }
3516 }
3517
3518 $index = World::chunkHash($x, $z);
3519 if(!isset($this->activeChunkPopulationTasks[$index])){
3520 throw new AssumptionFailedError("This should always be set, regardless of whether the task was orphaned or not");
3521 }
3522 if(!$this->activeChunkPopulationTasks[$index]){
3523 $this->logger->debug("Discarding orphaned population result for chunk x=$x,z=$z");
3524 unset($this->activeChunkPopulationTasks[$index]);
3525 }else{
3526 if($dirtyChunks === 0){
3527 $oldChunk = $this->loadChunk($x, $z);
3528 $this->setChunk($x, $z, $chunk);
3529
3530 foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
3531 World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
3532 if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
3533 throw new AssumptionFailedError("Adjacent chunks should be in range -1 ... +1 coordinates");
3534 }
3535 $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
3536 }
3537
3538 if(($oldChunk === null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
3539 if(ChunkPopulateEvent::hasHandlers()){
3540 (new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
3541 }
3542
3543 foreach($this->getChunkListeners($x, $z) as $listener){
3544 $listener->onChunkPopulated($x, $z, $chunk);
3545 }
3546 }
3547 }else{
3548 $this->logger->debug("Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
3549 }
3550
3551 //This needs to be in this specific spot because user code might call back to orderChunkPopulation().
3552 //If it does, and finds the promise, and doesn't find an active task associated with it, it will schedule
3553 //another PopulationTask. We don't want that because we're here processing the results.
3554 //We can't remove the promise from the array before setting the chunks in the world because that would lead
3555 //to the same problem. Therefore, it's necessary that this code be split into two if/else, with this in the
3556 //middle.
3557 unset($this->activeChunkPopulationTasks[$index]);
3558
3559 if($dirtyChunks === 0){
3560 $promise = $this->chunkPopulationRequestMap[$index] ?? null;
3561 if($promise !== null){
3562 unset($this->chunkPopulationRequestMap[$index]);
3563 $promise->resolve($chunk);
3564 }else{
3565 //Handlers of ChunkPopulateEvent, ChunkLoadEvent, or just ChunkListeners can cause this
3566 $this->logger->debug("Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
3567 }
3568 }else{
3569 //request failed, stick it back on the queue
3570 //we didn't resolve the promise or touch it in any way, so any fake chunk loaders are still valid and
3571 //don't need to be added a second time.
3572 $this->addChunkHashToPopulationRequestQueue($index);
3573 }
3574
3575 $this->drainPopulationRequestQueue();
3576 }
3577 $timings->stopTiming();
3578 }
3579
3580 public function doChunkGarbageCollection() : void{
3581 $this->timings->doChunkGC->startTiming();
3582
3583 foreach($this->chunks as $index => $chunk){
3584 if(!isset($this->unloadQueue[$index])){
3585 World::getXZ($index, $X, $Z);
3586 if(!$this->isSpawnChunk($X, $Z)){
3587 $this->unloadChunkRequest($X, $Z, true);
3588 }
3589 }
3590 $chunk->collectGarbage();
3591 }
3592
3593 $this->provider->doGarbageCollection();
3594
3595 $this->timings->doChunkGC->stopTiming();
3596 }
3597
3598 public function unloadChunks(bool $force = false) : void{
3599 if(count($this->unloadQueue) > 0){
3600 $maxUnload = 96;
3601 $now = microtime(true);
3602 foreach($this->unloadQueue as $index => $time){
3603 World::getXZ($index, $X, $Z);
3604
3605 if(!$force){
3606 if($maxUnload <= 0){
3607 break;
3608 }elseif($time > ($now - 30)){
3609 continue;
3610 }
3611 }
3612
3613 //If the chunk can't be unloaded, it stays on the queue
3614 if($this->unloadChunk($X, $Z, true)){
3615 unset($this->unloadQueue[$index]);
3616 --$maxUnload;
3617 }
3618 }
3619 }
3620 }
3621}
getBlock(?int $clickedFace=null)
Definition Item.php:491
pop(int $count=1)
Definition Item.php:430
removeWorkerStartHook(\Closure $hook)
getChunkListeners(int $chunkX, int $chunkZ)
Definition World.php:899
removeEntity(Entity $entity)
Definition World.php:2782
notifyNeighbourBlockUpdate(Vector3 $pos)
Definition World.php:1516
getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst=false)
Definition World.php:1524
getHighestAdjacentBlockLight(int $x, int $y, int $z)
Definition World.php:1903
setDisplayName(string $name)
Definition World.php:3242
getPotentialBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1808
removeOnUnloadCallback(\Closure $callback)
Definition World.php:676
isChunkLocked(int $chunkX, int $chunkZ)
Definition World.php:2619
setSpawnLocation(Vector3 $pos)
Definition World.php:2734
getPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1790
createBlockUpdatePackets(array $blocks)
Definition World.php:1099
getSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3178
registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:853
getBlockAt(int $x, int $y, int $z, bool $cached=true, bool $addToCache=true)
Definition World.php:1954
getChunkEntities(int $chunkX, int $chunkZ)
Definition World.php:2542
addEntity(Entity $entity)
Definition World.php:2748
getBlockLightAt(int $x, int $y, int $z)
Definition World.php:1833
getBlock(Vector3 $pos, bool $cached=true, bool $addToCache=true)
Definition World.php:1941
static chunkHash(int $x, int $z)
Definition World.php:383
broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet)
Definition World.php:803
getOrLoadChunkAtPosition(Vector3 $pos)
Definition World.php:2549
static chunkBlockHash(int $x, int $y, int $z)
Definition World.php:422
getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1888
getFullLight(Vector3 $pos)
Definition World.php:1747
isInWorld(int $x, int $y, int $z)
Definition World.php:1923
unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId)
Definition World.php:2604
getChunkLoaders(int $chunkX, int $chunkZ)
Definition World.php:786
getAdjacentChunks(int $x, int $z)
Definition World.php:2559
getChunkPlayers(int $chunkX, int $chunkZ)
Definition World.php:776
getTileAt(int $x, int $y, int $z)
Definition World.php:2499
getHighestAdjacentFullLightAt(int $x, int $y, int $z)
Definition World.php:1771
setChunkTickRadius(int $radius)
Definition World.php:1218
getViewersForPosition(Vector3 $pos)
Definition World.php:796
getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType=Entity::class, bool $includeDead=false)
Definition World.php:2442
__construct(private Server $server, string $name, private WritableWorldProvider $provider, private AsyncPool $workerPool)
Definition World.php:481
requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3399
addSound(Vector3 $pos, Sound $sound, ?array $players=null)
Definition World.php:704
registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1237
setBlock(Vector3 $pos, Block $block, bool $update=true)
Definition World.php:2009
getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2410
static getXZ(int $hash, ?int &$x, ?int &$z)
Definition World.php:448
getBlockCollisionBoxes(AxisAlignedBB $bb)
Definition World.php:1654
getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2392
isSpawnChunk(int $X, int $Z)
Definition World.php:3136
useBreakOn(Vector3 $vector, ?Item &$item=null, ?Player $player=null, bool $createParticles=false, array &$returnedItems=[])
Definition World.php:2124
getPotentialLight(Vector3 $pos)
Definition World.php:1779
addParticle(Vector3 $pos, Particle $particle, ?array $players=null)
Definition World.php:733
unregisterChunkListenerFromAll(ChunkListener $listener)
Definition World.php:886
loadChunk(int $x, int $z)
Definition World.php:2916
useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector=null, ?Player $player=null, bool $playSound=false, array &$returnedItems=[])
Definition World.php:2230
getHighestAdjacentPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1798
setBlockAt(int $x, int $y, int $z, Block $block, bool $update=true)
Definition World.php:2021
unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1247
getRealBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1823
static blockHash(int $x, int $y, int $z)
Definition World.php:402
getTile(Vector3 $pos)
Definition World.php:2492
getFullLightAt(int $x, int $y, int $z)
Definition World.php:1758
getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1896
orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3422
dropExperience(Vector3 $pos, int $amount)
Definition World.php:2101
isInLoadedTerrain(Vector3 $pos)
Definition World.php:2707
lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId)
Definition World.php:2587
getHighestBlockAt(int $x, int $z)
Definition World.php:2697
scheduleDelayedBlockUpdate(Vector3 $pos, int $delay)
Definition World.php:1475
static getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z)
Definition World.php:432
addOnUnloadCallback(\Closure $callback)
Definition World.php:671
requestSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3151
unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:870
getTile(int $x, int $y, int $z)
Definition Chunk.php:229
getHighestBlockAt(int $x, int $z)
Definition Chunk.php:121
getBlockStateId(int $x, int $y, int $z)
Definition Chunk.php:101