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