PocketMine-MP 5.21.2 git-a6534ecbbbcf369264567d27e5ed70f7f5be9816
Loading...
Searching...
No Matches
World.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
27namespace pocketmine\world;
28
61use pocketmine\item\ItemUseResult;
93use pocketmine\world\format\LightArray;
108use function abs;
109use function array_filter;
110use function array_key_exists;
111use function array_keys;
112use function array_map;
113use function array_merge;
114use function array_sum;
115use function assert;
116use function cos;
117use function count;
118use function floor;
119use function get_class;
120use function gettype;
121use function is_a;
122use function is_object;
123use function lcg_value;
124use function max;
125use function microtime;
126use function min;
127use function morton2d_decode;
128use function morton2d_encode;
129use function morton3d_decode;
130use function morton3d_encode;
131use function mt_rand;
132use function preg_match;
133use function spl_object_id;
134use function strtolower;
135use function trim;
136use const M_PI;
137use const PHP_INT_MAX;
138use const PHP_INT_MIN;
139
140#include <rules/World.h>
141
147class World implements ChunkManager{
148
149 private static int $worldIdCounter = 1;
150
151 public const Y_MAX = 320;
152 public const Y_MIN = -64;
153
154 public const TIME_DAY = 1000;
155 public const TIME_NOON = 6000;
156 public const TIME_SUNSET = 12000;
157 public const TIME_NIGHT = 13000;
158 public const TIME_MIDNIGHT = 18000;
159 public const TIME_SUNRISE = 23000;
160
161 public const TIME_FULL = 24000;
162
163 public const DIFFICULTY_PEACEFUL = 0;
164 public const DIFFICULTY_EASY = 1;
165 public const DIFFICULTY_NORMAL = 2;
166 public const DIFFICULTY_HARD = 3;
167
168 public const DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK = 3;
169
174 private array $players = [];
175
180 private array $entities = [];
185 private array $entityLastKnownPositions = [];
186
191 private array $entitiesByChunk = [];
192
197 public array $updateEntities = [];
198
199 private bool $inDynamicStateRecalculation = false;
204 private array $blockCache = [];
209 private array $blockCollisionBoxCache = [];
210
211 private int $sendTimeTicker = 0;
212
213 private int $worldId;
214
215 private int $providerGarbageCollectionTicker = 0;
216
217 private int $minY;
218 private int $maxY;
219
224 private array $registeredTickingChunks = [];
225
232 private array $validTickingChunks = [];
233
239 private array $recheckTickingChunks = [];
240
245 private array $chunkLoaders = [];
246
251 private array $chunkListeners = [];
256 private array $playerChunkListeners = [];
257
262 private array $packetBuffersByChunk = [];
263
268 private array $unloadQueue = [];
269
270 private int $time;
271 public bool $stopTime = false;
272
273 private float $sunAnglePercentage = 0.0;
274 private int $skyLightReduction = 0;
275
276 private string $folderName;
277 private string $displayName;
278
283 private array $chunks = [];
284
289 private array $changedBlocks = [];
290
292 private ReversePriorityQueue $scheduledBlockUpdateQueue;
297 private array $scheduledBlockUpdateQueueIndex = [];
298
300 private \SplQueue $neighbourBlockUpdateQueue;
305 private array $neighbourBlockUpdateQueueIndex = [];
306
311 private array $activeChunkPopulationTasks = [];
316 private array $chunkLock = [];
317 private int $maxConcurrentChunkPopulationTasks = 2;
322 private array $chunkPopulationRequestMap = [];
327 private \SplQueue $chunkPopulationRequestQueue;
332 private array $chunkPopulationRequestQueueIndex = [];
333
338 private array $generatorRegisteredWorkers = [];
339
340 private bool $autoSave = true;
341
342 private int $sleepTicks = 0;
343
344 private int $chunkTickRadius;
345 private int $tickedBlocksPerSubchunkPerTick = self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK;
350 private array $randomTickBlocks = [];
351
352 public WorldTimings $timings;
353
354 public float $tickRateTime = 0;
355
356 private bool $doingTick = false;
357
359 private string $generator;
360
361 private bool $unloaded = false;
366 private array $unloadCallbacks = [];
367
368 private ?BlockLightUpdate $blockLightUpdate = null;
369 private ?SkyLightUpdate $skyLightUpdate = null;
370
371 private \Logger $logger;
372
376 public static function chunkHash(int $x, int $z) : int{
377 return morton2d_encode($x, $z);
378 }
379
380 private const MORTON3D_BIT_SIZE = 21;
381 private const BLOCKHASH_Y_BITS = 9;
382 private const BLOCKHASH_Y_PADDING = 64; //size (in blocks) of padding after both boundaries of the Y axis
383 private const BLOCKHASH_Y_OFFSET = self::BLOCKHASH_Y_PADDING - self::Y_MIN;
384 private const BLOCKHASH_Y_MASK = (1 << self::BLOCKHASH_Y_BITS) - 1;
385 private const BLOCKHASH_XZ_MASK = (1 << self::MORTON3D_BIT_SIZE) - 1;
386 private const BLOCKHASH_XZ_EXTRA_BITS = (self::MORTON3D_BIT_SIZE - self::BLOCKHASH_Y_BITS) >> 1;
387 private const BLOCKHASH_XZ_EXTRA_MASK = (1 << self::BLOCKHASH_XZ_EXTRA_BITS) - 1;
388 private const BLOCKHASH_XZ_SIGN_SHIFT = 64 - self::MORTON3D_BIT_SIZE - self::BLOCKHASH_XZ_EXTRA_BITS;
389 private const BLOCKHASH_X_SHIFT = self::BLOCKHASH_Y_BITS;
390 private const BLOCKHASH_Z_SHIFT = self::BLOCKHASH_X_SHIFT + self::BLOCKHASH_XZ_EXTRA_BITS;
391
395 public static function blockHash(int $x, int $y, int $z) : int{
396 $shiftedY = $y + self::BLOCKHASH_Y_OFFSET;
397 if(($shiftedY & (~0 << self::BLOCKHASH_Y_BITS)) !== 0){
398 throw new \InvalidArgumentException("Y coordinate $y is out of range!");
399 }
400 //morton3d gives us 21 bits on each axis, but the Y axis only requires 9
401 //so we use the extra space on Y (12 bits) and add 6 extra bits from X and Z instead.
402 //if we ever need more space for Y (e.g. due to expansion), take bits from X/Z to compensate.
403 return morton3d_encode(
404 $x & self::BLOCKHASH_XZ_MASK,
405 ($shiftedY /* & self::BLOCKHASH_Y_MASK */) |
406 ((($x >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_X_SHIFT) |
407 ((($z >> self::MORTON3D_BIT_SIZE) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::BLOCKHASH_Z_SHIFT),
408 $z & self::BLOCKHASH_XZ_MASK
409 );
410 }
411
415 public static function chunkBlockHash(int $x, int $y, int $z) : int{
416 return morton3d_encode($x, $y, $z);
417 }
418
425 public static function getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z) : void{
426 [$baseX, $baseY, $baseZ] = morton3d_decode($hash);
427
428 $extraX = ((($baseY >> self::BLOCKHASH_X_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
429 $extraZ = ((($baseY >> self::BLOCKHASH_Z_SHIFT) & self::BLOCKHASH_XZ_EXTRA_MASK) << self::MORTON3D_BIT_SIZE);
430
431 $x = (($baseX & self::BLOCKHASH_XZ_MASK) | $extraX) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
432 $y = ($baseY & self::BLOCKHASH_Y_MASK) - self::BLOCKHASH_Y_OFFSET;
433 $z = (($baseZ & self::BLOCKHASH_XZ_MASK) | $extraZ) << self::BLOCKHASH_XZ_SIGN_SHIFT >> self::BLOCKHASH_XZ_SIGN_SHIFT;
434 }
435
441 public static function getXZ(int $hash, ?int &$x, ?int &$z) : void{
442 [$x, $z] = morton2d_decode($hash);
443 }
444
445 public static function getDifficultyFromString(string $str) : int{
446 switch(strtolower(trim($str))){
447 case "0":
448 case "peaceful":
449 case "p":
450 return World::DIFFICULTY_PEACEFUL;
451
452 case "1":
453 case "easy":
454 case "e":
455 return World::DIFFICULTY_EASY;
456
457 case "2":
458 case "normal":
459 case "n":
460 return World::DIFFICULTY_NORMAL;
461
462 case "3":
463 case "hard":
464 case "h":
465 return World::DIFFICULTY_HARD;
466 }
467
468 return -1;
469 }
470
474 public function __construct(
475 private Server $server,
476 string $name, //TODO: this should be folderName (named arguments BC break)
477 private WritableWorldProvider $provider,
478 private AsyncPool $workerPool
479 ){
480 $this->folderName = $name;
481 $this->worldId = self::$worldIdCounter++;
482
483 $this->displayName = $this->provider->getWorldData()->getName();
484 $this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName");
485
486 $this->minY = $this->provider->getWorldMinY();
487 $this->maxY = $this->provider->getWorldMaxY();
488
489 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_preparing($this->displayName)));
490 $generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
491 throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
492 $generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
493 $this->generator = $generator->getGeneratorClass();
494 $this->chunkPopulationRequestQueue = new \SplQueue();
495 $this->addOnUnloadCallback(function() : void{
496 $this->logger->debug("Cancelling unfulfilled generation requests");
497
498 foreach($this->chunkPopulationRequestMap as $chunkHash => $promise){
499 $promise->reject();
500 unset($this->chunkPopulationRequestMap[$chunkHash]);
501 }
502 if(count($this->chunkPopulationRequestMap) !== 0){
503 //TODO: this might actually get hit because generation rejection callbacks might try to schedule new
504 //requests, and we can't prevent that right now because there's no way to detect "unloading" state
505 throw new AssumptionFailedError("New generation requests scheduled during unload");
506 }
507 });
508
509 $this->scheduledBlockUpdateQueue = new ReversePriorityQueue();
510 $this->scheduledBlockUpdateQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH);
511
512 $this->neighbourBlockUpdateQueue = new \SplQueue();
513
514 $this->time = $this->provider->getWorldData()->getTime();
515
516 $cfg = $this->server->getConfigGroup();
517 $this->chunkTickRadius = min($this->server->getViewDistance(), max(0, $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_TICK_RADIUS, 4)));
518 if($cfg->getPropertyInt("chunk-ticking.per-tick", 40) <= 0){
519 //TODO: this needs l10n
520 $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.");
521 $this->chunkTickRadius = 0;
522 }
523 $this->tickedBlocksPerSubchunkPerTick = $cfg->getPropertyInt(YmlServerProperties::CHUNK_TICKING_BLOCKS_PER_SUBCHUNK_PER_TICK, self::DEFAULT_TICKED_BLOCKS_PER_SUBCHUNK_PER_TICK);
524 $this->maxConcurrentChunkPopulationTasks = $cfg->getPropertyInt(YmlServerProperties::CHUNK_GENERATION_POPULATION_QUEUE_SIZE, 2);
525
526 $this->initRandomTickBlocksFromConfig($cfg);
527
528 $this->timings = new WorldTimings($this);
529
530 $this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{
531 if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
532 $this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
533 unset($this->generatorRegisteredWorkers[$workerId]);
534 }
535 });
536 $workerPool = $this->workerPool;
537 $this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{
538 $workerPool->removeWorkerStartHook($workerStartHook);
539 });
540 }
541
542 private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
543 $dontTickBlocks = [];
544 $parser = StringToItemParser::getInstance();
545 foreach($cfg->getProperty(YmlServerProperties::CHUNK_TICKING_DISABLE_BLOCK_TICKING, []) as $name){
546 $name = (string) $name;
547 $item = $parser->parse($name);
548 if($item !== null){
549 $block = $item->getBlock();
550 }elseif(preg_match("/^-?\d+$/", $name) === 1){
551 //TODO: this is a really sketchy hack - remove this as soon as possible
552 try{
553 $blockStateData = GlobalBlockStateHandlers::getUpgrader()->upgradeIntIdMeta((int) $name, 0);
554 }catch(BlockStateDeserializeException){
555 continue;
556 }
557 $block = RuntimeBlockStateRegistry::getInstance()->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
558 }else{
559 //TODO: we probably ought to log an error here
560 continue;
561 }
562
563 if($block->getTypeId() !== BlockTypeIds::AIR){
564 $dontTickBlocks[$block->getTypeId()] = $name;
565 }
566 }
567
568 foreach(RuntimeBlockStateRegistry::getInstance()->getAllKnownStates() as $state){
569 $dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
570 if($dontTickName === null && $state->ticksRandomly()){
571 $this->randomTickBlocks[$state->getStateId()] = true;
572 }
573 }
574 }
575
576 public function getTickRateTime() : float{
577 return $this->tickRateTime;
578 }
579
580 public function registerGeneratorToWorker(int $worker) : void{
581 $this->logger->debug("Registering generator on worker $worker");
582 $this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker);
583 $this->generatorRegisteredWorkers[$worker] = true;
584 }
585
586 public function unregisterGenerator() : void{
587 foreach($this->workerPool->getRunningWorkers() as $i){
588 if(isset($this->generatorRegisteredWorkers[$i])){
589 $this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i);
590 }
591 }
592 $this->generatorRegisteredWorkers = [];
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 foreach($this->entitiesByChunk as $chunkHash => $entities){
636 self::getXZ($chunkHash, $chunkX, $chunkZ);
637
638 $leakedEntities = 0;
639 foreach($entities as $entity){
640 if(!$entity->isFlaggedForDespawn()){
641 $leakedEntities++;
642 }
643 $entity->close();
644 }
645 if($leakedEntities !== 0){
646 $this->logger->warning("$leakedEntities leaked entities found in ungenerated chunk $chunkX $chunkZ during unload, they won't be saved!");
647 }
648 }
649
650 $this->save();
651
652 $this->unregisterGenerator();
653
654 $this->provider->close();
655 $this->blockCache = [];
656 $this->blockCollisionBoxCache = [];
657
658 $this->unloaded = true;
659 }
660
662 public function addOnUnloadCallback(\Closure $callback) : void{
663 $this->unloadCallbacks[spl_object_id($callback)] = $callback;
664 }
665
667 public function removeOnUnloadCallback(\Closure $callback) : void{
668 unset($this->unloadCallbacks[spl_object_id($callback)]);
669 }
670
680 private function filterViewersForPosition(Vector3 $pos, array $allowed) : array{
681 $candidates = $this->getViewersForPosition($pos);
682 $filtered = [];
683 foreach($allowed as $player){
684 $k = spl_object_id($player);
685 if(isset($candidates[$k])){
686 $filtered[$k] = $candidates[$k];
687 }
688 }
689
690 return $filtered;
691 }
692
696 public function addSound(Vector3 $pos, Sound $sound, ?array $players = null) : void{
697 $players ??= $this->getViewersForPosition($pos);
698
699 if(WorldSoundEvent::hasHandlers()){
700 $ev = new WorldSoundEvent($this, $sound, $pos, $players);
701 $ev->call();
702 if($ev->isCancelled()){
703 return;
704 }
705
706 $sound = $ev->getSound();
707 $players = $ev->getRecipients();
708 }
709
710 $pk = $sound->encode($pos);
711 if(count($pk) > 0){
712 if($players === $this->getViewersForPosition($pos)){
713 foreach($pk as $e){
714 $this->broadcastPacketToViewers($pos, $e);
715 }
716 }else{
717 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
718 }
719 }
720 }
721
725 public function addParticle(Vector3 $pos, Particle $particle, ?array $players = null) : void{
726 $players ??= $this->getViewersForPosition($pos);
727
728 if(WorldParticleEvent::hasHandlers()){
729 $ev = new WorldParticleEvent($this, $particle, $pos, $players);
730 $ev->call();
731 if($ev->isCancelled()){
732 return;
733 }
734
735 $particle = $ev->getParticle();
736 $players = $ev->getRecipients();
737 }
738
739 $pk = $particle->encode($pos);
740 if(count($pk) > 0){
741 if($players === $this->getViewersForPosition($pos)){
742 foreach($pk as $e){
743 $this->broadcastPacketToViewers($pos, $e);
744 }
745 }else{
746 NetworkBroadcastUtils::broadcastPackets($this->filterViewersForPosition($pos, $players), $pk);
747 }
748 }
749 }
750
751 public function getAutoSave() : bool{
752 return $this->autoSave;
753 }
754
755 public function setAutoSave(bool $value) : void{
756 $this->autoSave = $value;
757 }
758
768 public function getChunkPlayers(int $chunkX, int $chunkZ) : array{
769 return $this->playerChunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
770 }
771
778 public function getChunkLoaders(int $chunkX, int $chunkZ) : array{
779 return $this->chunkLoaders[World::chunkHash($chunkX, $chunkZ)] ?? [];
780 }
781
788 public function getViewersForPosition(Vector3 $pos) : array{
789 return $this->getChunkPlayers($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
790 }
791
795 public function broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet) : void{
796 $this->broadcastPacketToPlayersUsingChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE, $packet);
797 }
798
799 private function broadcastPacketToPlayersUsingChunk(int $chunkX, int $chunkZ, ClientboundPacket $packet) : void{
800 if(!isset($this->packetBuffersByChunk[$index = World::chunkHash($chunkX, $chunkZ)])){
801 $this->packetBuffersByChunk[$index] = [$packet];
802 }else{
803 $this->packetBuffersByChunk[$index][] = $packet;
804 }
805 }
806
807 public function registerChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ, bool $autoLoad = true) : void{
808 $loaderId = spl_object_id($loader);
809
810 if(!isset($this->chunkLoaders[$chunkHash = World::chunkHash($chunkX, $chunkZ)])){
811 $this->chunkLoaders[$chunkHash] = [];
812 }elseif(isset($this->chunkLoaders[$chunkHash][$loaderId])){
813 return;
814 }
815
816 $this->chunkLoaders[$chunkHash][$loaderId] = $loader;
817
818 $this->cancelUnloadChunkRequest($chunkX, $chunkZ);
819
820 if($autoLoad){
821 $this->loadChunk($chunkX, $chunkZ);
822 }
823 }
824
825 public function unregisterChunkLoader(ChunkLoader $loader, int $chunkX, int $chunkZ) : void{
826 $chunkHash = World::chunkHash($chunkX, $chunkZ);
827 $loaderId = spl_object_id($loader);
828 if(isset($this->chunkLoaders[$chunkHash][$loaderId])){
829 if(count($this->chunkLoaders[$chunkHash]) === 1){
830 unset($this->chunkLoaders[$chunkHash]);
831 $this->unloadChunkRequest($chunkX, $chunkZ, true);
832 if(isset($this->chunkPopulationRequestMap[$chunkHash]) && !isset($this->activeChunkPopulationTasks[$chunkHash])){
833 $this->chunkPopulationRequestMap[$chunkHash]->reject();
834 unset($this->chunkPopulationRequestMap[$chunkHash]);
835 }
836 }else{
837 unset($this->chunkLoaders[$chunkHash][$loaderId]);
838 }
839 }
840 }
841
845 public function registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
846 $hash = World::chunkHash($chunkX, $chunkZ);
847 if(isset($this->chunkListeners[$hash])){
848 $this->chunkListeners[$hash][spl_object_id($listener)] = $listener;
849 }else{
850 $this->chunkListeners[$hash] = [spl_object_id($listener) => $listener];
851 }
852 if($listener instanceof Player){
853 $this->playerChunkListeners[$hash][spl_object_id($listener)] = $listener;
854 }
855 }
856
862 public function unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ) : void{
863 $hash = World::chunkHash($chunkX, $chunkZ);
864 if(isset($this->chunkListeners[$hash])){
865 if(count($this->chunkListeners[$hash]) === 1){
866 unset($this->chunkListeners[$hash]);
867 unset($this->playerChunkListeners[$hash]);
868 }else{
869 unset($this->chunkListeners[$hash][spl_object_id($listener)]);
870 unset($this->playerChunkListeners[$hash][spl_object_id($listener)]);
871 }
872 }
873 }
874
878 public function unregisterChunkListenerFromAll(ChunkListener $listener) : void{
879 foreach($this->chunkListeners as $hash => $listeners){
880 World::getXZ($hash, $chunkX, $chunkZ);
881 $this->unregisterChunkListener($listener, $chunkX, $chunkZ);
882 }
883 }
884
891 public function getChunkListeners(int $chunkX, int $chunkZ) : array{
892 return $this->chunkListeners[World::chunkHash($chunkX, $chunkZ)] ?? [];
893 }
894
898 public function sendTime(Player ...$targets) : void{
899 if(count($targets) === 0){
900 $targets = $this->players;
901 }
902 foreach($targets as $player){
903 $player->getNetworkSession()->syncWorldTime($this->time);
904 }
905 }
906
907 public function isDoingTick() : bool{
908 return $this->doingTick;
909 }
910
914 public function doTick(int $currentTick) : void{
915 if($this->unloaded){
916 throw new \LogicException("Attempted to tick a world which has been closed");
917 }
918
919 $this->timings->doTick->startTiming();
920 $this->doingTick = true;
921 try{
922 $this->actuallyDoTick($currentTick);
923 }finally{
924 $this->doingTick = false;
925 $this->timings->doTick->stopTiming();
926 }
927 }
928
929 protected function actuallyDoTick(int $currentTick) : void{
930 if(!$this->stopTime){
931 //this simulates an overflow, as would happen in any language which doesn't do stupid things to var types
932 if($this->time === PHP_INT_MAX){
933 $this->time = PHP_INT_MIN;
934 }else{
935 $this->time++;
936 }
937 }
938
939 $this->sunAnglePercentage = $this->computeSunAnglePercentage(); //Sun angle depends on the current time
940 $this->skyLightReduction = $this->computeSkyLightReduction(); //Sky light reduction depends on the sun angle
941
942 if(++$this->sendTimeTicker === 200){
943 $this->sendTime();
944 $this->sendTimeTicker = 0;
945 }
946
947 $this->unloadChunks();
948 if(++$this->providerGarbageCollectionTicker >= 6000){
949 $this->provider->doGarbageCollection();
950 $this->providerGarbageCollectionTicker = 0;
951 }
952
953 $this->timings->scheduledBlockUpdates->startTiming();
954 //Delayed updates
955 while($this->scheduledBlockUpdateQueue->count() > 0 && $this->scheduledBlockUpdateQueue->current()["priority"] <= $currentTick){
957 $vec = $this->scheduledBlockUpdateQueue->extract()["data"];
958 unset($this->scheduledBlockUpdateQueueIndex[World::blockHash($vec->x, $vec->y, $vec->z)]);
959 if(!$this->isInLoadedTerrain($vec)){
960 continue;
961 }
962 $block = $this->getBlock($vec);
963 $block->onScheduledUpdate();
964 }
965 $this->timings->scheduledBlockUpdates->stopTiming();
966
967 $this->timings->neighbourBlockUpdates->startTiming();
968 //Normal updates
969 while($this->neighbourBlockUpdateQueue->count() > 0){
970 $index = $this->neighbourBlockUpdateQueue->dequeue();
971 unset($this->neighbourBlockUpdateQueueIndex[$index]);
972 World::getBlockXYZ($index, $x, $y, $z);
973 if(!$this->isChunkLoaded($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)){
974 continue;
975 }
976
977 $block = $this->getBlockAt($x, $y, $z);
978
979 if(BlockUpdateEvent::hasHandlers()){
980 $ev = new BlockUpdateEvent($block);
981 $ev->call();
982 if($ev->isCancelled()){
983 continue;
984 }
985 }
986 foreach($this->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z)) as $entity){
987 $entity->onNearbyBlockChange();
988 }
989 $block->onNearbyBlockChange();
990 }
991
992 $this->timings->neighbourBlockUpdates->stopTiming();
993
994 $this->timings->entityTick->startTiming();
995 //Update entities that need update
996 foreach($this->updateEntities as $id => $entity){
997 if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
998 unset($this->updateEntities[$id]);
999 }
1000 if($entity->isFlaggedForDespawn()){
1001 $entity->close();
1002 }
1003 }
1004 $this->timings->entityTick->stopTiming();
1005
1006 $this->timings->randomChunkUpdates->startTiming();
1007 $this->tickChunks();
1008 $this->timings->randomChunkUpdates->stopTiming();
1009
1010 $this->executeQueuedLightUpdates();
1011
1012 if(count($this->changedBlocks) > 0){
1013 if(count($this->players) > 0){
1014 foreach($this->changedBlocks as $index => $blocks){
1015 if(count($blocks) === 0){ //blocks can be set normally and then later re-set with direct send
1016 continue;
1017 }
1018 World::getXZ($index, $chunkX, $chunkZ);
1019 if(!$this->isChunkLoaded($chunkX, $chunkZ)){
1020 //a previous chunk may have caused this one to be unloaded by a ChunkListener
1021 continue;
1022 }
1023 if(count($blocks) > 512){
1024 $chunk = $this->getChunk($chunkX, $chunkZ) ?? throw new AssumptionFailedError("We already checked that the chunk is loaded");
1025 foreach($this->getChunkPlayers($chunkX, $chunkZ) as $p){
1026 $p->onChunkChanged($chunkX, $chunkZ, $chunk);
1027 }
1028 }else{
1029 foreach($this->createBlockUpdatePackets($blocks) as $packet){
1030 $this->broadcastPacketToPlayersUsingChunk($chunkX, $chunkZ, $packet);
1031 }
1032 }
1033 }
1034 }
1035
1036 $this->changedBlocks = [];
1037
1038 }
1039
1040 if($this->sleepTicks > 0 && --$this->sleepTicks <= 0){
1041 $this->checkSleep();
1042 }
1043
1044 foreach($this->packetBuffersByChunk as $index => $entries){
1045 World::getXZ($index, $chunkX, $chunkZ);
1046 $chunkPlayers = $this->getChunkPlayers($chunkX, $chunkZ);
1047 if(count($chunkPlayers) > 0){
1048 NetworkBroadcastUtils::broadcastPackets($chunkPlayers, $entries);
1049 }
1050 }
1051
1052 $this->packetBuffersByChunk = [];
1053 }
1054
1055 public function checkSleep() : void{
1056 if(count($this->players) === 0){
1057 return;
1058 }
1059
1060 $resetTime = true;
1061 foreach($this->getPlayers() as $p){
1062 if(!$p->isSleeping()){
1063 $resetTime = false;
1064 break;
1065 }
1066 }
1067
1068 if($resetTime){
1069 $time = $this->getTimeOfDay();
1070
1071 if($time >= World::TIME_NIGHT && $time < World::TIME_SUNRISE){
1072 $this->setTime($this->getTime() + World::TIME_FULL - $time);
1073
1074 foreach($this->getPlayers() as $p){
1075 $p->stopSleep();
1076 }
1077 }
1078 }
1079 }
1080
1081 public function setSleepTicks(int $ticks) : void{
1082 $this->sleepTicks = $ticks;
1083 }
1084
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->blockCollisionBoxCache = [];
1142 }else{
1143 $count = 0;
1144 foreach($this->blockCache as $list){
1145 $count += count($list);
1146 if($count > 2048){
1147 $this->blockCache = [];
1148 break;
1149 }
1150 }
1151
1152 $count = 0;
1153 foreach($this->blockCollisionBoxCache as $list){
1154 $count += count($list);
1155 if($count > 2048){
1156 //TODO: Is this really the best logic?
1157 $this->blockCollisionBoxCache = [];
1158 break;
1159 }
1160 }
1161 }
1162 }
1163
1168 public function getRandomTickedBlocks() : array{
1169 return $this->randomTickBlocks;
1170 }
1171
1172 public function addRandomTickedBlock(Block $block) : void{
1173 if($block instanceof UnknownBlock){
1174 throw new \InvalidArgumentException("Cannot do random-tick on unknown block");
1175 }
1176 $this->randomTickBlocks[$block->getStateId()] = true;
1177 }
1178
1179 public function removeRandomTickedBlock(Block $block) : void{
1180 unset($this->randomTickBlocks[$block->getStateId()]);
1181 }
1182
1187 public function getChunkTickRadius() : int{
1188 return $this->chunkTickRadius;
1189 }
1190
1195 public function setChunkTickRadius(int $radius) : void{
1196 $this->chunkTickRadius = $radius;
1197 }
1198
1206 public function getTickingChunks() : array{
1207 return array_keys($this->validTickingChunks);
1208 }
1209
1214 public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1215 $chunkPosHash = World::chunkHash($chunkX, $chunkZ);
1216 $this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
1217 $this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
1218 }
1219
1224 public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
1225 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1226 $tickerId = spl_object_id($ticker);
1227 if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
1228 if(count($this->registeredTickingChunks[$chunkHash]) === 1){
1229 unset(
1230 $this->registeredTickingChunks[$chunkHash],
1231 $this->recheckTickingChunks[$chunkHash],
1232 $this->validTickingChunks[$chunkHash]
1233 );
1234 }else{
1235 unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
1236 }
1237 }
1238 }
1239
1240 private function tickChunks() : void{
1241 if($this->chunkTickRadius <= 0 || count($this->registeredTickingChunks) === 0){
1242 return;
1243 }
1244
1245 if(count($this->recheckTickingChunks) > 0){
1246 $this->timings->randomChunkUpdatesChunkSelection->startTiming();
1247
1248 $chunkTickableCache = [];
1249
1250 foreach($this->recheckTickingChunks as $hash => $_){
1251 World::getXZ($hash, $chunkX, $chunkZ);
1252 if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
1253 $this->validTickingChunks[$hash] = $hash;
1254 }
1255 }
1256 $this->recheckTickingChunks = [];
1257
1258 $this->timings->randomChunkUpdatesChunkSelection->stopTiming();
1259 }
1260
1261 foreach($this->validTickingChunks as $index => $_){
1262 World::getXZ($index, $chunkX, $chunkZ);
1263
1264 $this->tickChunk($chunkX, $chunkZ);
1265 }
1266 }
1267
1274 private function isChunkTickable(int $chunkX, int $chunkZ, array &$cache) : bool{
1275 for($cx = -1; $cx <= 1; ++$cx){
1276 for($cz = -1; $cz <= 1; ++$cz){
1277 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1278 if(isset($cache[$chunkHash])){
1279 if(!$cache[$chunkHash]){
1280 return false;
1281 }
1282 continue;
1283 }
1284 if($this->isChunkLocked($chunkX + $cx, $chunkZ + $cz)){
1285 $cache[$chunkHash] = false;
1286 return false;
1287 }
1288 $adjacentChunk = $this->getChunk($chunkX + $cx, $chunkZ + $cz);
1289 if($adjacentChunk === null || !$adjacentChunk->isPopulated()){
1290 $cache[$chunkHash] = false;
1291 return false;
1292 }
1293 $lightPopulatedState = $adjacentChunk->isLightPopulated();
1294 if($lightPopulatedState !== true){
1295 if($lightPopulatedState === false){
1296 $this->orderLightPopulation($chunkX + $cx, $chunkZ + $cz);
1297 }
1298 $cache[$chunkHash] = false;
1299 return false;
1300 }
1301
1302 $cache[$chunkHash] = true;
1303 }
1304 }
1305
1306 return true;
1307 }
1308
1318 private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{
1319 for($cx = -1; $cx <= 1; ++$cx){
1320 for($cz = -1; $cz <= 1; ++$cz){
1321 $chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
1322 unset($this->validTickingChunks[$chunkHash]);
1323 if(isset($this->registeredTickingChunks[$chunkHash])){
1324 $this->recheckTickingChunks[$chunkHash] = $chunkHash;
1325 }else{
1326 unset($this->recheckTickingChunks[$chunkHash]);
1327 }
1328 }
1329 }
1330 }
1331
1332 private function orderLightPopulation(int $chunkX, int $chunkZ) : void{
1333 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1334 $lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
1335 if($lightPopulatedState === false){
1336 $this->chunks[$chunkHash]->setLightPopulated(null);
1337 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1338
1339 $this->workerPool->submitTask(new LightPopulationTask(
1340 $this->chunks[$chunkHash],
1341 function(array $blockLight, array $skyLight, array $heightMap) use ($chunkX, $chunkZ) : void{
1348 if($this->unloaded || ($chunk = $this->getChunk($chunkX, $chunkZ)) === null || $chunk->isLightPopulated() === true){
1349 return;
1350 }
1351 //TODO: calculated light information might not be valid if the terrain changed during light calculation
1352
1353 $chunk->setHeightMapArray($heightMap);
1354 foreach($blockLight as $y => $lightArray){
1355 $chunk->getSubChunk($y)->setBlockLightArray($lightArray);
1356 }
1357 foreach($skyLight as $y => $lightArray){
1358 $chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
1359 }
1360 $chunk->setLightPopulated(true);
1361 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
1362 }
1363 ));
1364 }
1365 }
1366
1367 private function tickChunk(int $chunkX, int $chunkZ) : void{
1368 $chunk = $this->getChunk($chunkX, $chunkZ);
1369 if($chunk === null){
1370 //the chunk may have been unloaded during a previous chunk's update (e.g. during BlockGrowEvent)
1371 return;
1372 }
1373 foreach($this->getChunkEntities($chunkX, $chunkZ) as $entity){
1374 $entity->onRandomUpdate();
1375 }
1376
1377 $blockFactory = RuntimeBlockStateRegistry::getInstance();
1378 foreach($chunk->getSubChunks() as $Y => $subChunk){
1379 if(!$subChunk->isEmptyFast()){
1380 $k = 0;
1381 for($i = 0; $i < $this->tickedBlocksPerSubchunkPerTick; ++$i){
1382 if(($i % 5) === 0){
1383 //60 bits will be used by 5 blocks (12 bits each)
1384 $k = mt_rand(0, (1 << 60) - 1);
1385 }
1386 $x = $k & SubChunk::COORD_MASK;
1387 $y = ($k >> SubChunk::COORD_BIT_SIZE) & SubChunk::COORD_MASK;
1388 $z = ($k >> (SubChunk::COORD_BIT_SIZE * 2)) & SubChunk::COORD_MASK;
1389 $k >>= (SubChunk::COORD_BIT_SIZE * 3);
1390
1391 $state = $subChunk->getBlockStateId($x, $y, $z);
1392
1393 if(isset($this->randomTickBlocks[$state])){
1394 $block = $blockFactory->fromStateId($state);
1395 $block->position($this, $chunkX * Chunk::EDGE_LENGTH + $x, ($Y << SubChunk::COORD_BIT_SIZE) + $y, $chunkZ * Chunk::EDGE_LENGTH + $z);
1396 $block->onRandomTick();
1397 }
1398 }
1399 }
1400 }
1401 }
1402
1406 public function __debugInfo() : array{
1407 return [];
1408 }
1409
1410 public function save(bool $force = false) : bool{
1411
1412 if(!$this->getAutoSave() && !$force){
1413 return false;
1414 }
1415
1416 (new WorldSaveEvent($this))->call();
1417
1418 $timings = $this->timings->syncDataSave;
1419 $timings->startTiming();
1420
1421 $this->provider->getWorldData()->setTime($this->time);
1422 $this->saveChunks();
1423 $this->provider->getWorldData()->save();
1424
1425 $timings->stopTiming();
1426
1427 return true;
1428 }
1429
1430 public function saveChunks() : void{
1431 $this->timings->syncChunkSave->startTiming();
1432 try{
1433 foreach($this->chunks as $chunkHash => $chunk){
1434 self::getXZ($chunkHash, $chunkX, $chunkZ);
1435 $this->provider->saveChunk($chunkX, $chunkZ, new ChunkData(
1436 $chunk->getSubChunks(),
1437 $chunk->isPopulated(),
1438 array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($chunkX, $chunkZ), fn(Entity $e) => $e->canSaveWithChunk())),
1439 array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()),
1440 ), $chunk->getTerrainDirtyFlags());
1441 $chunk->clearTerrainDirtyFlags();
1442 }
1443 }finally{
1444 $this->timings->syncChunkSave->stopTiming();
1445 }
1446 }
1447
1452 public function scheduleDelayedBlockUpdate(Vector3 $pos, int $delay) : void{
1453 if(
1454 !$this->isInWorld($pos->x, $pos->y, $pos->z) ||
1455 (isset($this->scheduledBlockUpdateQueueIndex[$index = World::blockHash($pos->x, $pos->y, $pos->z)]) && $this->scheduledBlockUpdateQueueIndex[$index] <= $delay)
1456 ){
1457 return;
1458 }
1459 $this->scheduledBlockUpdateQueueIndex[$index] = $delay;
1460 $this->scheduledBlockUpdateQueue->insert(new Vector3((int) $pos->x, (int) $pos->y, (int) $pos->z), $delay + $this->server->getTick());
1461 }
1462
1463 private function tryAddToNeighbourUpdateQueue(int $x, int $y, int $z) : void{
1464 if($this->isInWorld($x, $y, $z)){
1465 $hash = World::blockHash($x, $y, $z);
1466 if(!isset($this->neighbourBlockUpdateQueueIndex[$hash])){
1467 $this->neighbourBlockUpdateQueue->enqueue($hash);
1468 $this->neighbourBlockUpdateQueueIndex[$hash] = true;
1469 }
1470 }
1471 }
1472
1479 private function internalNotifyNeighbourBlockUpdate(int $x, int $y, int $z) : void{
1480 $this->tryAddToNeighbourUpdateQueue($x, $y, $z);
1481 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1482 $this->tryAddToNeighbourUpdateQueue($x + $dx, $y + $dy, $z + $dz);
1483 }
1484 }
1485
1493 public function notifyNeighbourBlockUpdate(Vector3 $pos) : void{
1494 $this->internalNotifyNeighbourBlockUpdate($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ());
1495 }
1496
1501 public function getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst = false) : array{
1502 $minX = (int) floor($bb->minX - 1);
1503 $minY = (int) floor($bb->minY - 1);
1504 $minZ = (int) floor($bb->minZ - 1);
1505 $maxX = (int) floor($bb->maxX + 1);
1506 $maxY = (int) floor($bb->maxY + 1);
1507 $maxZ = (int) floor($bb->maxZ + 1);
1508
1509 $collides = [];
1510
1511 if($targetFirst){
1512 for($z = $minZ; $z <= $maxZ; ++$z){
1513 for($x = $minX; $x <= $maxX; ++$x){
1514 for($y = $minY; $y <= $maxY; ++$y){
1515 $block = $this->getBlockAt($x, $y, $z);
1516 if($block->collidesWithBB($bb)){
1517 return [$block];
1518 }
1519 }
1520 }
1521 }
1522 }else{
1523 for($z = $minZ; $z <= $maxZ; ++$z){
1524 for($x = $minX; $x <= $maxX; ++$x){
1525 for($y = $minY; $y <= $maxY; ++$y){
1526 $block = $this->getBlockAt($x, $y, $z);
1527 if($block->collidesWithBB($bb)){
1528 $collides[] = $block;
1529 }
1530 }
1531 }
1532 }
1533 }
1534
1535 return $collides;
1536 }
1537
1545 private function getBlockCollisionBoxesForCell(int $x, int $y, int $z) : array{
1546 $block = $this->getBlockAt($x, $y, $z);
1547 $boxes = $block->getCollisionBoxes();
1548
1549 $cellBB = AxisAlignedBB::one()->offset($x, $y, $z);
1550 foreach(Facing::OFFSET as [$dx, $dy, $dz]){
1551 $extraBoxes = $this->getBlockAt($x + $dx, $y + $dy, $z + $dz)->getCollisionBoxes();
1552 foreach($extraBoxes as $extraBox){
1553 if($extraBox->intersectsWith($cellBB)){
1554 $boxes[] = $extraBox;
1555 }
1556 }
1557 }
1558
1559 return $boxes;
1560 }
1561
1566 public function getBlockCollisionBoxes(AxisAlignedBB $bb) : array{
1567 $minX = (int) floor($bb->minX);
1568 $minY = (int) floor($bb->minY);
1569 $minZ = (int) floor($bb->minZ);
1570 $maxX = (int) floor($bb->maxX);
1571 $maxY = (int) floor($bb->maxY);
1572 $maxZ = (int) floor($bb->maxZ);
1573
1574 $collides = [];
1575
1576 for($z = $minZ; $z <= $maxZ; ++$z){
1577 for($x = $minX; $x <= $maxX; ++$x){
1578 $chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1579 for($y = $minY; $y <= $maxY; ++$y){
1580 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1581
1582 $boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z);
1583
1584 foreach($boxes as $blockBB){
1585 if($blockBB->intersectsWith($bb)){
1586 $collides[] = $blockBB;
1587 }
1588 }
1589 }
1590 }
1591 }
1592
1593 return $collides;
1594 }
1595
1603 public function getCollisionBoxes(Entity $entity, AxisAlignedBB $bb, bool $entities = true) : array{
1604 $collides = $this->getBlockCollisionBoxes($bb);
1605
1606 if($entities){
1607 foreach($this->getCollidingEntities($bb->expandedCopy(0.25, 0.25, 0.25), $entity) as $ent){
1608 $collides[] = clone $ent->boundingBox;
1609 }
1610 }
1611
1612 return $collides;
1613 }
1614
1619 public function computeSunAnglePercentage() : float{
1620 $timeProgress = ($this->time % self::TIME_FULL) / self::TIME_FULL;
1621
1622 //0.0 needs to be high noon, not dusk
1623 $sunProgress = $timeProgress + ($timeProgress < 0.25 ? 0.75 : -0.25);
1624
1625 //Offset the sun progress to be above the horizon longer at dusk and dawn
1626 //this is roughly an inverted sine curve, which pushes the sun progress back at dusk and forwards at dawn
1627 $diff = (((1 - ((cos($sunProgress * M_PI) + 1) / 2)) - $sunProgress) / 3);
1628
1629 return $sunProgress + $diff;
1630 }
1631
1635 public function getSunAnglePercentage() : float{
1636 return $this->sunAnglePercentage;
1637 }
1638
1642 public function getSunAngleRadians() : float{
1643 return $this->sunAnglePercentage * 2 * M_PI;
1644 }
1645
1649 public function getSunAngleDegrees() : float{
1650 return $this->sunAnglePercentage * 360.0;
1651 }
1652
1657 public function computeSkyLightReduction() : int{
1658 $percentage = max(0, min(1, -(cos($this->getSunAngleRadians()) * 2 - 0.5)));
1659
1660 //TODO: check rain and thunder level
1661
1662 return (int) ($percentage * 11);
1663 }
1664
1668 public function getSkyLightReduction() : int{
1669 return $this->skyLightReduction;
1670 }
1671
1676 public function getFullLight(Vector3 $pos) : int{
1677 $floorX = $pos->getFloorX();
1678 $floorY = $pos->getFloorY();
1679 $floorZ = $pos->getFloorZ();
1680 return $this->getFullLightAt($floorX, $floorY, $floorZ);
1681 }
1682
1687 public function getFullLightAt(int $x, int $y, int $z) : int{
1688 $skyLight = $this->getRealBlockSkyLightAt($x, $y, $z);
1689 if($skyLight < 15){
1690 return max($skyLight, $this->getBlockLightAt($x, $y, $z));
1691 }else{
1692 return $skyLight;
1693 }
1694 }
1695
1700 public function getHighestAdjacentFullLightAt(int $x, int $y, int $z) : int{
1701 return $this->getHighestAdjacentLight($x, $y, $z, $this->getFullLightAt(...));
1702 }
1703
1708 public function getPotentialLight(Vector3 $pos) : int{
1709 $floorX = $pos->getFloorX();
1710 $floorY = $pos->getFloorY();
1711 $floorZ = $pos->getFloorZ();
1712 return $this->getPotentialLightAt($floorX, $floorY, $floorZ);
1713 }
1714
1719 public function getPotentialLightAt(int $x, int $y, int $z) : int{
1720 return max($this->getPotentialBlockSkyLightAt($x, $y, $z), $this->getBlockLightAt($x, $y, $z));
1721 }
1722
1727 public function getHighestAdjacentPotentialLightAt(int $x, int $y, int $z) : int{
1728 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialLightAt(...));
1729 }
1730
1737 public function getPotentialBlockSkyLightAt(int $x, int $y, int $z) : int{
1738 if(!$this->isInWorld($x, $y, $z)){
1739 return $y >= self::Y_MAX ? 15 : 0;
1740 }
1741 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1742 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockSkyLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1743 }
1744 return 0; //TODO: this should probably throw instead (light not calculated yet)
1745 }
1746
1752 public function getRealBlockSkyLightAt(int $x, int $y, int $z) : int{
1753 $light = $this->getPotentialBlockSkyLightAt($x, $y, $z) - $this->skyLightReduction;
1754 return $light < 0 ? 0 : $light;
1755 }
1756
1762 public function getBlockLightAt(int $x, int $y, int $z) : int{
1763 if(!$this->isInWorld($x, $y, $z)){
1764 return 0;
1765 }
1766 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null && $chunk->isLightPopulated() === true){
1767 return $chunk->getSubChunk($y >> Chunk::COORD_BIT_SIZE)->getBlockLightArray()->get($x & SubChunk::COORD_MASK, $y & SubChunk::COORD_MASK, $z & SubChunk::COORD_MASK);
1768 }
1769 return 0; //TODO: this should probably throw instead (light not calculated yet)
1770 }
1771
1772 public function updateAllLight(int $x, int $y, int $z) : void{
1773 if(($chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) === null || $chunk->isLightPopulated() !== true){
1774 return;
1775 }
1776
1777 $blockFactory = RuntimeBlockStateRegistry::getInstance();
1778 $this->timings->doBlockSkyLightUpdates->startTiming();
1779 if($this->skyLightUpdate === null){
1780 $this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
1781 }
1782 $this->skyLightUpdate->recalculateNode($x, $y, $z);
1783 $this->timings->doBlockSkyLightUpdates->stopTiming();
1784
1785 $this->timings->doBlockLightUpdates->startTiming();
1786 if($this->blockLightUpdate === null){
1787 $this->blockLightUpdate = new BlockLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->light);
1788 }
1789 $this->blockLightUpdate->recalculateNode($x, $y, $z);
1790 $this->timings->doBlockLightUpdates->stopTiming();
1791 }
1792
1796 private function getHighestAdjacentLight(int $x, int $y, int $z, \Closure $lightGetter) : int{
1797 $max = 0;
1798 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1799 $x1 = $x + $offsetX;
1800 $y1 = $y + $offsetY;
1801 $z1 = $z + $offsetZ;
1802 if(
1803 !$this->isInWorld($x1, $y1, $z1) ||
1804 ($chunk = $this->getChunk($x1 >> Chunk::COORD_BIT_SIZE, $z1 >> Chunk::COORD_BIT_SIZE)) === null ||
1805 $chunk->isLightPopulated() !== true
1806 ){
1807 continue;
1808 }
1809 $max = max($max, $lightGetter($x1, $y1, $z1));
1810 }
1811 return $max;
1812 }
1813
1817 public function getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z) : int{
1818 return $this->getHighestAdjacentLight($x, $y, $z, $this->getPotentialBlockSkyLightAt(...));
1819 }
1820
1825 public function getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z) : int{
1826 return $this->getHighestAdjacentPotentialBlockSkyLight($x, $y, $z) - $this->skyLightReduction;
1827 }
1828
1832 public function getHighestAdjacentBlockLight(int $x, int $y, int $z) : int{
1833 return $this->getHighestAdjacentLight($x, $y, $z, $this->getBlockLightAt(...));
1834 }
1835
1836 private function executeQueuedLightUpdates() : void{
1837 if($this->blockLightUpdate !== null){
1838 $this->timings->doBlockLightUpdates->startTiming();
1839 $this->blockLightUpdate->execute();
1840 $this->blockLightUpdate = null;
1841 $this->timings->doBlockLightUpdates->stopTiming();
1842 }
1843
1844 if($this->skyLightUpdate !== null){
1845 $this->timings->doBlockSkyLightUpdates->startTiming();
1846 $this->skyLightUpdate->execute();
1847 $this->skyLightUpdate = null;
1848 $this->timings->doBlockSkyLightUpdates->stopTiming();
1849 }
1850 }
1851
1852 public function isInWorld(int $x, int $y, int $z) : bool{
1853 return (
1854 $x <= Limits::INT32_MAX && $x >= Limits::INT32_MIN &&
1855 $y < $this->maxY && $y >= $this->minY &&
1856 $z <= Limits::INT32_MAX && $z >= Limits::INT32_MIN
1857 );
1858 }
1859
1870 public function getBlock(Vector3 $pos, bool $cached = true, bool $addToCache = true) : Block{
1871 return $this->getBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $cached, $addToCache);
1872 }
1873
1883 public function getBlockAt(int $x, int $y, int $z, bool $cached = true, bool $addToCache = true) : Block{
1884 $relativeBlockHash = null;
1885 $chunkHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
1886
1887 if($this->isInWorld($x, $y, $z)){
1888 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1889
1890 if($cached && isset($this->blockCache[$chunkHash][$relativeBlockHash])){
1891 return $this->blockCache[$chunkHash][$relativeBlockHash];
1892 }
1893
1894 $chunk = $this->chunks[$chunkHash] ?? null;
1895 if($chunk !== null){
1896 $block = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
1897 }else{
1898 $addToCache = false;
1899 $block = VanillaBlocks::AIR();
1900 }
1901 }else{
1902 $block = VanillaBlocks::AIR();
1903 }
1904
1905 $block->position($this, $x, $y, $z);
1906
1907 if($this->inDynamicStateRecalculation){
1908 //this call was generated by a parent getBlock() call calculating dynamic stateinfo
1909 //don't calculate dynamic state and don't add to block cache (since it won't have dynamic state calculated).
1910 //this ensures that it's impossible for dynamic state properties to recursively depend on each other.
1911 $addToCache = false;
1912 }else{
1913 $this->inDynamicStateRecalculation = true;
1914 $replacement = $block->readStateFromWorld();
1915 if($replacement !== $block){
1916 $replacement->position($this, $x, $y, $z);
1917 $block = $replacement;
1918 }
1919 $this->inDynamicStateRecalculation = false;
1920 }
1921
1922 if($addToCache && $relativeBlockHash !== null){
1923 $this->blockCache[$chunkHash][$relativeBlockHash] = $block;
1924 }
1925
1926 return $block;
1927 }
1928
1934 public function setBlock(Vector3 $pos, Block $block, bool $update = true) : void{
1935 $this->setBlockAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z), $block, $update);
1936 }
1937
1946 public function setBlockAt(int $x, int $y, int $z, Block $block, bool $update = true) : void{
1947 if(!$this->isInWorld($x, $y, $z)){
1948 throw new \InvalidArgumentException("Pos x=$x,y=$y,z=$z is outside of the world bounds");
1949 }
1950 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
1951 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
1952 if($this->loadChunk($chunkX, $chunkZ) === null){ //current expected behaviour is to try to load the terrain synchronously
1953 throw new WorldException("Cannot set a block in un-generated terrain");
1954 }
1955
1956 $this->timings->setBlock->startTiming();
1957
1958 $this->unlockChunk($chunkX, $chunkZ, null);
1959
1960 $block = clone $block;
1961
1962 $block->position($this, $x, $y, $z);
1963 $block->writeStateToWorld();
1964 $pos = new Vector3($x, $y, $z);
1965
1966 $chunkHash = World::chunkHash($chunkX, $chunkZ);
1967 $relativeBlockHash = World::chunkBlockHash($x, $y, $z);
1968
1969 unset($this->blockCache[$chunkHash][$relativeBlockHash]);
1970 unset($this->blockCollisionBoxCache[$chunkHash][$relativeBlockHash]);
1971 //blocks like fences have collision boxes that reach into neighbouring blocks, so we need to invalidate the
1972 //caches for those blocks as well
1973 foreach(Facing::OFFSET as [$offsetX, $offsetY, $offsetZ]){
1974 $sideChunkPosHash = World::chunkHash(($x + $offsetX) >> Chunk::COORD_BIT_SIZE, ($z + $offsetZ) >> Chunk::COORD_BIT_SIZE);
1975 $sideChunkBlockHash = World::chunkBlockHash($x + $offsetX, $y + $offsetY, $z + $offsetZ);
1976 unset($this->blockCollisionBoxCache[$sideChunkPosHash][$sideChunkBlockHash]);
1977 }
1978
1979 if(!isset($this->changedBlocks[$chunkHash])){
1980 $this->changedBlocks[$chunkHash] = [];
1981 }
1982 $this->changedBlocks[$chunkHash][$relativeBlockHash] = $pos;
1983
1984 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
1985 $listener->onBlockChanged($pos);
1986 }
1987
1988 if($update){
1989 $this->updateAllLight($x, $y, $z);
1990 $this->internalNotifyNeighbourBlockUpdate($x, $y, $z);
1991 }
1992
1993 $this->timings->setBlock->stopTiming();
1994 }
1995
1996 public function dropItem(Vector3 $source, Item $item, ?Vector3 $motion = null, int $delay = 10) : ?ItemEntity{
1997 if($item->isNull()){
1998 return null;
1999 }
2000
2001 $itemEntity = new ItemEntity(Location::fromObject($source, $this, lcg_value() * 360, 0), $item);
2002
2003 $itemEntity->setPickupDelay($delay);
2004 $itemEntity->setMotion($motion ?? new Vector3(lcg_value() * 0.2 - 0.1, 0.2, lcg_value() * 0.2 - 0.1));
2005 $itemEntity->spawnToAll();
2006
2007 return $itemEntity;
2008 }
2009
2016 public function dropExperience(Vector3 $pos, int $amount) : array{
2018 $orbs = [];
2019
2020 foreach(ExperienceOrb::splitIntoOrbSizes($amount) as $split){
2021 $orb = new ExperienceOrb(Location::fromObject($pos, $this, lcg_value() * 360, 0), $split);
2022
2023 $orb->setMotion(new Vector3((lcg_value() * 0.2 - 0.1) * 2, lcg_value() * 0.4, (lcg_value() * 0.2 - 0.1) * 2));
2024 $orb->spawnToAll();
2025
2026 $orbs[] = $orb;
2027 }
2028
2029 return $orbs;
2030 }
2031
2040 public function useBreakOn(Vector3 $vector, Item &$item = null, ?Player $player = null, bool $createParticles = false, array &$returnedItems = []) : bool{
2041 $vector = $vector->floor();
2042
2043 $chunkX = $vector->getFloorX() >> Chunk::COORD_BIT_SIZE;
2044 $chunkZ = $vector->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2045 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2046 return false;
2047 }
2048
2049 $target = $this->getBlock($vector);
2050 $affectedBlocks = $target->getAffectedBlocks();
2051
2052 if($item === null){
2053 $item = VanillaItems::AIR();
2054 }
2055
2056 $drops = [];
2057 if($player === null || $player->hasFiniteResources()){
2058 $drops = array_merge(...array_map(fn(Block $block) => $block->getDrops($item), $affectedBlocks));
2059 }
2060
2061 $xpDrop = 0;
2062 if($player !== null && $player->hasFiniteResources()){
2063 $xpDrop = array_sum(array_map(fn(Block $block) => $block->getXpDropForTool($item), $affectedBlocks));
2064 }
2065
2066 if($player !== null){
2067 $ev = new BlockBreakEvent($player, $target, $item, $player->isCreative(), $drops, $xpDrop);
2068
2069 if($target instanceof Air || ($player->isSurvival() && !$target->getBreakInfo()->isBreakable()) || $player->isSpectator()){
2070 $ev->cancel();
2071 }
2072
2073 if($player->isAdventure(true) && !$ev->isCancelled()){
2074 $canBreak = false;
2075 $itemParser = LegacyStringToItemParser::getInstance();
2076 foreach($item->getCanDestroy() as $v){
2077 $entry = $itemParser->parse($v);
2078 if($entry->getBlock()->hasSameTypeId($target)){
2079 $canBreak = true;
2080 break;
2081 }
2082 }
2083
2084 if(!$canBreak){
2085 $ev->cancel();
2086 }
2087 }
2088
2089 $ev->call();
2090 if($ev->isCancelled()){
2091 return false;
2092 }
2093
2094 $drops = $ev->getDrops();
2095 $xpDrop = $ev->getXpDropAmount();
2096
2097 }elseif(!$target->getBreakInfo()->isBreakable()){
2098 return false;
2099 }
2100
2101 foreach($affectedBlocks as $t){
2102 $this->destroyBlockInternal($t, $item, $player, $createParticles, $returnedItems);
2103 }
2104
2105 $item->onDestroyBlock($target, $returnedItems);
2106
2107 if(count($drops) > 0){
2108 $dropPos = $vector->add(0.5, 0.5, 0.5);
2109 foreach($drops as $drop){
2110 if(!$drop->isNull()){
2111 $this->dropItem($dropPos, $drop);
2112 }
2113 }
2114 }
2115
2116 if($xpDrop > 0){
2117 $this->dropExperience($vector->add(0.5, 0.5, 0.5), $xpDrop);
2118 }
2119
2120 return true;
2121 }
2122
2126 private function destroyBlockInternal(Block $target, Item $item, ?Player $player, bool $createParticles, array &$returnedItems) : void{
2127 if($createParticles){
2128 $this->addParticle($target->getPosition()->add(0.5, 0.5, 0.5), new BlockBreakParticle($target));
2129 }
2130
2131 $target->onBreak($item, $player, $returnedItems);
2132
2133 $tile = $this->getTile($target->getPosition());
2134 if($tile !== null){
2135 $tile->onBlockDestroyed();
2136 }
2137 }
2138
2146 public function useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector = null, ?Player $player = null, bool $playSound = false, array &$returnedItems = []) : bool{
2147 $blockClicked = $this->getBlock($vector);
2148 $blockReplace = $blockClicked->getSide($face);
2149
2150 if($clickVector === null){
2151 $clickVector = new Vector3(0.0, 0.0, 0.0);
2152 }else{
2153 $clickVector = new Vector3(
2154 min(1.0, max(0.0, $clickVector->x)),
2155 min(1.0, max(0.0, $clickVector->y)),
2156 min(1.0, max(0.0, $clickVector->z))
2157 );
2158 }
2159
2160 if(!$this->isInWorld($blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z)){
2161 //TODO: build height limit messages for custom world heights and mcregion cap
2162 return false;
2163 }
2164 $chunkX = $blockReplace->getPosition()->getFloorX() >> Chunk::COORD_BIT_SIZE;
2165 $chunkZ = $blockReplace->getPosition()->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2166 if(!$this->isChunkLoaded($chunkX, $chunkZ) || $this->isChunkLocked($chunkX, $chunkZ)){
2167 return false;
2168 }
2169
2170 if($blockClicked->getTypeId() === BlockTypeIds::AIR){
2171 return false;
2172 }
2173
2174 if($player !== null){
2175 $ev = new PlayerInteractEvent($player, $item, $blockClicked, $clickVector, $face, PlayerInteractEvent::RIGHT_CLICK_BLOCK);
2176 if($player->isSneaking()){
2177 $ev->setUseItem(false);
2178 $ev->setUseBlock($item->isNull()); //opening doors is still possible when sneaking if using an empty hand
2179 }
2180 if($player->isSpectator()){
2181 $ev->cancel(); //set it to cancelled so plugins can bypass this
2182 }
2183
2184 $ev->call();
2185 if(!$ev->isCancelled()){
2186 if($ev->useBlock() && $blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2187 return true;
2188 }
2189
2190 if($ev->useItem()){
2191 $result = $item->onInteractBlock($player, $blockReplace, $blockClicked, $face, $clickVector, $returnedItems);
2192 if($result !== ItemUseResult::NONE){
2193 return $result === ItemUseResult::SUCCESS;
2194 }
2195 }
2196 }else{
2197 return false;
2198 }
2199 }elseif($blockClicked->onInteract($item, $face, $clickVector, $player, $returnedItems)){
2200 return true;
2201 }
2202
2203 if($item->isNull() || !$item->canBePlaced()){
2204 return false;
2205 }
2206 $hand = $item->getBlock($face);
2207 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2208
2209 if($hand->canBePlacedAt($blockClicked, $clickVector, $face, true)){
2210 $blockReplace = $blockClicked;
2211 //TODO: while this mimics the vanilla behaviour with replaceable blocks, we should really pass some other
2212 //value like NULL and let place() deal with it. This will look like a bug to anyone who doesn't know about
2213 //the vanilla behaviour.
2214 $face = Facing::UP;
2215 $hand->position($this, $blockReplace->getPosition()->x, $blockReplace->getPosition()->y, $blockReplace->getPosition()->z);
2216 }elseif(!$hand->canBePlacedAt($blockReplace, $clickVector, $face, false)){
2217 return false;
2218 }
2219
2220 $tx = new BlockTransaction($this);
2221 if(!$hand->place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player)){
2222 return false;
2223 }
2224
2225 foreach($tx->getBlocks() as [$x, $y, $z, $block]){
2226 $block->position($this, $x, $y, $z);
2227 foreach($block->getCollisionBoxes() as $collisionBox){
2228 if(count($this->getCollidingEntities($collisionBox)) > 0){
2229 return false; //Entity in block
2230 }
2231 }
2232 }
2233
2234 if($player !== null){
2235 $ev = new BlockPlaceEvent($player, $tx, $blockClicked, $item);
2236 if($player->isSpectator()){
2237 $ev->cancel();
2238 }
2239
2240 if($player->isAdventure(true) && !$ev->isCancelled()){
2241 $canPlace = false;
2242 $itemParser = LegacyStringToItemParser::getInstance();
2243 foreach($item->getCanPlaceOn() as $v){
2244 $entry = $itemParser->parse($v);
2245 if($entry->getBlock()->hasSameTypeId($blockClicked)){
2246 $canPlace = true;
2247 break;
2248 }
2249 }
2250
2251 if(!$canPlace){
2252 $ev->cancel();
2253 }
2254 }
2255
2256 $ev->call();
2257 if($ev->isCancelled()){
2258 return false;
2259 }
2260 }
2261
2262 if(!$tx->apply()){
2263 return false;
2264 }
2265 foreach($tx->getBlocks() as [$x, $y, $z, $_]){
2266 $tile = $this->getTileAt($x, $y, $z);
2267 if($tile !== null){
2268 //TODO: seal this up inside block placement
2269 $tile->copyDataFromItem($item);
2270 }
2271
2272 $this->getBlockAt($x, $y, $z)->onPostPlace();
2273 }
2274
2275 if($playSound){
2276 $this->addSound($hand->getPosition(), new BlockPlaceSound($hand));
2277 }
2278
2279 $item->pop();
2280
2281 return true;
2282 }
2283
2284 public function getEntity(int $entityId) : ?Entity{
2285 return $this->entities[$entityId] ?? null;
2286 }
2287
2294 public function getEntities() : array{
2295 return $this->entities;
2296 }
2297
2308 public function getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2309 $nearby = [];
2310
2311 foreach($this->getNearbyEntities($bb, $entity) as $ent){
2312 if($ent->canBeCollidedWith() && ($entity === null || $entity->canCollideWith($ent))){
2313 $nearby[] = $ent;
2314 }
2315 }
2316
2317 return $nearby;
2318 }
2319
2326 public function getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity = null) : array{
2327 $nearby = [];
2328
2329 $minX = ((int) floor($bb->minX - 2)) >> Chunk::COORD_BIT_SIZE;
2330 $maxX = ((int) floor($bb->maxX + 2)) >> Chunk::COORD_BIT_SIZE;
2331 $minZ = ((int) floor($bb->minZ - 2)) >> Chunk::COORD_BIT_SIZE;
2332 $maxZ = ((int) floor($bb->maxZ + 2)) >> Chunk::COORD_BIT_SIZE;
2333
2334 for($x = $minX; $x <= $maxX; ++$x){
2335 for($z = $minZ; $z <= $maxZ; ++$z){
2336 foreach($this->getChunkEntities($x, $z) as $ent){
2337 if($ent !== $entity && $ent->boundingBox->intersectsWith($bb)){
2338 $nearby[] = $ent;
2339 }
2340 }
2341 }
2342 }
2343
2344 return $nearby;
2345 }
2346
2358 public function getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType = Entity::class, bool $includeDead = false) : ?Entity{
2359 assert(is_a($entityType, Entity::class, true));
2360
2361 $minX = ((int) floor($pos->x - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2362 $maxX = ((int) floor($pos->x + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2363 $minZ = ((int) floor($pos->z - $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2364 $maxZ = ((int) floor($pos->z + $maxDistance)) >> Chunk::COORD_BIT_SIZE;
2365
2366 $currentTargetDistSq = $maxDistance ** 2;
2367
2372 $currentTarget = null;
2373
2374 for($x = $minX; $x <= $maxX; ++$x){
2375 for($z = $minZ; $z <= $maxZ; ++$z){
2376 foreach($this->getChunkEntities($x, $z) as $entity){
2377 if(!($entity instanceof $entityType) || $entity->isFlaggedForDespawn() || (!$includeDead && !$entity->isAlive())){
2378 continue;
2379 }
2380 $distSq = $entity->getPosition()->distanceSquared($pos);
2381 if($distSq < $currentTargetDistSq){
2382 $currentTargetDistSq = $distSq;
2383 $currentTarget = $entity;
2384 }
2385 }
2386 }
2387 }
2388
2389 return $currentTarget;
2390 }
2391
2398 public function getPlayers() : array{
2399 return $this->players;
2400 }
2401
2408 public function getTile(Vector3 $pos) : ?Tile{
2409 return $this->getTileAt((int) floor($pos->x), (int) floor($pos->y), (int) floor($pos->z));
2410 }
2411
2415 public function getTileAt(int $x, int $y, int $z) : ?Tile{
2416 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;
2417 }
2418
2419 public function getBiomeId(int $x, int $y, int $z) : int{
2420 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2421 return $chunk->getBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2422 }
2423 return BiomeIds::OCEAN; //TODO: this should probably throw instead (terrain not generated yet)
2424 }
2425
2426 public function getBiome(int $x, int $y, int $z) : Biome{
2427 return BiomeRegistry::getInstance()->getBiome($this->getBiomeId($x, $y, $z));
2428 }
2429
2430 public function setBiomeId(int $x, int $y, int $z, int $biomeId) : void{
2431 $chunkX = $x >> Chunk::COORD_BIT_SIZE;
2432 $chunkZ = $z >> Chunk::COORD_BIT_SIZE;
2433 $this->unlockChunk($chunkX, $chunkZ, null);
2434 if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){
2435 $chunk->setBiomeId($x & Chunk::COORD_MASK, $y & Chunk::COORD_MASK, $z & Chunk::COORD_MASK, $biomeId);
2436 }else{
2437 //if we allowed this, the modifications would be lost when the chunk is created
2438 throw new WorldException("Cannot set biome in a non-generated chunk");
2439 }
2440 }
2441
2446 public function getLoadedChunks() : array{
2447 return $this->chunks;
2448 }
2449
2450 public function getChunk(int $chunkX, int $chunkZ) : ?Chunk{
2451 return $this->chunks[World::chunkHash($chunkX, $chunkZ)] ?? null;
2452 }
2453
2458 public function getChunkEntities(int $chunkX, int $chunkZ) : array{
2459 return $this->entitiesByChunk[World::chunkHash($chunkX, $chunkZ)] ?? [];
2460 }
2461
2465 public function getOrLoadChunkAtPosition(Vector3 $pos) : ?Chunk{
2466 return $this->loadChunk($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2467 }
2468
2475 public function getAdjacentChunks(int $x, int $z) : array{
2476 $result = [];
2477 for($xx = -1; $xx <= 1; ++$xx){
2478 for($zz = -1; $zz <= 1; ++$zz){
2479 if($xx === 0 && $zz === 0){
2480 continue; //center chunk
2481 }
2482 $result[World::chunkHash($xx, $zz)] = $this->loadChunk($x + $xx, $z + $zz);
2483 }
2484 }
2485
2486 return $result;
2487 }
2488
2503 public function lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId) : void{
2504 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2505 if(isset($this->chunkLock[$chunkHash])){
2506 throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked");
2507 }
2508 $this->chunkLock[$chunkHash] = $lockId;
2509 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2510 }
2511
2520 public function unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId) : bool{
2521 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2522 if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){
2523 unset($this->chunkLock[$chunkHash]);
2524 $this->markTickingChunkForRecheck($chunkX, $chunkZ);
2525 return true;
2526 }
2527 return false;
2528 }
2529
2535 public function isChunkLocked(int $chunkX, int $chunkZ) : bool{
2536 return isset($this->chunkLock[World::chunkHash($chunkX, $chunkZ)]);
2537 }
2538
2539 public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
2540 $chunkHash = World::chunkHash($chunkX, $chunkZ);
2541 $oldChunk = $this->loadChunk($chunkX, $chunkZ);
2542 if($oldChunk !== null && $oldChunk !== $chunk){
2543 $deletedTiles = 0;
2544 $transferredTiles = 0;
2545 foreach($oldChunk->getTiles() as $oldTile){
2546 $tilePosition = $oldTile->getPosition();
2547 $localX = $tilePosition->getFloorX() & Chunk::COORD_MASK;
2548 $localY = $tilePosition->getFloorY();
2549 $localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
2550
2551 $newBlock = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ));
2552 $expectedTileClass = $newBlock->getIdInfo()->getTileClass();
2553 if(
2554 $expectedTileClass === null || //new block doesn't expect a tile
2555 !($oldTile instanceof $expectedTileClass) || //new block expects a different tile
2556 (($newTile = $chunk->getTile($localX, $localY, $localZ)) !== null && $newTile !== $oldTile) //new chunk already has a different tile
2557 ){
2558 $oldTile->close();
2559 $deletedTiles++;
2560 }else{
2561 $transferredTiles++;
2562 $chunk->addTile($oldTile);
2563 $oldChunk->removeTile($oldTile);
2564 }
2565 }
2566 if($deletedTiles > 0 || $transferredTiles > 0){
2567 $this->logger->debug("Replacement of chunk $chunkX $chunkZ caused deletion of $deletedTiles obsolete/conflicted tiles, and transfer of $transferredTiles");
2568 }
2569 }
2570
2571 $this->chunks[$chunkHash] = $chunk;
2572
2573 unset($this->blockCache[$chunkHash]);
2574 unset($this->blockCollisionBoxCache[$chunkHash]);
2575 unset($this->changedBlocks[$chunkHash]);
2576 $chunk->setTerrainDirty();
2577 $this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking
2578
2579 if(!$this->isChunkInUse($chunkX, $chunkZ)){
2580 $this->unloadChunkRequest($chunkX, $chunkZ);
2581 }
2582
2583 if($oldChunk === null){
2584 if(ChunkLoadEvent::hasHandlers()){
2585 (new ChunkLoadEvent($this, $chunkX, $chunkZ, $chunk, true))->call();
2586 }
2587
2588 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2589 $listener->onChunkLoaded($chunkX, $chunkZ, $chunk);
2590 }
2591 }else{
2592 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2593 $listener->onChunkChanged($chunkX, $chunkZ, $chunk);
2594 }
2595 }
2596
2597 for($cX = -1; $cX <= 1; ++$cX){
2598 for($cZ = -1; $cZ <= 1; ++$cZ){
2599 foreach($this->getChunkEntities($chunkX + $cX, $chunkZ + $cZ) as $entity){
2600 $entity->onNearbyBlockChange();
2601 }
2602 }
2603 }
2604 }
2605
2612 public function getHighestBlockAt(int $x, int $z) : ?int{
2613 if(($chunk = $this->loadChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE)) !== null){
2614 return $chunk->getHighestBlockAt($x & Chunk::COORD_MASK, $z & Chunk::COORD_MASK);
2615 }
2616 throw new WorldException("Cannot get highest block in an ungenerated chunk");
2617 }
2618
2622 public function isInLoadedTerrain(Vector3 $pos) : bool{
2623 return $this->isChunkLoaded($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2624 }
2625
2626 public function isChunkLoaded(int $x, int $z) : bool{
2627 return isset($this->chunks[World::chunkHash($x, $z)]);
2628 }
2629
2630 public function isChunkGenerated(int $x, int $z) : bool{
2631 return $this->loadChunk($x, $z) !== null;
2632 }
2633
2634 public function isChunkPopulated(int $x, int $z) : bool{
2635 $chunk = $this->loadChunk($x, $z);
2636 return $chunk !== null && $chunk->isPopulated();
2637 }
2638
2642 public function getSpawnLocation() : Position{
2643 return Position::fromObject($this->provider->getWorldData()->getSpawn(), $this);
2644 }
2645
2649 public function setSpawnLocation(Vector3 $pos) : void{
2650 $previousSpawn = $this->getSpawnLocation();
2651 $this->provider->getWorldData()->setSpawn($pos);
2652 (new SpawnChangeEvent($this, $previousSpawn))->call();
2653
2654 $location = Position::fromObject($pos, $this);
2655 foreach($this->players as $player){
2656 $player->getNetworkSession()->syncWorldSpawnPoint($location);
2657 }
2658 }
2659
2663 public function addEntity(Entity $entity) : void{
2664 if($entity->isClosed()){
2665 throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to world");
2666 }
2667 if($entity->getWorld() !== $this){
2668 throw new \InvalidArgumentException("Invalid Entity world");
2669 }
2670 if(array_key_exists($entity->getId(), $this->entities)){
2671 if($this->entities[$entity->getId()] === $entity){
2672 throw new \InvalidArgumentException("Entity " . $entity->getId() . " has already been added to this world");
2673 }else{
2674 throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId());
2675 }
2676 }
2677 $pos = $entity->getPosition()->asVector3();
2678 $this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
2679 $this->entityLastKnownPositions[$entity->getId()] = $pos;
2680
2681 if($entity instanceof Player){
2682 $this->players[$entity->getId()] = $entity;
2683 }
2684 $this->entities[$entity->getId()] = $entity;
2685 }
2686
2692 public function removeEntity(Entity $entity) : void{
2693 if($entity->getWorld() !== $this){
2694 throw new \InvalidArgumentException("Invalid Entity world");
2695 }
2696 if(!array_key_exists($entity->getId(), $this->entities)){
2697 throw new \InvalidArgumentException("Entity is not tracked by this world (possibly already removed?)");
2698 }
2699 $pos = $this->entityLastKnownPositions[$entity->getId()];
2700 $chunkHash = World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE);
2701 if(isset($this->entitiesByChunk[$chunkHash][$entity->getId()])){
2702 if(count($this->entitiesByChunk[$chunkHash]) === 1){
2703 unset($this->entitiesByChunk[$chunkHash]);
2704 }else{
2705 unset($this->entitiesByChunk[$chunkHash][$entity->getId()]);
2706 }
2707 }
2708 unset($this->entityLastKnownPositions[$entity->getId()]);
2709
2710 if($entity instanceof Player){
2711 unset($this->players[$entity->getId()]);
2712 $this->checkSleep();
2713 }
2714
2715 unset($this->entities[$entity->getId()]);
2716 unset($this->updateEntities[$entity->getId()]);
2717 }
2718
2722 public function onEntityMoved(Entity $entity) : void{
2723 if(!array_key_exists($entity->getId(), $this->entityLastKnownPositions)){
2724 //this can happen if the entity was teleported before addEntity() was called
2725 return;
2726 }
2727 $oldPosition = $this->entityLastKnownPositions[$entity->getId()];
2728 $newPosition = $entity->getPosition();
2729
2730 $oldChunkX = $oldPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2731 $oldChunkZ = $oldPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2732 $newChunkX = $newPosition->getFloorX() >> Chunk::COORD_BIT_SIZE;
2733 $newChunkZ = $newPosition->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2734
2735 if($oldChunkX !== $newChunkX || $oldChunkZ !== $newChunkZ){
2736 $oldChunkHash = World::chunkHash($oldChunkX, $oldChunkZ);
2737 if(isset($this->entitiesByChunk[$oldChunkHash][$entity->getId()])){
2738 if(count($this->entitiesByChunk[$oldChunkHash]) === 1){
2739 unset($this->entitiesByChunk[$oldChunkHash]);
2740 }else{
2741 unset($this->entitiesByChunk[$oldChunkHash][$entity->getId()]);
2742 }
2743 }
2744
2745 $newViewers = $this->getViewersForPosition($newPosition);
2746 foreach($entity->getViewers() as $player){
2747 if(!isset($newViewers[spl_object_id($player)])){
2748 $entity->despawnFrom($player);
2749 }else{
2750 unset($newViewers[spl_object_id($player)]);
2751 }
2752 }
2753 foreach($newViewers as $player){
2754 $entity->spawnTo($player);
2755 }
2756
2757 $newChunkHash = World::chunkHash($newChunkX, $newChunkZ);
2758 $this->entitiesByChunk[$newChunkHash][$entity->getId()] = $entity;
2759 }
2760 $this->entityLastKnownPositions[$entity->getId()] = $newPosition->asVector3();
2761 }
2762
2767 public function addTile(Tile $tile) : void{
2768 if($tile->isClosed()){
2769 throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to world");
2770 }
2771 $pos = $tile->getPosition();
2772 if(!$pos->isValid() || $pos->getWorld() !== $this){
2773 throw new \InvalidArgumentException("Invalid Tile world");
2774 }
2775 if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
2776 throw new \InvalidArgumentException("Tile position is outside the world bounds");
2777 }
2778
2779 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2780 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2781
2782 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2783 $this->chunks[$hash]->addTile($tile);
2784 }else{
2785 throw new \InvalidArgumentException("Attempted to create tile " . get_class($tile) . " in unloaded chunk $chunkX $chunkZ");
2786 }
2787
2788 //delegate tile ticking to the corresponding block
2789 $this->scheduleDelayedBlockUpdate($pos->asVector3(), 1);
2790 }
2791
2796 public function removeTile(Tile $tile) : void{
2797 $pos = $tile->getPosition();
2798 if(!$pos->isValid() || $pos->getWorld() !== $this){
2799 throw new \InvalidArgumentException("Invalid Tile world");
2800 }
2801
2802 $chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
2803 $chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
2804
2805 if(isset($this->chunks[$hash = World::chunkHash($chunkX, $chunkZ)])){
2806 $this->chunks[$hash]->removeTile($tile);
2807 }
2808 foreach($this->getChunkListeners($chunkX, $chunkZ) as $listener){
2809 $listener->onBlockChanged($pos->asVector3());
2810 }
2811 }
2812
2813 public function isChunkInUse(int $x, int $z) : bool{
2814 return isset($this->chunkLoaders[$index = World::chunkHash($x, $z)]) && count($this->chunkLoaders[$index]) > 0;
2815 }
2816
2823 public function loadChunk(int $x, int $z) : ?Chunk{
2824 if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){
2825 return $this->chunks[$chunkHash];
2826 }
2827
2828 $this->timings->syncChunkLoad->startTiming();
2829
2830 $this->cancelUnloadChunkRequest($x, $z);
2831
2832 $this->timings->syncChunkLoadData->startTiming();
2833
2834 $loadedChunkData = null;
2835
2836 try{
2837 $loadedChunkData = $this->provider->loadChunk($x, $z);
2838 }catch(CorruptedChunkException $e){
2839 $this->logger->critical("Failed to load chunk x=$x z=$z: " . $e->getMessage());
2840 }
2841
2842 $this->timings->syncChunkLoadData->stopTiming();
2843
2844 if($loadedChunkData === null){
2845 $this->timings->syncChunkLoad->stopTiming();
2846 return null;
2847 }
2848
2849 $chunkData = $loadedChunkData->getData();
2850 $chunk = new Chunk($chunkData->getSubChunks(), $chunkData->isPopulated());
2851 if(!$loadedChunkData->isUpgraded()){
2852 $chunk->clearTerrainDirtyFlags();
2853 }else{
2854 $this->logger->debug("Chunk $x $z has been upgraded, will be saved at the next autosave opportunity");
2855 }
2856 $this->chunks[$chunkHash] = $chunk;
2857 unset($this->blockCache[$chunkHash]);
2858 unset($this->blockCollisionBoxCache[$chunkHash]);
2859
2860 $this->initChunk($x, $z, $chunkData);
2861
2862 if(ChunkLoadEvent::hasHandlers()){
2863 (new ChunkLoadEvent($this, $x, $z, $this->chunks[$chunkHash], false))->call();
2864 }
2865
2866 if(!$this->isChunkInUse($x, $z)){
2867 $this->logger->debug("Newly loaded chunk $x $z has no loaders registered, will be unloaded at next available opportunity");
2868 $this->unloadChunkRequest($x, $z);
2869 }
2870 foreach($this->getChunkListeners($x, $z) as $listener){
2871 $listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
2872 }
2873 $this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded
2874
2875 $this->timings->syncChunkLoad->stopTiming();
2876
2877 return $this->chunks[$chunkHash];
2878 }
2879
2880 private function initChunk(int $chunkX, int $chunkZ, ChunkData $chunkData) : void{
2881 $logger = new \PrefixedLogger($this->logger, "Loading chunk $chunkX $chunkZ");
2882
2883 if(count($chunkData->getEntityNBT()) !== 0){
2884 $this->timings->syncChunkLoadEntities->startTiming();
2885 $entityFactory = EntityFactory::getInstance();
2886 foreach($chunkData->getEntityNBT() as $k => $nbt){
2887 try{
2888 $entity = $entityFactory->createFromData($this, $nbt);
2889 }catch(SavedDataLoadingException $e){
2890 $logger->error("Bad entity data at list position $k: " . $e->getMessage());
2891 $logger->logException($e);
2892 continue;
2893 }
2894 if($entity === null){
2895 $saveIdTag = $nbt->getTag("identifier") ?? $nbt->getTag("id");
2896 $saveId = "<unknown>";
2897 if($saveIdTag instanceof StringTag){
2898 $saveId = $saveIdTag->getValue();
2899 }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format
2900 $saveId = "legacy(" . $saveIdTag->getValue() . ")";
2901 }
2902 $logger->warning("Deleted unknown entity type $saveId");
2903 }
2904 //TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place
2905 //here, because entities currently add themselves to the world
2906 }
2907
2908 $this->timings->syncChunkLoadEntities->stopTiming();
2909 }
2910
2911 if(count($chunkData->getTileNBT()) !== 0){
2912 $this->timings->syncChunkLoadTileEntities->startTiming();
2913 $tileFactory = TileFactory::getInstance();
2914 foreach($chunkData->getTileNBT() as $k => $nbt){
2915 try{
2916 $tile = $tileFactory->createFromData($this, $nbt);
2917 }catch(SavedDataLoadingException $e){
2918 $logger->error("Bad tile entity data at list position $k: " . $e->getMessage());
2919 $logger->logException($e);
2920 continue;
2921 }
2922 if($tile === null){
2923 $logger->warning("Deleted unknown tile entity type " . $nbt->getString("id", "<unknown>"));
2924 continue;
2925 }
2926
2927 $tilePosition = $tile->getPosition();
2928 if(!$this->isChunkLoaded($tilePosition->getFloorX() >> Chunk::COORD_BIT_SIZE, $tilePosition->getFloorZ() >> Chunk::COORD_BIT_SIZE)){
2929 $logger->error("Found tile saved on wrong chunk - unable to fix due to correct chunk not loaded");
2930 }elseif(!$this->isInWorld($tilePosition->getFloorX(), $tilePosition->getFloorY(), $tilePosition->getFloorZ())){
2931 $logger->error("Cannot add tile with position outside the world bounds: x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z");
2932 }elseif($this->getTile($tilePosition) !== null){
2933 $logger->error("Cannot add tile at x=$tilePosition->x,y=$tilePosition->y,z=$tilePosition->z: Another tile is already at that position");
2934 }else{
2935 $this->addTile($tile);
2936 }
2937 }
2938
2939 $this->timings->syncChunkLoadTileEntities->stopTiming();
2940 }
2941 }
2942
2943 private function queueUnloadChunk(int $x, int $z) : void{
2944 $this->unloadQueue[World::chunkHash($x, $z)] = microtime(true);
2945 }
2946
2947 public function unloadChunkRequest(int $x, int $z, bool $safe = true) : bool{
2948 if(($safe && $this->isChunkInUse($x, $z)) || $this->isSpawnChunk($x, $z)){
2949 return false;
2950 }
2951
2952 $this->queueUnloadChunk($x, $z);
2953
2954 return true;
2955 }
2956
2957 public function cancelUnloadChunkRequest(int $x, int $z) : void{
2958 unset($this->unloadQueue[World::chunkHash($x, $z)]);
2959 }
2960
2961 public function unloadChunk(int $x, int $z, bool $safe = true, bool $trySave = true) : bool{
2962 if($safe && $this->isChunkInUse($x, $z)){
2963 return false;
2964 }
2965
2966 if(!$this->isChunkLoaded($x, $z)){
2967 return true;
2968 }
2969
2970 $this->timings->doChunkUnload->startTiming();
2971
2972 $chunkHash = World::chunkHash($x, $z);
2973
2974 $chunk = $this->chunks[$chunkHash] ?? null;
2975
2976 if($chunk !== null){
2977 if(ChunkUnloadEvent::hasHandlers()){
2978 $ev = new ChunkUnloadEvent($this, $x, $z, $chunk);
2979 $ev->call();
2980 if($ev->isCancelled()){
2981 $this->timings->doChunkUnload->stopTiming();
2982
2983 return false;
2984 }
2985 }
2986
2987 if($trySave && $this->getAutoSave()){
2988 $this->timings->syncChunkSave->startTiming();
2989 try{
2990 $this->provider->saveChunk($x, $z, new ChunkData(
2991 $chunk->getSubChunks(),
2992 $chunk->isPopulated(),
2993 array_map(fn(Entity $e) => $e->saveNBT(), array_filter($this->getChunkEntities($x, $z), fn(Entity $e) => $e->canSaveWithChunk())),
2994 array_map(fn(Tile $t) => $t->saveNBT(), $chunk->getTiles()),
2995 ), $chunk->getTerrainDirtyFlags());
2996 }finally{
2997 $this->timings->syncChunkSave->stopTiming();
2998 }
2999 }
3000
3001 foreach($this->getChunkListeners($x, $z) as $listener){
3002 $listener->onChunkUnloaded($x, $z, $chunk);
3003 }
3004
3005 foreach($this->getChunkEntities($x, $z) as $entity){
3006 if($entity instanceof Player){
3007 continue;
3008 }
3009 $entity->close();
3010 }
3011
3012 $chunk->onUnload();
3013 }
3014
3015 unset($this->chunks[$chunkHash]);
3016 unset($this->blockCache[$chunkHash]);
3017 unset($this->blockCollisionBoxCache[$chunkHash]);
3018 unset($this->changedBlocks[$chunkHash]);
3019 unset($this->registeredTickingChunks[$chunkHash]);
3020 $this->markTickingChunkForRecheck($x, $z);
3021
3022 if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
3023 $this->logger->debug("Rejecting population promise for chunk $x $z");
3024 $this->chunkPopulationRequestMap[$chunkHash]->reject();
3025 unset($this->chunkPopulationRequestMap[$chunkHash]);
3026 if(isset($this->activeChunkPopulationTasks[$chunkHash])){
3027 $this->logger->debug("Marking population task for chunk $x $z as orphaned");
3028 $this->activeChunkPopulationTasks[$chunkHash] = false;
3029 }
3030 }
3031
3032 $this->timings->doChunkUnload->stopTiming();
3033
3034 return true;
3035 }
3036
3040 public function isSpawnChunk(int $X, int $Z) : bool{
3041 $spawn = $this->getSpawnLocation();
3042 $spawnX = $spawn->x >> Chunk::COORD_BIT_SIZE;
3043 $spawnZ = $spawn->z >> Chunk::COORD_BIT_SIZE;
3044
3045 return abs($X - $spawnX) <= 1 && abs($Z - $spawnZ) <= 1;
3046 }
3047
3055 public function requestSafeSpawn(?Vector3 $spawn = null) : Promise{
3056 $resolver = new PromiseResolver();
3057 $spawn ??= $this->getSpawnLocation();
3058 /*
3059 * TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the provided
3060 * position, which is currently OK, but might be a problem in the future.
3061 */
3062 $this->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
3063 function() use ($spawn, $resolver) : void{
3064 $spawn = $this->getSafeSpawn($spawn);
3065 $resolver->resolve($spawn);
3066 },
3067 function() use ($resolver) : void{
3068 $resolver->reject();
3069 }
3070 );
3071
3072 return $resolver->getPromise();
3073 }
3074
3081 public function getSafeSpawn(?Vector3 $spawn = null) : Position{
3082 if(!($spawn instanceof Vector3) || $spawn->y <= $this->minY){
3083 $spawn = $this->getSpawnLocation();
3084 }
3085
3086 $max = $this->maxY;
3087 $v = $spawn->floor();
3088 $chunk = $this->getOrLoadChunkAtPosition($v);
3089 if($chunk === null){
3090 throw new WorldException("Cannot find a safe spawn point in non-generated terrain");
3091 }
3092 $x = (int) $v->x;
3093 $z = (int) $v->z;
3094 $y = (int) min($max - 2, $v->y);
3095 $wasAir = $this->getBlockAt($x, $y - 1, $z)->getTypeId() === BlockTypeIds::AIR; //TODO: bad hack, clean up
3096 for(; $y > $this->minY; --$y){
3097 if($this->getBlockAt($x, $y, $z)->isFullCube()){
3098 if($wasAir){
3099 $y++;
3100 }
3101 break;
3102 }else{
3103 $wasAir = true;
3104 }
3105 }
3106
3107 for(; $y >= $this->minY && $y < $max; ++$y){
3108 if(!$this->getBlockAt($x, $y + 1, $z)->isFullCube()){
3109 if(!$this->getBlockAt($x, $y, $z)->isFullCube()){
3110 return new Position($spawn->x, $y === (int) $spawn->y ? $spawn->y : $y, $spawn->z, $this);
3111 }
3112 }else{
3113 ++$y;
3114 }
3115 }
3116
3117 return new Position($spawn->x, $y, $spawn->z, $this);
3118 }
3119
3123 public function getTime() : int{
3124 return $this->time;
3125 }
3126
3130 public function getTimeOfDay() : int{
3131 return $this->time % self::TIME_FULL;
3132 }
3133
3138 public function getDisplayName() : string{
3139 return $this->displayName;
3140 }
3141
3145 public function setDisplayName(string $name) : void{
3146 (new WorldDisplayNameChangeEvent($this, $this->displayName, $name))->call();
3147
3148 $this->displayName = $name;
3149 $this->provider->getWorldData()->setName($name);
3150 }
3151
3155 public function getFolderName() : string{
3156 return $this->folderName;
3157 }
3158
3162 public function setTime(int $time) : void{
3163 $this->time = $time;
3164 $this->sendTime();
3165 }
3166
3170 public function stopTime() : void{
3171 $this->stopTime = true;
3172 $this->sendTime();
3173 }
3174
3178 public function startTime() : void{
3179 $this->stopTime = false;
3180 $this->sendTime();
3181 }
3182
3186 public function getSeed() : int{
3187 return $this->provider->getWorldData()->getSeed();
3188 }
3189
3190 public function getMinY() : int{
3191 return $this->minY;
3192 }
3193
3194 public function getMaxY() : int{
3195 return $this->maxY;
3196 }
3197
3198 public function getDifficulty() : int{
3199 return $this->provider->getWorldData()->getDifficulty();
3200 }
3201
3202 public function setDifficulty(int $difficulty) : void{
3203 if($difficulty < 0 || $difficulty > 3){
3204 throw new \InvalidArgumentException("Invalid difficulty level $difficulty");
3205 }
3206 (new WorldDifficultyChangeEvent($this, $this->getDifficulty(), $difficulty))->call();
3207 $this->provider->getWorldData()->setDifficulty($difficulty);
3208
3209 foreach($this->players as $player){
3210 $player->getNetworkSession()->syncWorldDifficulty($this->getDifficulty());
3211 }
3212 }
3213
3214 private function addChunkHashToPopulationRequestQueue(int $chunkHash) : void{
3215 if(!isset($this->chunkPopulationRequestQueueIndex[$chunkHash])){
3216 $this->chunkPopulationRequestQueue->enqueue($chunkHash);
3217 $this->chunkPopulationRequestQueueIndex[$chunkHash] = true;
3218 }
3219 }
3220
3224 private function enqueuePopulationRequest(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3225 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3226 $this->addChunkHashToPopulationRequestQueue($chunkHash);
3227 $resolver = $this->chunkPopulationRequestMap[$chunkHash] = new PromiseResolver();
3228 if($associatedChunkLoader === null){
3229 $temporaryLoader = new class implements ChunkLoader{};
3230 $this->registerChunkLoader($temporaryLoader, $chunkX, $chunkZ);
3231 $resolver->getPromise()->onCompletion(
3232 fn() => $this->unregisterChunkLoader($temporaryLoader, $chunkX, $chunkZ),
3233 static function() : void{}
3234 );
3235 }
3236 return $resolver->getPromise();
3237 }
3238
3239 private function drainPopulationRequestQueue() : void{
3240 $failed = [];
3241 while(count($this->activeChunkPopulationTasks) < $this->maxConcurrentChunkPopulationTasks && !$this->chunkPopulationRequestQueue->isEmpty()){
3242 $nextChunkHash = $this->chunkPopulationRequestQueue->dequeue();
3243 unset($this->chunkPopulationRequestQueueIndex[$nextChunkHash]);
3244 World::getXZ($nextChunkHash, $nextChunkX, $nextChunkZ);
3245 if(isset($this->chunkPopulationRequestMap[$nextChunkHash])){
3246 assert(!($this->activeChunkPopulationTasks[$nextChunkHash] ?? false), "Population for chunk $nextChunkX $nextChunkZ already running");
3247 if(
3248 !$this->orderChunkPopulation($nextChunkX, $nextChunkZ, null)->isResolved() &&
3249 !isset($this->activeChunkPopulationTasks[$nextChunkHash])
3250 ){
3251 $failed[] = $nextChunkHash;
3252 }
3253 }
3254 }
3255
3256 //these requests failed even though they weren't rate limited; we can't directly re-add them to the back of the
3257 //queue because it would result in an infinite loop
3258 foreach($failed as $hash){
3259 $this->addChunkHashToPopulationRequestQueue($hash);
3260 }
3261 }
3262
3268 private function checkChunkPopulationPreconditions(int $chunkX, int $chunkZ) : array{
3269 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3270 $resolver = $this->chunkPopulationRequestMap[$chunkHash] ?? null;
3271 if($resolver !== null && isset($this->activeChunkPopulationTasks[$chunkHash])){
3272 //generation is already running
3273 return [$resolver, false];
3274 }
3275
3276 $temporaryChunkLoader = new class implements ChunkLoader{};
3277 $this->registerChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3278 $chunk = $this->loadChunk($chunkX, $chunkZ);
3279 $this->unregisterChunkLoader($temporaryChunkLoader, $chunkX, $chunkZ);
3280 if($chunk !== null && $chunk->isPopulated()){
3281 //chunk is already populated; return a pre-resolved promise that will directly fire callbacks assigned
3282 $resolver ??= new PromiseResolver();
3283 unset($this->chunkPopulationRequestMap[$chunkHash]);
3284 $resolver->resolve($chunk);
3285 return [$resolver, false];
3286 }
3287 return [$resolver, true];
3288 }
3289
3301 public function requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3302 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3303 if(!$proceedWithPopulation){
3304 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3305 }
3306
3307 if(count($this->activeChunkPopulationTasks) >= $this->maxConcurrentChunkPopulationTasks){
3308 //too many chunks are already generating; delay resolution of the request until later
3309 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3310 }
3311 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3312 }
3313
3324 public function orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader) : Promise{
3325 [$resolver, $proceedWithPopulation] = $this->checkChunkPopulationPreconditions($chunkX, $chunkZ);
3326 if(!$proceedWithPopulation){
3327 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3328 }
3329
3330 return $this->internalOrderChunkPopulation($chunkX, $chunkZ, $associatedChunkLoader, $resolver);
3331 }
3332
3337 private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{
3338 $chunkHash = World::chunkHash($chunkX, $chunkZ);
3339
3340 $timings = $this->timings->chunkPopulationOrder;
3341 $timings->startTiming();
3342
3343 try{
3344 for($xx = -1; $xx <= 1; ++$xx){
3345 for($zz = -1; $zz <= 1; ++$zz){
3346 if($this->isChunkLocked($chunkX + $xx, $chunkZ + $zz)){
3347 //chunk is already in use by another generation request; queue the request for later
3348 return $resolver?->getPromise() ?? $this->enqueuePopulationRequest($chunkX, $chunkZ, $associatedChunkLoader);
3349 }
3350 }
3351 }
3352
3353 $this->activeChunkPopulationTasks[$chunkHash] = true;
3354 if($resolver === null){
3355 $resolver = new PromiseResolver();
3356 $this->chunkPopulationRequestMap[$chunkHash] = $resolver;
3357 }
3358
3359 $chunkPopulationLockId = new ChunkLockId();
3360
3361 $temporaryChunkLoader = new class implements ChunkLoader{
3362 };
3363 for($xx = -1; $xx <= 1; ++$xx){
3364 for($zz = -1; $zz <= 1; ++$zz){
3365 $this->lockChunk($chunkX + $xx, $chunkZ + $zz, $chunkPopulationLockId);
3366 $this->registerChunkLoader($temporaryChunkLoader, $chunkX + $xx, $chunkZ + $zz);
3367 }
3368 }
3369
3370 $centerChunk = $this->loadChunk($chunkX, $chunkZ);
3371 $adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
3372 $task = new PopulationTask(
3373 $this->worldId,
3374 $chunkX,
3375 $chunkZ,
3376 $centerChunk,
3377 $adjacentChunks,
3378 function(Chunk $centerChunk, array $adjacentChunks) use ($chunkPopulationLockId, $chunkX, $chunkZ, $temporaryChunkLoader) : void{
3379 if(!$this->isLoaded()){
3380 return;
3381 }
3382
3383 $this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
3384 }
3385 );
3386 $workerId = $this->workerPool->selectWorker();
3387 if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
3388 $this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
3389 unset($this->generatorRegisteredWorkers[$workerId]);
3390 }
3391 if(!isset($this->generatorRegisteredWorkers[$workerId])){
3392 $this->registerGeneratorToWorker($workerId);
3393 }
3394 $this->workerPool->submitTaskToWorker($task, $workerId);
3395
3396 return $resolver->getPromise();
3397 }finally{
3398 $timings->stopTiming();
3399 }
3400 }
3401
3406 private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
3407 $timings = $this->timings->chunkPopulationCompletion;
3408 $timings->startTiming();
3409
3410 $dirtyChunks = 0;
3411 for($xx = -1; $xx <= 1; ++$xx){
3412 for($zz = -1; $zz <= 1; ++$zz){
3413 $this->unregisterChunkLoader($temporaryChunkLoader, $x + $xx, $z + $zz);
3414 if(!$this->unlockChunk($x + $xx, $z + $zz, $chunkLockId)){
3415 $dirtyChunks++;
3416 }
3417 }
3418 }
3419
3420 $index = World::chunkHash($x, $z);
3421 if(!isset($this->activeChunkPopulationTasks[$index])){
3422 throw new AssumptionFailedError("This should always be set, regardless of whether the task was orphaned or not");
3423 }
3424 if(!$this->activeChunkPopulationTasks[$index]){
3425 $this->logger->debug("Discarding orphaned population result for chunk x=$x,z=$z");
3426 unset($this->activeChunkPopulationTasks[$index]);
3427 }else{
3428 if($dirtyChunks === 0){
3429 $oldChunk = $this->loadChunk($x, $z);
3430 $this->setChunk($x, $z, $chunk);
3431
3432 foreach($adjacentChunks as $relativeChunkHash => $adjacentChunk){
3433 World::getXZ($relativeChunkHash, $relativeX, $relativeZ);
3434 if($relativeX < -1 || $relativeX > 1 || $relativeZ < -1 || $relativeZ > 1){
3435 throw new AssumptionFailedError("Adjacent chunks should be in range -1 ... +1 coordinates");
3436 }
3437 $this->setChunk($x + $relativeX, $z + $relativeZ, $adjacentChunk);
3438 }
3439
3440 if(($oldChunk === null || !$oldChunk->isPopulated()) && $chunk->isPopulated()){
3441 if(ChunkPopulateEvent::hasHandlers()){
3442 (new ChunkPopulateEvent($this, $x, $z, $chunk))->call();
3443 }
3444
3445 foreach($this->getChunkListeners($x, $z) as $listener){
3446 $listener->onChunkPopulated($x, $z, $chunk);
3447 }
3448 }
3449 }else{
3450 $this->logger->debug("Discarding population result for chunk x=$x,z=$z - terrain was modified on the main thread before async population completed");
3451 }
3452
3453 //This needs to be in this specific spot because user code might call back to orderChunkPopulation().
3454 //If it does, and finds the promise, and doesn't find an active task associated with it, it will schedule
3455 //another PopulationTask. We don't want that because we're here processing the results.
3456 //We can't remove the promise from the array before setting the chunks in the world because that would lead
3457 //to the same problem. Therefore, it's necessary that this code be split into two if/else, with this in the
3458 //middle.
3459 unset($this->activeChunkPopulationTasks[$index]);
3460
3461 if($dirtyChunks === 0){
3462 $promise = $this->chunkPopulationRequestMap[$index] ?? null;
3463 if($promise !== null){
3464 unset($this->chunkPopulationRequestMap[$index]);
3465 $promise->resolve($chunk);
3466 }else{
3467 //Handlers of ChunkPopulateEvent, ChunkLoadEvent, or just ChunkListeners can cause this
3468 $this->logger->debug("Unable to resolve population promise for chunk x=$x,z=$z - populated chunk was forcibly unloaded while setting modified chunks");
3469 }
3470 }else{
3471 //request failed, stick it back on the queue
3472 //we didn't resolve the promise or touch it in any way, so any fake chunk loaders are still valid and
3473 //don't need to be added a second time.
3474 $this->addChunkHashToPopulationRequestQueue($index);
3475 }
3476
3477 $this->drainPopulationRequestQueue();
3478 }
3479 $timings->stopTiming();
3480 }
3481
3482 public function doChunkGarbageCollection() : void{
3483 $this->timings->doChunkGC->startTiming();
3484
3485 foreach($this->chunks as $index => $chunk){
3486 if(!isset($this->unloadQueue[$index])){
3487 World::getXZ($index, $X, $Z);
3488 if(!$this->isSpawnChunk($X, $Z)){
3489 $this->unloadChunkRequest($X, $Z, true);
3490 }
3491 }
3492 $chunk->collectGarbage();
3493 }
3494
3495 $this->provider->doGarbageCollection();
3496
3497 $this->timings->doChunkGC->stopTiming();
3498 }
3499
3500 public function unloadChunks(bool $force = false) : void{
3501 if(count($this->unloadQueue) > 0){
3502 $maxUnload = 96;
3503 $now = microtime(true);
3504 foreach($this->unloadQueue as $index => $time){
3505 World::getXZ($index, $X, $Z);
3506
3507 if(!$force){
3508 if($maxUnload <= 0){
3509 break;
3510 }elseif($time > ($now - 30)){
3511 continue;
3512 }
3513 }
3514
3515 //If the chunk can't be unloaded, it stays on the queue
3516 if($this->unloadChunk($X, $Z, true)){
3517 unset($this->unloadQueue[$index]);
3518 --$maxUnload;
3519 }
3520 }
3521 }
3522 }
3523}
despawnFrom(Player $player, bool $send=true)
Definition Entity.php:1542
getBlock(?int $clickedFace=null)
Definition Item.php:491
pop(int $count=1)
Definition Item.php:430
expandedCopy(float $x, float $y, float $z)
removeWorkerStartHook(\Closure $hook)
getChunkListeners(int $chunkX, int $chunkZ)
Definition World.php:891
removeEntity(Entity $entity)
Definition World.php:2692
notifyNeighbourBlockUpdate(Vector3 $pos)
Definition World.php:1493
getCollisionBlocks(AxisAlignedBB $bb, bool $targetFirst=false)
Definition World.php:1501
getHighestAdjacentBlockLight(int $x, int $y, int $z)
Definition World.php:1832
setDisplayName(string $name)
Definition World.php:3145
getPotentialBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1737
removeOnUnloadCallback(\Closure $callback)
Definition World.php:667
isChunkLocked(int $chunkX, int $chunkZ)
Definition World.php:2535
setSpawnLocation(Vector3 $pos)
Definition World.php:2649
useBreakOn(Vector3 $vector, Item &$item=null, ?Player $player=null, bool $createParticles=false, array &$returnedItems=[])
Definition World.php:2040
getPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1719
createBlockUpdatePackets(array $blocks)
Definition World.php:1092
getSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3081
registerChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:845
getBlockAt(int $x, int $y, int $z, bool $cached=true, bool $addToCache=true)
Definition World.php:1883
getChunkEntities(int $chunkX, int $chunkZ)
Definition World.php:2458
addEntity(Entity $entity)
Definition World.php:2663
getBlockLightAt(int $x, int $y, int $z)
Definition World.php:1762
getBlock(Vector3 $pos, bool $cached=true, bool $addToCache=true)
Definition World.php:1870
static chunkHash(int $x, int $z)
Definition World.php:376
broadcastPacketToViewers(Vector3 $pos, ClientboundPacket $packet)
Definition World.php:795
getOrLoadChunkAtPosition(Vector3 $pos)
Definition World.php:2465
static chunkBlockHash(int $x, int $y, int $z)
Definition World.php:415
getHighestAdjacentPotentialBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1817
getFullLight(Vector3 $pos)
Definition World.php:1676
isInWorld(int $x, int $y, int $z)
Definition World.php:1852
unlockChunk(int $chunkX, int $chunkZ, ?ChunkLockId $lockId)
Definition World.php:2520
getChunkLoaders(int $chunkX, int $chunkZ)
Definition World.php:778
getCollisionBoxes(Entity $entity, AxisAlignedBB $bb, bool $entities=true)
Definition World.php:1603
getAdjacentChunks(int $x, int $z)
Definition World.php:2475
getChunkPlayers(int $chunkX, int $chunkZ)
Definition World.php:768
getTileAt(int $x, int $y, int $z)
Definition World.php:2415
getHighestAdjacentFullLightAt(int $x, int $y, int $z)
Definition World.php:1700
setChunkTickRadius(int $radius)
Definition World.php:1195
getViewersForPosition(Vector3 $pos)
Definition World.php:788
getNearestEntity(Vector3 $pos, float $maxDistance, string $entityType=Entity::class, bool $includeDead=false)
Definition World.php:2358
__construct(private Server $server, string $name, private WritableWorldProvider $provider, private AsyncPool $workerPool)
Definition World.php:474
requestChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3301
addSound(Vector3 $pos, Sound $sound, ?array $players=null)
Definition World.php:696
registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1214
setBlock(Vector3 $pos, Block $block, bool $update=true)
Definition World.php:1934
getNearbyEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2326
static getXZ(int $hash, ?int &$x, ?int &$z)
Definition World.php:441
getBlockCollisionBoxes(AxisAlignedBB $bb)
Definition World.php:1566
getCollidingEntities(AxisAlignedBB $bb, ?Entity $entity=null)
Definition World.php:2308
isSpawnChunk(int $X, int $Z)
Definition World.php:3040
getPotentialLight(Vector3 $pos)
Definition World.php:1708
addParticle(Vector3 $pos, Particle $particle, ?array $players=null)
Definition World.php:725
unregisterChunkListenerFromAll(ChunkListener $listener)
Definition World.php:878
loadChunk(int $x, int $z)
Definition World.php:2823
useItemOn(Vector3 $vector, Item &$item, int $face, ?Vector3 $clickVector=null, ?Player $player=null, bool $playSound=false, array &$returnedItems=[])
Definition World.php:2146
getHighestAdjacentPotentialLightAt(int $x, int $y, int $z)
Definition World.php:1727
setBlockAt(int $x, int $y, int $z, Block $block, bool $update=true)
Definition World.php:1946
unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ)
Definition World.php:1224
getRealBlockSkyLightAt(int $x, int $y, int $z)
Definition World.php:1752
static blockHash(int $x, int $y, int $z)
Definition World.php:395
getTile(Vector3 $pos)
Definition World.php:2408
getFullLightAt(int $x, int $y, int $z)
Definition World.php:1687
getHighestAdjacentRealBlockSkyLight(int $x, int $y, int $z)
Definition World.php:1825
orderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader)
Definition World.php:3324
dropExperience(Vector3 $pos, int $amount)
Definition World.php:2016
isInLoadedTerrain(Vector3 $pos)
Definition World.php:2622
lockChunk(int $chunkX, int $chunkZ, ChunkLockId $lockId)
Definition World.php:2503
getHighestBlockAt(int $x, int $z)
Definition World.php:2612
scheduleDelayedBlockUpdate(Vector3 $pos, int $delay)
Definition World.php:1452
static getBlockXYZ(int $hash, ?int &$x, ?int &$y, ?int &$z)
Definition World.php:425
addOnUnloadCallback(\Closure $callback)
Definition World.php:662
requestSafeSpawn(?Vector3 $spawn=null)
Definition World.php:3055
unregisterChunkListener(ChunkListener $listener, int $chunkX, int $chunkZ)
Definition World.php:862
getTile(int $x, int $y, int $z)
Definition Chunk.php:225
getHighestBlockAt(int $x, int $z)
Definition Chunk.php:118
getBlockStateId(int $x, int $y, int $z)
Definition Chunk.php:98