├── resources ├── config.yml └── message.ini ├── src └── Deceitya │ └── WorldEditya2 │ ├── Cache │ ├── CacheFileNotFoundException.php │ ├── WECache.php │ ├── CacheManager.php │ └── CacheProvider.php │ ├── Exception │ └── ChunkNotLoadedException.php │ ├── Config │ ├── WorldEdityaConfig.php │ └── MessageContainer.php │ ├── Selection │ ├── SelectionListener.php │ └── Selection.php │ ├── Command │ ├── Pos1Command.php │ ├── Pos2Command.php │ ├── UndoCommand.php │ ├── SetCommand.php │ └── ReplaceCommand.php │ ├── Main.php │ ├── Task │ ├── SetTask.php │ ├── ReplaceTask.php │ └── UndoTask.php │ └── WorldEdityaAPI.php ├── .poggit.yml ├── README.md ├── plugin.yml └── LICENSE /resources/config.yml: -------------------------------------------------------------------------------- 1 | pos-item: 285 -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Cache/CacheFileNotFoundException.php: -------------------------------------------------------------------------------- 1 | ` (metaは省略可能) | 14 | | `//replace` | `選択範囲内の指定のブロックを置き換える` | `//replace <対象のブロック> <置換後のブロック>` | 15 | | `//undo` | `最後の操作前の状態に戻す` | `//undo` | 16 | -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: WorldEditya2 2 | main: Deceitya\WorldEditya2\Main 3 | version: 1.1.0 4 | api: 3.12.0 5 | load: POSTWORLD 6 | author: deceitya 7 | description: わ~るどえでぃちゃ 8 | website: https://github.com/deceitya/WorldEditya2 9 | 10 | permissions: 11 | worldeditya2.command.pos1: 12 | description: Allows the admin to run the pos1 command 13 | default: op 14 | worldeditya2.command.pos2: 15 | description: Allows the admin to run the pos2 command 16 | default: op 17 | worldeditya2.command.set: 18 | description: Allows the admin to run the set command 19 | default: op 20 | worldeditya2.command.undo: 21 | description: Allows the admin to run the undo command 22 | default: op 23 | worldeditya2.command.replace: 24 | description: Allows the admin to run the replace command 25 | default: op -------------------------------------------------------------------------------- /resources/message.ini: -------------------------------------------------------------------------------- 1 | command.pos1.description="1つ目の座標を設定します。" 2 | command.pos1.success="1つ目の座標を設定しました。 (%0, %1, %2 / %3)" 3 | 4 | command.pos2.description="2つ目の座標を設定します。" 5 | command.pos2.success="2つ目の座標を設定しました。 (%0, %1, %2 / %3)" 6 | 7 | command.replace.description="選択範囲内の指定のブロックを置き換えます。" 8 | command.replace.start="&7#%0 %1が //replace を開始します。(%2ブロック)" 9 | command.replace.complete="&7#%0 完了しました。" 10 | 11 | command.set.description="選択範囲を指定したブロックで埋めます。" 12 | command.set.start="&7#%0 %1が //set を開始します。 (%2ブロック)" 13 | command.set.complete="&7#%0 完了しました。" 14 | 15 | command.undo.description="最後の操作前の状態に戻します。" 16 | command.undo.no_cache="&cキャッシュがありません。" 17 | command.undo.start="&7#%0 %1が //undo を開始します。" 18 | command.undo.complete="&7#%0 完了しました。" 19 | 20 | command.error.run_as_player="&cこのコマンドはプレイヤーとしてのみ実行できます。" 21 | 22 | chunk.not_loaded="&cチャンクが読み込まれていないため、実行することができません。" 23 | 24 | pos.invalid="&c正しく座標を設定してください。&f@npos1 : %0@npos2 : %1" -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Config/WorldEdityaConfig.php: -------------------------------------------------------------------------------- 1 | reload(); 22 | } 23 | 24 | /** 25 | * リロード 26 | * 27 | * @return void 28 | */ 29 | public function reload() 30 | { 31 | Main::getInstance()->reloadConfig(); 32 | $config = Main::getInstance()->getConfig(); 33 | 34 | $this->selectionItemId = $config->get('pos-item'); 35 | } 36 | 37 | /** 38 | * 範囲選択アイテムIDを取得 39 | * 40 | * @return integer 41 | */ 42 | public function getSelectionItemId(): int 43 | { 44 | return $this->selectionItemId; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Config/MessageContainer.php: -------------------------------------------------------------------------------- 1 | string> */ 22 | private static $messages = []; 23 | 24 | public static function init() 25 | { 26 | self::$messages = parse_ini_file(Main::getInstance()->getResourcesFolder().'message.ini'); 27 | } 28 | 29 | /** 30 | * テキストを取得 31 | * 32 | * @param string $key 33 | * @param string ...$params 34 | * @return string 35 | */ 36 | public static function get(string $key, string ...$params): string 37 | { 38 | if (!isset(self::$messages[$key])) { 39 | return $key; 40 | } 41 | 42 | $search = ['@n']; 43 | $replace = ["\n"]; 44 | $count = count($params); 45 | for ($i = 0; $i < $count; $i++) { 46 | $search[] = "%{$i}"; 47 | $replace[] = $params[$i]; 48 | } 49 | 50 | return TextFormat::colorize(str_replace($search, $replace, self::$messages[$key])); 51 | } 52 | 53 | private function __construct() 54 | { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Selection/SelectionListener.php: -------------------------------------------------------------------------------- 1 | item = Main::getInstance()->getWEConfing()->getSelectionItemId(); 25 | } 26 | 27 | public function setFirstPosition(PlayerInteractEvent $event) 28 | { 29 | if ($event->getAction() !== PlayerInteractEvent::RIGHT_CLICK_BLOCK) { 30 | return; 31 | } 32 | 33 | $player = $event->getPlayer(); 34 | if ($player->getInventory()->getItemInHand()->getId() === $this->item && $player->hasPermission('worldeditya2.command.pos1')) { 35 | $event->setCancelled(); 36 | 37 | $block = $event->getBlock()->floor(); 38 | $player->getServer()->dispatchCommand($player, "/pos1 {$block->x} {$block->y} {$block->z}"); 39 | } 40 | } 41 | 42 | public function setSecondPosition(BlockBreakEvent $event) 43 | { 44 | $player = $event->getPlayer(); 45 | if ($player->getInventory()->getItemInHand()->getId() === $this->item && $player->hasPermission('worldeditya2.command.pos2')) { 46 | $event->setCancelled(); 47 | 48 | $block = $event->getBlock()->floor(); 49 | $player->getServer()->dispatchCommand($player, "/pos2 {$block->x} {$block->y} {$block->z}"); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Command/Pos1Command.php: -------------------------------------------------------------------------------- 1 | registerArgument(0, new IntegerArgument('x', true)); 28 | $this->registerArgument(1, new IntegerArgument('y', true)); 29 | $this->registerArgument(2, new IntegerArgument('z', true)); 30 | 31 | $this->setPermission('worldeditya2.command.pos1'); 32 | } 33 | 34 | public function onRun(CommandSender $sender, string $aliasUsed, array $args): void 35 | { 36 | if (!($sender instanceof Player)) { 37 | $sender->sendMessage(MessageContainer::get('command.error.run_as_player')); 38 | 39 | return; 40 | } 41 | 42 | $count = count($args); 43 | $pos = null; 44 | if ($count === 0) { 45 | $pos = Position::fromObject($sender->floor(), $sender->level); 46 | } elseif ($count === 3) { 47 | $pos = Position::fromObject(new Vector3($args['x'], $args['y'], $args['z']), $sender->level); 48 | } else { 49 | $this->sendError(self::ERR_INSUFFICIENT_ARGUMENTS); 50 | 51 | return; 52 | } 53 | 54 | $selection = Selection::getSelection($sender); 55 | $selection->setFirstPosition($pos); 56 | 57 | $sender->sendMessage(MessageContainer::get( 58 | 'command.pos1.success', 59 | (string) $pos->x, 60 | (string) $pos->y, 61 | (string) $pos->z, 62 | $pos->level->getFolderName() 63 | )); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Command/Pos2Command.php: -------------------------------------------------------------------------------- 1 | registerArgument(0, new IntegerArgument('x', true)); 28 | $this->registerArgument(1, new IntegerArgument('y', true)); 29 | $this->registerArgument(2, new IntegerArgument('z', true)); 30 | 31 | $this->setPermission('worldeditya2.command.pos2'); 32 | } 33 | 34 | public function onRun(CommandSender $sender, string $aliasUsed, array $args): void 35 | { 36 | if (!($sender instanceof Player)) { 37 | $sender->sendMessage(MessageContainer::get('command.error.run_as_player')); 38 | 39 | return; 40 | } 41 | 42 | $count = count($args); 43 | $pos = null; 44 | if ($count === 0) { 45 | $pos = Position::fromObject($sender->floor(), $sender->level); 46 | } elseif ($count === 3) { 47 | $pos = Position::fromObject(new Vector3($args['x'], $args['y'], $args['z']), $sender->level); 48 | } else { 49 | $this->sendError(self::ERR_INSUFFICIENT_ARGUMENTS); 50 | 51 | return; 52 | } 53 | 54 | $selection = Selection::getSelection($sender); 55 | $selection->setSecondPosition($pos); 56 | 57 | $sender->sendMessage(MessageContainer::get( 58 | 'command.pos2.success', 59 | (string) $pos->x, 60 | (string) $pos->y, 61 | (string) $pos->z, 62 | $pos->level->getFolderName() 63 | )); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Command/UndoCommand.php: -------------------------------------------------------------------------------- 1 | setPermission('worldeditya2.command.undo'); 25 | } 26 | 27 | public function onRun(CommandSender $sender, string $aliasUsed, array $args): void 28 | { 29 | if (!($sender instanceof Player)) { 30 | $sender->sendMessage(MessageContainer::get('command.error.run_as_player')); 31 | 32 | return; 33 | } 34 | 35 | $cache = CacheManager::getInstance()->drop($sender->getName()); 36 | if ($cache === null) { 37 | $sender->sendMessage(MessageContainer::get('command.undo.no_cache')); 38 | 39 | return; 40 | } 41 | 42 | $level = $cache->getStartPosition()->level; 43 | $chunks = []; 44 | foreach ($cache->getChunks() as $chunk) { 45 | $chunks[] = $level->getChunk($chunk->getX(), $chunk->getZ()); 46 | } 47 | 48 | $task = new UndoTask( 49 | $chunks, 50 | $cache->getChunks(), 51 | $cache->getStartPosition(), 52 | $cache->getEndPosition(), 53 | function (UndoTask $task) { 54 | Server::getInstance()->broadcastMessage(MessageContainer::get('command.undo.complete', (string) $task->getTaskId())); 55 | } 56 | ); 57 | $sender->getServer()->getAsyncPool()->submitTask($task); 58 | 59 | $sender->getServer()->broadcastMessage(MessageContainer::get( 60 | 'command.undo.start', 61 | (string) $task->getTaskId(), 62 | $sender->getName() 63 | )); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Main.php: -------------------------------------------------------------------------------- 1 | config = new WorldEdityaConfig; 44 | $this->config->reload(); 45 | 46 | MessageContainer::init(); 47 | $this->getServer()->getPluginManager()->registerEvents(new SelectionListener, $this); 48 | $this->getServer()->getCommandMap()->registerAll('WorldEditya2', [ 49 | new Pos1Command('/pos1', MessageContainer::get('command.pos1.description')), 50 | new Pos2Command('/pos2', MessageContainer::get('command.pos2.description')), 51 | new ReplaceCommand('/replace', MessageContainer::get('command.replace.description')), 52 | new SetCommand('/set', MessageContainer::get('command.set.description')), 53 | new UndoCommand('/undo', MessageContainer::get('command.undo.description')) 54 | ]); 55 | } 56 | 57 | public function onDisable() 58 | { 59 | CacheManager::getInstance()->clear(); 60 | } 61 | 62 | public function getResourcesFolder(): string 63 | { 64 | return "{$this->getFile()}resources/"; 65 | } 66 | 67 | public function getWEConfing(): WorldedityaConfig 68 | { 69 | return $this->config; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Cache/WECache.php: -------------------------------------------------------------------------------- 1 | chunks = $chunks; 39 | $this->start = $start; 40 | $this->end = $end; 41 | } 42 | 43 | /** 44 | * @return Chunk[] 45 | */ 46 | public function getChunks(): array 47 | { 48 | return $this->chunks; 49 | } 50 | 51 | public function getStartPosition(): Position 52 | { 53 | return $this->start; 54 | } 55 | 56 | public function getEndPosition(): Position 57 | { 58 | return $this->end; 59 | } 60 | 61 | public function serialize() 62 | { 63 | return serialize([ 64 | array_map(function (Chunk $chunk) { 65 | return $chunk->fastSerialize(); 66 | }, $this->chunks), 67 | [$this->start->x, $this->start->y, $this->start->z], 68 | [$this->end->x, $this->end->y, $this->end->z], 69 | $this->start->level->getFolderName(), 70 | ]); 71 | } 72 | 73 | public function unserialize($serialized) 74 | { 75 | $data = unserialize($serialized); 76 | Server::getInstance()->loadLevel($data[3]); 77 | $level = Server::getInstance()->getLevelByName($data[3]); 78 | $this->chunks = array_map(function ($chunkData) { 79 | return Chunk::fastDeserialize($chunkData); 80 | }, $data[0]); 81 | $this->start = Position::fromObject(new Vector3($data[1][0], $data[1][1], $data[1][2]), $level); 82 | $this->end = Position::fromObject(new Vector3($data[2][0], $data[2][1], $data[2][2]), $level); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Cache/CacheManager.php: -------------------------------------------------------------------------------- 1 | provider = new CacheProvider(Main::getInstance()->getDataFolder().'caches/'); 46 | } 47 | 48 | /** 49 | * キャッシュを追加 50 | * 51 | * @param string $player 52 | * @param WECache $cache 53 | * @return void 54 | */ 55 | public function add(string $player, WECache $cache) 56 | { 57 | if (!isset($this->caches[$player])) { 58 | $this->caches[$player] = []; 59 | } 60 | 61 | $key = "$player".count($this->caches[$player]); 62 | array_unshift($this->caches[$player], $key); 63 | $this->provider->set($key, serialize($cache)); 64 | } 65 | 66 | /** 67 | * 一番新しいキャッシュを取得 68 | * 69 | * @param string $player 70 | * @return WECache|null 71 | */ 72 | public function drop(string $player): ?WECache 73 | { 74 | if (!isset($this->caches[$player])) { 75 | return null; 76 | } 77 | 78 | $key = array_shift($this->caches[$player]); 79 | if ($key === null) { 80 | return null; 81 | } 82 | 83 | $cache = unserialize($this->provider->get($key)); 84 | $this->provider->delete($key); 85 | 86 | return $cache; 87 | } 88 | 89 | /** 90 | * キャッシュを全削除 91 | * 92 | * @return void 93 | */ 94 | public function clear() 95 | { 96 | $this->caches = []; 97 | $this->provider->deleteAll(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Cache/CacheProvider.php: -------------------------------------------------------------------------------- 1 | folder = $folder; 36 | $this->ext = $ext; 37 | } 38 | 39 | /** 40 | * キャッシュ取得 41 | * 42 | * @param string $key 43 | * @return string 44 | */ 45 | public function get(string $key): string 46 | { 47 | $file = $this->getFile($key); 48 | if (file_exists($file)) { 49 | return file_get_contents($file); 50 | } else { 51 | throw new CacheFileNotFoundException($file); 52 | } 53 | } 54 | 55 | /** 56 | * キャッシュを保存・上書き 57 | * 58 | * @param string $key 59 | * @param string $data 60 | * @return void 61 | */ 62 | public function set(string $key, string $data) 63 | { 64 | file_put_contents($this->getFile($key), $data); 65 | } 66 | 67 | /** 68 | * キャッシュファイル削除 69 | * 70 | * @param string $key 71 | * @return void 72 | */ 73 | public function delete(string $key) 74 | { 75 | return unlink($this->getFile($key)); 76 | } 77 | 78 | /** 79 | * 全部のキャッシュのキーを取得 80 | * 81 | * @return Generator 82 | */ 83 | public function getKeys(): Generator 84 | { 85 | $files = glob("{$this->folder}*{$this->ext}"); 86 | foreach ($files as $file) { 87 | yield str_replace([$this->folder, $this->ext], ['', ''], $file); 88 | } 89 | } 90 | 91 | /** 92 | * 全キャッシュファイル削除 93 | * 94 | * @return void 95 | */ 96 | public function deleteAll() 97 | { 98 | foreach ($this->getKeys() as $key) { 99 | $this->delete($key); 100 | } 101 | } 102 | 103 | /** 104 | * キャッシュファイル取得 105 | * 106 | * @param string $key 107 | * @return string 108 | */ 109 | private function getFile(string $key): string 110 | { 111 | return "{$this->folder}{$key}{$this->ext}"; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Task/SetTask.php: -------------------------------------------------------------------------------- 1 | chunks = array_map(function (Chunk $chunk) { 49 | return $chunk->fastSerialize(); 50 | }, $chunks); 51 | $this->start = [$start->x, $start->y, $start->z]; 52 | $this->end = [$end->x, $end->y, $end->z]; 53 | $this->id = $block->getId(); 54 | $this->meta = $block->getDamage(); 55 | $this->level = $start->level->getId(); 56 | $this->onDone = $onDone; 57 | } 58 | 59 | public function onRun() 60 | { 61 | $chunks = []; 62 | foreach ($this->chunks as $chunkData) { 63 | $chunk = Chunk::fastDeserialize($chunkData); 64 | $chunks[Level::chunkHash($chunk->getX(), $chunk->getZ())] = $chunk; 65 | } 66 | 67 | for ($x = $this->start[0]; $x <= $this->end[0]; $x++) { 68 | $chunkX = $x >> 4; 69 | $blockX = $x % 16; 70 | for ($z = $this->start[2]; $z <= $this->end[2]; $z++) { 71 | $blockZ = $z % 16; 72 | $chunk = $chunks[Level::chunkHash($chunkX, $z >> 4)]; 73 | for ($y = $this->start[1]; $y <= $this->end[1]; $y++) { 74 | $chunk->setBlock($blockX, $y, $blockZ, $this->id, $this->meta); 75 | } 76 | } 77 | } 78 | 79 | $this->setResult(serialize($chunks)); 80 | } 81 | 82 | public function onCompletion(Server $server) 83 | { 84 | $level = $server->getLevel($this->level); 85 | foreach (unserialize($this->getResult()) as $chunk) { 86 | $level->setChunk($chunk->getX(), $chunk->getZ(), $chunk); 87 | } 88 | 89 | if ($this->onDone !== null) { 90 | $onDone = $this->onDone; 91 | $onDone($this); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Task/ReplaceTask.php: -------------------------------------------------------------------------------- 1 | chunks = array_map(function (Chunk $chunk) { 50 | return $chunk->fastSerialize(); 51 | }, $chunks); 52 | $this->start = [$start->x, $start->y, $start->z]; 53 | $this->end = [$end->x, $end->y, $end->z]; 54 | $this->search = [$search->getId(), $search->getDamage()]; 55 | $this->replace = [$replace->getId(), $replace->getDamage()]; 56 | $this->level = $start->level->getId(); 57 | $this->onDone = $onDone; 58 | } 59 | 60 | public function onRun() 61 | { 62 | $chunks = []; 63 | foreach ($this->chunks as $chunkData) { 64 | $chunk = Chunk::fastDeserialize($chunkData); 65 | $chunks[Level::chunkHash($chunk->getX(), $chunk->getZ())] = $chunk; 66 | } 67 | 68 | for ($x = $this->start[0]; $x <= $this->end[0]; $x++) { 69 | $chunkX = $x >> 4; 70 | $blockX = $x % 16; 71 | for ($z = $this->start[2]; $z <= $this->end[2]; $z++) { 72 | $blockZ = $z % 16; 73 | $chunk = $chunks[Level::chunkHash($chunkX, $z >> 4)]; 74 | for ($y = $this->start[1]; $y <= $this->end[1]; $y++) { 75 | if ($chunk->getBlockId($blockX, $y, $blockZ) === $this->search[0] && $chunk->getBlockData($blockX, $y, $blockZ) === $this->search[1]) { 76 | $chunk->setBlock($blockX, $y, $blockZ, $this->replace[0], $this->replace[1]); 77 | } 78 | } 79 | } 80 | } 81 | 82 | $this->setResult(serialize($chunks)); 83 | } 84 | 85 | public function onCompletion(Server $server) 86 | { 87 | $level = $server->getLevel($this->level); 88 | foreach (unserialize($this->getResult()) as $chunk) { 89 | $level->setChunk($chunk->getX(), $chunk->getZ(), $chunk); 90 | } 91 | 92 | if ($this->onDone !== null) { 93 | $onDone = $this->onDone; 94 | $onDone($this); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/WorldEdityaAPI.php: -------------------------------------------------------------------------------- 1 | floor(); 33 | $end = Selection::maxComponents($pos1, $pos2)->floor(); 34 | $chunks = self::getChunks($level, $start, $end); 35 | 36 | Server::getInstance()->getAsyncPool()->submitTask(new SetTask($chunks, Position::fromObject($start, $level), Position::fromObject($end, $level), $block, $onDone)); 37 | } catch (\Exception $e) { 38 | Server::getInstance()->getLogger()->logException($e); 39 | 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** 47 | * //replace 48 | * 49 | * @param Level $level 50 | * @param Vector3 $pos1 51 | * @param Vector3 $pos2 52 | * @param Block $search 53 | * @param Block $replace 54 | * @param callable|null $onDone 55 | * @return boolean 56 | */ 57 | public static function replace(Level $level, Vector3 $pos1, Vector3 $pos2, Block $search, Block $replace, ?callable $onDone = null): bool 58 | { 59 | try { 60 | $start = Selection::minComponents($pos1, $pos2)->floor(); 61 | $end = Selection::maxComponents($pos1, $pos2)->floor(); 62 | $chunks = self::getChunks($level, $start, $end); 63 | 64 | Server::getInstance()->getAsyncPool()->submitTask(new ReplaceTask($chunks, Position::fromObject($start, $level), Position::fromObject($end, $level), $search, $replace, $onDone)); 65 | } catch (\Exception $e) { 66 | Server::getInstance()->getLogger()->logException($e); 67 | 68 | return false; 69 | } 70 | 71 | return true; 72 | } 73 | 74 | private static function getChunks(Level $level, Vector3 $start, Vector3 $end): array 75 | { 76 | $chunks = []; 77 | $maxX = $end->x >> 4; 78 | $maxZ = $end->z >> 4; 79 | for ($x = $start->x >> 4; $x <= $maxX; $x++) { 80 | for ($z = $start->z >> 4; $z <= $maxZ; $z++) { 81 | $chunk = $level->getChunk($x, $z); 82 | if ($chunk !== null) { 83 | $chunks[] = $chunk; 84 | } else { 85 | throw new ChunkNotLoadedException("X:{$x} Z:{$z}"); 86 | } 87 | } 88 | } 89 | 90 | return $chunks; 91 | } 92 | 93 | private function __construct() 94 | { 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Task/UndoTask.php: -------------------------------------------------------------------------------- 1 | currents = array_map(function (Chunk $chunk) { 41 | return $chunk->fastSerialize(); 42 | }, $currents); 43 | $this->caches = array_map(function (Chunk $chunk) { 44 | return $chunk->fastSerialize(); 45 | }, $caches); 46 | $this->start = [$start->x, $start->y, $start->z]; 47 | $this->end = [$end->x, $end->y, $end->z]; 48 | $this->level = $start->level->getId(); 49 | $this->onDone = $onDone; 50 | } 51 | 52 | public function onRun() 53 | { 54 | $currents = []; 55 | $caches = []; 56 | foreach ($this->currents as $chunkData) { 57 | $chunk = Chunk::fastDeserialize($chunkData); 58 | $currents[Level::chunkHash($chunk->getX(), $chunk->getZ())] = $chunk; 59 | } 60 | foreach ($this->caches as $chunkData) { 61 | $chunk = Chunk::fastDeserialize($chunkData); 62 | $caches[Level::chunkHash($chunk->getX(), $chunk->getZ())] = $chunk; 63 | } 64 | 65 | for ($x = $this->start[0]; $x <= $this->end[0]; $x++) { 66 | $chunkX = $x >> 4; 67 | $blockX = $x % 16; 68 | for ($z = $this->start[2]; $z <= $this->end[2]; $z++) { 69 | $hash = Level::chunkHash($chunkX, $z >> 4); 70 | $current = $currents[$hash]; 71 | $cache = $caches[$hash]; 72 | $blockZ = $z % 16; 73 | for ($y = $this->start[1]; $y <= $this->end[1]; $y++) { 74 | $current->setBlock( 75 | $blockX, 76 | $y, 77 | $blockZ, 78 | $cache->getBlockId($blockX, $y, $blockZ), 79 | $cache->getBlockData($blockX, $y, $blockZ) 80 | ); 81 | } 82 | } 83 | } 84 | 85 | $this->setResult(serialize($currents)); 86 | } 87 | 88 | public function onCompletion(Server $server) 89 | { 90 | $level = $server->getLevel($this->level); 91 | foreach (unserialize($this->getResult()) as $chunk) { 92 | $level->setChunk($chunk->getX(), $chunk->getZ(), $chunk); 93 | } 94 | 95 | if ($this->onDone !== null) { 96 | $onDone = $this->onDone; 97 | $onDone($this); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Selection/Selection.php: -------------------------------------------------------------------------------- 1 | Selection] */ 22 | private static $selections = []; 23 | 24 | /** 25 | * プレイヤーのSelectionを返す 26 | * 27 | * @param Player $player 28 | * @return Selection 29 | */ 30 | public static function getSelection(Player $player): Selection 31 | { 32 | $name = $player->getName(); 33 | if (!isset(self::$selections[$name])) { 34 | self::$selections[$name] = new Selection; 35 | } 36 | 37 | return self::$selections[$name]; 38 | } 39 | 40 | public static function maxComponents(Vector3 ...$positions): Vector3 41 | { 42 | $xList = $yList = $zList = []; 43 | foreach ($positions as $position) { 44 | $xList[] = $position->x; 45 | $yList[] = $position->y; 46 | $zList[] = $position->z; 47 | } 48 | 49 | return new Vector3(max($xList), max($yList), max($zList)); 50 | } 51 | 52 | public static function minComponents(Vector3 ...$positions): Vector3 53 | { 54 | $xList = $yList = $zList = []; 55 | foreach ($positions as $position) { 56 | $xList[] = $position->x; 57 | $yList[] = $position->y; 58 | $zList[] = $position->z; 59 | } 60 | 61 | return new Vector3(min($xList), min($yList), min($zList)); 62 | } 63 | 64 | /** @var Position|null */ 65 | private $pos1 = null; 66 | /** @var Position|null */ 67 | private $pos2 = null; 68 | 69 | public function getFirstPosition(): ?Position 70 | { 71 | return $this->pos1; 72 | } 73 | 74 | public function setFirstPosition(Position $pos) 75 | { 76 | $this->pos1 = $pos; 77 | } 78 | 79 | public function getSecondPosition(): ?Position 80 | { 81 | return $this->pos2; 82 | } 83 | 84 | public function setSecondPosition(Position $pos) 85 | { 86 | $this->pos2 = $pos; 87 | } 88 | 89 | /** 90 | * 始点 91 | * 92 | * @return Position 93 | */ 94 | public function getStartPosition(): Position 95 | { 96 | return Position::fromObject(self::minComponents($this->pos1, $this->pos2), $this->pos1->level); 97 | } 98 | 99 | /** 100 | * 終点 101 | * 102 | * @return Position 103 | */ 104 | public function getEndPosition(): Position 105 | { 106 | return Position::fromObject(self::maxComponents($this->pos1, $this->pos2), $this->pos1->level); 107 | } 108 | 109 | /** 110 | * 選択範囲のブロック数 111 | * 112 | * @return integer 113 | */ 114 | public function count(): int 115 | { 116 | if ($this->pos1 instanceof Position && $this->pos2 instanceof Position) { 117 | $min = self::minComponents($this->pos1, $this->pos2); 118 | $max = self::maxComponents($this->pos1, $this->pos2); 119 | 120 | return ($max->x - $min->x + 1) * ($max->y - $min->y + 1) * ($max->z - $min->z + 1); 121 | } 122 | 123 | return 0; 124 | } 125 | 126 | /** 127 | * 実行可能か 128 | * 129 | * @return boolean pos1とpos2のワールドが違う場合false 130 | */ 131 | public function canExecute(): bool 132 | { 133 | return ($this->pos1 instanceof Position && $this->pos2 instanceof Position) && $this->pos1->level === $this->pos2->level; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Command/SetCommand.php: -------------------------------------------------------------------------------- 1 | registerArgument(0, new RawStringArgument('block')); 33 | 34 | $this->setPermission('worldeditya2.command.set'); 35 | } 36 | 37 | public function onRun(CommandSender $sender, string $aliasUsed, array $args): void 38 | { 39 | if (!($sender instanceof Player)) { 40 | $sender->sendMessage(MessageContainer::get('command.error.run_as_player')); 41 | 42 | return; 43 | } 44 | 45 | $selection = Selection::getSelection($sender); 46 | if ($selection->canExecute()) { 47 | $blockData = explode(':', $args['block']); 48 | $start = $selection->getStartPosition(); 49 | $end = $selection->getEndPosition(); 50 | $chunks = []; 51 | $maxx = $end->x >> 4; 52 | $maxz = $end->z >> 4; 53 | for ($x = $start->x >> 4; $x <= $maxx; $x++) { 54 | for ($z = $start->z >> 4; $z <= $maxz; $z++) { 55 | $chunk = $sender->level->getChunk($x, $z); 56 | if ($chunk !== null) { 57 | $chunks[] = $chunk; 58 | } else { 59 | $sender->sendMessage(MessageContainer::get('chunk.not_loaded')); 60 | 61 | return; 62 | } 63 | } 64 | } 65 | 66 | $block = null; 67 | try { 68 | $block = BlockFactory::get((int) $blockData[0], (int) (isset($blockData[1]) ? $blockData[1] : 0)); 69 | } catch (InvalidArgumentException $e) { 70 | $sender->sendMessage(TextFormat::RED . $e->getMessage()); 71 | 72 | return; 73 | } 74 | 75 | $task = new SetTask( 76 | $chunks, 77 | $start, 78 | $end, 79 | $block, 80 | function (SetTask $task) { 81 | Server::getInstance()->broadcastMessage(MessageContainer::get('command.set.complete', (string) $task->getTaskId())); 82 | } 83 | ); 84 | $sender->getServer()->getAsyncPool()->submitTask($task); 85 | 86 | CacheManager::getInstance()->add($sender->getName(), new WECache($chunks, $start, $end)); 87 | 88 | $sender->getServer()->broadcastMessage(MessageContainer::get( 89 | 'command.set.start', 90 | (string) $task->getTaskId(), 91 | $sender->getName(), 92 | (string) $selection->count() 93 | )); 94 | } else { 95 | $pos1 = $selection->getFirstPosition(); 96 | $pos2 = $selection->getSecondPosition(); 97 | $sender->sendMessage(MessageContainer::get( 98 | 'pos.invalid', 99 | $pos1 === null ? 'None' : "{$pos1->x}, {$pos1->y}, {$pos1->z} / {$pos1->level->getFolderName()}", 100 | $pos2 === null ? 'None' : "{$pos2->x}, {$pos2->y}, {$pos2->z} / {$pos2->level->getFolderName()}" 101 | )); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Deceitya/WorldEditya2/Command/ReplaceCommand.php: -------------------------------------------------------------------------------- 1 | registerArgument(0, new RawStringArgument('search')); 33 | $this->registerArgument(1, new RawStringArgument('replace')); 34 | 35 | $this->setPermission('worldeditya2.command.replace'); 36 | } 37 | 38 | public function onRun(CommandSender $sender, string $aliasUsed, array $args): void 39 | { 40 | if (!($sender instanceof Player)) { 41 | $sender->sendMessage(MessageContainer::get('command.error.run_as_player')); 42 | 43 | return; 44 | } 45 | 46 | $selection = Selection::getSelection($sender); 47 | if ($selection->canExecute()) { 48 | $start = $selection->getStartPosition(); 49 | $end = $selection->getEndPosition(); 50 | $chunks = []; 51 | $maxx = $end->x >> 4; 52 | $maxz = $end->z >> 4; 53 | for ($x = $start->x >> 4; $x <= $maxx; $x++) { 54 | for ($z = $start->z >> 4; $z <= $maxz; $z++) { 55 | $chunk = $sender->level->getChunk($x, $z); 56 | if ($chunk !== null) { 57 | $chunks[] = $chunk; 58 | } else { 59 | $sender->sendMessage(MessageContainer::get('chunk.not_loaded')); 60 | 61 | return; 62 | } 63 | } 64 | } 65 | 66 | $searchData = explode(':', $args['search']); 67 | $replaceData = explode(':', $args['replace']); 68 | $search = null; 69 | $replace = null; 70 | try { 71 | $search = BlockFactory::get((int) $searchData[0], (int) (isset($searchData[1]) ? $searchData[1] : 0)); 72 | $replace = BlockFactory::get((int) $replaceData[0], (int) (isset($replaceData[1]) ? $replaceData[1] : 0)); 73 | } catch (InvalidArgumentException $e) { 74 | $sender->sendMessage(TextFormat::RED . $e->getMessage()); 75 | 76 | return; 77 | } 78 | 79 | $task = new ReplaceTask( 80 | $chunks, 81 | $start, 82 | $end, 83 | $search, 84 | $replace, 85 | function (ReplaceTask $task) { 86 | Server::getInstance()->broadcastMessage(MessageContainer::get('command.replace.complete', (string) $task->getTaskId())); 87 | } 88 | ); 89 | $sender->getServer()->getAsyncPool()->submitTask($task); 90 | 91 | CacheManager::getInstance()->add($sender->getName(), new WECache($chunks, $start, $end)); 92 | 93 | $sender->getServer()->broadcastMessage(MessageContainer::get( 94 | 'command.replace.start', 95 | (string) $task->getTaskId(), 96 | $sender->getName(), 97 | (string) $selection->count() 98 | )); 99 | } else { 100 | $pos1 = $selection->getFirstPosition(); 101 | $pos2 = $selection->getSecondPosition(); 102 | $sender->sendMessage(MessageContainer::get( 103 | 'pos.invalid', 104 | $pos1 === null ? 'None' : "{$pos1->x}, {$pos1->y}, {$pos1->z} / {$pos1->level->getFolderName()}", 105 | $pos2 === null ? 'None' : "{$pos2->x}, {$pos2->y}, {$pos2->z} / {$pos2->level->getFolderName()}" 106 | )); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | --------------------------------------------------------------------------------