├── plugin.yml ├── .poggit.yml ├── README.md ├── src └── muqsit │ └── aggressiveoptz │ ├── component │ ├── defaults │ │ ├── utils │ │ │ └── FallingBlockChunkInfo.php │ │ ├── LiquidFallingOptimizationComponent.php │ │ ├── BreakVerticallySupportedBlockOptimizationComponent.php │ │ └── FallingBlockOptimizationComponent.php │ ├── OptimizationComponent.php │ ├── OptimizationComponentFactory.php │ └── OptimizationComponentManager.php │ ├── helper │ ├── world │ │ ├── AggressiveOptzChunkCache.php │ │ ├── AggressiveOptzWorldCache.php │ │ └── AggressiveOptzWorldCacheManager.php │ └── AggressiveOptzHelper.php │ ├── Loader.php │ └── AggressiveOptzApi.php └── resources └── components.json /plugin.yml: -------------------------------------------------------------------------------- 1 | name: AggressiveOptz 2 | main: muqsit\aggressiveoptz\Loader 3 | api: 5.0.0 4 | version: 0.0.11 -------------------------------------------------------------------------------- /.poggit.yml: -------------------------------------------------------------------------------- 1 | --- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/Muqsit/AggressiveOptz 2 | build-by-default: true 3 | branches: 4 | - master 5 | projects: 6 | AggressiveOptz: 7 | path: "" 8 | 9 | ... 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AggressiveOptz 2 | Aggressive optimizations that may tamper with vanilla behaviour in an attempt to reduce overall CPU cost.
3 | List of built-in optimizations can be found [here](https://github.com/Muqsit/AggressiveOptz/wiki/Built-in-Optimization-Components). 4 | -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/component/defaults/utils/FallingBlockChunkInfo.php: -------------------------------------------------------------------------------- 1 | */ 12 | public array $queued = []; 13 | 14 | public function __construct(){ 15 | } 16 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/component/OptimizationComponent.php: -------------------------------------------------------------------------------- 1 | $config 13 | * @return static 14 | */ 15 | public static function fromConfig(array $config); 16 | 17 | public function enable(AggressiveOptzApi $api) : void; 18 | 19 | public function disable(AggressiveOptzApi $api) : void; 20 | } -------------------------------------------------------------------------------- /resources/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "aggressiveoptz:break_vertically_supported_block": { 3 | "enabled": true, 4 | "configuration": { 5 | "blocks": ["carpet"] 6 | } 7 | }, 8 | "aggressiveoptz:falling_block": { 9 | "enabled": true, 10 | "configuration": { 11 | "falling_block_queue_size": 16, 12 | "falling_block_max_height": 16, 13 | "falling_block_max_count": 16 14 | } 15 | }, 16 | "aggressiveoptz:liquid_falling": { 17 | "enabled": true, 18 | "configuration": {} 19 | } 20 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/helper/world/AggressiveOptzChunkCache.php: -------------------------------------------------------------------------------- 1 | */ 10 | private array $cache = []; 11 | 12 | public function set(string $key, mixed $value) : void{ 13 | $this->cache[$key] = $value; 14 | } 15 | 16 | public function remove(string $key) : void{ 17 | unset($this->cache[$key]); 18 | } 19 | 20 | public function get(string $key, mixed $default = null) : mixed{ 21 | return $this->cache[$key] ?? $default; 22 | } 23 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/helper/AggressiveOptzHelper.php: -------------------------------------------------------------------------------- 1 | world_cache_manager = new AggressiveOptzWorldCacheManager(); 16 | } 17 | 18 | public function init(AggressiveOptzApi $api) : void{ 19 | $this->world_cache_manager->init($api); 20 | } 21 | 22 | public function getWorldCacheManager() : AggressiveOptzWorldCacheManager{ 23 | return $this->world_cache_manager; 24 | } 25 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/component/OptimizationComponentFactory.php: -------------------------------------------------------------------------------- 1 | > */ 13 | private array $registered = []; 14 | 15 | public function exists(string $identifier) : bool{ 16 | return array_key_exists($identifier, $this->registered); 17 | } 18 | 19 | /** 20 | * @param string $identifier 21 | * @param class-string $component 22 | */ 23 | public function register(string $identifier, string $component) : void{ 24 | if($this->exists($identifier)){ 25 | throw new InvalidArgumentException("Tried to override an already existing component with the identifier \"{$identifier}\" ({$this->registered[$identifier]})"); 26 | } 27 | 28 | $this->registered[$identifier] = $component; 29 | } 30 | 31 | /** 32 | * @param string $identifier 33 | * @param array $config 34 | * @return OptimizationComponent 35 | */ 36 | public function build(string $identifier, array $config) : OptimizationComponent{ 37 | return $this->registered[$identifier]::fromConfig($config); 38 | } 39 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/helper/world/AggressiveOptzWorldCache.php: -------------------------------------------------------------------------------- 1 | */ 12 | private array $chunks = []; 13 | 14 | /** @var array */ 15 | private array $cache = []; 16 | 17 | public function __construct(World $world){ 18 | foreach($world->getLoadedChunks() as $chunk_hash => $_){ 19 | World::getXZ($chunk_hash, $chunkX, $chunkZ); 20 | $this->onChunkLoad($chunkX, $chunkZ); 21 | } 22 | } 23 | 24 | public function onChunkLoad(int $x, int $z) : void{ 25 | $this->chunks[World::chunkHash($x, $z)] = new AggressiveOptzChunkCache(); 26 | } 27 | 28 | public function onChunkUnload(int $x, int $z) : void{ 29 | unset($this->chunks[World::chunkHash($x, $z)]); 30 | } 31 | 32 | public function getChunk(int $x, int $z) : ?AggressiveOptzChunkCache{ 33 | return $this->chunks[World::chunkHash($x, $z)] ?? null; 34 | } 35 | 36 | public function set(string $key, mixed $value) : void{ 37 | $this->cache[$key] = $value; 38 | } 39 | 40 | public function remove(string $key) : void{ 41 | unset($this->cache[$key]); 42 | } 43 | 44 | public function get(string $key, mixed $default = null) : mixed{ 45 | return $this->cache[$key] ?? $default; 46 | } 47 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/Loader.php: -------------------------------------------------------------------------------- 1 | saveResource(self::COMPONENTS_CONFIG_FILE); 18 | $this->api = new AggressiveOptzApi($this); 19 | } 20 | 21 | protected function onEnable() : void{ 22 | $this->api->load(); 23 | 24 | $contents = Filesystem::fileGetContents($this->getDataFolder() . self::COMPONENTS_CONFIG_FILE); 25 | $this->loadComponentsFromConfig(json_decode($contents, true, 512, JSON_THROW_ON_ERROR)); 26 | 27 | $this->api->init(); 28 | } 29 | 30 | protected function onDisable() : void{ 31 | } 32 | 33 | public function getApi() : AggressiveOptzApi{ 34 | return $this->api; 35 | } 36 | 37 | /** 38 | * @param array}> $config 39 | */ 40 | public function loadComponentsFromConfig(array $config) : void{ 41 | $component_manager = $this->api->getComponentManager(); 42 | foreach($config as $identifier => $data){ 43 | if(!$this->api->getComponentFactory()->exists($identifier)){ 44 | $this->getLogger()->warning("Component {$identifier} is not registered. This configuration entry will be skipped."); 45 | continue; 46 | } 47 | if($data["enabled"]){ 48 | $component_manager->enable($identifier, $data["configuration"]); 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/component/OptimizationComponentManager.php: -------------------------------------------------------------------------------- 1 | */ 19 | private array $enabled = []; 20 | 21 | public function __construct(AggressiveOptzApi $api){ 22 | $this->api = $api; 23 | $this->logger = new PrefixedLogger($api->getLogger(), "OC-Manager"); 24 | } 25 | 26 | public function getLogger() : Logger{ 27 | return $this->logger; 28 | } 29 | 30 | public function isEnabled(string $identifier) : bool{ 31 | return array_key_exists($identifier, $this->enabled); 32 | } 33 | 34 | /** 35 | * @param string $identifier 36 | * @param array $config 37 | */ 38 | public function enable(string $identifier, array $config) : void{ 39 | if($this->isEnabled($identifier)){ 40 | throw new InvalidArgumentException("Tried to enable an already enabled component: {$identifier}"); 41 | } 42 | 43 | $this->enabled[$identifier] = $this->api->getComponentFactory()->build($identifier, $config); 44 | $this->enabled[$identifier]->enable($this->api); 45 | $this->logger->debug("Enabled component: {$identifier}"); 46 | } 47 | 48 | public function disable(string $identifier) : void{ 49 | if(!$this->isEnabled($identifier)){ 50 | throw new InvalidArgumentException("Tried to disable an already disabled component: {$identifier}"); 51 | } 52 | 53 | $this->enabled[$identifier]->disable($this->api); 54 | unset($this->enabled[$identifier]); 55 | $this->logger->debug("Disabled component: {$identifier}"); 56 | } 57 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/component/defaults/LiquidFallingOptimizationComponent.php: -------------------------------------------------------------------------------- 1 | unregister !== null){ 32 | throw new LogicException("Tried to register event handler twice"); 33 | } 34 | 35 | $liquids = []; 36 | foreach(RuntimeBlockStateRegistry::getInstance()->getAllKnownStates() as $block){ 37 | if($block instanceof Liquid && $block->isFalling() && $block->getDecay() === 0){ 38 | $liquids[$block->getStateId()] = true; 39 | } 40 | } 41 | 42 | $air_id = VanillaBlocks::AIR()->getStateId(); 43 | $this->unregister = $api->registerEvent(function(BlockSpreadEvent $event) use($liquids, $air_id) : void{ 44 | $new_state = $event->getNewState(); 45 | if(!array_key_exists($new_state->getStateId(), $liquids)){ 46 | return; 47 | } 48 | 49 | $pos = $new_state->getPosition(); 50 | $world = $pos->getWorld(); 51 | 52 | /** @var int $x */ 53 | $x = $pos->x; 54 | /** @var int $y */ 55 | $y = $pos->y; 56 | /** @var int $z */ 57 | $z = $pos->z; 58 | 59 | $chunk = $world->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE); 60 | if($chunk === null){ 61 | return; 62 | } 63 | 64 | $xc = $x & Chunk::COORD_MASK; 65 | $zc = $z & Chunk::COORD_MASK; 66 | $last_y = null; 67 | while(--$y >= 0){ 68 | if($chunk->getBlockStateId($xc, $y, $zc) !== $air_id){ 69 | break; 70 | } 71 | $world->setBlockAt($x, $y, $z, $new_state, false); 72 | $last_y = $y; 73 | } 74 | 75 | if($last_y !== null){ 76 | $source = $event->getSource(); 77 | if($source instanceof Liquid){ 78 | $world->scheduleDelayedBlockUpdate(new Vector3($x, $last_y, $z), max(1, $source->tickRate())); 79 | } 80 | } 81 | }); 82 | } 83 | 84 | public function disable(AggressiveOptzApi $api) : void{ 85 | if($this->unregister === null){ 86 | throw new LogicException("Tried to unregister an unregistered event handler"); 87 | } 88 | 89 | ($this->unregister)(); 90 | $this->unregister = null; 91 | } 92 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/helper/world/AggressiveOptzWorldCacheManager.php: -------------------------------------------------------------------------------- 1 | */ 21 | private array $worlds = []; 22 | 23 | /** @var array */ 24 | private array $unload_listeners = []; 25 | 26 | public function __construct(){ 27 | } 28 | 29 | public function init(AggressiveOptzApi $api) : void{ 30 | $api->registerEvent(function(WorldLoadEvent $event) : void{ 31 | $this->onWorldLoad($event->getWorld()); 32 | }, EventPriority::LOWEST); 33 | $api->registerEvent(function(WorldUnloadEvent $event) : void{ 34 | $this->onWorldUnload($event->getWorld()); 35 | }, EventPriority::MONITOR); 36 | $api->registerEvent(function(ChunkLoadEvent $event) : void{ 37 | $this->onChunkLoad($event->getWorld(), $event->getChunkX(), $event->getChunkZ()); 38 | }, EventPriority::LOWEST); 39 | $api->registerEvent(function(ChunkUnloadEvent $event) : void{ 40 | $this->onChunkUnload($event->getWorld(), $event->getChunkX(), $event->getChunkZ()); 41 | }, EventPriority::MONITOR); 42 | 43 | foreach($api->getServer()->getWorldManager()->getWorlds() as $world){ 44 | $this->onWorldLoad($world); 45 | } 46 | } 47 | 48 | private function onWorldLoad(World $world) : void{ 49 | $this->worlds[$world->getId()] = new AggressiveOptzWorldCache($world); 50 | } 51 | 52 | private function onWorldUnload(World $world) : void{ 53 | $cache = $this->worlds[$id = $world->getId()]; 54 | foreach($this->unload_listeners as $listener){ 55 | $listener($world, $cache); 56 | } 57 | unset($this->worlds[$id]); 58 | } 59 | 60 | private function onChunkLoad(World $world, int $x, int $z) : void{ 61 | $this->worlds[$world->getId()]->onChunkLoad($x, $z); 62 | } 63 | 64 | private function onChunkUnload(World $world, int $x, int $z) : void{ 65 | if(array_key_exists($id = $world->getId(), $this->worlds)){ // WorldUnloadEvent is called before ChunkUnloadEvent :( 66 | $this->worlds[$id]->onChunkUnload($x, $z); 67 | } 68 | } 69 | 70 | public function get(World $world) : AggressiveOptzWorldCache{ 71 | return $this->worlds[$world->getId()]; 72 | } 73 | 74 | /** 75 | * @param Closure(World, AggressiveOptzWorldCache) : void $listener 76 | * @return Closure() : void 77 | */ 78 | public function registerUnloadListener(Closure $listener) : Closure{ 79 | $this->unload_listeners[$id = spl_object_id($listener)] = $listener; 80 | return function() use($id) : void{ 81 | unset($this->unload_listeners[$id]); 82 | }; 83 | } 84 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/AggressiveOptzApi.php: -------------------------------------------------------------------------------- 1 | loader = $loader; 35 | $this->helper = new AggressiveOptzHelper(); 36 | } 37 | 38 | public function load() : void{ 39 | $this->loadComponent(); 40 | } 41 | 42 | private function loadComponent() : void{ 43 | $prefix = strtolower($this->loader->getName()); 44 | 45 | $this->component_factory = new OptimizationComponentFactory(); 46 | $this->component_factory->register("{$prefix}:break_vertically_supported_block", BreakVerticallySupportedBlockOptimizationComponent::class); 47 | $this->component_factory->register("{$prefix}:falling_block", FallingBlockOptimizationComponent::class); 48 | $this->component_factory->register("{$prefix}:liquid_falling", LiquidFallingOptimizationComponent::class); 49 | 50 | $this->component_manager = new OptimizationComponentManager($this); 51 | } 52 | 53 | public function init() : void{ 54 | $this->helper->init($this); 55 | } 56 | 57 | public function getHelper() : AggressiveOptzHelper{ 58 | return $this->helper; 59 | } 60 | 61 | public function getServer() : Server{ 62 | return $this->loader->getServer(); 63 | } 64 | 65 | public function getScheduler() : TaskScheduler{ 66 | return $this->loader->getScheduler(); 67 | } 68 | 69 | public function getLogger() : Logger{ 70 | return $this->loader->getLogger(); 71 | } 72 | 73 | public function getComponentFactory() : OptimizationComponentFactory{ 74 | return $this->component_factory; 75 | } 76 | 77 | public function getComponentManager() : OptimizationComponentManager{ 78 | return $this->component_manager; 79 | } 80 | 81 | /** 82 | * Registers an event handler and returns a closure which unregisters 83 | * the handler. 84 | * 85 | * @template TEvent of Event 86 | * @param Closure(TEvent) : void $event_handler 87 | * @param int $priority 88 | * @param bool $handleCancelled 89 | * @return Closure() : void 90 | */ 91 | public function registerEvent(Closure $event_handler, int $priority = EventPriority::NORMAL, bool $handleCancelled = false) : Closure{ 92 | $event_class_instance = (new ReflectionFunction($event_handler))->getParameters()[0]->getType(); 93 | if(!($event_class_instance instanceof ReflectionNamedType)){ 94 | throw new InvalidArgumentException("Invalid parameter #1 supplied to event handler"); 95 | } 96 | 97 | /** @var class-string $event_class */ 98 | $event_class = $event_class_instance->getName(); 99 | try{ 100 | $listener = $this->getServer()->getPluginManager()->registerEvent($event_class, $event_handler, $priority, $this->loader, $handleCancelled); 101 | }catch(ReflectionException $e){ 102 | throw new RuntimeException($e->getMessage(), $e->getCode(), $e); 103 | } 104 | 105 | return static function() use($event_class, $listener) : void{ 106 | HandlerListManager::global()->getListFor($event_class)->unregister($listener); 107 | }; 108 | } 109 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/component/defaults/BreakVerticallySupportedBlockOptimizationComponent.php: -------------------------------------------------------------------------------- 1 | parse($block_string) ?? throw new InvalidArgumentException("Invalid block {$block_string}"); 31 | $block = $item->getBlock(); 32 | $block->getTypeId() !== BlockTypeIds::AIR || throw new InvalidArgumentException("Invalid block {$block_string}"); 33 | $blocks[] = $block; 34 | } 35 | return new self($blocks); 36 | } 37 | 38 | /** @var array */ 39 | readonly private array $blocks; 40 | 41 | /** @var Closure */ 42 | private array $unregisters = []; 43 | 44 | /** 45 | * @param list $blocks 46 | */ 47 | public function __construct(array $blocks){ 48 | $block_ids = []; 49 | foreach($blocks as $block){ 50 | $block_ids[$block->getTypeId()] = true; 51 | } 52 | $this->blocks = $block_ids; 53 | } 54 | 55 | public function enable(AggressiveOptzApi $api) : void{ 56 | if(count($this->unregisters) > 0){ 57 | throw new LogicException("Tried to register event handlers twice"); 58 | } 59 | $this->unregisters = [ 60 | $api->registerEvent(function(BlockUpdateEvent $event) : void{ 61 | $block = $event->getBlock(); 62 | $type_id = $block->getTypeId(); 63 | if(!isset($this->blocks[$type_id])){ 64 | return; 65 | } 66 | if(!$block->getSide(Facing::DOWN)->canBeReplaced()){ 67 | return; 68 | } 69 | $pos = $block->getPosition(); 70 | 71 | /** @var list $unsupported_blocks */ 72 | $unsupported_blocks = []; 73 | for($y = $pos->y; $y < World::Y_MAX; ++$y){ 74 | $unsupported_block = $pos->world->getBlockAt($pos->x, $y, $pos->z); 75 | if($unsupported_block->getTypeId() !== $type_id){ 76 | break; 77 | } 78 | $unsupported_blocks[] = $unsupported_block; 79 | } 80 | if(count($unsupported_blocks) === 0){ 81 | return; 82 | } 83 | $event->cancel(); 84 | 85 | $air_block = VanillaBlocks::AIR(); 86 | $item = VanillaItems::AIR(); 87 | $drops = []; 88 | foreach($unsupported_blocks as $unsupported_block){ 89 | array_push($drops, ...$unsupported_block->getDrops($item)); 90 | $pos = $unsupported_block->getPosition(); 91 | $pos->world->setBlockAt($pos->x, $pos->y, $pos->z, $air_block, false); 92 | } 93 | 94 | $source = $block->getPosition()->add(0.5, 0.5, 0.5); 95 | foreach($this->compressItems($drops) as $item){ 96 | $pos->world->dropItem($source, $item); 97 | } 98 | }) 99 | ]; 100 | } 101 | 102 | /** 103 | * @param list $items 104 | * @return list 105 | */ 106 | private function compressItems(array $items) : array{ 107 | /** @var array> $buckets */ 108 | $buckets = []; // put items of same Item::getStateId() in one bucket to reduce Item::equals() calls 109 | while(($item = array_pop($items)) !== null){ 110 | $state_id = $item->getStateId(); 111 | if(!isset($buckets[$state_id])){ 112 | $buckets[$state_id][] = $item; 113 | continue; 114 | } 115 | foreach($buckets[$state_id] as $entry){ 116 | if($entry->equals($item)){ 117 | $entry->setCount($entry->getCount() + $item->getCount()); 118 | } 119 | } 120 | } 121 | // flatten buckets 122 | $result = []; 123 | foreach($buckets as $bucket){ 124 | array_push($result, ...$bucket); 125 | } 126 | return $result; 127 | } 128 | 129 | public function disable(AggressiveOptzApi $api) : void{ 130 | if(count($this->unregisters) === 0){ 131 | throw new LogicException("Tried to unregister an unregistered event handler"); 132 | } 133 | 134 | foreach($this->unregisters as $unregister){ 135 | $unregister(); 136 | } 137 | $this->unregisters = []; 138 | } 139 | } -------------------------------------------------------------------------------- /src/muqsit/aggressiveoptz/component/defaults/FallingBlockOptimizationComponent.php: -------------------------------------------------------------------------------- 1 | */ 43 | private array $unregisters = []; 44 | 45 | /** @var array */ 46 | private array $entity_spawn_chunks = []; 47 | 48 | public function __construct(int $falling_block_queue_size, int $falling_block_max_height, int $falling_block_max_count){ 49 | if($falling_block_queue_size < 0){ 50 | throw new InvalidArgumentException("Falling block queue size cannot be negative"); 51 | } 52 | $this->falling_block_queue_size = $falling_block_queue_size; 53 | 54 | if($falling_block_max_height < 0){ 55 | throw new InvalidArgumentException("Falling block queue size cannot be negative"); 56 | } 57 | if($falling_block_queue_size > $falling_block_max_height){ 58 | throw new InvalidArgumentException("Falling block queue size cannot be greater than falling block max height"); 59 | } 60 | $this->falling_block_max_height = $falling_block_max_height >= World::Y_MAX ? -1 : $falling_block_max_height; 61 | 62 | $this->falling_block_max_count = $falling_block_max_count; 63 | } 64 | 65 | private function getChunkInfo(AggressiveOptzChunkCache $chunk) : FallingBlockChunkInfo{ 66 | $info = $chunk->get(self::CACHE_KEY_FALLING_BLOCK_INFO); 67 | if($info === null){ 68 | $chunk->set(self::CACHE_KEY_FALLING_BLOCK_INFO, $info = new FallingBlockChunkInfo()); 69 | } 70 | return $info; 71 | } 72 | 73 | private function canBeReplaced(int $state_id) : bool{ 74 | static $cache = []; 75 | return $cache[$state_id] ??= RuntimeBlockStateRegistry::getInstance()->fromStateId($state_id)->canBeReplaced(); 76 | } 77 | 78 | public function enable(AggressiveOptzApi $api) : void{ 79 | if(count($this->unregisters) > 0){ 80 | throw new LogicException("Tried to register event handlers twice"); 81 | } 82 | 83 | $world_cache_manager = $api->getHelper()->getWorldCacheManager(); 84 | $registry = RuntimeBlockStateRegistry::getInstance(); 85 | $this->unregisters = [ 86 | $api->registerEvent(function(EntitySpawnEvent $event) use($world_cache_manager) : void{ 87 | $entity = $event->getEntity(); 88 | if(!($entity instanceof FallingBlock) || $entity->isClosed() || $entity->isFlaggedForDespawn()){ 89 | return; 90 | } 91 | 92 | $real_pos = $entity->getPosition(); 93 | $world = $real_pos->getWorld(); 94 | 95 | $chunk = $world_cache_manager->get($world)->getChunk($chunkX = $real_pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $chunkZ = $real_pos->getFloorZ() >> Chunk::COORD_BIT_SIZE); 96 | if($chunk !== null){ 97 | $this->entity_spawn_chunks[$entity->getId()] = World::chunkHash($chunkX, $chunkZ); 98 | $info = $this->getChunkInfo($chunk); 99 | $count = ++$info->entity_count; 100 | }else{ 101 | $count = 1; 102 | } 103 | 104 | $motion = $entity->getMotion(); 105 | if($motion->x != 0.0 || $motion->z != 0.0){ 106 | // not moved exclusively by gravitation 107 | return; 108 | } 109 | 110 | $iterator = new SubChunkExplorer($world); 111 | $pos = $real_pos->add(-$entity->size->getWidth() / 2, $entity->size->getHeight(), -$entity->size->getWidth() / 2)->floor(); 112 | 113 | /** @var int $x */ 114 | $x = $pos->x; 115 | /** @var int $y */ 116 | $y = $pos->y; 117 | /** @var int $z */ 118 | $z = $pos->z; 119 | 120 | $xc = $x & Chunk::COORD_MASK; 121 | $zc = $z & Chunk::COORD_MASK; 122 | 123 | if($count >= $this->falling_block_queue_size){ 124 | while($y > 0){ 125 | if($iterator->moveTo($x, $y, $z) === SubChunkExplorerStatus::INVALID){ 126 | break; 127 | } 128 | 129 | assert($iterator->currentSubChunk !== null); 130 | if(!$this->canBeReplaced($iterator->currentSubChunk->getBlockStateId($xc, $y & Chunk::COORD_MASK, $zc))){ 131 | $entity->teleport(new Vector3($real_pos->x, $y + 1 + ($entity->size->getHeight() / 2), $real_pos->z)); 132 | $entity->setMotion($motion); 133 | break; 134 | } 135 | --$y; 136 | } 137 | }elseif($this->falling_block_max_height !== -1){ 138 | $begin = $y; 139 | while($y > 0){ 140 | if($iterator->moveTo($x, $y, $z) === SubChunkExplorerStatus::INVALID){ 141 | break; 142 | } 143 | 144 | assert($iterator->currentSubChunk !== null); 145 | if(!$this->canBeReplaced($iterator->currentSubChunk->getBlockStateId($xc, $y & Chunk::COORD_MASK, $zc))){ 146 | break; 147 | } 148 | 149 | --$y; 150 | } 151 | if($begin - $y >= $this->falling_block_max_height){ 152 | $entity->teleport(new Vector3($real_pos->x, $y + 1 + ($entity->size->getHeight() / 2), $real_pos->z)); 153 | $entity->setMotion($motion); 154 | } 155 | } 156 | }), 157 | 158 | $api->registerEvent(function(EntityDespawnEvent $event) use($world_cache_manager) : void{ 159 | $entity = $event->getEntity(); 160 | if(!array_key_exists($id = $entity->getId(), $this->entity_spawn_chunks)){ 161 | return; 162 | } 163 | 164 | World::getXZ($this->entity_spawn_chunks[$id], $chunkX, $chunkZ); 165 | unset($this->entity_spawn_chunks[$id]); 166 | $chunk = $world_cache_manager->get($world = $entity->getWorld())->getChunk($chunkX, $chunkZ); 167 | 168 | if($chunk === null){ 169 | return; 170 | } 171 | 172 | $info = $this->getChunkInfo($chunk); 173 | --$info->entity_count; 174 | if(!$world->isChunkLoaded($chunkX, $chunkZ) || ($hash = array_key_first($info->queued)) === null){ 175 | return; 176 | } 177 | 178 | /** @var int $hash */ 179 | unset($info->queued[$hash]); 180 | 181 | World::getBlockXYZ($hash, $x, $y, $z); 182 | $block = $world->getBlockAt($x, $y, $z); 183 | if($block instanceof Fallable){ 184 | ($ev = new BlockUpdateEvent($block))->call(); 185 | if(!$ev->isCancelled()){ 186 | $block->onNearbyBlockChange(); 187 | } 188 | } 189 | }), 190 | 191 | $api->registerEvent(function(BlockUpdateEvent $event) use($world_cache_manager, $registry) : void{ 192 | $block = $event->getBlock(); 193 | if(!($block instanceof Fallable)){ 194 | return; 195 | } 196 | 197 | $pos = $block->getPosition(); 198 | $world = $pos->getWorld(); 199 | 200 | /** @var int $x */ 201 | $x = $pos->x; 202 | /** @var int $y */ 203 | $y = $pos->y; 204 | /** @var int $z */ 205 | $z = $pos->z; 206 | 207 | $chunkX = $x >> Chunk::COORD_BIT_SIZE; 208 | $chunkZ = $z >> Chunk::COORD_BIT_SIZE; 209 | $chunk = $world->getChunk($chunkX, $chunkZ); 210 | if($chunk === null){ 211 | return; 212 | } 213 | 214 | $fb_chunk = $world_cache_manager->get($world)->getChunk($chunkX, $chunkZ); 215 | if($fb_chunk === null){ 216 | return; 217 | } 218 | 219 | $info = $this->getChunkInfo($fb_chunk); 220 | 221 | if($y > World::Y_MIN + 1 && $world->getBlockAt($x, $y - 1, $z) instanceof Air){ 222 | $block_state_id = $block->getStateId(); 223 | $explorer = new SubChunkExplorer($world); 224 | $cx = $x & Chunk::COORD_MASK; 225 | $cz = $z & Chunk::COORD_MASK; 226 | $depth = 1; 227 | $cursor = $y - 1; 228 | $last_block_id = null; 229 | while( 230 | $explorer->moveTo($x, $cursor, $z) !== SubChunkExplorerStatus::INVALID && 231 | $this->canBeReplaced($last_block_id = $explorer->currentSubChunk->getBlockStateId($cx, $cursor & Chunk::COORD_MASK, $cz)) 232 | ){ 233 | ++$depth; 234 | --$cursor; 235 | } 236 | if($last_block_id === null || !$registry->fromStateId($last_block_id)->hasEntityCollision()){ 237 | $cursor = $y + 1; 238 | $tower_height = 0; 239 | while( 240 | $explorer->moveTo($x, $cursor, $z) !== SubChunkExplorerStatus::INVALID && 241 | $explorer->currentSubChunk->getBlockStateId($cx, $cursor & Chunk::COORD_MASK, $cz) === $block_state_id) 242 | { 243 | ++$tower_height; 244 | ++$cursor; 245 | } 246 | if($tower_height > $depth){ 247 | for($i = 1; $i < $depth; $i++){ 248 | $world->setBlockAt($x, $y - $i, $z, $block, false); 249 | } 250 | $block_air = VanillaBlocks::AIR(); 251 | for($i = ($tower_height - $depth) + 2; $i <= $tower_height; $i++){ 252 | $world->setBlockAt($x, $y + $i, $z, $block_air, false); 253 | } 254 | $event->cancel(); 255 | $explorer->invalidate(); 256 | return; 257 | } 258 | } 259 | $explorer->invalidate(); 260 | } 261 | 262 | if($info->entity_count >= $this->falling_block_max_count){ 263 | $event->cancel(); 264 | $info->queued[World::blockHash($x, $y, $z)] = null; 265 | return; 266 | } 267 | 268 | unset($info->queued[World::blockHash($x, $y, $z)]); 269 | }), 270 | 271 | $world_cache_manager->registerUnloadListener(function(World $world, AggressiveOptzWorldCache $cache) : void{ 272 | foreach($world->getEntities() as $entity){ 273 | unset($this->entity_spawn_chunks[$entity->getId()]); 274 | } 275 | }) 276 | ]; 277 | } 278 | 279 | public function disable(AggressiveOptzApi $api) : void{ 280 | if(count($this->unregisters) === 0){ 281 | throw new LogicException("Tried to unregister an unregistered event handler"); 282 | } 283 | 284 | foreach($this->unregisters as $unregister){ 285 | $unregister(); 286 | } 287 | $this->unregisters = []; 288 | } 289 | } --------------------------------------------------------------------------------