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..912db82
--- /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..1363f10 100644
--- a/plugin.yml
+++ b/plugin.yml
@@ -1,15 +1,9 @@
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
- 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/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..1d78fa5 100644
--- a/src/cosmoverse/fasterchest/Loader.php
+++ b/src/cosmoverse/fasterchest/Loader.php
@@ -4,24 +4,26 @@
namespace cosmoverse\fasterchest;
+use cosmoverse\fasterchest\barrel\FasterBarrel;
+use cosmoverse\fasterchest\command\FasterchestCommand;
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;
-use pocketmine\command\Command;
-use pocketmine\command\CommandSender;
use pocketmine\event\Listener;
use pocketmine\event\world\ChunkLoadEvent;
use pocketmine\event\world\ChunkUnloadEvent;
use pocketmine\event\world\WorldLoadEvent;
use pocketmine\event\world\WorldUnloadEvent;
-use pocketmine\player\Player;
use pocketmine\plugin\PluginBase;
use pocketmine\scheduler\ClosureTask;
-use pocketmine\utils\TextFormat;
+use pocketmine\Server;
+use pocketmine\utils\SingletonTrait;
use pocketmine\world\World;
use pocketmine\YmlServerProperties;
use ReflectionProperty;
@@ -29,325 +31,354 @@
use SOFe\AwaitGenerator\Await;
use Symfony\Component\Filesystem\Path;
use function count;
-use function implode;
use const LEVELDB_ZLIB_RAW_COMPRESSION;
-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;
- }
-}
+final class Loader extends PluginBase implements Listener {
+ use SingletonTrait;
+
+ 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{
+ self::setInstance($this);
+
+ 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);
+ }
+
+ Server::getInstance()->getCommandMap()->register("fasterchest", new FasterchestCommand());
+ }
+
+ 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 instanceof VanillaChestTile){
+ $item_count = $this->convertChestTile($tile);
+ $tile_type = "chest";
+ } elseif($tile instanceof VanillaBarrelTile){
+ $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;
+ }
+}
\ 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..76b0c4f
--- /dev/null
+++ b/src/cosmoverse/fasterchest/barrel/FasterBarrel.php
@@ -0,0 +1,123 @@
+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;
+ }
+
+ public 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
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