PocketMine-MP 5.35.1 git-05a71d8cc5185aa9e46ef5f9754bb862464c13e0
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
95use pocketmine\world\format\LightArray;
112use function abs;
113use function array_filter;
114use function array_key_exists;
115use function array_keys;
116use function array_map;
117use function array_merge;
118use function array_sum;
119use function array_values;
120use function assert;
121use function cos;
122use function count;
123use function floor;
124use function get_class;
125use function gettype;
126use function is_a;
127use function is_object;
128use function max;
129use function microtime;
130use function min;
131use function morton2d_decode;
132use function morton2d_encode;
133use function morton3d_decode;
134use function morton3d_encode;
135use function mt_rand;
136use function preg_match;
137use function spl_object_id;
138use function strtolower;
139use function trim;
140use const M_PI;
141use const PHP_INT_MAX;
142use const PHP_INT_MIN;
143
144#include <rules/World.h>
145
151class World implements ChunkManager{
152
153 private static int $worldIdCounter = 1;
154
155 public const Y_MAX = 320;
156 public const Y_MIN = -64;
157
158 public const TIME_DAY = 1000;
159 public const TIME_NOON = 6000;
160 public const TIME_SUNSET = 12000;
161 public const TIME_NIGHT = 13000;
162 public const TIME_MIDNIGHT = 18000;
163 public const TIME_SUNRISE = 23000;
164
165 public const TIME_FULL = 24000;
166
167 public const DIFFICULTY_PEACEFUL = 0;
168 public const DIFFICULTY_EASY = 1;
169 public const DIFFICULTY_NORMAL = 2;
170 public const DIFFICULTY_HARD = 3;
171
172 public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
173
174 //TODO: this could probably do with being a lot bigger
175 private const BLOCK_CACHE_SIZE_CAP = 2048;
176
181 private array $players = [];
182
187 private array $entities = [];
192 private array $entityLastKnownPositions = [];
193
198 private array $entitiesByChunk = [];
199
204 public array $updateEntities = [];
205
206 private bool $inDynamicStateRecalculation = false;
211 private array $blockCache = [];
212 private int $blockCacheSize = 0;
217 private array $blockCollisionBoxCache = [];
218
219 private int $sendTimeTicker = 0;
220
221 private int $worldId;
222
223 private int $providerGarbageCollectionTicker = 0;
224
225 private int $minY;
226 private int $maxY;
227
232 private array $registeredTickingChunks = [];
233
240 private array $validTickingChunks = [];
241
247 private array $recheckTickingChunks = [];
248
253 private array $chunkLoaders = [];
254
259 private array $chunkListeners = [];
264 private array $playerChunkListeners = [];
265
270 private array $packetBuffersByChunk = [];
271
276 private array $unloadQueue = [];
277
278 private int $time;
279 public bool $stopTime = false;
280
281 private float $sunAnglePercentage = 0.0;
282 private int $skyLightReduction = 0;
283
284 private string $folderName;
285 private string $displayName;
286
291 private array $chunks = [];
292
297 private array $knownUngeneratedChunks = [];
298
303 private array $changedBlocks = [];
304
306 private ReversePriorityQueue $scheduledBlockUpdateQueue;
311 private array $scheduledBlockUpdateQueueIndex = [];
312
314 private \SplQueue $neighbourBlockUpdateQueue;
319 private array $neighbourBlockUpdateQueueIndex = [];
320
325 private array $activeChunkPopulationTasks = [];
330 private array $chunkLock = [];
331 private int $maxConcurrentChunkPopulationTasks = 2;
336 private array $chunkPopulationRequestMap = [];
341 private \SplQueue $chunkPopulationRequestQueue;
346 private array $chunkPopulationRequestQueueIndex = [];
347
348 private readonly GeneratorExecutor $generatorExecutor;
349
350 private bool $autoSave = true;
351
352 private int $sleepTicks = 0;
353
354 private int $chunkTickRadius;
355 private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK;
360 private array $randomTickBlocks = [];
361
362 public WorldTimings $timings;
363
364 public float $tickRateTime = 0;
365
366 private bool $doingTick = false;
367
368 private bool $unloaded = false;
373 private array $unloadCallbacks = [];
374
375 private ?BlockLightUpdate $blockLightUpdate = null;
376 private ?SkyLightUpdate $skyLightUpdate = null;
377
378 private \Logger $logger;
379
380 private RuntimeBlockStateRegistry $blockStateRegistry;
381
385 public static function chunkHash(int $x, int $z) : int{
386 return morton2d_encode($x, $z);
387 }
388
389 private const MORTON3D_BIT_SIZE = 21;
390 private const BLOCKHASH_Y_BITS = 9;
391 private const BLOCKHASH_Y_PADDING = 64; //size (in blocks) of padding after both boundaries of the Y axis
392 private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
393 private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
394 private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
395 private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
396 private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
397 private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
398 private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
399 private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS;
400
404 public static function blockHash(int $x, int $y, int $z) : int{
405 $shiftedY = $y + self::BLOCKHASH_Y_OFFSET;
406 if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){
407 throw new \InvalidArgumentException("Y coordinate $y is out of range!");
408 }
409 //morton3d gives us 21 bits on each axis, but the Y axis only requires 9
410 //so we use the extra space on Y (12 bits) and add 6 extra bits from X and Z instead.
411 //if we ever need more space for Y (e.g. due to expansion), take bits from X/Z to compensate.
412 return morton3d_encode(
413 $x & self::BLOCKHASH_XZ_MASK,
414 ($shiftedY /* & self::BLOCKHASH_Y_MASK */) |
415 ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) |
416 ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT),
417 $z & self::BLOCKHASH_XZ_MASK
418 );
419 }
420
424 public static function chunkBlockHash(int $x, int $y, int $z) : int{
425 return morton3d_encode($x, $y, $z);
426 }
427
434 public static function getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z) : void{
435 [$baseX, $baseY, $baseZ] = morton3d_decode($hash);
436
437 $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
438 $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
439
440 $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
441 $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET;
442 $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
443 }
444
450 public static function getXZ(int $hash, ?int &$x, ?int &$z) : void{
451 [$x, $z] = morton2d_decode($hash);
452 }
453
454 public static function getDifficultyFromString(string $str) : int{
455 switch(strtolower(trim($str))){
456 case "0":
457 case "peaceful":
458 case "p":
459 return World::DIFFICULTY_PEACEFUL;
460
461 case "1":
462 case "easy":
463 case "e":
464 return World::DIFFICULTY_EASY;
465
466 case "2":
467 case "normal":
468 case "n":
469 return World::DIFFICULTY_NORMAL;
470
471 case "3":
472 case "hard":
473 case "h":
474 return World::DIFFICULTY_HARD;
475 }
476
477 return -1;
478 }
479
483 public function __construct(
484 private Server $server,
485 string $name, //TODO: this should be folderName (named arguments BC break)
486 private WritableWorldProvider $provider,
487 private AsyncPool $workerPool
488 ){
489 $this->folderName = $name;
490 $this->worldId = self::$worldIdCounter++;
491
492 $this->displayName = $this->provider->getWorldData()->getName();
493 $this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName");
494
495 $this->blockStateRegistry = RuntimeBlockStateRegistry::getInstance();
496 $this->minY = $this->provider->getWorldMinY();
497 $this->maxY = $this->provider->getWorldMaxY();
498
499 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
500 $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
501 throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
502 $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
503
504 $executorSetupParameters = new GeneratorExecutorSetupParameters(
505 worldMinY: $this->minY,
506 worldMaxY: $this->maxY,
507 generatorSeed: $this->getSeed(),
508 generatorClass: $generator->getGeneratorClass(),
509 generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
510 );
511 $this->generatorExecutor = $generator->isFast() ?
512 new SyncGeneratorExecutor($executorSetupParameters) :
514 $this->logger,
515 $this->workerPool,
516 $executorSetupParameters,
517 $this->worldId
518 );
519
520 $this->chunkPopulationRequestQueue = new \SplQueue();
521 $this->addOnUnloadCallback(function() : void{
522 $this->logger->debug("Cancelling unfulfilled generation requests");
523
524 foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){
525 $promise->reject();
526 unset($this->chunkPopulationRequestMap[$chunkHash]);
527 }
528 if(count($this->chunkPopulationRequestMap) !== 0){
529 //TODO: this might actually get hit because generation rejection callbacks might try to schedule new
530 //requests, and we can't prevent that right now because there's no way to detect "unloading" state
531 throw new AssumptionFailedError("New generation requests scheduled during unload");
532 }
533 });
534
535 $this->scheduledBlockUpdateQueue = new ReversePriorityQueue();
536 $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
537
538 $this->neighbourBlockUpdateQueue = new \SplQueue();
539
540 $this->time = $this->provider->getWorldData()->getTime();
541
542 $cfg = $this->server->getConfigGroup();
543 $this->chunkTickRadius = min($this->server->getViewDistance(), max(0, $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_TICK_RADIUS, 4)));
544 if($cfg->getPropertyInt("chunk-ticking.per-tick", 40) <= 0){
545 //TODO: this needs l10n
546 $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.");
547 $this->chunkTickRadius = 0;
548 }
549 $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_BLOCKS_PER_SUBCHUNK_PER_TICK, self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
550 $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2);
551
552 $this->initRandomTickBlocksFromConfig($cfg);
553
554 $this->timings = new WorldTimings($this);
555 }
556
557 private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
558 $dontTickBlocks = [];
559 $parser = StringToItemParser::getInstance();
560 foreach($cfg->getProperty(YmlServerProperties::CHUNK_TICKING_DISABLE_BLOCK_TICKING, []) as $name){
561 $name = (string) $name;
562 $item = $parser->parse($name);
563 if($item !== null){
564 $block = $item->getBlock();
565 }elseif(preg_match("/^-?\d+$/", $name) === 1){
566 //TODO: this is a really sketchy hack - remove this as soon as possible
567 try{
568 $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((int) $name, 0);
569 }catch(BlockStateDeserializeException){
570 continue;
571 }
572 $block = $this->blockStateRegistry->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
573 }else{
574 //TODO: we probably ought to log an error here
575 continue;
576 }
577
578 if($block->getTypeId() !== BlockTypeIds::AIR){
579 $dontTickBlocks[$block->getTypeId()] = $name;
580 }
581 }
582
583 foreach($this->blockStateRegistry->getAllKnownStates() as $state){
584 $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
585 if($dontTickName === null && $state->ticksRandomly()){
586 $this->randomTickBlocks[$state->getStateId()] = true;
587 }
588 }
589 }
590
591 public function getTickRateTime() : float{
592 return $this->tickRateTime;
593 }
594
595 public function getServer() : Server{
596 return $this->server;
597 }
598
599 public function getLogger() : \Logger{
600 return $this->logger;
601 }
602
603 final public function getProvider() : WritableWorldProvider{
604 return $this->provider;
605 }
606
610 final public function getId() : int{
611 return $this->worldId;
612 }
613
614 public function isLoaded() : bool{
615 return !$this->unloaded;
616 }
617
621 public function onUnload() : void{
622 if($this->unloaded){
623 throw new \LogicException("Tried to close a world which is already closed");
624 }
625
626 foreach($this->unloadCallbacks as $callback){
627 $callback();
628 }
629 $this->unloadCallbacks = [];
630
631 foreach($this->chunks as $chunkHash => $chunk){
632 self::getXZ($chunkHash, $chunkX, $chunkZ);
633 $this->unloadChunk($chunkX, $chunkZ, false);
634 }
635 $this->knownUngeneratedChunks = [];
636 foreach($this->entitiesByChunk as $chunkHash => $entities){
637 self::getXZ($chunkHash, $chunkX, $chunkZ);
638
639 $leakedEntities = 0;
640 foreach($entities as $entity){
641 if(!$entity->isFlaggedForDespawn()){
642 $leakedEntities++;
643 }
644 $entity->close();
645 }
646 if($leakedEntities !== 0){
647 $this->logger->warning("$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!");
648 }
649 }
650
651 $this->save();
652
653 $this->generatorExecutor->shutdown();
654
655 $this->provider->close();
656 $this->blockCache = [];
657 $this->blockCacheSize = 0;
658 $this->blockCollisionBoxCache = [];
659
660 $this->unloaded = true;
661 }
662
664 public function addOnUnloadCallback(\Closure $callback) : void{
665 $this->unloadCallbacks[spl_object_id($callback)] = $callback;
666 }
667
669 public function removeOnUnloadCallback(\Closure $callback) : void{
670 unset($this->unloadCallbacks[spl_object_id($callback)]);
671 }
672
681 private function filterViewersForPosition(Vector3 $pos, array $allowed) : array{
682 $candidates = $this->getViewersForPosition($pos);
683 $filtered = [];
684 foreach($allowed as $player){
685 $k = spl_object_id($player);
686 if(isset($candidates[$k])){
687 $filtered[$k] = $candidates[$k];
688 }
689 }
690
691 return $filtered;
692 }
693
697 public function addSound(Vector3 $pos, Sound $sound, ?array $players = null) : void{
698 $players ??= $this->getViewersForPosition($pos);
699
700 if(WorldSoundEvent::hasHandlers()){
701 $ev = new WorldSoundEvent($this, $sound, $pos, $players);
702 $ev->call();
703 if($ev->isCancelled()){
704 return;
705 }
706
707 $sound = $ev->getSound();
708 $players = $ev->getRecipients();
709 }
710
711 $pk = $sound->encode($pos);
712 if(count($pk) > 0){
713 if($players === $this->getViewersForPosition($pos)){
714 foreach($pk as $e){
715 $this->broadcastPacketToViewers($pos, $e);
716 }
717 }else{
718 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
719 }
720 }
721 }
722
726 public function addParticle(Vector3 $pos, Particle $particle, ?array $players = null) : void{
727 $players ??= $this->getViewersForPosition($pos);
728
729 if(WorldParticleEvent::hasHandlers()){
730 $ev = new WorldParticleEvent($this, $particle, $pos, $players);
731 $ev->call();
732 if($ev->isCancelled()){
733 return;
734 }
735
736 $particle = $ev->getParticle();
737 $players = $ev->getRecipients();
738 }
739
740 $pk = $particle->encode($pos);
741 if(count($pk) > 0){
742 if($players === $this->getViewersForPosition($pos)){
743 foreach($pk as $e){
744 $this->broadcastPacketToViewers($pos, $e);
745 }
746 }else{
747 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
748 }
749 }
750 }
751
752 public function getAutoSave() : bool{
753 return $this->autoSave;
754 }
755
756 public function setAutoSave(bool $value) : void{
757 $this->autoSave = $value;
758 }
759
769 public function getChunkPlayers(int $chunkX, int $chunkZ) : array{
770 return $this->playerChunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
771 }
772
779 public function getChunkLoaders(int $chunkX, int $chunkZ) : array{
780 return $this->chunkLoaders[World::chunkHash($chunkX, $chunkZ)] ?? [];
781 }
782
789 public function getViewersForPosition(Vector3 $pos) : array{
790 return $this->getChunkPlayers($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
791 }
792
796 public function broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet) : void{
797 $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE, $packet);
798 }
799
800 private function broadcastPacketToPlayersUsingChunk(int $chunkX, int $chunkZ, ClientboundPacket $packet) : void{
801 if(!isset($this->packetBuffersByChunk[$index = World::chunkHash($chunkX, $chunkZ)])){
802 $this->packetBuffersByChunk[$index] = [$packet];
803 }else{
804 $this->packetBuffersByChunk[$index][] = $packet;
805 }
806 }
807
808 public function registerChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ, bool $autoLoad = true) : void{
809 $loaderId = spl_object_id($loader);
810
811 if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){
812 $this->chunkLoaders[$chunkHash] = [];
813 }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){
814 return;
815 }
816
817 $this->chunkLoaders[$chunkHash][$loaderId] = $loader;
818
819 $this->cancelUnloadChunkRequest($chunkX, $chunkZ);
820
821 if($autoLoad){
822 $this->loadChunk($chunkX, $chunkZ);
823 }
824 }
825
826 public function unregisterChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ) : void{
827 $chunkHash = World::chunkHash($chunkX, $chunkZ);
828 $loaderId = spl_object_id($loader);
829 if(isset($this->chunkLoaders[$chunkHash][$loaderId])){
830 if(count($this->chunkLoaders[$chunkHash]) === 1){
831 unset($this->chunkLoaders[$chunkHash]);
832 $this->unloadChunkRequest($chunkX, $chunkZ, true);
833 if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){
834 $this->chunkPopulationRequestMap[$chunkHash]->reject();
835 unset($this->chunkPopulationRequestMap[$chunkHash]);
836 }
837 }else{
838 unset($this->chunkLoaders[$chunkHash][$loaderId]);
839 }
840 }
841 }
842
846 public function registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
847 $hash = World::chunkHash($chunkX, $chunkZ);
848 if(isset($this->chunkListeners[$hash])){
849 $this->chunkListeners[$hash][spl_object_id($listener)] = $listener;
850 }else{
851 $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener];
852 }
853 if($listener instanceof Player){
854 $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener;
855 }
856 }
857
863 public function unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
864 $hash = World::chunkHash($chunkX, $chunkZ);
865 if(isset($this->chunkListeners[$hash])){
866 if(count($this->chunkListeners[$hash]) === 1){
867 unset($this->chunkListeners[$hash]);
868 unset($this->playerChunkListeners[$hash]);
869 }else{
870 unset($this->chunkListeners[$hash][spl_object_id($listener)]);
871 unset($this->playerChunkListeners[$hash][spl_object_id($listener)]);
872 }
873 }
874 }
875
879 public function unregisterChunkListenerFromAll(ChunkListener $listener) : void{
880 foreach($this->chunkListeners as $hash => $listeners){
881 World::getXZ($hash, $chunkX, $chunkZ);
882 $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
883 }
884 }
885
892 public function getChunkListeners(int $chunkX, int $chunkZ) : array{
893 return $this->chunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
894 }
895
899 public function sendTime(Player ...$targets) : void{
900 if(count($targets) === 0){
901 $targets = $this->players;
902 }
903 foreach($targets as $player){
904 $player->getNetworkSession()->syncWorldTime($this->time);
905 }
906 }
907
908 public function isDoingTick() : bool{
909 return $this->doingTick;
910 }
911
915 public function doTick(int $currentTick) : void{
916 if($this->unloaded){
917 throw new \LogicException("Attempted to tick a world which has been closed");
918 }
919
920 $this->timings->doTick->startTiming();
921 $this->doingTick = true;
922 try{
923 $this->actuallyDoTick($currentTick);
924 }finally{
925 $this->doingTick = false;
926 $this->timings->doTick->stopTiming();
927 }
928 }
929
930 protected function actuallyDoTick(int $currentTick) : void{
931 if(!$this->stopTime){
932 //this simulates an overflow, as would happen in any language which doesn't do stupid things to var types
933 if($this->time === PHP_INT_MAX){
934 $this->time = PHP_INT_MIN;
935 }else{
936 $this->time++;
937 }
938 }
939
940 $this->sunAnglePercentage = $this->computeSunAnglePercentage(); //Sun angle depends on the current time
941 $this->skyLightReduction = $this->computeSkyLightReduction(); //Sky light reduction depends on the sun angle
942
943 if(++$this->sendTimeTicker === 200){
944 $this->sendTime();
945 $this->sendTimeTicker = 0;
946 }
947
948 $this->unloadChunks();
949 if(++$this->providerGarbageCollectionTicker >= 6000){
950 $this->provider->doGarbageCollection();
951 $this->providerGarbageCollectionTicker = 0;
952 }
953
954 $this->timings->scheduledBlockUpdates->startTiming();
955 //Delayed updates
956 while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){
958 $vec = $this->scheduledBlockUpdateQueue->extract()["data"];
959 unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
960 if(!$this->isInLoadedTerrain($vec)){
961 continue;
962 }
963 $block = $this->getBlock($vec);
964 $block->onScheduledUpdate();
965 }
966 $this->timings->scheduledBlockUpdates->stopTiming();
967
968 $this->timings->neighbourBlockUpdates->startTiming();
969 //Normal updates
970 while($this->neighbourBlockUpdateQueue->count() > 0){
971 $index = $this->neighbourBlockUpdateQueue->dequeue();
972 unset($this->neighbourBlockUpdateQueueIndex[$index]);
973 World::getBlockXYZ($index, $x, $y, $z);
974 if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){
975 continue;
976 }
977
978 $block = $this->getBlockAt($x, $y, $z);
979
980 if(BlockUpdateEvent::hasHandlers()){
981 $ev = new BlockUpdateEvent($block);
982 $ev->call();
983 if($ev->isCancelled()){
984 continue;
985 }
986 }
987 foreach($this->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z)) as $entity){
988 $entity->onNearbyBlockChange();
989 }
990 $block->onNearbyBlockChange();
991 }
992
993 $this->timings->neighbourBlockUpdates->stopTiming();
994
995 $this->timings->entityTick->startTiming();
996 //Update entities that need update
997 foreach($this->updateEntities as $id => $entity){
998 if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
999 unset($this->updateEntities[$id]);
1000 }
1001 if($entity->isFlaggedForDespawn()){
1002 $entity->close();
1003 }
1004 }
1005 $this->timings->entityTick->stopTiming();
1006
1007 $this->timings->randomChunkUpdates->startTiming();
1008 $this->tickChunks();
1009 $this->timings->randomChunkUpdates->stopTiming();
1010
1011 $this->executeQueuedLightUpdates();
1012
1013 if(count($this->changedBlocks) > 0){
1014 if(count($this->players) > 0){
1015 foreach($this->changedBlocks as $index => $blocks){
1016 if(count($blocks) === 0){ //blocks can be set normally and then later re-set with direct send
1017 continue;
1018 }
1019 World::getXZ($index, $chunkX, $chunkZ);
1020 if(!$this->isChunkLoaded($chunkX, $chunkZ)){
1021 //a previous chunk may have caused this one to be unloaded by a ChunkListener
1022 continue;
1023 }
1024 if(count($blocks) > 512){
1025 $chunk = $this->getChunk($chunkX, $chunkZ) ?? throw new AssumptionFailedError("We already checked that the chunk is loaded");
1026 foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){
1027 $p->onChunkChanged($chunkX, $chunkZ, $chunk);
1028 }
1029 }else{
1030 foreach($this->createBlockUpdatePackets($blocks) as $packet){
1031 $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet);
1032 }
1033 }
1034 }
1035 }
1036
1037 $this->changedBlocks = [];
1038
1039 }
1040
1041 if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
1042 $this->checkSleep();
1043 }
1044
1045 foreach($this->packetBuffersByChunk as $index => $entries){
1046 World::getXZ($index, $chunkX, $chunkZ);
1047 $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ);
1048 if(count($chunkPlayers) > 0){
1049 NetworkBroadcastUtils::broadcastPackets($chunkPlayers, $entries);
1050 }
1051 }
1052
1053 $this->packetBuffersByChunk = [];
1054 }
1055
1056 public function checkSleep() : void{
1057 if(count($this->players) === 0){
1058 return;
1059 }
1060
1061 $resetTime = true;
1062 foreach($this->getPlayers() as $p){
1063 if(!$p->isSleeping()){
1064 $resetTime = false;
1065 break;
1066 }
1067 }
1068
1069 if($resetTime){
1070 $time = $this->getTimeOfDay();
1071
1072 if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
1073 $this->setTime($this->getTime() + World::TIME_FULL - $time);
1074
1075 foreach($this->getPlayers() as $p){
1076 $p->stopSleep();
1077 }
1078 }
1079 }
1080 }
1081
1082 public function setSleepTicks(int $ticks) : void{
1083 $this->sleepTicks = $ticks;
1084 }
1085
1092 public function createBlockUpdatePackets(array $blocks) : array{
1093 $packets = [];
1094
1095 $blockTranslator = TypeConverter::getInstance()->getBlockTranslator();
1096
1097 foreach($blocks as $b){
1098 if(!($b instanceof Vector3)){
1099 throw new \TypeError("Expected Vector3 in blocks array, got " . (is_object($b) ? get_class($b) : gettype($b)));
1100 }
1101
1102 $fullBlock = $this->getBlockAt($b->x, $b->y, $b->z);
1103 $blockPosition = BlockPosition::fromVector3($b);
1104
1105 $tile = $this->getTileAt($b->x, $b->y, $b->z);
1106 if($tile instanceof Spawnable){
1107 $expectedClass = $fullBlock->getIdInfo()->getTileClass();
1108 if($expectedClass !== null && $tile instanceof $expectedClass && count($fakeStateProperties = $tile->getRenderUpdateBugWorkaroundStateProperties($fullBlock)) > 0){
1109 $originalStateData = $blockTranslator->internalIdToNetworkStateData($fullBlock->getStateId());
1110 $fakeStateData = new BlockStateData(
1111 $originalStateData->getName(),
1112 array_merge($originalStateData->getStates(), $fakeStateProperties),
1113 $originalStateData->getVersion()
1114 );
1115 $packets[] = UpdateBlockPacket::create(
1116 $blockPosition,
1117 $blockTranslator->getBlockStateDictionary()->lookupStateIdFromData($fakeStateData) ?? throw new AssumptionFailedError("Unmapped fake blockstate data: " . $fakeStateData->toNbt()),
1118 UpdateBlockPacket::FLAG_NETWORK,
1119 UpdateBlockPacket::DATA_LAYER_NORMAL
1120 );
1121 }
1122 }
1123 $packets[] = UpdateBlockPacket::create(
1124 $blockPosition,
1125 $blockTranslator->internalIdToNetworkId($fullBlock->getStateId()),
1126 UpdateBlockPacket::FLAG_NETWORK,
1127 UpdateBlockPacket::DATA_LAYER_NORMAL
1128 );
1129
1130 if($tile instanceof Spawnable){
1131 $packets[] = BlockActorDataPacket::create($blockPosition, $tile->getSerializedSpawnCompound());
1132 }
1133 }
1134
1135 return $packets;
1136 }
1137
1138 public function clearCache(bool $force = false) : void{
1139 if($force){
1140 $this->blockCache = [];
1141 $this->blockCacheSize = 0;
1142 $this->blockCollisionBoxCache = [];
1143 }else{
1144 //Recalculate this when we're asked - blockCacheSize may be higher than the real size
1145 $this->blockCacheSize = 0;
1146 foreach($this->blockCache as $list){
1147 $this->blockCacheSize += count($list);
1148 if($this->blockCacheSize > self::BLOCK_CACHE_SIZE_CAP){
1149 $this->blockCache = [];
1150 $this->blockCacheSize = 0;
1151 break;
1152 }
1153 }
1154
1155 $count = 0;
1156 foreach($this->blockCollisionBoxCache as $list){
1157 $count += count($list);
1158 if($count > self::BLOCK_CACHE_SIZE_CAP){
1159 //TODO: Is this really the best logic?
1160 $this->blockCollisionBoxCache = [];
1161 break;
1162 }
1163 }
1164 }
1165 }
1166
1167 private function trimBlockCache() : void{
1168 $before = $this->blockCacheSize;
1169 //Since PHP maintains key order, earliest in foreach should be the oldest entries
1170 //Older entries are less likely to be hot, so destroying these should usually have the lowest impact on performance
1171 foreach($this->blockCache as $chunkHash => $blocks){
1172 unset($this->blockCache[$chunkHash]);
1173 $this->blockCacheSize -= count($blocks);
1174 if($this->blockCacheSize < self::BLOCK_CACHE_SIZE_CAP){
1175 break;
1176 }
1177 }
1178 }
1179
1184 public function getRandomTickedBlocks() : array{
1185 return $this->randomTickBlocks;
1186 }
1187
1188 public function addRandomTickedBlock(Block $block) : void{
1189 if($block instanceof UnknownBlock){
1190 throw new \InvalidArgumentException("Cannot do random-tick on unknown block");
1191 }
1192 $this->randomTickBlocks[$block->getStateId()] = true;
1193 }
1194
1195 public function removeRandomTickedBlock(Block $block) : void{
1196 unset($this->randomTickBlocks[$block->getStateId()]);
1197 }
1198
1203 public function getChunkTickRadius() : int{
1204 return $this->chunkTickRadius;
1205 }
1206
1211 public function setChunkTickRadius(int $radius) : void{
1212 $this->chunkTickRadius = $radius;
1213 }
1214
1222 public function getTickingChunks() : array{
1223 return array_keys($this->validTickingChunks);
1224 }
1225
1230 public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1231 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
1232 $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
1233 $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
1234 }
1235
1240 public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1241 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1242 $tickerId = spl_object_id($ticker);
1243 if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
1244 if(count($this->registeredTickingChunks[$chunkHash]) === 1){
1245 unset(
1246 $this->registeredTickingChunks[$chunkHash],
1247 $this->recheckTickingChunks[$chunkHash],
1248 $this->validTickingChunks[$chunkHash]
1249 );
1250 }else{
1251 unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
1252 }
1253 }
1254 }
1255
1256 private function tickChunks() : void{
1257 if($this->chunkTickRadius <= 0 || count($this->registeredTickingChunks) === 0){
1258 return;
1259 }
1260
1261 if(count($this->recheckTickingChunks) > 0){
1262 $this->timings->randomChunkUpdatesChunkSelection->startTiming();
1263
1264 $chunkTickableCache = [];
1265
1266 foreach($this->recheckTickingChunks as $hash => $_){
1267 World::getXZ($hash, $chunkX, $chunkZ);
1268 if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
1269 $this->validTickingChunks[$hash] = $hash;
1270 }
1271 }
1272 $this->recheckTickingChunks = [];
1273
1274 $this->timings->randomChunkUpdatesChunkSelection->stopTiming();
1275 }
1276
1277 foreach($this->validTickingChunks as $index => $_){
1278 World::getXZ($index, $chunkX, $chunkZ);
1279
1280 $this->tickChunk($chunkX, $chunkZ);
1281 }
1282 }
1283
1290 private function isChunkTickable(int $chunkX, int $chunkZ, array &$cache) : bool{
1291 for($cx = -1; $cx <= 1; ++$cx){
1292 for($cz = -1; $cz <= 1; ++$cz){
1293 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1294 if(isset($cache[$chunkHash])){
1295 if(!$cache[$chunkHash]){
1296 return false;
1297 }
1298 continue;
1299 }
1300 if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
1301 $cache[$chunkHash] = false;
1302 return false;
1303 }
1304 $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
1305 if($adjacentChunk === null || !$adjacentChunk->isPopulated()){
1306 $cache[$chunkHash] = false;
1307 return false;
1308 }
1309 $lightPopulatedState = $adjacentChunk->isLightPopulated();
1310 if($lightPopulatedState !== true){
1311 if($lightPopulatedState === false){
1312 $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
1313 }
1314 $cache[$chunkHash] = false;
1315 return false;
1316 }
1317
1318 $cache[$chunkHash] = true;
1319 }
1320 }
1321
1322 return true;
1323 }
1324
1334 private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{
1335 for($cx = -1; $cx <= 1; ++$cx){
1336 for($cz = -1; $cz <= 1; ++$cz){
1337 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1338 unset($this->validTickingChunks[$chunkHash]);
1339 if(isset($this->registeredTickingChunks[$chunkHash])){
1340 $this->recheckTickingChunks[$chunkHash] = $chunkHash;
1341 }else{
1342 unset($this->recheckTickingChunks[$chunkHash]);
1343 }
1344 }
1345 }
1346 }
1347
1348 private function orderLightPopulation(int $chunkX, int $chunkZ) : void{
1349 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1350 $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
1351 if($lightPopulatedState === false){
1352 $this->chunks[$chunkHash]->setLightPopulated(null);
1353 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1354
1355 $this->workerPool->submitTask(new LightPopulationTask(
1356 $this->chunks[$chunkHash],
1357 function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : void{
1364 if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){
1365 return;
1366 }
1367 //TODO: calculated light information might not be valid if the terrain changed during light calculation
1368
1369 $chunk->setHeightMapArray($heightMap);
1370 foreach($blockLight as $y => $lightArray){
1371 $chunk->getSubChunk($y)->setBlockLightArray($lightArray);
1372 }
1373 foreach($skyLight as $y => $lightArray){
1374 $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
1375 }
1376 $chunk->setLightPopulated(true);
1377 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1378 }
1379 ));
1380 }
1381 }
1382
1383 private function tickChunk(int $chunkX, int $chunkZ) : void{
1384 $chunk = $this->getChunk($chunkX, $chunkZ);
1385 if($chunk === null){
1386 //the chunk may have been unloaded during a previous chunk's update (e.g. during BlockGrowEvent)
1387 return;
1388 }
1389 foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
1390 $entity->onRandomUpdate();
1391 }
1392
1393 $blockFactory = $this->blockStateRegistry;
1394 foreach($chunk->getSubChunks() as $Y => $subChunk){
1395 if(!$subChunk->isEmptyFast()){
1396 $k = 0;
1397 for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){
1398 if(($i % 5) === 0){
1399 //60 bits will be used by 5 blocks (12 bits each)
1400 $k = mt_rand(0, (1 << 60) - 1);
1401 }
1402 $x = $k & SubChunk::COORD_MASK;
1403 $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK;
1404 $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK;
1405 $k >>= (SubChunk::COORD_BIT_SIZE * 3);
1406
1407 $state = $subChunk->getBlockStateId($x, $y, $z);
1408
1409 if(isset($this->randomTickBlocks[$state])){
1410 $block = $blockFactory->fromStateId($state);
1411 $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
1412 $block->onRandomTick();
1413 }
1414 }
1415 }
1416 }
1417 }
1418
1422 public function __debugInfo() : array{
1423 return [];
1424 }
1425
1426 public function save(bool $force = false) : bool{
1427
1428 if(!$this->getAutoSave() && !$force){
1429 return false;
1430 }
1431
1432 (new WorldSaveEvent($this))->call();
1433
1434 $timings = $this->timings->syncDataSave;
1435 $timings->startTiming();
1436
1437 $this->provider->getWorldData()->setTime($this->time);
1438 $this->saveChunks();
1439 $this->provider->getWorldData()->save();
1440
1441 $timings->stopTiming();
1442
1443 return true;
1444 }
1445
1446 public function saveChunks() : void{
1447 $this->timings->syncChunkSave->startTiming();
1448 try{
1449 foreach($this->chunks as $chunkHash => $chunk){
1450 self::getXZ($chunkHash, $chunkX, $chunkZ);
1451 $this->provider->saveChunk($chunkX, $chunkZ, new ChunkData(
1452 $chunk->getSubChunks(),
1453 $chunk->isPopulated(),
1454 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk()))),
1455 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
1456 ), $chunk->getTerrainDirtyFlags());
1457 $chunk->clearTerrainDirtyFlags();
1458 }
1459 }finally{
1460 $this->timings->syncChunkSave->stopTiming();
1461 }
1462 }
1463
1468 public function scheduleDelayedBlockUpdate(Vector3 $pos, int $delay) : void{
1469 if(
1470 !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
1471 (isset($this->scheduledBlockUpdateQueueIndex[$index = World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
1472 ){
1473 return;
1474 }
1475 $this->scheduledBlockUpdateQueueIndex[$index] = $delay;
1476 $this->scheduledBlockUpdateQueue->insert(new Vector3((int) $pos->x, (int) $pos->y, (int) $pos->z), $delay + $this->server->getTick());
1477 }
1478
1479 private function tryAddToNeighbourUpdateQueue(int $x, int $y, int $z) : void{
1480 if($this->isInWorld($x, $y, $z)){
1481 $hash = World::blockHash($x, $y, $z);
1482 if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){
1483 $this->neighbourBlockUpdateQueue->enqueue($hash);
1484 $this->neighbourBlockUpdateQueueIndex[$hash] = true;
1485 }
1486 }
1487 }
1488
1495 private function internalNotifyNeighbourBlockUpdate(int $x, int $y, int $z) : void{
1496 $this->tryAddToNeighbourUpdateQueue($x, $y, $z);
1497 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1498 $this->tryAddToNeighbourUpdateQueue($x + $dx, $y + $dy, $z + $dz);
1499 }
1500 }
1501
1509 public function notifyNeighbourBlockUpdate(Vector3 $pos) : void{
1510 $this->internalNotifyNeighbourBlockUpdate($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ());
1511 }
1512
1517 public function getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst = false) : array{
1518 $minX = (int) floor($bb->minX - 1);
1519 $minY = (int) floor($bb->minY - 1);
1520 $minZ = (int) floor($bb->minZ - 1);
1521 $maxX = (int) floor($bb->maxX + 1);
1522 $maxY = (int) floor($bb->maxY + 1);
1523 $maxZ = (int) floor($bb->maxZ + 1);
1524
1525 $collides = [];
1526
1527 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1528 if($targetFirst){
1529 for($z = $minZ; $z <= $maxZ; ++$z){
1530 $zOverflow = $z === $minZ || $z === $maxZ;
1531 for($x = $minX; $x <= $maxX; ++$x){
1532 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1533 for($y = $minY; $y <= $maxY; ++$y){
1534 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1535
1536 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1537 if($overflow ?
1538 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1539 match ($stateCollisionInfo) {
1540 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1541 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1542 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1543 }
1544 ){
1545 return [$this->getBlockAt($x, $y, $z)];
1546 }
1547 }
1548 }
1549 }
1550 }else{
1551 //TODO: duplicated code :( this way is better for performance though
1552 for($z = $minZ; $z <= $maxZ; ++$z){
1553 $zOverflow = $z === $minZ || $z === $maxZ;
1554 for($x = $minX; $x <= $maxX; ++$x){
1555 $zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
1556 for($y = $minY; $y <= $maxY; ++$y){
1557 $overflow = $zxOverflow || $y === $minY || $y === $maxY;
1558
1559 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1560 if($overflow ?
1561 $stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
1562 match ($stateCollisionInfo) {
1563 RuntimeBlockStateRegistry::COLLISION_CUBE => true,
1564 RuntimeBlockStateRegistry::COLLISION_NONE => false,
1565 default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
1566 }
1567 ){
1568 $collides[] = $this->getBlockAt($x, $y, $z);
1569 }
1570 }
1571 }
1572 }
1573 }
1574
1575 return $collides;
1576 }
1577
1582 private function getBlockCollisionInfo(int $x, int $y, int $z, array $collisionInfo) : int{
1583 if(!$this->isInWorld($x, $y, $z)){
1584 return RuntimeBlockStateRegistry::COLLISION_NONE;
1585 }
1586 $chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1587 if($chunk === null){
1588 return RuntimeBlockStateRegistry::COLLISION_NONE;
1589 }
1590 $stateId = $chunk
1591 ->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)
1592 ->getBlockStateId(
1593 $x & SubChunk::COORD_MASK,
1594 $y & SubChunk::COORD_MASK,
1595 $z & SubChunk::COORD_MASK
1596 );
1597 return $collisionInfo[$stateId];
1598 }
1599
1611 private function getBlockCollisionBoxesForCell(int $x, int $y, int $z, array $collisionInfo) : array{
1612 $stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
1613 $boxes = match($stateCollisionInfo){
1614 RuntimeBlockStateRegistry::COLLISION_NONE => [],
1615 RuntimeBlockStateRegistry::COLLISION_CUBE => [AxisAlignedBB::one()->offset($x, $y, $z)],
1616 default => $this->getBlockAt($x, $y, $z)->getCollisionBoxes()
1617 };
1618
1619 //overlapping AABBs can't make any difference if this is a cube, so we can save some CPU cycles in this common case
1620 if($stateCollisionInfo !== RuntimeBlockStateRegistry::COLLISION_CUBE){
1621 $cellBB = null;
1622 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1623 $offsetX = $x + $dx;
1624 $offsetY = $y + $dy;
1625 $offsetZ = $z + $dz;
1626 $stateCollisionInfo = $this->getBlockCollisionInfo($offsetX, $offsetY, $offsetZ, $collisionInfo);
1627 if($stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW){
1628 //avoid allocating this unless it's needed
1629 $cellBB ??= AxisAlignedBB::one()->offset($x, $y, $z);
1630 $extraBoxes = $this->getBlockAt($offsetX, $offsetY, $offsetZ)->getCollisionBoxes();
1631 foreach($extraBoxes as $extraBox){
1632 if($extraBox->intersectsWith($cellBB)){
1633 $boxes[] = $extraBox;
1634 }
1635 }
1636 }
1637 }
1638 }
1639
1640 return $boxes;
1641 }
1642
1647 public function getBlockCollisionBoxes(AxisAlignedBB $bb) : array{
1648 $minX = (int) floor($bb->minX);
1649 $minY = (int) floor($bb->minY);
1650 $minZ = (int) floor($bb->minZ);
1651 $maxX = (int) floor($bb->maxX);
1652 $maxY = (int) floor($bb->maxY);
1653 $maxZ = (int) floor($bb->maxZ);
1654
1655 $collides = [];
1656
1657 $collisionInfo = $this->blockStateRegistry->collisionInfo;
1658
1659 for($z = $minZ; $z <= $maxZ; ++$z){
1660 for($x = $minX; $x <= $maxX; ++$x){
1661 $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1662 for($y = $minY; $y <= $maxY; ++$y){
1663 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1664
1665 $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z, $collisionInfo);
1666
1667 foreach($boxes as $blockBB){
1668 if($blockBB->intersectsWith($bb)){
1669 $collides[] = $blockBB;
1670 }
1671 }
1672 }
1673 }
1674 }
1675
1676 return $collides;
1677 }
1678
1686 public function getCollisionBoxes(Entity $entity, AxisAlignedBB $bb, bool $entities = true) : array{
1687 $collides = $this->getBlockCollisionBoxes($bb);
1688
1689 if($entities){
1690 foreach($this->getCollidingEntities($bb->expandedCopy(0.25, 0.25, 0.25), $entity) as $ent){
1691 $collides[] = clone $ent->boundingBox;
1692 }
1693 }
1694
1695 return $collides;
1696 }
1697
1702 public function computeSunAnglePercentage() : float{
1703 $timeProgress = ($this->time % self::TIME_FULL) / self::TIME_FULL;
1704
1705 //0.0 needs to be high noon, not dusk
1706 $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25);
1707
1708 //Offset the sun progress to be above the horizon longer at dusk and dawn
1709 //this is roughly an inverted sine curve, which pushes the sun progress back at dusk and forwards at dawn
1710 $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3);
1711
1712 return $sunProgress + $diff;
1713 }
1714
1718 public function getSunAnglePercentage() : float{
1719 return $this->sunAnglePercentage;
1720 }
1721
1725 public function getSunAngleRadians() : float{
1726 return $this->sunAnglePercentage * 2 * M_PI;
1727 }
1728
1732 public function getSunAngleDegrees() : float{
1733 return $this->sunAnglePercentage * 360.0;
1734 }
1735
1740 public function computeSkyLightReduction() : int{
1741 $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5)));
1742
1743 //TODO: check rain and thunder level
1744
1745 return (int) ($percentage * 11);
1746 }
1747
1751 public function getSkyLightReduction() : int{
1752 return $this->skyLightReduction;
1753 }
1754
1759 public function getFullLight(Vector3 $pos) : int{
1760 $floorX = $pos->getFloorX();
1761 $floorY = $pos->getFloorY();
1762 $floorZ = $pos->getFloorZ();
1763 return $this->getFullLightAt($floorX, $floorY, $floorZ);
1764 }
1765
1770 public function getFullLightAt(int $x, int $y, int $z) : int{
1771 $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z);
1772 if($skyLight < 15){
1773 return max($skyLight, $this->getBlockLightAt($x, $y, $z));
1774 }else{
1775 return $skyLight;
1776 }
1777 }
1778
1783 public function getHighestAdjacentFullLightAt(int $x, int $y, int $z) : int{
1784 return $this->getHighestAdjacentLight($x, $y, $z, $this->getFullLightAt(...));
1785 }
1786
1791 public function getPotentialLight(Vector3 $pos) : int{
1792 $floorX = $pos->getFloorX();
1793 $floorY = $pos->getFloorY();
1794 $floorZ = $pos->getFloorZ();
1795 return $this->getPotentialLightAt($floorX, $floorY, $floorZ);
1796 }
1797
1802 public function getPotentialLightAt(int $x, int $y, int $z) : int{
1803 return max($this->getPotentialBlockSkyLightAt($x, $y, $z), $this->getBlockLightAt($x, $y, $z));
1804 }
1805
1810 public function getHighestAdjacentPotentialLightAt(int $x, int $y, int $z) : int{
1811 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialLightAt(...));
1812 }
1813
1820 public function getPotentialBlockSkyLightAt(int $x, int $y, int $z) : int{
1821 if(!$this->isInWorld($x, $y, $z)){
1822 return $y >= self::Y_MAX ? 15 : 0;
1823 }
1824 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1825 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1826 }
1827 return 0; //TODO: this should probably throw instead (light not calculated yet)
1828 }
1829
1835 public function getRealBlockSkyLightAt(int $x, int $y, int $z) : int{
1836 $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction;
1837 return $light < 0 ? 0 : $light;
1838 }
1839
1845 public function getBlockLightAt(int $x, int $y, int $z) : int{
1846 if(!$this->isInWorld($x, $y, $z)){
1847 return 0;
1848 }
1849 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1850 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1851 }
1852 return 0; //TODO: this should probably throw instead (light not calculated yet)
1853 }
1854
1855 public function updateAllLight(int $x, int $y, int $z) : void{
1856 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
1857 return;
1858 }
1859
1860 $blockFactory = $this->blockStateRegistry;
1861 $this->timings->doBlockSkyLightUpdates->startTiming();
1862 if($this->skyLightUpdate === null){
1863 $this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
1864 }
1865 $this->skyLightUpdate->recalculateNode($x, $y, $z);
1866 $this->timings->doBlockSkyLightUpdates->stopTiming();
1867
1868 $this->timings->doBlockLightUpdates->startTiming();
1869 if($this->blockLightUpdate === null){
1870 $this->blockLightUpdate = new BlockLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light);
1871 }
1872 $this->blockLightUpdate->recalculateNode($x, $y, $z);
1873 $this->timings->doBlockLightUpdates->stopTiming();
1874 }
1875
1879 private function getHighestAdjacentLight(int $x, int $y, int $z, \Closure $lightGetter) : int{
1880 $max = 0;
1881 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1882 $x1 = $x + $offsetX;
1883 $y1 = $y + $offsetY;
1884 $z1 = $z + $offsetZ;
1885 if(
1886 !$this->isInWorld($x1, $y1, $z1) ||
1887 ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) === null ||
1888 $chunk->isLightPopulated() !== true
1889 ){
1890 continue;
1891 }
1892 $max = max($max, $lightGetter($x1, $y1, $z1));
1893 }
1894 return $max;
1895 }
1896
1900 public function getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z) : int{
1901 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialBlockSkyLightAt(...));
1902 }
1903
1908 public function getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z) : int{
1909 return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction;
1910 }
1911
1915 public function getHighestAdjacentBlockLight(int $x, int $y, int $z) : int{
1916 return $this->getHighestAdjacentLight($x, $y, $z, $this->getBlockLightAt(...));
1917 }
1918
1919 private function executeQueuedLightUpdates() : void{
1920 if($this->blockLightUpdate !== null){
1921 $this->timings->doBlockLightUpdates->startTiming();
1922 $this->blockLightUpdate->execute();
1923 $this->blockLightUpdate = null;
1924 $this->timings->doBlockLightUpdates->stopTiming();
1925 }
1926
1927 if($this->skyLightUpdate !== null){
1928 $this->timings->doBlockSkyLightUpdates->startTiming();
1929 $this->skyLightUpdate->execute();
1930 $this->skyLightUpdate = null;
1931 $this->timings->doBlockSkyLightUpdates->stopTiming();
1932 }
1933 }
1934
1935 public function isInWorld(int $x, int $y, int $z) : bool{
1936 return (
1937 $x <= Limits::INT32_MAX && $x >= Limits::INT32_MIN &&
1938 $y < $this->maxY && $y >= $this->minY &&
1939 $z <= Limits::INT32_MAX && $z >= Limits::INT32_MIN
1940 );
1941 }
1942
1953 public function getBlock(Vector3 $pos, bool $cached = true, bool $addToCache = true) : Block{
1954 return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache);
1955 }
1956
1966 public function getBlockAt(int $x, int $y, int $z, bool $cached = true, bool $addToCache = true) : Block{
1967 $relativeBlockHash = null;
1968 $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1969
1970 if($this->isInWorld($x, $y, $z)){
1971 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1972
1973 if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
1974 return $this->blockCache[$chunkHash][$relativeBlockHash];
1975 }
1976
1977 $chunk = $this->chunks[$chunkHash] ?? null;
1978 if($chunk !== null){
1979 $block = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
1980 }else{
1981 $addToCache = false;
1982 $block = VanillaBlocks::AIR();
1983 }
1984 }else{
1985 $block = VanillaBlocks::AIR();
1986 }
1987
1988 $block->position($this, $x, $y, $z);
1989
1990 if($this->inDynamicStateRecalculation){
1991 //this call was generated by a parent getBlock() call calculating dynamic stateinfo
1992 //don't calculate dynamic state and don't add to block cache (since it won't have dynamic state calculated).
1993 //this ensures that it's impossible for dynamic state properties to recursively depend on each other.
1994 $addToCache = false;
1995 }else{
1996 $this->inDynamicStateRecalculation = true;
1997 $replacement = $block->readStateFromWorld();
1998 if($replacement !== $block){
1999 $replacement->position($this, $x, $y, $z);
2000 $block = $replacement;
2001 }
2002 $this->inDynamicStateRecalculation = false;
2003 }
2004
2005 if($addToCache && $relativeBlockHash !== null){
2006 $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
2007
2008 if(++$this->blockCacheSize >= self::BLOCK_CACHE_SIZE_CAP){
2009 $this->trimBlockCache();
2010 }
2011 }
2012
2013 return $block;
2014 }
2015
2021 public function setBlock(Vector3 $pos, Block $block, bool $update = true) : void{
2022 $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update);
2023 }
2024
2033 public function setBlockAt(int $x, int $y, int $z, Block $block, bool $update = true) : void{
2034 if(!$this->isInWorld($x, $y, $z)){
2035 throw new \InvalidArgumentException("Pos x=$x,y=$y,z=$z is outside of the world bounds");
2036 }
2037 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2038 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2039 if($this->loadChunk($chunkX, $chunkZ) === null){ //current expected behaviour is to try to load the terrain synchronously
2040 throw new WorldException("Cannot set a block in un-generated terrain");
2041 }
2042
2043 //TODO: this computes state ID twice (we do it again in writeStateToWorld()). Not great for performance :(
2044 $stateId = $block->getStateId();
2045 if(!$this->blockStateRegistry->hasStateId($stateId)){
2046 throw new \LogicException("Block state ID not known to RuntimeBlockStateRegistry (probably not registered)");
2047 }
2048 if(!GlobalBlockStateHandlers::getSerializer()->isRegistered($block)){
2049 throw new \LogicException("Block not registered with GlobalBlockStateHandlers serializer");
2050 }
2051
2052 $this->timings->setBlock->startTiming();
2053
2054 $this->unlockChunk($chunkX, $chunkZ, null);
2055
2056 $block = clone $block;
2057
2058 $block->position($this, $x, $y, $z);
2059 $block->writeStateToWorld();
2060 $pos = new Vector3($x, $y, $z);
2061
2062 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2063 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
2064
2065 unset($this->blockCache[$chunkHash][$relativeBlockHash]);
2066 $this->blockCacheSize--;
2067 unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
2068 //blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the
2069 //caches for those blocks as well
2070 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
2071 $sideChunkPosHash = World::chunkHash(($x + $offsetX) >> Chunk::COORD_BIT_SIZE, ($z + $offsetZ) >> Chunk::COORD_BIT_SIZE);
2072 $sideChunkBlockHash = World::chunkBlockHash($x + $offsetX, $y + $offsetY, $z + $offsetZ);
2073 unset($this->blockCollisionBoxCache[$sideChunkPosHash][$sideChunkBlockHash]);
2074 }
2075
2076 if(!isset($this->changedBlocks[$chunkHash])){
2077 $this->changedBlocks[$chunkHash] = [];
2078 }
2079 $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
2080
2081 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2082 $listener->onBlockChanged($pos);
2083 }
2084
2085 if($update){
2086 $this->updateAllLight($x, $y, $z);
2087 $this->internalNotifyNeighbourBlockUpdate($x, $y, $z);
2088 }
2089
2090 $this->timings->setBlock->stopTiming();
2091 }
2092
2093 public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion = null, int $delay = 10) : ?ItemEntity{
2094 if($item->isNull()){
2095 return null;
2096 }
2097
2098 $itemEntity = new ItemEntity(Location::fromObject($source, $this, Utils::getRandomFloat() * 360, 0), $item);
2099
2100 $itemEntity->setPickupDelay($delay);
2101 $itemEntity->setMotion($motion ?? new Vector3(Utils::getRandomFloat() * 0.2 - 0.1, 0.2, Utils::getRandomFloat() * 0.2 - 0.1));
2102 $itemEntity->spawnToAll();
2103
2104 return $itemEntity;
2105 }
2106
2113 public function dropExperience(Vector3 $pos, int $amount) : array{
2114 $orbs = [];
2115
2116 foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){
2117 $orb = new ExperienceOrb(Location::fromObject($pos, $this, Utils::getRandomFloat() * 360, 0), $split);
2118
2119 $orb->setMotion(new Vector3((Utils::getRandomFloat() * 0.2 - 0.1) * 2, Utils::getRandomFloat() * 0.4, (Utils::getRandomFloat() * 0.2 - 0.1) * 2));
2120 $orb->spawnToAll();
2121
2122 $orbs[] = $orb;
2123 }
2124
2125 return $orbs;
2126 }
2127
2136 public function useBreakOn(Vector3 $vector, ?Item &$item = null, ?Player $player = null, bool $createParticles = false, array &$returnedItems = []) : bool{
2137 $vector = $vector->floor();
2138
2139 $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE;
2140 $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2141 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2142 return false;
2143 }
2144
2145 $target = $this->getBlock($vector);
2146 $affectedBlocks = $target->getAffectedBlocks();
2147
2148 if($item === null){
2149 $item = VanillaItems::AIR();
2150 }
2151
2152 $drops = [];
2153 if($player === null || $player->hasFiniteResources()){
2154 $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
2155 }
2156
2157 $xpDrop = 0;
2158 if($player !== null && $player->hasFiniteResources()){
2159 $xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
2160 }
2161
2162 if($player !== null){
2163 $ev = new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
2164
2165 if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
2166 $ev->cancel();
2167 }
2168
2169 if($player->isAdventure(true) && !$ev->isCancelled()){
2170 $canBreak = false;
2171 $itemParser = LegacyStringToItemParser::getInstance();
2172 foreach($item->getCanDestroy() as $v){
2173 $entry = $itemParser->parse($v);
2174 if($entry->getBlock()->hasSameTypeId($target)){
2175 $canBreak = true;
2176 break;
2177 }
2178 }
2179
2180 if(!$canBreak){
2181 $ev->cancel();
2182 }
2183 }
2184
2185 $ev->call();
2186 if($ev->isCancelled()){
2187 return false;
2188 }
2189
2190 $drops = $ev->getDrops();
2191 $xpDrop = $ev->getXpDropAmount();
2192
2193 }elseif(!$target->getBreakInfo()->isBreakable()){
2194 return false;
2195 }
2196
2197 foreach($affectedBlocks as $t){
2198 $this->destroyBlockInternal($t, $item, $player, $createParticles, $returnedItems);
2199 }
2200
2201 $item->onDestroyBlock($target, $returnedItems);
2202
2203 if(count($drops) > 0){
2204 $dropPos = $vector->add(0.5, 0.5, 0.5);
2205 foreach($drops as $drop){
2206 if(!$drop->isNull()){
2207 $this->dropItem($dropPos, $drop);
2208 }
2209 }
2210 }
2211
2212 if($xpDrop > 0){
2213 $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop);
2214 }
2215
2216 return true;
2217 }
2218
2222 private function destroyBlockInternal(Block $target, Item $item, ?Player $player, bool $createParticles, array &$returnedItems) : void{
2223 if($createParticles){
2224 $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5), new BlockBreakParticle($target));
2225 }
2226
2227 $target->onBreak($item, $player, $returnedItems);
2228
2229 $tile = $this->getTile($target->getPosition());
2230 if($tile !== null){
2231 $tile->onBlockDestroyed();
2232 }
2233 }
2234
2242 public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{
2243 $blockClicked = $this->getBlock($vector);
2244 $blockReplace = $blockClicked->getSide($face);
2245
2246 if($clickVector === null){
2247 $clickVector = new Vector3(0.0, 0.0, 0.0);
2248 }else{
2249 $clickVector = new Vector3(
2250 min(1.0, max(0.0, $clickVector->x)),
2251 min(1.0, max(0.0, $clickVector->y)),
2252 min(1.0, max(0.0, $clickVector->z))
2253 );
2254 }
2255
2256 if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){
2257 //TODO: build height limit messages for custom world heights and mcregion cap
2258 return false;
2259 }
2260 $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE;
2261 $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2262 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2263 return false;
2264 }
2265
2266 if($blockClicked->getTypeId() === BlockTypeIds::AIR){
2267 return false;
2268 }
2269
2270 if($player !== null){
2271 $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
2272 if($player->isSneaking()){
2273 $ev->setUseItem(false);
2274 $ev->setUseBlock($item->isNull()); //opening doors is still possible when sneaking if using an empty hand
2275 }
2276 if($player->isSpectator()){
2277 $ev->cancel(); //set it to cancelled so plugins can bypass this
2278 }
2279
2280 $ev->call();
2281 if(!$ev->isCancelled()){
2282 if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2283 return true;
2284 }
2285
2286 if($ev->useItem()){
2287 $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
2288 if($result !== ItemUseResult::NONE){
2289 return $result === ItemUseResult::SUCCESS;
2290 }
2291 }
2292 }else{
2293 return false;
2294 }
2295 }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2296 return true;
2297 }
2298
2299 if($item->isNull() || !$item->canBePlaced()){
2300 return false;
2301 }
2302
2303 //TODO: while passing Facing::UP mimics the vanilla behaviour with replaceable blocks, we should really pass
2304 //some other value like NULL and let place() deal with it. This will look like a bug to anyone who doesn't know
2305 //about the vanilla behaviour.
2306 $tx =
2307 $item->getPlacementTransaction($blockClicked, $blockClicked, Facing::UP, $clickVector, $player) ??
2308 $item->getPlacementTransaction($blockReplace, $blockClicked, $face, $clickVector, $player);
2309 if($tx === null){
2310 //no placement options available
2311 return false;
2312 }
2313
2314 foreach($tx->getBlocks() as [$x, $y, $z, $block]){
2315 $block->position($this, $x, $y, $z);
2316 foreach($block->getCollisionBoxes() as $collisionBox){
2317 if(count($this->getCollidingEntities($collisionBox)) > 0){
2318 return false; //Entity in block
2319 }
2320 }
2321 }
2322
2323 if($player !== null){
2324 $ev = new BlockPlaceEvent($player, $tx, $blockClicked, $item);
2325 if($player->isSpectator()){
2326 $ev->cancel();
2327 }
2328
2329 if($player->isAdventure(true) && !$ev->isCancelled()){
2330 $canPlace = false;
2331 $itemParser = LegacyStringToItemParser::getInstance();
2332 foreach($item->getCanPlaceOn() as $v){
2333 $entry = $itemParser->parse($v);
2334 if($entry->getBlock()->hasSameTypeId($blockClicked)){
2335 $canPlace = true;
2336 break;
2337 }
2338 }
2339
2340 if(!$canPlace){
2341 $ev->cancel();
2342 }
2343 }
2344
2345 $ev->call();
2346 if($ev->isCancelled()){
2347 return false;
2348 }
2349 }
2350
2351 if(!$tx->apply()){
2352 return false;
2353 }
2354 $first = true;
2355 foreach($tx->getBlocks() as [$x, $y, $z, $_]){
2356 $tile = $this->getTileAt($x, $y, $z);
2357 if($tile !== null){
2358 //TODO: seal this up inside block placement
2359 $tile->copyDataFromItem($item);
2360 }
2361
2362 $placed = $this->getBlockAt($x, $y, $z);
2363 $placed->onPostPlace();
2364 if($first && $playSound){
2365 $this->addSound($placed->getPosition(), new BlockPlaceSound($placed));
2366 }
2367 $first = false;
2368 }
2369
2370 $item->pop();
2371
2372 return true;
2373 }
2374
2375 public function getEntity(int $entityId) : ?Entity{
2376 return $this->entities[$entityId] ?? null;
2377 }
2378
2385 public function getEntities() : array{
2386 return $this->entities;
2387 }
2388
2399 public function getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2400 $nearby = [];
2401
2402 foreach($this->getNearbyEntities($bb, $entity) as $ent){
2403 if($ent->canBeCollidedWith() && ($entity === null || $entity->canCollideWith($ent))){
2404 $nearby[] = $ent;
2405 }
2406 }
2407
2408 return $nearby;
2409 }
2410
2417 public function getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2418 $nearby = [];
2419
2420 $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE;
2421 $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE;
2422 $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE;
2423 $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE;
2424
2425 for($x = $minX; $x <= $maxX; ++$x){
2426 for($z = $minZ; $z <= $maxZ; ++$z){
2427 foreach($this->getChunkEntities($x, $z) as $ent){
2428 if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
2429 $nearby[] = $ent;
2430 }
2431 }
2432 }
2433 }
2434
2435 return $nearby;
2436 }
2437
2449 public function getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType = Entity::class, bool $includeDead = false) : ?Entity{
2450 assert(is_a($entityType, Entity::class, true));
2451
2452 $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2453 $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2454 $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2455 $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2456
2457 $currentTargetDistSq = $maxDistance ** 2;
2458
2463 $currentTarget = null;
2464
2465 for($x = $minX; $x <= $maxX; ++$x){
2466 for($z = $minZ; $z <= $maxZ; ++$z){
2467 foreach($this->getChunkEntities($x, $z) as $entity){
2468 if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
2469 continue;
2470 }
2471 $distSq = $entity->getPosition()->distanceSquared($pos);
2472 if($distSq < $currentTargetDistSq){
2473 $currentTargetDistSq = $distSq;
2474 $currentTarget = $entity;
2475 }
2476 }
2477 }
2478 }
2479
2480 return $currentTarget;
2481 }
2482
2489 public function getPlayers() : array{
2490 return $this->players;
2491 }
2492
2499 public function getTile(Vector3 $pos) : ?Tile{
2500 return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z));
2501 }
2502
2506 public function getTileAt(int $x, int $y, int $z) : ?Tile{
2507 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;
2508 }
2509
2510 public function getBiomeId(int $x, int $y, int $z) : int{
2511 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2512 return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2513 }
2514 return BiomeIds::OCEAN; //TODO: this should probably throw instead (terrain not generated yet)
2515 }
2516
2517 public function getBiome(int $x, int $y, int $z) : Biome{
2518 return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
2519 }
2520
2521 public function setBiomeId(int $x, int $y, int $z, int $biomeId) : void{
2522 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2523 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2524 $this->unlockChunk($chunkX, $chunkZ, null);
2525 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
2526 $chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
2527 }else{
2528 //if we allowed this, the modifications would be lost when the chunk is created
2529 throw new WorldException("Cannot set biome in a non-generated chunk");
2530 }
2531 }
2532
2537 public function getLoadedChunks() : array{
2538 return $this->chunks;
2539 }
2540
2541 public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{
2542 return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
2543 }
2544
2549 public function getChunkEntities(int $chunkX, int $chunkZ) : array{
2550 return $this->entitiesByChunk[World::chunkHash($chunkX, $chunkZ)] ?? [];
2551 }
2552
2556 public function getOrLoadChunkAtPosition(Vector3 $pos) : ?Chunk{
2557 return $this->loadChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2558 }
2559
2566 public function getAdjacentChunks(int $x, int $z) : array{
2567 $result = [];
2568 for($xx = -1; $xx <= 1; ++$xx){
2569 for($zz = -1; $zz <= 1; ++$zz){
2570 if($xx === 0 && $zz === 0){
2571 continue; //center chunk
2572 }
2573 $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
2574 }
2575 }
2576
2577 return $result;
2578 }
2579
2594 public function lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId) : void{
2595 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2596 if(isset($this->chunkLock[$chunkHash])){
2597 throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked");
2598 }
2599 $this->chunkLock[$chunkHash] = $lockId;
2600 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2601 }
2602
2611 public function unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId) : bool{
2612 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2613 if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){
2614 unset($this->chunkLock[$chunkHash]);
2615 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2616 return true;
2617 }
2618 return false;
2619 }
2620
2626 public function isChunkLocked(int $chunkX, int $chunkZ) : bool{
2627 return isset($this->chunkLock[World::chunkHash($chunkX, $chunkZ)]);
2628 }
2629
2630 public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
2631 foreach($chunk->getSubChunks() as $subChunk){
2632 foreach($subChunk->getBlockLayers() as $blockLayer){
2633 foreach($blockLayer->getPalette() as $blockStateId){
2634 if(!$this->blockStateRegistry->hasStateId($blockStateId)){
2635 throw new \InvalidArgumentException("Provided chunk contains unknown/unregistered blocks (found unknown state ID $blockStateId)");
2636 }
2637 }
2638 }
2639 }
2640
2641 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2642 $oldChunk = $this->loadChunk($chunkX, $chunkZ);
2643 if($oldChunk !== null && $oldChunk !== $chunk){
2644 $deletedTiles = 0;
2645 $transferredTiles = 0;
2646 foreach($oldChunk->getTiles() as $oldTile){
2647 $tilePosition = $oldTile->getPosition();
2648 $localX = $tilePosition->getFloorX() & Chunk::COORD_MASK;
2649 $localY = $tilePosition->getFloorY();
2650 $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
2651
2652 $newBlock = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ));
2653 $expectedTileClass = $newBlock->getIdInfo()->getTileClass();
2654 if(
2655 $expectedTileClass === null || //new block doesn't expect a tile
2656 !($oldTile instanceof $expectedTileClass) || //new block expects a different tile
2657 (($newTile = $chunk->getTile($localX, $localY, $localZ)) !== null && $newTile !== $oldTile) //new chunk already has a different tile
2658 ){
2659 $oldTile->close();
2660 $deletedTiles++;
2661 }else{
2662 $transferredTiles++;
2663 $chunk->addTile($oldTile);
2664 $oldChunk->removeTile($oldTile);
2665 }
2666 }
2667 if($deletedTiles > 0 || $transferredTiles > 0){
2668 $this->logger->debug("Replacement of chunk $chunkX $chunkZ caused deletion of $deletedTiles obsolete/conflicted tiles, and transfer of $transferredTiles");
2669 }
2670 }
2671
2672 $this->chunks[$chunkHash] = $chunk;
2673 unset($this->knownUngeneratedChunks[$chunkHash]);
2674
2675 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2676 unset($this->blockCache[$chunkHash]);
2677 unset($this->blockCollisionBoxCache[$chunkHash]);
2678 unset($this->changedBlocks[$chunkHash]);
2679 $chunk->setTerrainDirty();
2680 $this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking
2681
2682 if(!$this->isChunkInUse($chunkX, $chunkZ)){
2683 $this->unloadChunkRequest($chunkX, $chunkZ);
2684 }
2685
2686 if($oldChunk === null){
2687 if(ChunkLoadEvent::hasHandlers()){
2688 (new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk, true))->call();
2689 }
2690
2691 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2692 $listener->onChunkLoaded($chunkX, $chunkZ, $chunk);
2693 }
2694 }else{
2695 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2696 $listener->onChunkChanged($chunkX, $chunkZ, $chunk);
2697 }
2698 }
2699
2700 for($cX = -1; $cX <= 1; ++$cX){
2701 for($cZ = -1; $cZ <= 1; ++$cZ){
2702 foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
2703 $entity->onNearbyBlockChange();
2704 }
2705 }
2706 }
2707 }
2708
2715 public function getHighestBlockAt(int $x, int $z) : ?int{
2716 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2717 return $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2718 }
2719 throw new WorldException("Cannot get highest block in an ungenerated chunk");
2720 }
2721
2725 public function isInLoadedTerrain(Vector3 $pos) : bool{
2726 return $this->isChunkLoaded($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2727 }
2728
2729 public function isChunkLoaded(int $x, int $z) : bool{
2730 return isset($this->chunks[World::chunkHash($x, $z)]);
2731 }
2732
2733 public function isChunkGenerated(int $x, int $z) : bool{
2734 return $this->loadChunk($x, $z) !== null;
2735 }
2736
2737 public function isChunkPopulated(int $x, int $z) : bool{
2738 $chunk = $this->loadChunk($x, $z);
2739 return $chunk !== null && $chunk->isPopulated();
2740 }
2741
2745 public function getSpawnLocation() : Position{
2746 return Position::fromObject($this->provider->getWorldData()->getSpawn(), $this);
2747 }
2748
2752 public function setSpawnLocation(Vector3 $pos) : void{
2753 $previousSpawn = $this->getSpawnLocation();
2754 $this->provider->getWorldData()->setSpawn($pos);
2755 (new SpawnChangeEvent($this, $previousSpawn))->call();
2756
2757 $location = Position::fromObject($pos, $this);
2758 foreach($this->players as $player){
2759 $player->getNetworkSession()->syncWorldSpawnPoint($location);
2760 }
2761 }
2762
2766 public function addEntity(Entity $entity) : void{
2767 if($entity->isClosed()){
2768 throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world");
2769 }
2770 if($entity->getWorld() !== $this){
2771 throw new \InvalidArgumentException("Invalid Entity world");
2772 }
2773 if(array_key_exists($entity->getId(), $this->entities)){
2774 if($this->entities[$entity->getId()] === $entity){
2775 throw new \InvalidArgumentException("Entity " . $entity->getId() . " has already been added to this world");
2776 }else{
2777 throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId());
2778 }
2779 }
2780 if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof NeverSavedWithChunkEntity){
2781 //canSaveWithChunk is mutable, so that means it could be toggled after adding the entity and cause a crash
2782 //later on. Better we just force all entities to have a save ID, even if it might not be needed.
2783 throw new \LogicException("Entity " . $entity::class . " is not registered for a save ID in EntityFactory");
2784 }
2785 $pos = $entity->getPosition()->asVector3();
2786 $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
2787 $this->entityLastKnownPositions[$entity->getId()] = $pos;
2788
2789 if($entity instanceof Player){
2790 $this->players[$entity->getId()] = $entity;
2791 }
2792 $this->entities[$entity->getId()] = $entity;
2793 }
2794
2800 public function removeEntity(Entity $entity) : void{
2801 if($entity->getWorld() !== $this){
2802 throw new \InvalidArgumentException("Invalid Entity world");
2803 }
2804 if(!array_key_exists($entity->getId(), $this->entities)){
2805 throw new \InvalidArgumentException("Entity is not tracked by this world (possibly already removed?)");
2806 }
2807 $pos = $this->entityLastKnownPositions[$entity->getId()];
2808 $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2809 if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
2810 if(count($this->entitiesByChunk[$chunkHash]) === 1){
2811 unset($this->entitiesByChunk[$chunkHash]);
2812 }else{
2813 unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
2814 }
2815 }
2816 unset($this->entityLastKnownPositions[$entity->getId()]);
2817
2818 if($entity instanceof Player){
2819 unset($this->players[$entity->getId()]);
2820 $this->checkSleep();
2821 }
2822
2823 unset($this->entities[$entity->getId()]);
2824 unset($this->updateEntities[$entity->getId()]);
2825 }
2826
2830 public function onEntityMoved(Entity $entity) : void{
2831 if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){
2832 //this can happen if the entity was teleported before addEntity() was called
2833 return;
2834 }
2835 $oldPosition = $this->entityLastKnownPositions[$entity->getId()];
2836 $newPosition = $entity->getPosition();
2837
2838 $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2839 $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2840 $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2841 $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2842
2843 if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
2844 $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
2845 if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
2846 if(count($this->entitiesByChunk[$oldChunkHash]) === 1){
2847 unset($this->entitiesByChunk[$oldChunkHash]);
2848 }else{
2849 unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
2850 }
2851 }
2852
2853 $newViewers = $this->getViewersForPosition($newPosition);
2854 foreach($entity->getViewers() as $player){
2855 if(!isset($newViewers[spl_object_id($player)])){
2856 $entity->despawnFrom($player);
2857 }else{
2858 unset($newViewers[spl_object_id($player)]);
2859 }
2860 }
2861 foreach($newViewers as $player){
2862 $entity->spawnTo($player);
2863 }
2864
2865 $newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
2866 $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
2867 }
2868 $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
2869 }
2870
2875 public function addTile(Tile $tile) : void{
2876 if($tile->isClosed()){
2877 throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world");
2878 }
2879 $pos = $tile->getPosition();
2880 if(!$pos->isValid() || $pos->getWorld() !== $this){
2881 throw new \InvalidArgumentException("Invalid Tile world");
2882 }
2883 if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
2884 throw new \InvalidArgumentException("Tile position is outside the world bounds");
2885 }
2886 if(!TileFactory::getInstance()->isRegistered($tile::class)){
2887 throw new \LogicException("Tile " . $tile::class . " is not registered for a save ID in TileFactory");
2888 }
2889
2890 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2891 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2892
2893 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2894 $this->chunks[$hash]->addTile($tile);
2895 }else{
2896 throw new \InvalidArgumentException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ");
2897 }
2898
2899 //delegate tile ticking to the corresponding block
2900 $this->scheduleDelayedBlockUpdate($pos->asVector3(), 1);
2901 }
2902
2907 public function removeTile(Tile $tile) : void{
2908 $pos = $tile->getPosition();
2909 if(!$pos->isValid() || $pos->getWorld() !== $this){
2910 throw new \InvalidArgumentException("Invalid Tile world");
2911 }
2912
2913 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2914 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2915
2916 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2917 $this->chunks[$hash]->removeTile($tile);
2918 }
2919 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2920 $listener->onBlockChanged($pos->asVector3());
2921 }
2922 }
2923
2924 public function isChunkInUse(int $x, int $z) : bool{
2925 return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
2926 }
2927
2934 public function loadChunk(int $x, int $z) : ?Chunk{
2935 if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){
2936 return $this->chunks[$chunkHash];
2937 }
2938 if(isset($this->knownUngeneratedChunks[$chunkHash])){
2939 return null;
2940 }
2941
2942 $this->timings->syncChunkLoad->startTiming();
2943
2944 $this->cancelUnloadChunkRequest($x, $z);
2945
2946 $this->timings->syncChunkLoadData->startTiming();
2947
2948 $loadedChunkData = null;
2949
2950 try{
2951 $loadedChunkData = $this->provider->loadChunk($x, $z);
2952 }catch(CorruptedChunkException $e){
2953 $this->logger->critical("Failed to load chunk x=$x z=$z: " . $e->getMessage());
2954 }
2955
2956 $this->timings->syncChunkLoadData->stopTiming();
2957
2958 if($loadedChunkData === null){
2959 $this->timings->syncChunkLoad->stopTiming();
2960 $this->knownUngeneratedChunks[$chunkHash] = true;
2961 return null;
2962 }
2963
2964 $chunkData = $loadedChunkData->getData();
2965 $chunk = new Chunk($chunkData->getSubChunks(), $chunkData->isPopulated());
2966 if(!$loadedChunkData->isUpgraded()){
2967 $chunk->clearTerrainDirtyFlags();
2968 }else{
2969 $this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
2970 }
2971 $this->chunks[$chunkHash] = $chunk;
2972
2973 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
2974 unset($this->blockCache[$chunkHash]);
2975 unset($this->blockCollisionBoxCache[$chunkHash]);
2976
2977 $this->initChunk($x, $z, $chunkData, $chunk);
2978
2979 if(ChunkLoadEvent::hasHandlers()){
2980 (new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], false))->call();
2981 }
2982
2983 if(!$this->isChunkInUse($x, $z)){
2984 $this->logger->debug("Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity");
2985 $this->unloadChunkRequest($x, $z);
2986 }
2987 foreach($this->getChunkListeners($x, $z) as $listener){
2988 $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
2989 }
2990 $this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded
2991
2992 $this->timings->syncChunkLoad->stopTiming();
2993
2994 return $this->chunks[$chunkHash];
2995 }
2996
2997 private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, Chunk $chunk) : void{
2998 $logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ");
2999
3000 if(count($chunkData->getEntityNBT()) !== 0){
3001 $this->timings->syncChunkLoadEntities->startTiming();
3002 $entityFactory = EntityFactory::getInstance();
3003
3004 $deletedEntities = [];
3005 foreach($chunkData->getEntityNBT() as $k => $nbt){
3006 try{
3007 $entity = $entityFactory->createFromData($this, $nbt);
3008 }catch(SavedDataLoadingException $e){
3009 $logger->error("Bad entity data at list position $k: " . $e->getMessage());
3010 $logger->logException($e);
3011 continue;
3012 }
3013 if($entity === null){
3014 $saveIdTag = $nbt->getTag("identifier") ?? $nbt->getTag("id");
3015 $saveId = "<unknown>";
3016 if($saveIdTag instanceof StringTag){
3017 $saveId = $saveIdTag->getValue();
3018 }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format
3019 $saveId = "legacy(" . $saveIdTag->getValue() . ")";
3020 }
3021 $deletedEntities[$saveId] = ($deletedEntities[$saveId] ?? 0) + 1;
3022 }
3023 //TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place
3024 //here, because entities currently add themselves to the world
3025 }
3026
3027 foreach(Utils::promoteKeys($deletedEntities) as $saveId => $count){
3028 $logger->warning("Deleted unknown entity type $saveId x$count");
3029 }
3030 $this->timings->syncChunkLoadEntities->stopTiming();
3031 }
3032
3033 if(count($chunkData->getTileNBT()) !== 0){
3034 $this->timings->syncChunkLoadTileEntities->startTiming();
3035 $tileFactory = TileFactory::getInstance();
3036
3037 $deletedTiles = [];
3038 foreach($chunkData->getTileNBT() as $k => $nbt){
3039 try{
3040 $tile = $tileFactory->createFromData($this, $nbt);
3041 }catch(SavedDataLoadingException $e){
3042 $logger->error("Bad tile entity data at list position $k: " . $e->getMessage());
3043 $logger->logException($e);
3044 continue;
3045 }
3046 if($tile === null){
3047 $saveId = $nbt->getString("id", "<unknown>");
3048 $deletedTiles[$saveId] = ($deletedTiles[$saveId] ?? 0) + 1;
3049 continue;
3050 }
3051
3052 $tilePosition = $tile->getPosition();
3053 if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
3054 $logger->error("Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
3055 }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
3056 $logger->error("Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
3057 }elseif($this->getTile($tilePosition) !== null){
3058 $logger->error("Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
3059 }else{
3060 $this->addTile($tile);
3061 }
3062 $expectedStateId = $chunk->getBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK);
3063 $actualStateId = $this->getBlock($tilePosition)->getStateId();
3064 if($expectedStateId !== $actualStateId){
3065 //state ID was updated by readStateFromWorld - typically because the block pulled some data from the tile
3066 //make sure this is synced to the chunk
3067 //TODO: in the future we should pull tile reading logic out of readStateFromWorld() and do it only
3068 //when the tile is loaded - this would be cleaner and faster
3069 $chunk->setBlockStateId($tilePosition->getFloorX() & Chunk::COORD_MASK, $tilePosition->getFloorY(), $tilePosition->getFloorZ() & Chunk::COORD_MASK, $actualStateId);
3070 $this->logger->debug("Tile " . $tile::class . " at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z updated block state ID from $expectedStateId to $actualStateId");
3071 }
3072 }
3073
3074 foreach(Utils::promoteKeys($deletedTiles) as $saveId => $count){
3075 $logger->warning("Deleted unknown tile entity type $saveId x$count");
3076 }
3077
3078 $this->timings->syncChunkLoadTileEntities->stopTiming();
3079 }
3080 }
3081
3082 private function queueUnloadChunk(int $x, int $z) : void{
3083 $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true);
3084 }
3085
3086 public function unloadChunkRequest(int $x, int $z, bool $safe = true) : bool{
3087 if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
3088 return false;
3089 }
3090
3091 $this->queueUnloadChunk($x, $z);
3092
3093 return true;
3094 }
3095
3096 public function cancelUnloadChunkRequest(int $x, int $z) : void{
3097 unset($this->unloadQueue[World::chunkHash($x, $z)]);
3098 }
3099
3100 public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{
3101 if($safe && $this->isChunkInUse($x, $z)){
3102 return false;
3103 }
3104
3105 if(!$this->isChunkLoaded($x, $z)){
3106 return true;
3107 }
3108
3109 $this->timings->doChunkUnload->startTiming();
3110
3111 $chunkHash = World::chunkHash($x, $z);
3112
3113 $chunk = $this->chunks[$chunkHash] ?? null;
3114
3115 if($chunk !== null){
3116 if(ChunkUnloadEvent::hasHandlers()){
3117 $ev = new ChunkUnloadEvent($this, $x, $z, $chunk);
3118 $ev->call();
3119 if($ev->isCancelled()){
3120 $this->timings->doChunkUnload->stopTiming();
3121
3122 return false;
3123 }
3124 }
3125
3126 if($trySave && $this->getAutoSave()){
3127 $this->timings->syncChunkSave->startTiming();
3128 try{
3129 $this->provider->saveChunk($x, $z, new ChunkData(
3130 $chunk->getSubChunks(),
3131 $chunk->isPopulated(),
3132 array_map(fn(Entity $e) => $e->saveNBT(), array_values(array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk()))),
3133 array_map(fn(Tile $t) => $t->saveNBT(), array_values($chunk->getTiles())),
3134 ), $chunk->getTerrainDirtyFlags());
3135 }finally{
3136 $this->timings->syncChunkSave->stopTiming();
3137 }
3138 }
3139
3140 foreach($this->getChunkListeners($x, $z) as $listener){
3141 $listener->onChunkUnloaded($x, $z, $chunk);
3142 }
3143
3144 foreach($this->getChunkEntities($x, $z) as $entity){
3145 if($entity instanceof Player){
3146 continue;
3147 }
3148 $entity->close();
3149 }
3150
3151 $chunk->onUnload();
3152 }
3153
3154 unset($this->chunks[$chunkHash]);
3155 $this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
3156 unset($this->blockCache[$chunkHash]);
3157 unset($this->blockCollisionBoxCache[$chunkHash]);
3158 unset($this->changedBlocks[$chunkHash]);
3159 unset($this->registeredTickingChunks[$chunkHash]);
3160 $this->markTickingChunkForRecheck($x, $z);
3161
3162 if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
3163 $this->logger->debug("Rejecting population promise for chunk $x $z");
3164 $this->chunkPopulationRequestMap[$chunkHash]->reject();
3165 unset($this->chunkPopulationRequestMap[$chunkHash]);
3166 if(isset($this->activeChunkPopulationTasks[$chunkHash])){
3167 $this->logger->debug("Marking population task for chunk $x $z as orphaned");
3168 $this->activeChunkPopulationTasks[$chunkHash] = false;
3169 }
3170 }
3171
3172 $this->timings->doChunkUnload->stopTiming();
3173
3174 return true;
3175 }
3176
3180 public function isSpawnChunk(int $X, int $Z) : bool{
3181 $spawn = $this->getSpawnLocation();
3182 $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
3183 $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
3184
3185 return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
3186 }
3187
3195 public function requestSafeSpawn(?Vector3 $spawn = null) : Promise{
3197 $resolver = new PromiseResolver();
3198 $spawn ??= $this->getSpawnLocation();
3199 /*
3200 * TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the provided
3201 * position, which is currently OK, but might be a problem in the future.
3202 */
3203 $this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
3204 function() use ($spawn, $resolver) : void{
3205 $spawn = $this->getSafeSpawn($spawn);
3206 $resolver->resolve($spawn);
3207 },
3208 function() use ($resolver) : void{
3209 $resolver->reject();
3210 }
3211 );
3212
3213 return $resolver->getPromise();
3214 }
3215
3222 public function getSafeSpawn(?Vector3 $spawn = null) : Position{
3223 if(!($spawn instanceof Vector3) || $spawn->y <= $this->minY){
3224 $spawn = $this->getSpawnLocation();
3225 }
3226
3227 $max = $this->maxY;
3228 $v = $spawn->floor();
3229 $chunk = $this->getOrLoadChunkAtPosition($v);
3230 if($chunk === null){
3231 throw new WorldException("Cannot find a safe spawn point in non-generated terrain");
3232 }
3233 $x = (int) $v->x;
3234 $z = (int) $v->z;
3235 $y = (int) min($max - 2, $v->y);
3236 $wasAir = $this->getBlockAt($x, $y - 1, $z)->getTypeId() === BlockTypeIds::AIR; //TODO: bad hack, clean up
3237 for(; $y > $this->minY; --$y){
3238 if($this->getBlockAt($x, $y, $z)->isFullCube()){
3239 if($wasAir){
3240 $y++;
3241 }
3242 break;
3243 }else{
3244 $wasAir = true;
3245 }
3246 }
3247
3248 for(; $y >= $this->minY && $y < $max; ++$y){
3249 if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
3250 if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
3251 return new Position($spawn->x, $y === (int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
3252 }
3253 }else{
3254 ++$y;
3255 }
3256 }
3257
3258 return new Position($spawn->x, $y, $spawn->z, $this);
3259 }
3260
3264 public function getTime() : int{
3265 return $this->time;
3266 }
3267
3271 public function getTimeOfDay() : int{
3272 return $this->time % self::TIME_FULL;
3273 }
3274
3279 public function getDisplayName() : string{
3280 return $this->displayName;
3281 }
3282
3286 public function setDisplayName(string $name) : void{
3287 (new WorldDisplayNameChangeEvent($this, $this->displayName, $name))->call();
3288
3289 $this->displayName = $name;
3290 $this->provider->getWorldData()->setName($name);
3291 }
3292
3296 public function getFolderName() : string{
3297 return $this->folderName;
3298 }
3299
3303 public function setTime(int $time) : void{
3304 $this->time = $time;
3305 $this->sendTime();
3306 }
3307
3311 public function stopTime() : void{
3312 $this->stopTime = true;
3313 $this->sendTime();
3314 }
3315
3319 public function startTime() : void{
3320 $this->stopTime = false;
3321 $this->sendTime();
3322 }
3323
3327 public function getSeed() : int{
3328 return $this->provider->getWorldData()->getSeed();
3329 }
3330
3331 public function getMinY() : int{
3332 return $this->minY;
3333 }
3334
3335 public function getMaxY() : int{
3336 return $this->maxY;
3337 }
3338
3339 public function getDifficulty() : int{
3340 return $this->provider->getWorldData()->getDifficulty();
3341 }
3342
3343 public function setDifficulty(int $difficulty) : void{
3344 if($difficulty < 0 || $difficulty > 3){
3345 throw new \InvalidArgumentException("Invalid difficulty level $difficulty");
3346 }
3347 (new WorldDifficultyChangeEvent($this, $this->getDifficulty(), $difficulty))->call();
3348 $this->provider->getWorldData()->setDifficulty($difficulty);
3349
3350 foreach($this->players as $player){
3351 $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty());
3352 }
3353 }
3354
3355 private function addChunkHashToPopulationRequestQueue(int $chunkHash) : void{
3356 if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
3357 $this->chunkPopulationRequestQueue->enqueue($chunkHash);
3358 $this->chunkPopulationRequestQueueIndex[$chunkHash] = true;
3359 }
3360 }
3361
3365 private function enqueuePopulationRequest(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3366 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3367 $this->addChunkHashToPopulationRequestQueue($chunkHash);
3369 $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver();
3370 if($associatedChunkLoader === null){
3371 $temporaryLoader = new class implements ChunkLoader{};
3372 $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
3373 $resolver->getPromise()->onCompletion(
3374 fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
3375 static function() : void{}
3376 );
3377 }
3378 return $resolver->getPromise();
3379 }
3380
3381 private function drainPopulationRequestQueue() : void{
3382 $failed = [];
3383 while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
3384 $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
3385 unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
3386 World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
3387 if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
3388 assert(!($this->activeChunkPopulationTasks[$nextChunkHash] ?? false), "Population for chunk $nextChunkX $nextChunkZ already running");
3389 if(
3390 !$this->orderChunkPopulation($nextChunkX, $nextChunkZ, null)->isResolved() &&
3391 !isset($this->activeChunkPopulationTasks[$nextChunkHash])
3392 ){
3393 $failed[] = $nextChunkHash;
3394 }
3395 }
3396 }
3397
3398 //these requests failed even though they weren't rate limited; we can't directly re-add them to the back of the
3399 //queue because it would result in an infinite loop
3400 foreach($failed as $hash){
3401 $this->addChunkHashToPopulationRequestQueue($hash);
3402 }
3403 }
3404
3410 private function checkChunkPopulationPreconditions(int $chunkX, int $chunkZ) : array{
3411 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3412 $resolver = $this->chunkPopulationRequestMap[$chunkHash] ?? null;
3413 if($resolver !== null && isset($this->activeChunkPopulationTasks[$chunkHash])){
3414 //generation is already running
3415 return [$resolver, false];
3416 }
3417
3418 $temporaryChunkLoader = new class implements ChunkLoader{};
3419 $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3420 $chunk = $this->loadChunk($chunkX, $chunkZ);
3421 $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3422 if($chunk !== null && $chunk->isPopulated()){
3423 //chunk is already populated; return a pre-resolved promise that will directly fire callbacks assigned
3424 $resolver ??= new PromiseResolver();
3425 unset($this->chunkPopulationRequestMap[$chunkHash]);
3426 $resolver->resolve($chunk);
3427 return [$resolver, false];
3428 }
3429 return [$resolver, true];
3430 }
3431
3443 public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3444 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3445 if(!$proceedWithPopulation){
3446 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3447 }
3448
3449 if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
3450 //too many chunks are already generating; delay resolution of the request until later
3451 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3452 }
3453 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3454 }
3455
3466 public function orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3467 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3468 if(!$proceedWithPopulation){
3469 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3470 }
3471
3472 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3473 }
3474
3479 private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{
3480 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3481
3482 $timings = $this->timings->chunkPopulationOrder;
3483 $timings->startTiming();
3484
3485 try{
3486 for($xx = -1; $xx <= 1; ++$xx){
3487 for($zz = -1; $zz <= 1; ++$zz){
3488 if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
3489 //chunk is already in use by another generation request; queue the request for later
3490 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3491 }
3492 }
3493 }
3494
3495 $this->activeChunkPopulationTasks[$chunkHash] = true;
3496 if($resolver === null){
3497 $resolver = new PromiseResolver();
3498 $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
3499 }
3500
3501 $chunkPopulationLockId = new ChunkLockId();
3502
3503 $temporaryChunkLoader = new class implements ChunkLoader{
3504 };
3505 for($xx = -1; $xx <= 1; ++$xx){
3506 for($zz = -1; $zz <= 1; ++$zz){
3507 $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
3508 $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
3509 }
3510 }
3511
3512 $centerChunk = $this->loadChunk($chunkX, $chunkZ);
3513 $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
3514
3515 $this->generatorExecutor->populate(
3516 $chunkX,
3517 $chunkZ,
3518 $centerChunk,
3519 $adjacentChunks,
3520 function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) : void{
3521 if(!$this->isLoaded()){
3522 return;
3523 }
3524
3525 $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
3526 }
3527 );
3528
3529 return $resolver->getPromise();
3530 }finally{
3531 $timings->stopTiming();
3532 }
3533 }
3534
3539 private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
3540 $timings = $this->timings->chunkPopulationCompletion;
3541 $timings->startTiming();
3542
3543 $dirtyChunks = 0;
3544 for($xx = -1; $xx <= 1; ++$xx){
3545 for($zz = -1; $zz <= 1; ++$zz){
3546 $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
3547 if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
3548 $dirtyChunks++;
3549 }
3550 }
3551 }
3552
3553 $index = World::chunkHash($x, $z);
3554 if(!isset($this->activeChunkPopulationTasks[$index])){
3555 throw new AssumptionFailedError("This should always be set, regardless of whether the task was orphaned or not");
3556 }
3557 if(!$this->activeChunkPopulationTasks[$index]){
3558 $this->logger->debug("Discarding orphaned population result for chunk x=$x,z=$z");
3559 unset($this->activeChunkPopulationTasks[$index]);
3560 }else{
3561 if($dirtyChunks === 0){
3562 $oldChunk = $this->loadChunk($x, $z);
3563 $this->setChunk($x, $z, $chunk);
3564
3565 foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
3566 World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
3567 if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
3568 throw new AssumptionFailedError("Adjacent chunks should be in range -1 ... +1 coordinates");
3569 }
3570 $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
3571 }
3572
3573 if(($oldChunk === null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
3574 if(ChunkPopulateEvent::hasHandlers()){
3575 (new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
3576 }
3577
3578 foreach($this->getChunkListeners($x, $z) as $listener){
3579 $listener->onChunkPopulated($x, $z, $chunk);
3580 }
3581 }
3582 }else{
3583 $this->logger->debug("Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
3584 }
3585
3586 //This needs to be in this specific spot because user code might call back to orderChunkPopulation().
3587 //If it does, and finds the promise, and doesn't find an active task associated with it, it will schedule
3588 //another PopulationTask. We don't want that because we're here processing the results.
3589 //We can't remove the promise from the array before setting the chunks in the world because that would lead
3590 //to the same problem. Therefore, it's necessary that this code be split into two if/else, with this in the
3591 //middle.
3592 unset($this->activeChunkPopulationTasks[$index]);
3593
3594 if($dirtyChunks === 0){
3595 $promise = $this->chunkPopulationRequestMap[$index] ?? null;
3596 if($promise !== null){
3597 unset($this->chunkPopulationRequestMap[$index]);
3598 $promise->resolve($chunk);
3599 }else{
3600 //Handlers of ChunkPopulateEvent, ChunkLoadEvent, or just ChunkListeners can cause this
3601 $this->logger->debug("Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
3602 }
3603 }else{
3604 //request failed, stick it back on the queue
3605 //we didn't resolve the promise or touch it in any way, so any fake chunk loaders are still valid and
3606 //don't need to be added a second time.
3607 $this->addChunkHashToPopulationRequestQueue($index);
3608 }
3609
3610 $this->drainPopulationRequestQueue();
3611 }
3612 $timings->stopTiming();
3613 }
3614
3615 public function doChunkGarbageCollection() : void{
3616 $this->timings->doChunkGC->startTiming();
3617
3618 foreach($this->chunks as $index => $chunk){
3619 if(!isset($this->unloadQueue[$index])){
3620 World::getXZ($index, $X, $Z);
3621 if(!$this->isSpawnChunk($X, $Z)){
3622 $this->unloadChunkRequest($X, $Z, true);
3623 }
3624 }
3625 $chunk->collectGarbage();
3626 }
3627
3628 $this->provider->doGarbageCollection();
3629
3630 $this->timings->doChunkGC->stopTiming();
3631 }
3632
3633 public function unloadChunks(bool $force = false) : void{
3634 if(count($this->unloadQueue) > 0){
3635 $maxUnload = 96;
3636 $now = microtime(true);
3637 foreach($this->unloadQueue as $index => $time){
3638 World::getXZ($index, $X, $Z);
3639
3640 if(!$force){
3641 if($maxUnload <= 0){
3642 break;
3643 }elseif($time > ($now - 30)){
3644 continue;
3645 }
3646 }
3647
3648 //If the chunk can't be unloaded, it stays on the queue
3649 if($this->unloadChunk($X, $Z, true)){
3650 unset($this->unloadQueue[$index]);
3651 --$maxUnload;
3652 }
3653 }
3654 }
3655 }
3656}
despawnFrom(Player $player, bool $send=true)
Definition Entity.php:1568
pop(int $count=1)
Definition Item.php:427
expandedCopy(float $x, float $y, float $z)
getChunkListeners(int $chunkX, int $chunkZ)
Definition World.php:892
removeEntity(Entity $entity)
Definition World.php:2800
notifyNeighbourBlockUpdate(Vector3 $pos)
Definition World.php:1509
getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst=false)
Definition World.php:1517
getHighestAdjacentBlockLight(int $x, int $y, int $z)
Definition World.php:1915
setDisplayName(string $name)
Definition World.php:3286
getPotentialBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1820
removeOnUnloadCallback(\Closure $callback)
Definition World.php:669
isChunkLocked(int $chunkX, int $chunkZ)
Definition World.php:2626
setSpawnLocation(Vector3 $pos)
Definition World.php:2752
getPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1802
createBlockUpdatePackets(array $blocks)
Definition World.php:1092
getSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3222
registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:846
getBlockAt(int $x, int $y, int $z, bool $cached=true, bool $addToCache=true)
Definition World.php:1966
getChunkEntities(int $chunkX, int $chunkZ)
Definition World.php:2549
addEntity(Entity $entity)
Definition World.php:2766
getBlockLightAt(int $x, int $y, int $z)
Definition World.php:1845
getBlock(Vector3 $pos, bool $cached=true, bool $addToCache=true)
Definition World.php:1953
static chunkHash(int $x, int $z)
Definition World.php:385
broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet)
Definition World.php:796
getOrLoadChunkAtPosition(Vector3 $pos)
Definition World.php:2556
static chunkBlockHash(int $x, int $y, int $z)
Definition World.php:424
getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1900
getFullLight(Vector3 $pos)
Definition World.php:1759
isInWorld(int $x, int $y, int $z)
Definition World.php:1935
unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId)
Definition World.php:2611
getChunkLoaders(int $chunkX, int $chunkZ)
Definition World.php:779
getCollisionBoxes(Entity $entity, AxisAlignedBB $bb, bool $entities=true)
Definition World.php:1686
getAdjacentChunks(int $x, int $z)
Definition World.php:2566
getChunkPlayers(int $chunkX, int $chunkZ)
Definition World.php:769
getTileAt(int $x, int $y, int $z)
Definition World.php:2506
getHighestAdjacentFullLightAt(int $x, int $y, int $z)
Definition World.php:1783
setChunkTickRadius(int $radius)
Definition World.php:1211
getViewersForPosition(Vector3 $pos)
Definition World.php:789
getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType=Entity::class, bool $includeDead=false)
Definition World.php:2449
__construct(private Server $server, string $name, private WritableWorldProvider $provider, private AsyncPool $workerPool)
Definition World.php:483
requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3443
addSound(Vector3 $pos, Sound $sound, ?array $players=null)
Definition World.php:697
registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1230
setBlock(Vector3 $pos, Block $block, bool $update=true)
Definition World.php:2021
getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2417
static getXZ(int $hash, ?int &$x, ?int &$z)
Definition World.php:450
getBlockCollisionBoxes(AxisAlignedBB $bb)
Definition World.php:1647
getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2399
isSpawnChunk(int $X, int $Z)
Definition World.php:3180
useBreakOn(Vector3 $vector, ?Item &$item=null, ?Player $player=null, bool $createParticles=false, array &$returnedItems=[])
Definition World.php:2136
getPotentialLight(Vector3 $pos)
Definition World.php:1791
addParticle(Vector3 $pos, Particle $particle, ?array $players=null)
Definition World.php:726
unregisterChunkListenerFromAll(ChunkListener $listener)
Definition World.php:879
loadChunk(int $x, int $z)
Definition World.php:2934
useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector=null, ?Player $player=null, bool $playSound=false, array &$returnedItems=[])
Definition World.php:2242
getHighestAdjacentPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1810
setBlockAt(int $x, int $y, int $z, Block $block, bool $update=true)
Definition World.php:2033
unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1240
getRealBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1835
static blockHash(int $x, int $y, int $z)
Definition World.php:404
getTile(Vector3 $pos)
Definition World.php:2499
getFullLightAt(int $x, int $y, int $z)
Definition World.php:1770
getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1908
orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3466
dropExperience(Vector3 $pos, int $amount)
Definition World.php:2113
isInLoadedTerrain(Vector3 $pos)
Definition World.php:2725
lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId)
Definition World.php:2594
getHighestBlockAt(int $x, int $z)
Definition World.php:2715
scheduleDelayedBlockUpdate(Vector3 $pos, int $delay)
Definition World.php:1468
static getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z)
Definition World.php:434
addOnUnloadCallback(\Closure $callback)
Definition World.php:664
requestSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3195
unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:863