From fea87fcd75bf9604d43e2ac9a8cc6955446426be Mon Sep 17 00:00:00 2001 From: TrixNEW Date: Fri, 15 Aug 2025 00:24:17 -0400 Subject: [PATCH 1/5] Adding Barrel Support (Untested) --- .idea/.gitignore | 8 + .idea/FasterChest.iml | 10 + .idea/discord.xml | 14 + .idea/modules.xml | 8 + .idea/php.xml | 27 + .idea/vcs.xml | 6 + plugin.yml | 6 +- .../DefaultFasterChestSerializer.php | 4 +- .../fasterchest/FasterChestChunkListener.php | 94 ++- src/cosmoverse/fasterchest/Loader.php | 734 ++++++++++-------- .../fasterchest/barrel/FasterBarrel.php | 124 +++ .../barrel/FasterBarrelInventoryListener.php | 38 + 12 files changed, 709 insertions(+), 364 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/FasterChest.iml create mode 100644 .idea/discord.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/php.xml create mode 100644 .idea/vcs.xml create mode 100644 src/cosmoverse/fasterchest/barrel/FasterBarrel.php create mode 100644 src/cosmoverse/fasterchest/barrel/FasterBarrelInventoryListener.php diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/FasterChest.iml b/.idea/FasterChest.iml new file mode 100644 index 0000000..77098d0 --- /dev/null +++ b/.idea/FasterChest.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 0000000..104c42f --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1bbf855 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..5835214 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/plugin.yml b/plugin.yml index ec4e06b..7bbd184 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,14 +1,16 @@ name: FasterChest main: cosmoverse\fasterchest\Loader api: 5.0.0 -version: 0.0.1 +version: 2.0.0-beta author: Muqsit + commands: fasterchest: - description: Convert or revert chests in worlds between vanilla chests and faster chests + description: FasterChest management commands usage: /fasterchest convert/revert permission: fasterchest.command.fasterchest aliases: ["fasterchests"] + permissions: fasterchest.command.fasterchest: description: Ability to use /fasterchest command diff --git a/src/cosmoverse/fasterchest/DefaultFasterChestSerializer.php b/src/cosmoverse/fasterchest/DefaultFasterChestSerializer.php index 3e3cd38..ce40a61 100644 --- a/src/cosmoverse/fasterchest/DefaultFasterChestSerializer.php +++ b/src/cosmoverse/fasterchest/DefaultFasterChestSerializer.php @@ -16,14 +16,14 @@ use pocketmine\nbt\TreeRoot; use function assert; -final class DefaultFasterChestSerializer implements FasterChestSerializer{ +final readonly class DefaultFasterChestSerializer implements FasterChestSerializer{ public static function instance() : self{ static $instance = null; return $instance ??= new self(); } - readonly private BigEndianNbtSerializer $serializer; + private BigEndianNbtSerializer $serializer; private function __construct(){ $this->serializer = new BigEndianNbtSerializer(); diff --git a/src/cosmoverse/fasterchest/FasterChestChunkListener.php b/src/cosmoverse/fasterchest/FasterChestChunkListener.php index 627c749..90fa76c 100644 --- a/src/cosmoverse/fasterchest/FasterChestChunkListener.php +++ b/src/cosmoverse/fasterchest/FasterChestChunkListener.php @@ -5,6 +5,7 @@ namespace cosmoverse\fasterchest; use Closure; +use pocketmine\block\tile\Barrel as VanillaBarrelTile; use pocketmine\block\tile\Chest as VanillaChestTile; use pocketmine\math\Vector3; use pocketmine\world\ChunkListener; @@ -13,47 +14,54 @@ final class FasterChestChunkListener implements ChunkListener{ - /** @var array */ - private array $exclude_list = []; - - public function __construct( - readonly private Loader $loader, - readonly private World $world - ){} - - public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{ - } - - public function onChunkLoaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{ - } - - public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{ - } - - public function onChunkPopulated(int $chunkX, int $chunkZ, Chunk $chunk) : void{ - } - - public function onBlockChanged(Vector3 $block) : void{ - $tile = $this->world->getTileAt($block->x, $block->y, $block->z); - if($tile === null || $tile::class !== VanillaChestTile::class || isset($this->exclude_list[World::blockHash($block->x, $block->y, $block->z)])){ - return; - } - $this->loader->convertTile($tile); - } - - /** - * @param int $x - * @param int $y - * @param int $z - * @param Closure() : void $callback - */ - public function excluding(int $x, int $y, int $z, Closure $callback) : void{ - $hash = World::blockHash($x, $y, $z); - $this->exclude_list[$hash] = true; - try{ - $callback(); - }finally{ - unset($this->exclude_list[$hash]); - } - } + /** @var array */ + private array $exclude_list = []; + + public function __construct( + readonly private Loader $loader, + readonly private World $world + ){} + + public function onChunkChanged(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + } + + public function onChunkLoaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + } + + public function onChunkUnloaded(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + } + + public function onChunkPopulated(int $chunkX, int $chunkZ, Chunk $chunk) : void{ + } + + public function onBlockChanged(Vector3 $block) : void{ + $tile = $this->world->getTileAt($block->x, $block->y, $block->z); + $hash = World::blockHash($block->x, $block->y, $block->z); + + if($tile === null || isset($this->exclude_list[$hash])){ + return; + } + + if($tile::class === VanillaChestTile::class){ + $this->loader->convertChestTile($tile); + } elseif($tile::class === VanillaBarrelTile::class){ + $this->loader->convertBarrelTile($tile); + } + } + + /** + * @param int $x + * @param int $y + * @param int $z + * @param Closure() : void $callback + */ + public function excluding(int $x, int $y, int $z, Closure $callback) : void{ + $hash = World::blockHash($x, $y, $z); + $this->exclude_list[$hash] = true; + try{ + $callback(); + }finally{ + unset($this->exclude_list[$hash]); + } + } } \ No newline at end of file diff --git a/src/cosmoverse/fasterchest/Loader.php b/src/cosmoverse/fasterchest/Loader.php index d1f600e..419fe08 100644 --- a/src/cosmoverse/fasterchest/Loader.php +++ b/src/cosmoverse/fasterchest/Loader.php @@ -4,10 +4,13 @@ namespace cosmoverse\fasterchest; +use cosmoverse\fasterchest\barrel\FasterBarrel; use Generator; use LevelDB; +use pocketmine\block\Barrel; use pocketmine\block\BlockIdentifier; use pocketmine\block\Chest; +use pocketmine\block\tile\Barrel as VanillaBarrelTile; use pocketmine\block\tile\Chest as VanillaChestTile; use pocketmine\block\tile\TileFactory; use pocketmine\block\VanillaBlocks; @@ -34,320 +37,417 @@ final class Loader extends PluginBase implements Listener{ - public const TILE_ID = "fasterchest:chest"; - - private Chest $internal_block; - - /** @var array */ - private array $chunk_listeners = []; - - protected function onLoad() : void{ - TileFactory::getInstance()->register(FasterChest::class, [self::TILE_ID]); - if($this->getServer()->getConfigGroup()->getPropertyInt(YmlServerProperties::DEBUG_LEVEL, 1) > 1){ - FasterChest::$logger = $this->getLogger(); - } - if(!isset(FasterChest::$serializer)){ // in case other plugins over-rode it - FasterChest::$serializer = DefaultFasterChestSerializer::instance(); - } - - // we will be using an 'internal block' to set FasterChest tile in worlds (we will NOT use World::addTile, ::removeTile) - // this 'internal block' is the vanilla chest block but backed with a FasterChest tile class. - $internal_block = VanillaBlocks::CHEST(); - $_idInfo = new ReflectionProperty($internal_block, "idInfo"); - $_idInfo->setValue($internal_block, new BlockIdentifier($internal_block->getIdInfo()->getBlockTypeId(), FasterChest::class)); - $this->internal_block = $internal_block; - } - - protected function onEnable() : void{ - FasterChest::$database = new LevelDB(Path::join($this->getDataFolder(), "chest.db"), [ - "compression" => LEVELDB_ZLIB_RAW_COMPRESSION, - "block_size" => 64 * 1024 - ]); - - $this->getServer()->getPluginManager()->registerEvents($this, $this); - foreach($this->getServer()->getWorldManager()->getWorlds() as $world){ - $this->applyListenerToWorld($world); - } - } - - protected function onDisable() : void{ - } - - private function applyListenerToWorld(World $world) : void{ - $listener = ($this->chunk_listeners[$world->getId()] ??= new FasterChestChunkListener($this, $world)); - foreach($world->getLoadedChunks() as $hash => $_){ - World::getXZ($hash, $x, $z); - $world->registerChunkListener($listener, $x, $z); - } - } - - /** - * @param WorldLoadEvent $event - * @priority LOWEST - */ - public function onWorldLoad(WorldLoadEvent $event) : void{ - $this->applyListenerToWorld($event->getWorld()); - } - - /** - * @param WorldUnloadEvent $event - * @priority MONITOR - */ - public function onWorldUnload(WorldUnloadEvent $event) : void{ - $world = $event->getWorld(); - $listener = $this->chunk_listeners[$world->getId()]; - unset($this->chunk_listeners[$world->getId()]); - $world->unregisterChunkListenerFromAll($listener); - } - - /** - * @param ChunkLoadEvent $event - * @priority LOWEST - */ - public function onChunkLoad(ChunkLoadEvent $event) : void{ - $world = $event->getWorld(); - if(isset($this->chunk_listeners[$world->getId()])){ - $world->registerChunkListener($this->chunk_listeners[$world->getId()], $event->getChunkX(), $event->getChunkZ()); - } - } - - /** - * @param ChunkUnloadEvent $event - * @priority MONITOR - */ - public function onChunkUnload(ChunkUnloadEvent $event) : void{ - $world = $event->getWorld(); - if(isset($this->chunk_listeners[$world->getId()])){ - $world->unregisterChunkListener($this->chunk_listeners[$world->getId()], $event->getChunkX(), $event->getChunkZ()); - } - } - - /** - * @return Generator - */ - private function sleep() : Generator{ - return Await::promise(fn($resolve) => $this->getScheduler()->scheduleDelayedTask(new ClosureTask($resolve), 1)); - } - - public function convertTile(VanillaChestTile $tile) : ?int{ - $position = $tile->getPosition(); - $world = $position->world; - $block = $world->getBlockAt($position->x, $position->y, $position->z); - if(!($block instanceof Chest)){ - return null; - } - - $tile->unpair(); - $contents = $tile->getRealInventory()->getContents(); - $name = $tile->hasName() ? $tile->getName() : null; - $new_block = (clone $this->internal_block)->setFacing($block->getFacing()); - - $world->setBlockAt($position->x, $position->y, $position->z, $new_block, false); - $new_tile = $world->getTileAt($position->x, $position->y, $position->z); - $new_tile instanceof FasterChest || throw new RuntimeException("Expected internal block to set a faster chest tile, got " . ($new_tile !== null ? $new_tile::class : "null")); - $new_tile->getRealInventory()->setContents($contents); - if($name !== null){ - $new_tile->setName($name); - } - $world->getBlockAt($position->x, $position->y, $position->z)->onPostPlace(); // pair into double chest - return count($contents); - } - - public function revertTile(FasterChest $tile) : ?int{ - $position = $tile->getPosition(); - $world = $position->world; - - $block = $world->getBlockAt($position->x, $position->y, $position->z); - if(!($block instanceof Chest)){ - return null; - } - - $tile->unpair(); - $identifier = FasterChest::dbIdFromPosition($position); - FasterChest::$database->delete($identifier); - $contents = $tile->getRealInventory()->getContents(); - $name = $tile->hasName() ? $tile->getName() : null; - $listener = $this->chunk_listeners[$world->getId()]; - $new_block = VanillaBlocks::CHEST()->setFacing($block->getFacing()); - - $world->setBlockAt($position->x, $position->y, $position->z, VanillaBlocks::AIR(), false); // yes, this is necessary. new tile is not overridden otherwise. and no removeTile() is not a viable alternative. - $listener->excluding($position->x, $position->y, $position->z, static fn() => $world->setBlockAt($position->x, $position->y, $position->z, $new_block, false)); - $new_tile = $world->getTileAt($position->x, $position->y, $position->z); - $new_tile instanceof VanillaChestTile || throw new RuntimeException("Chest block did not set a chest tile, got " . ($new_tile !== null ? $new_tile::class : "null")); - $new_tile->getRealInventory()->setContents($contents); - if($name !== null){ - $new_tile->setName($name); - } - $world->getBlockAt($position->x, $position->y, $position->z)->onPostPlace(); // pair into double chest - return count($contents); - } - - /** - * @param World $world - * @param int $max_ops_per_tick maximum chunk reads per tick - when this value is exceeded, this task sleeps (waits for next server tick) - * @return Generator - */ - public function convertWorld(World $world, int $max_ops_per_tick = 128) : Generator{ - $total_conversions = 0; - $read = 0; - foreach($world->getProvider()->getAllChunks(false, $this->getLogger()) as $coords => $data){ - if(++$read % $max_ops_per_tick === 0){ - // let server breathe for a while to release memory and perform other operations - yield from $this->sleep(); - } - - [$x, $z] = $coords; - - $was_loaded = $world->isChunkLoaded($x, $z); - if(!$was_loaded && count($data->getData()->getTileNBT()) === 0){ // fast filter to avoid chunk load+unload - continue; - } - - $chunk = $world->loadChunk($x, $z); - if($chunk === null){ - continue; - } - - $converted = 0; - foreach($chunk->getTiles() as $tile){ - if($tile::class !== VanillaChestTile::class){ - continue; - } - - $item_count = $this->convertTile($tile); - if($item_count === null){ - continue; - } - - $this->getLogger()->info("Converted tile at {$tile->getPosition()} ({$item_count} item(s))"); - $converted++; - } - - if($was_loaded){ - $world->unloadChunk($x, $z); - } - $total_conversions += $converted; - } - return $total_conversions; - } - - /** - * @param World $world - * @param int $max_ops_per_tick maximum chunk reads per tick - when this value is exceeded, this task sleeps (waits for next server tick) - * @return Generator - */ - public function revertWorld(World $world, int $max_ops_per_tick = 128) : Generator{ - $total_reversions = 0; - $read = 0; - foreach($world->getProvider()->getAllChunks(false, $this->getLogger()) as $coords => $data){ - if(++$read % $max_ops_per_tick === 0){ - // breathing exercise: all this php shitcode on a loop will otherwise eat your computer and your dog - yield from $this->sleep(); - } - - [$x, $z] = $coords; - $was_loaded = $world->isChunkLoaded($x, $z); - if(!$was_loaded && count($data->getData()->getTileNBT()) === 0){ // fast filter to avoid chunk load+unload - continue; - } - - $chunk = $world->loadChunk($x, $z); - if($chunk === null){ - continue; - } - - $reverted = 0; - foreach($chunk->getTiles() as $tile){ - if(!($tile instanceof FasterChest)){ - continue; - } - - $item_count = $this->revertTile($tile); - if($item_count === null){ - continue; - } - - $this->getLogger()->info("Reverted tile at {$tile->getPosition()} ({$item_count} item(s))"); - $reverted++; - } - - if($was_loaded){ - $world->unloadChunk($x, $z); - } - $total_reversions += $reverted; - } - return $total_reversions; - } - - public function onCommand(CommandSender $sender, Command $command, string $label, array $args) : bool{ - if(isset($args[0])){ - if($args[0] === "convert"){ - if(isset($args[1])){ - $world = $args[1]; - if(!$this->getServer()->getWorldManager()->loadWorld($world)){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be loaded."); - return true; - } - - $world = $this->getServer()->getWorldManager()->getWorldByName($world); - if($world === null){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be retrieved."); - return true; - } - - $sender->sendMessage(TextFormat::YELLOW . "Converting chests in {$world->getFolderName()}..."); - Await::f2c(function() use($world, $sender) : Generator{ - $result = yield from $this->convertWorld($world); - if(!($sender instanceof Player) || $sender->isConnected()){ - $sender->sendMessage(TextFormat::GREEN . "Converted {$result} chest(s) in {$world->getFolderName()}."); - } - }); - return true; - } - }elseif($args[0] === "revert"){ - if(isset($args[1])){ - $world = $args[1]; - if(!$this->getServer()->getWorldManager()->loadWorld($world)){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be loaded."); - return true; - } - - $world = $this->getServer()->getWorldManager()->getWorldByName($world); - if($world === null){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be retrieved."); - return true; - } - - $sender->sendMessage(TextFormat::YELLOW . "Reverting chests in {$world->getFolderName()}..."); - Await::f2c(function() use($world, $sender) : Generator{ - $result = yield from $this->revertWorld($world); - if(!($sender instanceof Player) || $sender->isConnected()){ - $sender->sendMessage(TextFormat::GREEN . "Reverted {$result} chest(s) in {$world->getFolderName()}."); - } - }); - return true; - } - } - } - - $sender->sendMessage(TextFormat::BOLD . TextFormat::YELLOW . "{$this->getName()} Help Command"); - $sender->sendMessage(TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . "- convert all vanilla chests in world to a fast chest"); - $sender->sendMessage(TextFormat::YELLOW . "/{$label} revert " . TextFormat::GRAY . "- revert all fast chests in world to a vanilla chest"); - $sender->sendMessage(" "); - $sender->sendMessage(TextFormat::YELLOW . "When should I use these commands?"); - $sender->sendMessage(implode(" ", [ - TextFormat::GRAY . "When you first install this plugin on your server, run " . TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . " on the", - "your main worlds (i.e., worlds that take long to /save-all). If you choose to uninstall this plugin from your server, be sure to run" . TextFormat::YELLOW, - "/{$label} revert " . TextFormat::GRAY . " on all of your worlds." - ])); - $sender->sendMessage(" "); - $sender->sendMessage(TextFormat::YELLOW . "What are the consequences of not running these commands?"); - $sender->sendMessage(implode(" ", [ - TextFormat::GRAY . "Not executing " . TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . " at the time of installation (or later) means existing", - "vanilla chests in your worlds will still impact /save-all performance. This will not affect your world data in any way. However, failing to execute", - TextFormat::YELLOW . "/{$label} revert " . TextFormat::GRAY . " before uninstalling this plugin means newly placed chests (and also those that were", - "converted) will be corrupted." - ])); - return true; - } -} + public const CHEST_TILE_ID = "fasterchest:chest"; + public const BARREL_TILE_ID = "fasterchest:barrel"; + + private Chest $internal_chest_block; + private Barrel $internal_barrel_block; + + /** @var array */ + private array $chunk_listeners = []; + + /** + * @throws \ReflectionException + */ + protected function onLoad() : void{ + TileFactory::getInstance()->register(FasterChest::class, [self::CHEST_TILE_ID]); + TileFactory::getInstance()->register(FasterBarrel::class, [self::BARREL_TILE_ID]); + + if($this->getServer()->getConfigGroup()->getPropertyInt(YmlServerProperties::DEBUG_LEVEL, 1) > 1){ + FasterChest::$logger = $this->getLogger(); + FasterBarrel::$logger = $this->getLogger(); + } + + if(!isset(FasterChest::$serializer)){ + FasterChest::$serializer = DefaultFasterChestSerializer::instance(); + } + if(!isset(FasterBarrel::$serializer)){ + FasterBarrel::$serializer = DefaultFasterChestSerializer::instance(); + } + + // we will be using an 'internal block' to set FasterChest tile in worlds (we will NOT use World::addTile, ::removeTile) + // this 'internal block' is the vanilla chest block but backed with a FasterChest tile class. + $internal_chest_block = VanillaBlocks::CHEST(); + $_idInfo = new ReflectionProperty($internal_chest_block, "idInfo"); + $_idInfo->setValue($internal_chest_block, new BlockIdentifier($internal_chest_block->getIdInfo()->getBlockTypeId(), FasterChest::class)); + $this->internal_chest_block = $internal_chest_block; + + $internal_barrel_block = VanillaBlocks::BARREL(); + $_idInfo = new ReflectionProperty($internal_barrel_block, "idInfo"); + $_idInfo->setValue($internal_barrel_block, new BlockIdentifier($internal_barrel_block->getIdInfo()->getBlockTypeId(), FasterBarrel::class)); + $this->internal_barrel_block = $internal_barrel_block; + } + + protected function onEnable() : void{ + FasterChest::$database = new LevelDB(Path::join($this->getDataFolder(), "chest.db"), [ + "compression" => LEVELDB_ZLIB_RAW_COMPRESSION, + "block_size" => 64 * 1024 + ]); + FasterBarrel::$database = FasterChest::$database; + + $this->getServer()->getPluginManager()->registerEvents($this, $this); + foreach($this->getServer()->getWorldManager()->getWorlds() as $world){ + $this->applyListenerToWorld($world); + } + } + + protected function onDisable() : void{ + } + + private function applyListenerToWorld(World $world) : void{ + $listener = ($this->chunk_listeners[$world->getId()] ??= new FasterChestChunkListener($this, $world)); + foreach($world->getLoadedChunks() as $hash => $_){ + World::getXZ($hash, $x, $z); + $world->registerChunkListener($listener, $x, $z); + } + } + + /** + * @param WorldLoadEvent $event + * @priority LOWEST + */ + public function onWorldLoad(WorldLoadEvent $event) : void{ + $this->applyListenerToWorld($event->getWorld()); + } + + /** + * @param WorldUnloadEvent $event + * @priority MONITOR + */ + public function onWorldUnload(WorldUnloadEvent $event) : void{ + $world = $event->getWorld(); + $listener = $this->chunk_listeners[$world->getId()]; + unset($this->chunk_listeners[$world->getId()]); + $world->unregisterChunkListenerFromAll($listener); + } + + /** + * @param ChunkLoadEvent $event + * @priority LOWEST + */ + public function onChunkLoad(ChunkLoadEvent $event) : void{ + $world = $event->getWorld(); + if(isset($this->chunk_listeners[$world->getId()])){ + $world->registerChunkListener($this->chunk_listeners[$world->getId()], $event->getChunkX(), $event->getChunkZ()); + } + } + + /** + * @param ChunkUnloadEvent $event + * @priority MONITOR + */ + public function onChunkUnload(ChunkUnloadEvent $event) : void{ + $world = $event->getWorld(); + if(isset($this->chunk_listeners[$world->getId()])){ + $world->unregisterChunkListener($this->chunk_listeners[$world->getId()], $event->getChunkX(), $event->getChunkZ()); + } + } + + /** + * @return Generator + */ + private function sleep() : Generator{ + return Await::promise(fn($resolve) => $this->getScheduler()->scheduleDelayedTask(new ClosureTask($resolve), 1)); + } + + public function convertChestTile(VanillaChestTile $tile) : ?int{ + $position = $tile->getPosition(); + $world = $position->world; + $block = $world->getBlockAt($position->x, $position->y, $position->z); + if(!($block instanceof Chest)){ + return null; + } + + $tile->unpair(); + $contents = $tile->getRealInventory()->getContents(); + $name = $tile->hasName() ? $tile->getName() : null; + $new_block = (clone $this->internal_chest_block)->setFacing($block->getFacing()); + + $world->setBlockAt($position->x, $position->y, $position->z, $new_block, false); + $new_tile = $world->getTileAt($position->x, $position->y, $position->z); + $new_tile instanceof FasterChest || throw new RuntimeException("Expected internal block to set a faster chest tile, got " . ($new_tile !== null ? $new_tile::class : "null")); + $new_tile->getRealInventory()->setContents($contents); + if($name !== null){ + $new_tile->setName($name); + } + $world->getBlockAt($position->x, $position->y, $position->z)->onPostPlace(); + return count($contents); + } + + public function convertBarrelTile(VanillaBarrelTile $tile) : ?int{ + $position = $tile->getPosition(); + $world = $position->world; + $block = $world->getBlockAt($position->x, $position->y, $position->z); + if(!($block instanceof Barrel)){ + return null; + } + + $contents = $tile->getInventory()->getContents(); + $name = $tile->hasName() ? $tile->getName() : null; + $new_block = (clone $this->internal_barrel_block)->setFacing($block->getFacing()); + + $world->setBlockAt($position->x, $position->y, $position->z, $new_block, false); + $new_tile = $world->getTileAt($position->x, $position->y, $position->z); + $new_tile instanceof FasterBarrel || throw new RuntimeException("Expected internal block to set a faster barrel tile, got " . ($new_tile !== null ? $new_tile::class : "null")); + $new_tile->getInventory()->setContents($contents); + if($name !== null){ + $new_tile->setName($name); + } + return count($contents); + } + + public function convertTile($tile) : ?int{ + if($tile instanceof VanillaChestTile){ + return $this->convertChestTile($tile); + } + if($tile instanceof VanillaBarrelTile){ + return $this->convertBarrelTile($tile); + } + return null; + } + + public function revertChestTile(FasterChest $tile) : ?int{ + $position = $tile->getPosition(); + $world = $position->world; + + $block = $world->getBlockAt($position->x, $position->y, $position->z); + if(!($block instanceof Chest)){ + return null; + } + + $tile->unpair(); + $identifier = FasterChest::dbIdFromPosition($position); + FasterChest::$database->delete($identifier); + $contents = $tile->getRealInventory()->getContents(); + $name = $tile->hasName() ? $tile->getName() : null; + $listener = $this->chunk_listeners[$world->getId()]; + $new_block = VanillaBlocks::CHEST()->setFacing($block->getFacing()); + + $world->setBlockAt($position->x, $position->y, $position->z, VanillaBlocks::AIR(), false); + $listener->excluding($position->x, $position->y, $position->z, static fn() => $world->setBlockAt($position->x, $position->y, $position->z, $new_block, false)); + $new_tile = $world->getTileAt($position->x, $position->y, $position->z); + $new_tile instanceof VanillaChestTile || throw new RuntimeException("Chest block did not set a chest tile, got " . ($new_tile !== null ? $new_tile::class : "null")); + $new_tile->getRealInventory()->setContents($contents); + if($name !== null){ + $new_tile->setName($name); + } + $world->getBlockAt($position->x, $position->y, $position->z)->onPostPlace(); + return count($contents); + } + + public function revertBarrelTile(FasterBarrel $tile) : ?int{ + $position = $tile->getPosition(); + $world = $position->world; + + $block = $world->getBlockAt($position->x, $position->y, $position->z); + if(!($block instanceof Barrel)){ + return null; + } + + $identifier = FasterBarrel::dbIdFromPosition($position); + FasterBarrel::$database->delete($identifier); + $contents = $tile->getInventory()->getContents(); + $name = $tile->hasName() ? $tile->getName() : null; + $listener = $this->chunk_listeners[$world->getId()]; + $new_block = VanillaBlocks::BARREL()->setFacing($block->getFacing()); + + $world->setBlockAt($position->x, $position->y, $position->z, VanillaBlocks::AIR(), false); + $listener->excluding($position->x, $position->y, $position->z, static fn() => $world->setBlockAt($position->x, $position->y, $position->z, $new_block, false)); + $new_tile = $world->getTileAt($position->x, $position->y, $position->z); + $new_tile instanceof VanillaBarrelTile || throw new RuntimeException("Barrel block did not set a barrel tile, got " . ($new_tile !== null ? $new_tile::class : "null")); + $new_tile->getInventory()->setContents($contents); + if($name !== null){ + $new_tile->setName($name); + } + return count($contents); + } + + public function revertTile($tile) : ?int{ + if($tile instanceof FasterChest){ + return $this->revertChestTile($tile); + } + if($tile instanceof FasterBarrel){ + return $this->revertBarrelTile($tile); + } + return null; + } + + /** + * @param World $world + * @param int $max_ops_per_tick maximum chunk reads per tick - when this value is exceeded, this task sleeps (waits for next server tick) + * @return Generator + */ + public function convertWorld(World $world, int $max_ops_per_tick = 128) : Generator{ + $total_conversions = 0; + $read = 0; + foreach($world->getProvider()->getAllChunks(false, $this->getLogger()) as $coords => $data){ + if(++$read % $max_ops_per_tick === 0){ + yield from $this->sleep(); + } + + [$x, $z] = $coords; + + $was_loaded = $world->isChunkLoaded($x, $z); + if(!$was_loaded && count($data->getData()->getTileNBT()) === 0){ + continue; + } + + $chunk = $world->loadChunk($x, $z); + if($chunk === null){ + continue; + } + + $converted = 0; + foreach($chunk->getTiles() as $tile){ + $item_count = null; + $tile_type = null; + + if($tile::class === VanillaChestTile::class){ + $item_count = $this->convertChestTile($tile); + $tile_type = "chest"; + } elseif($tile::class === VanillaBarrelTile::class){ + $item_count = $this->convertBarrelTile($tile); + $tile_type = "barrel"; + } + + if($item_count === null){ + continue; + } + + $this->getLogger()->info("Converted {$tile_type} at {$tile->getPosition()} ({$item_count} item(s))"); + $converted++; + } + + if(!$was_loaded){ + $world->unloadChunk($x, $z); + } + $total_conversions += $converted; + } + return $total_conversions; + } + + /** + * @param World $world + * @param int $max_ops_per_tick maximum chunk reads per tick - when this value is exceeded, this task sleeps (waits for next server tick) + * @return Generator + */ + public function revertWorld(World $world, int $max_ops_per_tick = 128) : Generator{ + $total_reversions = 0; + $read = 0; + foreach($world->getProvider()->getAllChunks(false, $this->getLogger()) as $coords => $data){ + if(++$read % $max_ops_per_tick === 0){ + yield from $this->sleep(); + } + + [$x, $z] = $coords; + $was_loaded = $world->isChunkLoaded($x, $z); + if(!$was_loaded && count($data->getData()->getTileNBT()) === 0){ + continue; + } + + $chunk = $world->loadChunk($x, $z); + if($chunk === null){ + continue; + } + + $reverted = 0; + foreach($chunk->getTiles() as $tile){ + $item_count = null; + $tile_type = null; + + if($tile instanceof FasterChest){ + $item_count = $this->revertChestTile($tile); + $tile_type = "chest"; + } elseif($tile instanceof FasterBarrel){ + $item_count = $this->revertBarrelTile($tile); + $tile_type = "barrel"; + } + + if($item_count === null){ + continue; + } + + $this->getLogger()->info("Reverted {$tile_type} at {$tile->getPosition()} ({$item_count} item(s))"); + $reverted++; + } + + if(!$was_loaded){ + $world->unloadChunk($x, $z); + } + $total_reversions += $reverted; + } + return $total_reversions; + } + + public function onCommand(CommandSender $sender, Command $command, string $label, array $args) : bool{ + if(isset($args[0])){ + if($args[0] === "convert"){ + if(isset($args[1])){ + $world = $args[1]; + if(!$this->getServer()->getWorldManager()->loadWorld($world)){ + $sender->sendMessage(TextFormat::RED . "World {$world} could not be loaded."); + return true; + } + + $world = $this->getServer()->getWorldManager()->getWorldByName($world); + if($world === null){ + $sender->sendMessage(TextFormat::RED . "World {$world} could not be retrieved."); + return true; + } + + $sender->sendMessage(TextFormat::YELLOW . "Converting chests and barrels in {$world->getFolderName()}..."); + Await::f2c(function() use($world, $sender) : Generator{ + $result = yield from $this->convertWorld($world); + if(!($sender instanceof Player) || $sender->isConnected()){ + $sender->sendMessage(TextFormat::GREEN . "Converted {$result} container(s) in {$world->getFolderName()}."); + } + }); + return true; + } + }elseif($args[0] === "revert"){ + if(isset($args[1])){ + $world = $args[1]; + if(!$this->getServer()->getWorldManager()->loadWorld($world)){ + $sender->sendMessage(TextFormat::RED . "World {$world} could not be loaded."); + return true; + } + + $world = $this->getServer()->getWorldManager()->getWorldByName($world); + if($world === null){ + $sender->sendMessage(TextFormat::RED . "World {$world} could not be retrieved."); + return true; + } + + $sender->sendMessage(TextFormat::YELLOW . "Reverting chests and barrels in {$world->getFolderName()}..."); + Await::f2c(function() use($world, $sender) : Generator{ + $result = yield from $this->revertWorld($world); + if(!($sender instanceof Player) || $sender->isConnected()){ + $sender->sendMessage(TextFormat::GREEN . "Reverted {$result} container(s) in {$world->getFolderName()}."); + } + }); + return true; + } + } + } + + $sender->sendMessage(TextFormat::BOLD . TextFormat::YELLOW . "{$this->getName()} Help Command"); + $sender->sendMessage(TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . "- convert all vanilla chests and barrels in world to fast containers"); + $sender->sendMessage(TextFormat::YELLOW . "/{$label} revert " . TextFormat::GRAY . "- revert all fast containers in world to vanilla containers"); + $sender->sendMessage(" "); + $sender->sendMessage(TextFormat::YELLOW . "When should I use these commands?"); + $sender->sendMessage(implode(" ", [ + TextFormat::GRAY . "When you first install this plugin on your server, run " . TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . " on the", + "your main worlds (i.e., worlds that take long to /save-all). If you choose to uninstall this plugin from your server, be sure to run" . TextFormat::YELLOW, + "/{$label} revert " . TextFormat::GRAY . " on all of your worlds." + ])); + $sender->sendMessage(" "); + $sender->sendMessage(TextFormat::YELLOW . "What are the consequences of not running these commands?"); + $sender->sendMessage(implode(" ", [ + TextFormat::GRAY . "Not executing " . TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . " at the time of installation (or later) means existing", + "vanilla containers in your worlds will still impact /save-all performance. This will not affect your world data in any way. However, failing to execute", + TextFormat::YELLOW . "/{$label} revert " . TextFormat::GRAY . " before uninstalling this plugin means newly placed containers (and also those that were", + "converted) will be corrupted." + ])); + return true; + } +} \ No newline at end of file diff --git a/src/cosmoverse/fasterchest/barrel/FasterBarrel.php b/src/cosmoverse/fasterchest/barrel/FasterBarrel.php new file mode 100644 index 0000000..bb2d1e5 --- /dev/null +++ b/src/cosmoverse/fasterchest/barrel/FasterBarrel.php @@ -0,0 +1,124 @@ +x, + (string) (int) $position->y, + (string) (int) $position->z, + $position->world->getFolderName() + ]); + } + + private string $db_identifier; + public bool $initialized = false; + + /** @var self::STATE_* */ + private int $state = self::STATE_NOT_LOADED; + + public function __construct(World $world, Vector3 $pos){ + $this->db_identifier = self::dbIdFromPosition(Position::fromObject($pos, $world)); + parent::__construct($world, $pos); + $this->inventory->getListeners()->add(FasterBarrelInventoryListener::instance()); + } + + public function readSaveData(CompoundTag $nbt) : void{ + $this->initialized = (bool) $nbt->getByte(self::TAG_INITIALIZED, 0); + parent::readSaveData($nbt); + } + + protected function writeSaveData(CompoundTag $nbt) : void{ + $nbt->setByte(self::TAG_INITIALIZED, (int) $this->initialized); + parent::writeSaveData($nbt); + } + + protected function loadItems(CompoundTag $tag) : void{ + } + + protected function saveItems(CompoundTag $tag) : void{ + if($this->state !== self::STATE_UNSAVED_CHANGES){ + self::$logger?->info("No need to save barrel at {$this->position}"); + return; + } + + $buffer = self::$serializer->serialize($this->inventory->getContents()); + self::$database->put($this->db_identifier, $buffer); + $this->state = self::STATE_ALL_SAVED_CHANGES; + self::$logger?->info("Saved barrel at {$this->position}"); + } + + public function getInventory() : BarrelInventory{ + $this->loadFromDb(); + return parent::getInventory(); + } + + public function getDbIdentifier() : string{ + return $this->db_identifier; + } + + /** + * @return self::STATE_* + */ + public function getState() : int{ + return $this->state; + } + + public function setUnsavedChanges() : void{ + $this->state = self::STATE_UNSAVED_CHANGES; + } + + public function loadFromDb(bool $force = false) : void{ + if($this->state !== self::STATE_NOT_LOADED && !$force){ + return; + } + $this->state = self::STATE_LOADING; + + if(!$this->initialized){ + $this->initialized = true; + self::$database->delete($this->db_identifier); + self::$logger?->info("Initialized barrel at {$this->position}"); + }else{ + $buffer = self::$database->get($this->db_identifier); + $contents = $buffer !== false ? self::$serializer->deserialize($buffer) : []; + $this->inventory->getListeners()->remove(FasterBarrelInventoryListener::instance()); + $this->inventory->setContents($contents); + $this->inventory->getListeners()->add(FasterBarrelInventoryListener::instance()); + self::$logger?->info("Loaded barrel from database at {$this->position}"); + } + $this->state = self::STATE_ALL_SAVED_CHANGES; + } + + protected function addAdditionalSpawnData(CompoundTag $nbt) : void{ + parent::addAdditionalSpawnData($nbt); + $nbt->setString(Tile::TAG_ID, TileFactory::getInstance()->getSaveId(Barrel::class)); + } +} \ No newline at end of file diff --git a/src/cosmoverse/fasterchest/barrel/FasterBarrelInventoryListener.php b/src/cosmoverse/fasterchest/barrel/FasterBarrelInventoryListener.php new file mode 100644 index 0000000..801979a --- /dev/null +++ b/src/cosmoverse/fasterchest/barrel/FasterBarrelInventoryListener.php @@ -0,0 +1,38 @@ +onAnyChange($inventory); + } + + public function onContentChange(Inventory $inventory, array $oldContents) : void{ + $this->onAnyChange($inventory); + } + + private function onAnyChange(Inventory $inventory) : void{ + assert($inventory instanceof BarrelInventory); + $position = $inventory->getHolder(); + $tile = $position->world->getTileAt($position->x, $position->y, $position->z); + if($tile instanceof FasterBarrel){ + $tile->setUnsavedChanges(); + } + } +} \ No newline at end of file From cbf6cbe1cbb9d08d5aecd0831c6918acc34e9ff5 Mon Sep 17 00:00:00 2001 From: TrixNEW Date: Fri, 15 Aug 2025 01:05:06 -0400 Subject: [PATCH 2/5] Impl Command --- plugin.yml | 10 +-- src/cosmoverse/fasterchest/Loader.php | 82 ++----------------- .../fasterchest/barrel/FasterBarrel.php | 1 - .../command/FasterchestCommand.php | 56 +++++++++++++ 4 files changed, 66 insertions(+), 83 deletions(-) create mode 100644 src/cosmoverse/fasterchest/command/FasterchestCommand.php diff --git a/plugin.yml b/plugin.yml index 7bbd184..1363f10 100644 --- a/plugin.yml +++ b/plugin.yml @@ -4,14 +4,6 @@ api: 5.0.0 version: 2.0.0-beta author: Muqsit -commands: - fasterchest: - description: FasterChest management commands - usage: /fasterchest convert/revert - permission: fasterchest.command.fasterchest - aliases: ["fasterchests"] - permissions: - fasterchest.command.fasterchest: - description: Ability to use /fasterchest command + manage.fasterchest.command: default: op \ No newline at end of file diff --git a/src/cosmoverse/fasterchest/Loader.php b/src/cosmoverse/fasterchest/Loader.php index 419fe08..892ef47 100644 --- a/src/cosmoverse/fasterchest/Loader.php +++ b/src/cosmoverse/fasterchest/Loader.php @@ -5,6 +5,7 @@ namespace cosmoverse\fasterchest; use cosmoverse\fasterchest\barrel\FasterBarrel; +use cosmoverse\fasterchest\command\FasterchestCommand; use Generator; use LevelDB; use pocketmine\block\Barrel; @@ -24,6 +25,8 @@ use pocketmine\player\Player; use pocketmine\plugin\PluginBase; use pocketmine\scheduler\ClosureTask; +use pocketmine\Server; +use pocketmine\utils\SingletonTrait; use pocketmine\utils\TextFormat; use pocketmine\world\World; use pocketmine\YmlServerProperties; @@ -35,7 +38,8 @@ use function implode; use const LEVELDB_ZLIB_RAW_COMPRESSION; -final class Loader extends PluginBase implements Listener{ +final class Loader extends PluginBase implements Listener { + use SingletonTrait; public const CHEST_TILE_ID = "fasterchest:chest"; public const BARREL_TILE_ID = "fasterchest:barrel"; @@ -50,6 +54,8 @@ final class Loader extends PluginBase implements Listener{ * @throws \ReflectionException */ protected function onLoad() : void{ + self::setInstance($this); + TileFactory::getInstance()->register(FasterChest::class, [self::CHEST_TILE_ID]); TileFactory::getInstance()->register(FasterBarrel::class, [self::BARREL_TILE_ID]); @@ -89,6 +95,8 @@ protected function onEnable() : void{ foreach($this->getServer()->getWorldManager()->getWorlds() as $world){ $this->applyListenerToWorld($world); } + + Server::getInstance()->getCommandMap()->register("fasterchest", new FasterchestCommand()); } protected function onDisable() : void{ @@ -378,76 +386,4 @@ public function revertWorld(World $world, int $max_ops_per_tick = 128) : Generat } return $total_reversions; } - - public function onCommand(CommandSender $sender, Command $command, string $label, array $args) : bool{ - if(isset($args[0])){ - if($args[0] === "convert"){ - if(isset($args[1])){ - $world = $args[1]; - if(!$this->getServer()->getWorldManager()->loadWorld($world)){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be loaded."); - return true; - } - - $world = $this->getServer()->getWorldManager()->getWorldByName($world); - if($world === null){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be retrieved."); - return true; - } - - $sender->sendMessage(TextFormat::YELLOW . "Converting chests and barrels in {$world->getFolderName()}..."); - Await::f2c(function() use($world, $sender) : Generator{ - $result = yield from $this->convertWorld($world); - if(!($sender instanceof Player) || $sender->isConnected()){ - $sender->sendMessage(TextFormat::GREEN . "Converted {$result} container(s) in {$world->getFolderName()}."); - } - }); - return true; - } - }elseif($args[0] === "revert"){ - if(isset($args[1])){ - $world = $args[1]; - if(!$this->getServer()->getWorldManager()->loadWorld($world)){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be loaded."); - return true; - } - - $world = $this->getServer()->getWorldManager()->getWorldByName($world); - if($world === null){ - $sender->sendMessage(TextFormat::RED . "World {$world} could not be retrieved."); - return true; - } - - $sender->sendMessage(TextFormat::YELLOW . "Reverting chests and barrels in {$world->getFolderName()}..."); - Await::f2c(function() use($world, $sender) : Generator{ - $result = yield from $this->revertWorld($world); - if(!($sender instanceof Player) || $sender->isConnected()){ - $sender->sendMessage(TextFormat::GREEN . "Reverted {$result} container(s) in {$world->getFolderName()}."); - } - }); - return true; - } - } - } - - $sender->sendMessage(TextFormat::BOLD . TextFormat::YELLOW . "{$this->getName()} Help Command"); - $sender->sendMessage(TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . "- convert all vanilla chests and barrels in world to fast containers"); - $sender->sendMessage(TextFormat::YELLOW . "/{$label} revert " . TextFormat::GRAY . "- revert all fast containers in world to vanilla containers"); - $sender->sendMessage(" "); - $sender->sendMessage(TextFormat::YELLOW . "When should I use these commands?"); - $sender->sendMessage(implode(" ", [ - TextFormat::GRAY . "When you first install this plugin on your server, run " . TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . " on the", - "your main worlds (i.e., worlds that take long to /save-all). If you choose to uninstall this plugin from your server, be sure to run" . TextFormat::YELLOW, - "/{$label} revert " . TextFormat::GRAY . " on all of your worlds." - ])); - $sender->sendMessage(" "); - $sender->sendMessage(TextFormat::YELLOW . "What are the consequences of not running these commands?"); - $sender->sendMessage(implode(" ", [ - TextFormat::GRAY . "Not executing " . TextFormat::YELLOW . "/{$label} convert " . TextFormat::GRAY . " at the time of installation (or later) means existing", - "vanilla containers in your worlds will still impact /save-all performance. This will not affect your world data in any way. However, failing to execute", - TextFormat::YELLOW . "/{$label} revert " . TextFormat::GRAY . " before uninstalling this plugin means newly placed containers (and also those that were", - "converted) will be corrupted." - ])); - return true; - } } \ No newline at end of file diff --git a/src/cosmoverse/fasterchest/barrel/FasterBarrel.php b/src/cosmoverse/fasterchest/barrel/FasterBarrel.php index bb2d1e5..94fd85d 100644 --- a/src/cosmoverse/fasterchest/barrel/FasterBarrel.php +++ b/src/cosmoverse/fasterchest/barrel/FasterBarrel.php @@ -19,7 +19,6 @@ class FasterBarrel extends Barrel{ public const TAG_INITIALIZED = "fasterbarrel:initialized"; - public const STATE_NOT_LOADED = 0; public const STATE_LOADING = 1; public const STATE_UNSAVED_CHANGES = 2; diff --git a/src/cosmoverse/fasterchest/command/FasterchestCommand.php b/src/cosmoverse/fasterchest/command/FasterchestCommand.php new file mode 100644 index 0000000..e1881ff --- /dev/null +++ b/src/cosmoverse/fasterchest/command/FasterchestCommand.php @@ -0,0 +1,56 @@ +"); + $this->setPermission("manage.fasterchest.command"); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args): void { + if (!$this->testPermissionSilent($sender)) { + $sender->sendMessage("You can't do TS."); + return; + } + + if (count($args) < 2 || !in_array($args[0], ["convert", "revert"])) { + $sender->sendMessage(TextFormat::YELLOW . "/$commandLabel convert " . TextFormat::GRAY . " - Convert containers to fast storage"); + $sender->sendMessage(TextFormat::YELLOW . "/$commandLabel revert " . TextFormat::GRAY . " - Revert containers to vanilla storage"); + return; + } + + $action = $args[0]; + $worldName = $args[1]; + $wm = Server::getInstance()->getWorldManager(); + $world = $wm->getWorldByName($worldName) ?? ($wm->loadWorld($worldName) ? $wm->getWorldByName($worldName) : null); + + if (is_null($world)) { + $sender->sendMessage(TextFormat::RED . "World '$worldName' could not be found or loaded."); + return; + } + + $actionText = $action === "convert" ? "Converting" : "Reverting"; + $sender->sendMessage(TextFormat::YELLOW . "$actionText containers in {$world->getFolderName()}..."); + + Await::f2c(function() use ($world, $sender, $action): Generator { + $plugin = Loader::getInstance(); + $result = $action === "convert" ? yield from $plugin->convertWorld($world) : yield from $plugin->revertWorld($world); + + if (!($sender instanceof Player) || $sender->isConnected()) { + $pastTense = $action === "convert" ? "Converted" : "Reverted"; + $sender->sendMessage(TextFormat::GREEN . "$pastTense $result container(s) in {$world->getFolderName()}."); + } + }); + } +} \ No newline at end of file From 62d407a10c9b507fb5abe3ac207684c475cddf63 Mon Sep 17 00:00:00 2001 From: TrixNEW Date: Fri, 15 Aug 2025 15:53:27 -0400 Subject: [PATCH 3/5] Optimizing Imports --- .idea/discord.xml | 2 +- src/cosmoverse/fasterchest/Loader.php | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.idea/discord.xml b/.idea/discord.xml index 104c42f..912db82 100644 --- a/.idea/discord.xml +++ b/.idea/discord.xml @@ -1,7 +1,7 @@ -