diff --git a/README.md b/README.md index e8e74a8..5949126 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,16 @@ _The `EasyLibrary` is a powerful plugin for PocketMine-MP that serves as a colle To use EasyLibrary's features you can do it in 2 ways, use the one you find most interesting, I recommend using it as a plug for greater ease! - **As a plugin:** Being the simplest way where you just need to add EasyLibrary.phar to your server and it will start all the library components, you can modify which components you want to keep active in the plugin's `config.yml` if you want! -- **As a inyernal library:** You must add it inside the plugin's src, keeping it in the root src folder for everything to work normally. Using this method, you will have to start the components yourself in your main. +- **As a internal library:** You must add it inside the plugin's src, keeping it in the root src folder for everything to work normally. Using this method, you will have to start the components yourself in your main. ### Examples -Example PocketMine-MP plugin with EasyLibrary, showing some basic features of the Library: [PluginExample](https://github.com/ImperaZim/EasyLibrary-Plugin-Example). +Example PocketMine-MP plugin with EasyLibrary, showing some basic features of the Library: [LibForm Docs](https://github.com/ImperaZim/LibForm), [LibCommand Docs](https://github.com/ImperaZim/LibCommand). ## Licensing information This project is licensed under MIT. Please see the [LICENSE](/LICENSE) file for details. ## Credits Public libraries integrated into the source code to facilitate the use of EasyLibrary. I do not have any rights to the libraries below! -- [CortexPE/Commando](https://github.com/CortexPE/Commando) - [Muqsit/InvMenu](https://github.com/Muqsit/InvMenu) -- [Muqsit/SimplePacketHandler](https://github.com/Muqsit/SimplePacketHandler) - [CustomiesDevs/Customies](https://github.com/CustomiesDevs/Customies) diff --git a/changelogs/1.3.md b/changelogs/1.3.md index ec697e3..cd02b5c 100644 --- a/changelogs/1.3.md +++ b/changelogs/1.3.md @@ -1,12 +1,14 @@ -# 1.3.0 +**Versions:** [1.3.0](https://github.com/ImperaZim/EasyLibrary/blob/development/changelogs/1.3.md#13) | [1.3.1](https://github.com/ImperaZim/EasyLibrary/blob/development/changelogs/1.3.md#131) | [1.3.2](https://github.com/ImperaZim/EasyLibrary/blob/development/changelogs/1.3.md#132) + +# 1.3 Released February 14, 2025 -**For PocketMine-MP 5.24.0** +**For PocketMine-MP 5.24.X** -1.3.0 is a major feature update to EasyLibrary, including support for PocketMine-MP 5.24.0 plugins, bringing a large list of features and improvements over the previous version. +1.3.0 is a major feature update to EasyLibrary, including support for PocketMine-MP 5.24.X plugins, bringing a large list of features and improvements over the previous version. -It is **not** compatible with plugins written for version 1.2.0 or lower, and plugins may require code changes to work with it. +It is **not** compatible with plugins written for version 1.2.X or lower, and plugins may require code changes to work with it. **As this changelog is quite large, its accuracy and completeness cannot be guaranteed. Make sure you're looking at the [latest revision](https://github.com/ImperaZim/EasyLibrary/blob/development/changelogs/1.3.md), as it may be amended after relatest** @@ -912,8 +914,148 @@ echo "Current time: " . TimeUtils::GetCurrentTime(); --- # 1.3.1 -**For PocketMine-MP 5.25.0** +**For PocketMine-MP 5.25.X** This is an update to fix changes made and bugs caused by PocketMine-MP 5.25x. It is not compatible with plugins written for version 1.3.0 or lower, and plugins may require code changes to work with it. + + +# 1.3.2 +**For PocketMine-MP 5.25.X** + +## Components + +### General + +### `imperazim\components\trigger` + +- **`ItemTrigger:`** Creates a trigger that executes an action for players who have a specific item in their inventory. + Example usage: + ```php + use imperazim\components\trigger\ItemTrigger; + use pocketmine\item\GoldenApple; + + // Automatically checks if the player has the item and executes the action + $itemTrigger = new ItemTrigger( + GoldenApple::class, + function(Player $player, PlayerInventory $inventory): void { + $player->sendMessage("You have a golden apple in your inventory!"); + // Optional: Interact with the inventory + $inventory->removeItem($inventory->getItem(0)); // Removes first slot item + } + ); + ``` + No need to manually write item-checking conditions - simply provide the item class and callback. + + +## Vendor + +### `imperazim\vendor\customies\item` + +### Core Mechanics +- **`StackedByDataComponent`**: Controls item stacking behavior for variants with different aux values. + **Example:** + ```php + $item->addComponent(new StackedByDataComponent(true)); // Allows stacking of items with different aux values + ``` + +- **`ShouldDespawnComponent`**: Toggles item despawn mechanics for floating entities. + **Example:** + ```php + $item->addComponent(new ShouldDespawnComponent(false)); // Prevents the item from despawning + ``` + +- **`ShooterComponent`**: Enables projectile launching (requires `UseModifiersComponent`). + **Example:** + ```php + $item->addComponent(new ShooterComponent("arrow", ...view component)); // Shoots arrows with a speed of 1.5 + ``` + +- **`RecordComponent`**: Adds music playback functionality to items. + **Example:** + ```php + $item->addComponent(new RecordComponent(1, 60, "record.song")); // Plays a specific music record + ``` + +- **`ItemTaskComponent`**: Creates a new ItemTaskComponent to define a task that checks for specific items in a player's inventory. + **Example:** + ```php + $item->addComponent(new ItemTaskComponent( + self::class, + function(Player $player, Item $item): void { + $player->sendMessage("You are holding a Diamond Sword!"); + }, + ItemTaskComponent::FILTER_HAND + )); + ``` + +### Visual & Interaction +- **`HoverTextColorComponent`**: Sets custom color for item name hover text. + **Example:** + ```php + $item->addComponent(new HoverTextColorComponent("color")); // Sets hover text color to red. See: https://minecraft.wiki/w/Formatting_codes#Color_codes + ``` + +- **`GlintComponent`**: Toggles enchantment glow effect. + **Example:** + ```php + $item->addComponent(new GlintComponent(true)); // Adds an enchantment glow to the item + ``` + +- **`InteractButtonComponent`**: Configures touch control interaction button (supports custom text). + **Example:** + ```php + $item->addComponent(new InteractButtonComponent("Use Item")); // Displays "Use Item" on the interact button + ``` + +- **`LiquidClippedComponent`**: Defines liquid block interaction behavior. + **Example:** + ```php + $item->addComponent(new LiquidClippedComponent(true)); // Allows interaction with liquid blocks + ``` + +### Item Customization +- **`DyeableComponent`**: Enables cauldron-based dyeing (requires `dyed` texture definition). + **Example:** + ```php + $item->addComponent(new DyeableComponent("#hex")); // Allows the item to be dyed in a cauldron + ``` + +- **`EnchantableSlotComponent`**: Restricts applicable enchantment types (e.g., bow-only enchants). + **Example:** + ```php + $item->addComponent(new EnchantableSlotComponent(self::SLOT_NAME)); // Allows only slot_name enchantments + ``` + +- **`EnchantableValueComponent`**: Sets base enchantment strength. + **Example:** + ```php + $item->addComponent(new EnchantableValueComponent(10)); // Sets base enchantment strength to 10 + ``` + +### Combat & Durability +- **`DamageComponent`**: Increases attack damage (positive values only). + **Example:** + ```php + $item->addComponent(new DamageComponent(5)); // Adds 5 extra damage to the item + ``` + +- **`DamageAbsorptionComponent`**: Reduces incoming damage when equipped as armor (requires durability). + **Example:** + ```php + $item->addComponent(new DamageAbsorptionComponent(3)); // Absorbs 3 points of damage + ``` + +### Specialized Systems +- **`BundleInteractionComponent`**: Enables bundle storage mechanics (requires `storage_item` component). + **Example:** + ```php + $item->addComponent(new BundleInteractionComponent(4)); // Enables Bundle Interactions with 4 slots + ``` + +- **`UseModifiersComponent`**: Configures item usage timing for food/throwable/shooter items. + **Example:** + ```php + $item->addComponent(new UseModifiersComponent(1.0, 1.5)); // Sets item usage modifiers +1.0 with duration to 1.5 seconds + ``` \ No newline at end of file diff --git a/src/Library.php b/src/Library.php index b73088f..9d2fae8 100644 --- a/src/Library.php +++ b/src/Library.php @@ -5,28 +5,38 @@ use imperazim\components\plugin\PluginToolkit; use imperazim\components\plugin\traits\PluginToolkitTrait; +use imperazim\components\commands\PluginsCommand; +use imperazim\components\commands\VersionCommand; + /** * Class Library * TODO: This class should not be called in other plugins! */ final class Library extends PluginToolkit { - use PluginToolkitTrait; - - public LibraryComponents $componentsManager; - - /** - * This method is called when the plugin is enabled. - */ - protected function onEnable(): void { - self::setInstance($this); - $this->componentsManager = new LibraryComponents($this); - $this->componentsManager->enableComponents(); - } - - /** - * This method is called when the plugin is disabled. - */ - protected function onDisable(): void { - $this->componentsManager->disableComponents(); - } + use PluginToolkitTrait; + + public LibraryComponents $componentsManager; + + /** + * This method is called when the plugin is enabled. + */ + protected function onEnable(): void { + self::setInstance($this); + LibraryModules::loadModules($this); + + $this->componentsManager = new LibraryComponents($this); + $this->componentsManager->enableComponents(); + + $this->overwriteCommands($this, [ + 'plugins' => PluginsCommand::class, + 'version' => VersionCommand::class, + ]); + } + + /** + * This method is called when the plugin is disabled. + */ + protected function onDisable(): void { + $this->componentsManager->disableComponents(); + } } \ No newline at end of file diff --git a/src/LibraryComponents.php b/src/LibraryComponents.php index 7eae745..602a733 100644 --- a/src/LibraryComponents.php +++ b/src/LibraryComponents.php @@ -2,17 +2,15 @@ declare(strict_types = 1); -use imperazim\bugfixes\BugFixesManager; +use imperazim\vendor\fix\BugFixesManager; use imperazim\components\filesystem\File; use imperazim\components\world\WorldManager; use imperazim\components\plugin\PluginToolkit; use imperazim\components\trigger\TriggerManager; -use imperazim\components\command\CommandManager; use imperazim\components\hud\bossbar\BossBarManager; use imperazim\vendor\invmenu\InvMenuManager; -use imperazim\vendor\commando\CommandoManager; use imperazim\vendor\customies\CustomiesManager; use imperazim\vendor\customies\enchantment\CustomiesEchantmentManager; @@ -30,10 +28,8 @@ final class LibraryComponents { 'World' => WorldManager::class, 'BossBar' => BossBarManager::class, 'InvMenu' => InvMenuManager::class, - 'Command' => CommandManager::class, 'Triggers' => TriggerManager::class, 'BugFixes' => BugFixesManager::class, - 'Commando' => CommandoManager::class, 'Customies' => CustomiesManager::class, 'CustomiesEnchantment' => CustomiesEchantmentManager::class, ]; diff --git a/src/LibraryModules.php b/src/LibraryModules.php new file mode 100644 index 0000000..caf006e --- /dev/null +++ b/src/LibraryModules.php @@ -0,0 +1,24 @@ +getServer()->getPluginManager(); + if ($pluginManager->getPlugin('LibCommand') !== null) { + LibCommandHooker::registerInterceptor($plugin); + } + } + +} \ No newline at end of file diff --git a/src/imperazim/command/Command.php b/src/imperazim/command/Command.php new file mode 100644 index 0000000..45b3eb3 --- /dev/null +++ b/src/imperazim/command/Command.php @@ -0,0 +1,178 @@ +plugin = $plugin; + $config = $this->onBuild(); + + $name = $config["name"] ?? "command"; + $description = $config["description"] ?? ""; + $aliases = $config["aliases"] ?? []; + $permission = $config["permission"] ?? DefaultPermissions::ROOT_USER; + + parent::__construct( + $name, + $description, + $this->buildUsage($config), + $aliases + ); + + // Add permission constraint if none provided + $config["constraints"][] = new PermissionConstraint($permission); + + // Register constraints + foreach ($config["constraints"] ?? [] as $constraint) { + $this->addConstraint($constraint); + } + + // Register subcommands + foreach ($config["subcommands"] ?? [] as $subcommand) { + $this->addSubCommand($subcommand); + } + + // Register arguments + foreach ($config["arguments"] ?? [] as $argument) { + $this->addArgument($argument); + } + + $this->setPermission($permission); + } + + /** + * Gets the plugin that owns this command. + * + * @return Plugin The owning plugin + */ + public function getPlugin(): Plugin { + return $this->plugin; + } + + /** + * Executes the command. + * + * @param CommandSender $sender The command sender + * @param string $label The command label used + * @param array $rawArgs Raw arguments passed to the command + */ + public function execute( + CommandSender $sender, + string $label, + array $rawArgs + ): void { + // Handle subcommands first (before checking parent constraints) + if (!empty($rawArgs)) { + $key = strtolower(array_shift($rawArgs)); + foreach ($this->getSubCommands() as $sub) { + $name = $sub->getName(); + $aliases = $sub->getAliases(); + if ($key === $name || in_array($key, $aliases, true)) { + $sub->execute($sender, $label . " " . $name, $rawArgs); + return; + } + } + } + + // Check constraints only for the main command (not subcommands) + $constraintResult = $this->testConstraints($sender); + if (!$constraintResult["success"]) { + $this->onFailure( + new CommandFailure($sender, CommandFailure::CONSTRAINT_FAILED, [ + "failed_constraints" => $constraintResult["failed_constraints"], + ]) + ); + return; + } + + // Parse and validate arguments + $processedArgs = $this->parseRawArgs($rawArgs); + $validation = $this->validateArguments($sender, $processedArgs); + if (!$validation["valid"]) { + $this->onFailure( + new CommandFailure($sender, $validation["type"], [ + "message" => $validation["message"], + "argument_errors" => $validation["argument_errors"] ?? [], + ]) + ); + return; + } + + $parsedArgs = $this->parseArguments($sender, $rawArgs); + + try { + // Execute command logic + $this->onExecute(new CommandResult($sender, $parsedArgs, $label)); + } catch (\Throwable $e) { + try { + $this->onFailure( + new CommandFailure($sender, CommandFailure::EXECUTION_ERROR, [ + "message" => $e->getMessage(), + "file" => $e->getFile(), + "line" => $e->getLine(), + "trace" => $e->getTraceAsString(), + ]) + ); + } catch (\Throwable $e) { + $this->plugin->getLogger()->warning("Fatal error: " . $e->getMessage()); + $this->plugin + ->getLogger() + ->warning("Location: {$e->getFile()}:{$e->getLine()}"); + } + } + } + + /** + * Builds the command configuration. + * Must return an array with command settings. + * + * @return array Command configuration + */ + abstract public function onBuild(): array; + + /** + * Executes the command logic. + * + * @param CommandResult $result The command execution result + */ + abstract public function onExecute(CommandResult $result): void; + + /** + * Handles command execution failures. + * + * @param CommandFailure $failure The failure details + */ + abstract public function onFailure(CommandFailure $failure): void; +} diff --git a/src/imperazim/command/CommandArguments.php b/src/imperazim/command/CommandArguments.php new file mode 100644 index 0000000..64fc7af --- /dev/null +++ b/src/imperazim/command/CommandArguments.php @@ -0,0 +1,119 @@ +sender = $sender; + $this->arguments = $arguments; + } + + /** + * Checks if an argument exists. + * + * @param mixed $offset Argument name + * @return bool True if exists, false otherwise + */ + public function offsetExists(mixed $offset): bool { + return isset($this->arguments[$offset]); + } + + /** + * Gets an argument value. + * + * @param mixed $offset Argument name + * @return mixed Argument value or null + */ + public function offsetGet(mixed $offset): mixed { + return $this->arguments[$offset] ?? null; + } + + /** + * Sets an argument value. + * + * @param mixed $offset Argument name + * @param mixed $value Argument value + * + * @throws InvalidArgumentException If offset is null + */ + public function offsetSet(mixed $offset, mixed $value): void { + if ($offset === null) { + throw new InvalidArgumentException("Argument must have a name"); + } + $this->arguments[$offset] = $value; + } + + /** + * Removes an argument. + * + * @param mixed $offset Argument name + */ + public function offsetUnset(mixed $offset): void { + unset($this->arguments[$offset]); + } + + /** + * Gets an iterator for the arguments. + * + * @return Traversable Argument iterator + */ + public function getIterator(): Traversable { + return new ArrayIterator($this->arguments); + } + + /** + * Counts the number of arguments. + * + * @return int Number of arguments + */ + public function count(): int { + return count($this->arguments); + } + + /** + * Converts arguments to an array. + * + * @return array Arguments as array + */ + public function toArray(): array { + return $this->arguments; + } + + /** + * Gets an argument value with default fallback. + * + * @param string $key Argument name + * @param mixed $default Default value if not found + * @return mixed Argument value or default + */ + public function get(string $key, mixed $default = null): mixed { + return $this->arguments[$key] ?? $default; + } +} diff --git a/src/imperazim/command/LibCommand.php b/src/imperazim/command/LibCommand.php new file mode 100644 index 0000000..f84a264 --- /dev/null +++ b/src/imperazim/command/LibCommand.php @@ -0,0 +1,23 @@ +registerOutgoing(new LibCommandInterceptor()); + + self::$registered = true; + } +} \ No newline at end of file diff --git a/src/imperazim/command/LibCommandInterceptor.php b/src/imperazim/command/LibCommandInterceptor.php new file mode 100644 index 0000000..ae93515 --- /dev/null +++ b/src/imperazim/command/LibCommandInterceptor.php @@ -0,0 +1,154 @@ +getPlayer(); + if (!($player instanceof Player)) { + return true; + } + + $server = Server::getInstance(); + foreach ($packet->commandData as $name => $data) { + $cmd = $server->getCommandMap()->getCommand($name); + + // Skip non-custom commands + if (!($cmd instanceof Command)) { + continue; + } + + // Apply constraints + foreach ($cmd->getConstraints() as $constraint) { + if (!$constraint->isSatisfiedBy($player)) { + unset($packet->commandData[$name]); + continue 2; + } + } + + // Rebuild command UI + $packet->commandData[$name]->overloads = $this->getOverloads($player, $cmd); + } + + // Update dynamic enums + $packet->softEnums = CommandEnumManager::getEnums(); + return true; + } + + /** + * Generates command overloads for the command UI. + * + * @param CommandSender $sender The command sender + * @param Command|SubCommand $command The command or subcommand + * + * @return array Generated command overloads + */ + private function getOverloads(CommandSender $sender, Command|SubCommand $command): array { + $overloads = []; + + // Process subcommands + foreach ($command->getSubCommands() as $sub) { + // Check subcommand constraints + foreach ($sub->getConstraints() as $c) { + if (!$c->isSatisfiedBy($sender)) { + continue 2; + } + } + + // Create subcommand parameter + $param = new CommandParameter(); + $param->paramName = $sub->getName(); + $param->paramType = AvailableCommandsPacket::ARG_FLAG_ENUM | AvailableCommandsPacket::ARG_FLAG_VALID; + $param->isOptional = false; + $param->enum = new CommandEnum($sub->getName(), [$sub->getName()]); + + // Recursively process child subcommands + $child = $this->getOverloads($sender, $sub); + if (!empty($child)) { + foreach ($child as $ov) { + $overloads[] = new CommandOverload(false, [$param, ...$ov->getParameters()]); + } + } else { + $overloads[] = new CommandOverload(false, [$param]); + } + } + + // Process arguments + $args = $command->getArguments(); + $sets = []; + + // Normalize arguments to sets + foreach ($args as $arg) { + $sets[] = is_array($arg) ? $arg : [$arg]; + } + + // Generate argument combinations + $indexes = array_fill(0, count($sets), 0); + $total = array_product(array_map(fn($s) => count($s), $sets)); + + for ($i = 0; $i < $total; ++$i) { + $params = []; + foreach ($indexes as $k => $idx) { + $argument = $sets[$k][$idx]; + $param = clone $argument->getParameterData(); + + // Handle enums + if (isset($param->enum) && $param->enum instanceof CommandEnum) { + $ref = new \ReflectionProperty(CommandEnum::class, 'enumName'); + $ref->setAccessible(true); + $ref->setValue($param->enum, 'enum#' . spl_object_id($param->enum)); + } + $params[] = $param; + } + $overloads[] = new CommandOverload(false, $params); + + // Increment indexes + for ($j = count($indexes) - 1; $j >= 0; --$j) { + $indexes[$j]++; + if ($indexes[$j] < count($sets[$j])) break; + $indexes[$j] = 0; + } + } + + return $overloads; + } +} \ No newline at end of file diff --git a/src/imperazim/command/SubCommand.php b/src/imperazim/command/SubCommand.php new file mode 100644 index 0000000..bbd9062 --- /dev/null +++ b/src/imperazim/command/SubCommand.php @@ -0,0 +1,176 @@ +parent = $parent; + $this->config = $this->onBuild(); + + $permission = $this->config['permission'] ?? DefaultPermissions::ROOT_USER; + $this->config['constraints'][] = new PermissionConstraint($permission); + + // Register constraints + foreach ($this->config['constraints'] ?? [] as $constraint) { + $this->addConstraint($constraint); + } + + // Register arguments + foreach ($this->config['arguments'] ?? [] as $argument) { + $this->addArgument($argument); + } + } + + /** + * Gets the owning plugin. + * + * @return Plugin The plugin instance + */ + public function getPlugin(): Plugin { + return $this->parent->getPlugin(); + } + + /** + * Gets the subcommand name. + * + * @return string The subcommand name + */ + public function getName(): string { + return $this->config['name'] ?? 'subcommand'; + } + + /** + * Gets the subcommand description. + * + * @return string The subcommand description + */ + public function getDescription(): string { + return $this->config['description'] ?? '...'; + } + + /** + * Gets the subcommand aliases. + * + * @return array Subcommand aliases + */ + public function getAliases(): array { + return $this->config['aliases'] ?? []; + } + + /** + * Gets child subcommands. + * + * @return array Child subcommands + */ + public function getSubCommands(): array { + return $this->config['subcommands'] ?? []; + } + + /** + * Executes the subcommand. + * + * @param CommandSender $sender The command sender + * @param string $label The command label used + * @param array $rawArgs Raw arguments passed to the subcommand + */ + public function execute(CommandSender $sender, string $label, array $rawArgs): void { + // Check constraints + $res = $this->testConstraints($sender); + if (!$res['success']) { + $this->onFailure(new CommandFailure( + $sender, + CommandFailure::CONSTRAINT_FAILED, + ['failed_constraints' => $res['failed_constraints']] + )); + return; + } + + // Handle child subcommands + if (!empty($this->getSubCommands()) && !empty($rawArgs)) { + $key = strtolower(array_shift($rawArgs)); + foreach ($this->getSubCommands() as $sub) { + $name = $sub->getName(); + $aliases = $sub->getAliases(); + if ($key === $name || in_array($key, $aliases, true)) { + $sub->execute($sender, $label . ' ' . $name, $rawArgs); + return; + } + } + } + + // Parse and validate arguments + $processed = $this->parseRawArgs($rawArgs); + $valid = $this->validateArguments($sender, $processed); + if (!$valid['valid']) { + $this->onFailure(new CommandFailure( + $sender, + $valid['type'], + ['message' => $valid['message']] + )); + return; + } + + $parsed = $this->parseArguments($sender, $rawArgs); + + try { + // Execute subcommand logic + $this->onExecute(new CommandResult($sender, $parsed, $label)); + } catch (\Throwable $e) { + $this->onFailure(new CommandFailure( + $sender, + CommandFailure::EXECUTION_ERROR, + ['error' => $e->getMessage()] + )); + } + } + + /** + * Builds the subcommand configuration. + * Must return an array with subcommand settings. + * + * @return array Subcommand configuration + */ + abstract public function onBuild(): array; + + /** + * Executes the subcommand logic. + * + * @param CommandResult $result The command execution result + */ + abstract public function onExecute(CommandResult $result): void; + + /** + * Handles subcommand execution failures. + * + * @param CommandFailure $failure The failure details + */ + abstract public function onFailure(CommandFailure $failure): void; +} \ No newline at end of file diff --git a/src/imperazim/command/argument/Argument.php b/src/imperazim/command/argument/Argument.php new file mode 100644 index 0000000..12651d5 --- /dev/null +++ b/src/imperazim/command/argument/Argument.php @@ -0,0 +1,117 @@ +parameterData === null) { + $param = new CommandParameter(); + $param->paramName = $this->name; + $param->paramType = AvailableCommandsPacket::ARG_FLAG_VALID | $this->getNetworkType(); + $param->isOptional = $this->optional; + $this->parameterData = $param; + } + } + + /** + * Gets the argument name + * + * @return string Argument name + */ + public function getName(): string { + return $this->name; + } + + /** + * Checks if the argument is optional + * + * @return bool True if optional, false if required + */ + public function isOptional(): bool { + return $this->optional; + } + + /** + * Gets the network-ready parameter data + * + * @return CommandParameter Network command parameter data + */ + public function getParameterData(): CommandParameter { + return $this->parameterData; + } + + /** + * Returns the formatted representation for usage strings + * + * @return string Formatted usage representation + */ + public function getUsageFormatted(): string { + $name = $this->getName(); + $type = $this->getTypeName(); + return $this->optional ? "[{$name}: $type]" : "<{$name}: $type>"; + } + + /** + * Gets the human-readable type name for documentation + * + * @return string Type name (e.g., "int", "string") + */ + abstract public function getTypeName(): string; + + /** + * Gets the network type flag for command registration + * + * @return int Network type constant from AvailableCommandsPacket + */ + abstract public function getNetworkType(): int; + + /** + * Validates if the argument can be parsed from input + * + * @param string $testString Input string to test + * @param CommandSender $sender Command executor + * @return bool True if parsable, false otherwise + */ + abstract public function canParse(string $testString, CommandSender $sender): bool; + + /** + * Parses the input string into the appropriate value type + * + * @param string $argument Input string to parse + * @param CommandSender $sender Command executor + * @return mixed Parsed value (type depends on implementation) + * + * @throws ArgumentException If parsing fails + */ + abstract public function parse(string $argument, CommandSender $sender): mixed; +} \ No newline at end of file diff --git a/src/imperazim/command/argument/BooleanArgument.php b/src/imperazim/command/argument/BooleanArgument.php new file mode 100644 index 0000000..f8f0c00 --- /dev/null +++ b/src/imperazim/command/argument/BooleanArgument.php @@ -0,0 +1,74 @@ +choices = $choices; + parent::__construct($name, $optional); + } + + /** + * Gets the human-readable type name + * + * @return string "enum" + */ + public function getTypeName(): string { + return "enum"; + } + + /** + * Gets the network type flag + * + * @return int Combination of ARG_FLAG_ENUM and ARG_TYPE_STRING + */ + public function getNetworkType(): int { + return AvailableCommandsPacket::ARG_FLAG_ENUM | AvailableCommandsPacket::ARG_TYPE_STRING; + } + + /** + * Gets the network parameter data with enum definition + * + * @return CommandParameter Network parameter data with enum + */ + public function getParameterData(): CommandParameter { + $param = parent::getParameterData(); + $param->enum = new CommandEnum($this->getName() . "Enum", $this->choices); + return $param; + } + + /** + * Checks if input is in the predefined choices + * + * @param string $testString Input string to test + * @param CommandSender $sender Command executor + * @return bool True if input matches an enum choice + */ + public function canParse(string $testString, CommandSender $sender): bool { + return in_array($testString, $this->choices, true); + } + + /** + * Returns the input string if valid + * + * @param string $value Input string to parse + * @param CommandSender $sender Command executor + * @return string The input value + * + * @throws ArgumentException If input is not a valid choice + */ + public function parse(string $value, CommandSender $sender): mixed { + if (!in_array($value, $this->choices, true)) { + throw new ArgumentException("Invalid choice. Valid options: " . implode(", ", $this->choices)); + } + return $value; + } + + /** + * Gets available enum choices + * + * @return string[] Array of valid choices + */ + public function getChoices(): array { + return $this->choices; + } +} \ No newline at end of file diff --git a/src/imperazim/command/argument/FloatArgument.php b/src/imperazim/command/argument/FloatArgument.php new file mode 100644 index 0000000..b62b5e4 --- /dev/null +++ b/src/imperazim/command/argument/FloatArgument.php @@ -0,0 +1,92 @@ +min === null || $value >= $this->min) && + ($this->max === null || $value <= $this->max); + } + + /** + * Parses input into float value + * + * @param string $value Input string to parse + * @param CommandSender $sender Command executor + * @return float Parsed float value + * + * @throws ArgumentException If value is out of range + */ + public function parse(string $value, CommandSender $sender): mixed { + $floatValue = (float)$value; + + if ($this->min !== null && $floatValue < $this->min) { + throw new ArgumentException("Value must be at least {$this->min}"); + } + + if ($this->max !== null && $floatValue > $this->max) { + throw new ArgumentException("Value must be at most {$this->max}"); + } + + return $floatValue; + } +} \ No newline at end of file diff --git a/src/imperazim/command/argument/IntegerArgument.php b/src/imperazim/command/argument/IntegerArgument.php new file mode 100644 index 0000000..9150f7d --- /dev/null +++ b/src/imperazim/command/argument/IntegerArgument.php @@ -0,0 +1,91 @@ +min === null || $value >= $this->min) && + ($this->max === null || $value <= $this->max); + } + + /** + * Parses input into integer value + * + * @param string $value Input string to parse + * @param CommandSender $sender Command executor + * @return int Parsed integer value + * + * @throws ArgumentException If value is out of range + */ + public function parse(string $value, CommandSender $sender): mixed { + $intValue = (int)$value; + + if ($this->min !== null && $intValue < $this->min) { + throw new ArgumentException("Value must be at least {$this->min}"); + } + + if ($this->max !== null && $intValue > $this->max) { + throw new ArgumentException("Value must be at most {$this->max}"); + } + + return $intValue; + } +} \ No newline at end of file diff --git a/src/imperazim/command/argument/ItemArgument.php b/src/imperazim/command/argument/ItemArgument.php new file mode 100644 index 0000000..2cd40cb --- /dev/null +++ b/src/imperazim/command/argument/ItemArgument.php @@ -0,0 +1,69 @@ +parse($testString) ?? LegacyStringToItemParser::getInstance()->parse($testString); + return $item !== null; + } + + /** + * Parses input into Item object + * + * @param string $value Input string to parse + * @param CommandSender $sender Command executor + * @return Item Parsed item instance + * + * @throws ArgumentException If item identifier is unknown + */ + public function parse(string $value, CommandSender $sender): mixed { + $item = StringToItemParser::getInstance()->parse($value) ?? LegacyStringToItemParser::getInstance()->parse($value); + + if ($item === null) { + throw new ArgumentException("Unknown item '{$value}'"); + } + + return $item; + } +} \ No newline at end of file diff --git a/src/imperazim/command/argument/PlayerArgument.php b/src/imperazim/command/argument/PlayerArgument.php new file mode 100644 index 0000000..47d5644 --- /dev/null +++ b/src/imperazim/command/argument/PlayerArgument.php @@ -0,0 +1,66 @@ +getPlayerByPrefix($testString) !== null; + } + + /** + * Parses input into Player object + * + * @param string $value Input string to parse + * @param CommandSender $sender Command executor + * @return Player Parsed player instance + * + * @throws ArgumentException If player not found + */ + public function parse(string $value, CommandSender $sender): mixed { + $player = Server::getInstance()->getPlayerByPrefix($value); + if ($player === null) { + throw new ArgumentException("Player '{$value}' not found"); + } + + return $player; + } +} \ No newline at end of file diff --git a/src/imperazim/command/argument/StringArgument.php b/src/imperazim/command/argument/StringArgument.php new file mode 100644 index 0000000..95efde3 --- /dev/null +++ b/src/imperazim/command/argument/StringArgument.php @@ -0,0 +1,56 @@ +isValidEntityIdentifier($testString); + } + + /** + * Parses input into Entity or array of entities + * + * @param string $value Input string to parse + * @param CommandSender $sender Command executor + * @return Entity|Entity[]|null Parsed entity/entities + * + * @throws ArgumentException If target not found + */ + public function parse(string $value, CommandSender $sender): mixed { + if (preg_match('/^@[aeprs]($|\[)/', $value)) { + return $this->parseSelector($value, $sender); + } + + $entity = $this->findEntity($value, $sender); + if ($entity === null) { + throw new ArgumentException("Target '{$value}' not found"); + } + + return $entity; + } + + /** + * Checks if input is a valid entity identifier + * + * @param string $testString Input string to test + * @return bool True if valid identifier (1-16 word characters) + */ + private function isValidEntityIdentifier(string $testString): bool { + return preg_match('/^\w{1,16}$/', $testString); + } + + /** + * Parses entity selector into entity/entities + * + * @param string $selector Selector string (@p, @a, etc.) + * @param CommandSender $sender Command executor + * @return Entity|Entity[]|null Parsed entity/entities + * + * @throws ArgumentException For unknown selector types + */ + private function parseSelector(string $selector, CommandSender $sender): mixed { + $type = $selector[1]; + $args = []; + + // Extract arguments if present [arg=value] + if (strpos($selector, '[') !== false) { + preg_match('/\[(.*?)\]/', $selector, $matches); + if (isset($matches[1])) { + parse_str(str_replace(',', '&', $matches[1]), $args); + } + } + + switch ($type) { + case 'a': // @a - All players + return $this->getAllPlayers($args); + + case 'e': // @e - All entities + return $this->getAllEntities($args); + + case 'p': // @p - Nearest player + return $this->getNearestPlayer($sender, $args); + + case 'r': // @r - Random player + return $this->getRandomPlayer($args); + + case 's': // @s - The sender + return $sender instanceof Entity ? $sender : null; + + default: + throw new ArgumentException("Unknown selector type: @{$type}"); + } + } + + /** + * Gets all players matching filter criteria + * + * @param array $args Selector arguments (e.g., ['world' => 'world_name']) + * @return Player[] Filtered players + */ + private function getAllPlayers(array $args): array { + $players = Server::getInstance()->getOnlinePlayers(); + + // Filter by world if specified + if (isset($args['world'])) { + $worldName = $args['world']; + $players = array_filter($players, fn(Player $p) => + $p->getWorld()->getFolderName() === $worldName + ); + } + + return array_values($players); + } + + /** + * Gets all entities matching filter criteria + * + * @param array $args Selector arguments + * @return Entity[] Filtered entities + */ + private function getAllEntities(array $args): array { + $entities = []; + + foreach (Server::getInstance()->getWorldManager()->getWorlds() as $world) { + // Filter by world if specified + if (isset($args['world']) && $world->getFolderName() !== $args['world']) { + continue; + } + + foreach ($world->getEntities() as $entity) { + // Filter by type if specified + if (isset($args['type']) && get_class($entity) !== $args['type']) { + continue; + } + + $entities[] = $entity; + } + } + + return $entities; + } + + /** + * Gets nearest player to sender + * + * @param CommandSender $sender Command executor + * @param array $args Selector arguments + * @return Player|null Nearest player or sender if none found + */ + private function getNearestPlayer(CommandSender $sender, array $args): ?Player { + if (!$sender instanceof Player) { + return null; + } + + $nearest = null; + $minDistance = PHP_INT_MAX; + $senderPos = $sender->getPosition(); + + foreach (Server::getInstance()->getOnlinePlayers() as $player) { + // Filter by world + if (isset($args['world']) && + $player->getWorld()->getFolderName() !== $args['world']) { + continue; + } + + $distance = $player->getPosition()->distance($senderPos); + if ($distance < $minDistance && $player !== $sender) { + $minDistance = $distance; + $nearest = $player; + } + } + + return $nearest ?? $sender; + } + + /** + * Gets random player + * + * @param array $args Selector arguments + * @return Player|null Random player or null if none available + */ + private function getRandomPlayer(array $args): ?Player { + $players = Server::getInstance()->getOnlinePlayers(); + + // Filter by world if specified + if (isset($args['world'])) { + $players = array_filter($players, fn(Player $p) => + $p->getWorld()->getFolderName() === $args['world'] + ); + } + + return count($players) > 0 ? $players[array_rand($players)] : null; + } + + /** + * Finds entity by identifier + * + * @param string $identifier Entity identifier (name or ID) + * @param CommandSender $sender Command executor + * @return Entity|null Found entity or null + */ + private function findEntity(string $identifier, CommandSender $sender): ?Entity { + $server = Server::getInstance(); + + // 1. Try to find player by exact name + $player = $server->getPlayerExact($identifier); + if ($player !== null) { + return $player; + } + + // 2. Try to find player by prefix + $player = $server->getPlayerByPrefix($identifier); + if ($player !== null) { + return $player; + } + + // 3. Search all entities in the same world as sender + if ($sender instanceof Player) { + $world = $sender->getWorld(); + foreach ($world->getEntities() as $entity) { + if ($entity instanceof Player) continue; + + if (strtolower($entity->getNameTag()) === strtolower($identifier) || + $entity->getId() === (int)$identifier) { + return $entity; + } + } + } + + // 4. Search all entities globally + foreach ($server->getWorldManager()->getWorlds() as $world) { + foreach ($world->getEntities() as $entity) { + if ($entity instanceof Player) continue; + + if (strtolower($entity->getNameTag()) === strtolower($identifier) || + $entity->getId() === (int)$identifier) { + return $entity; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/imperazim/command/argument/WorldArgument.php b/src/imperazim/command/argument/WorldArgument.php new file mode 100644 index 0000000..7ffdfde --- /dev/null +++ b/src/imperazim/command/argument/WorldArgument.php @@ -0,0 +1,77 @@ +getWorldManager()->getWorldByName($testString) !== null; + } + + /** + * Parses input into World object + * + * @param string $value Input string to parse + * @param CommandSender $sender Command executor + * @return World Parsed world instance + * + * @throws ArgumentException If world not found + */ + public function parse(string $value, CommandSender $sender): mixed { + $world = $this->getWorldManager()->getWorldByName($value); + + if ($world === null) { + $worlds = implode(', ', array_keys($this->getWorldManager()->getWorlds())); + throw new ArgumentException("World '{$value}' not found. Available worlds: {$worlds}"); + } + + return $world; + } + + /** + * Gets WorldManager instance + * + * @return WorldManager Server world manager + */ + private function getWorldManager(): WorldManager { + return Server::getInstance()->getWorldManager(); + } +} \ No newline at end of file diff --git a/src/imperazim/command/constraint/Constraint.php b/src/imperazim/command/constraint/Constraint.php new file mode 100644 index 0000000..b196ffb --- /dev/null +++ b/src/imperazim/command/constraint/Constraint.php @@ -0,0 +1,36 @@ +getSenderKey($sender)] = microtime(true); + } + + /** + * Notifies sender about remaining cooldown time + * + * @param CommandSender $sender Command executor + */ + public function onFailure(CommandSender $sender): void { + $remaining = $this->getRemainingCooldown($sender); + $sender->sendMessage(TextFormat::RED . "Please wait {$remaining} seconds before using this command again"); + } + + /** + * Checks if cooldown period has expired + * + * @param CommandSender $sender Command executor + * @return bool True if cooldown expired, false otherwise + */ + public function isSatisfiedBy(CommandSender $sender): bool { + $key = $this->getSenderKey($sender); + $currentTime = microtime(true); + if (!isset(self::$lastUsed[$key])) { + return true; + } + return ($currentTime - self::$lastUsed[$key]) >= $this->cooldownSeconds; + } + + /** + * Generates unique key for sender + * + * @param CommandSender $sender Command executor + * @return string Unique identifier (UUID for players, 'console' otherwise) + */ + private function getSenderKey(CommandSender $sender): string { + return $sender instanceof Player ? $sender->getUniqueId()->toString() : 'console'; + } + + /** + * Calculates remaining cooldown time + * + * @param CommandSender $sender Command executor + * @return float Remaining cooldown in seconds (formatted to 1 decimal) + */ + private function getRemainingCooldown(CommandSender $sender): float { + $key = $this->getSenderKey($sender); + $lastTime = self::$lastUsed[$key] ?? 0; + $elapsed = microtime(true) - $lastTime; + return max(0, number_format($this->cooldownSeconds - $elapsed, 1)); + } +} \ No newline at end of file diff --git a/src/imperazim/command/constraint/GameModeConstraint.php b/src/imperazim/command/constraint/GameModeConstraint.php new file mode 100644 index 0000000..1cdf76f --- /dev/null +++ b/src/imperazim/command/constraint/GameModeConstraint.php @@ -0,0 +1,56 @@ +getModeName(); + $sender->sendMessage(TextFormat::RED . "You must be in {$modeName} mode to use this command"); + } + + /** + * Checks if sender is in required game mode + * + * @param CommandSender $sender Command executor + * @return bool True if game mode matches, false otherwise + */ + public function isSatisfiedBy(CommandSender $sender): bool { + return ($sender instanceof Player) && + ($sender->getGamemode() === $this->gameMode); + } + + /** + * Gets human-readable game mode name + * + * @return string Mode name (Survival, Creative, etc.) + */ + private function getModeName(): string { + return match($this->gameMode->id()) { + GameMode::SURVIVAL()->id() => "Survival", + GameMode::CREATIVE()->id() => "Creative", + GameMode::ADVENTURE()->id() => "Adventure", + GameMode::SPECTATOR()->id() => "Spectator", + default => "Unknown" + }; + } + } \ No newline at end of file diff --git a/src/imperazim/command/constraint/InGameConstraint.php b/src/imperazim/command/constraint/InGameConstraint.php new file mode 100644 index 0000000..a6874bd --- /dev/null +++ b/src/imperazim/command/constraint/InGameConstraint.php @@ -0,0 +1,33 @@ +sendMessage(TextFormat::RED . 'This command can only be used in-game'); + } + + /** + * Checks if sender is an in-game player + * + * @param CommandSender $sender Command executor + * @return bool True if player, false if console + */ + public function isSatisfiedBy(CommandSender $sender): bool { + return $sender instanceof Player; + } +} \ No newline at end of file diff --git a/src/imperazim/command/constraint/PermissionConstraint.php b/src/imperazim/command/constraint/PermissionConstraint.php new file mode 100644 index 0000000..ccac2f5 --- /dev/null +++ b/src/imperazim/command/constraint/PermissionConstraint.php @@ -0,0 +1,37 @@ +sendMessage(TextFormat::RED . "You don't have permission to use this command!"); + } + + /** + * Checks if sender has required permission + * + * @param CommandSender $sender Command executor + * @return bool True if permission granted, false otherwise + */ + public function isSatisfiedBy(CommandSender $sender): bool { + return $sender->hasPermission($this->permission); + } +} \ No newline at end of file diff --git a/src/imperazim/command/constraint/RequireConsoleConstraint.php b/src/imperazim/command/constraint/RequireConsoleConstraint.php new file mode 100644 index 0000000..8c46969 --- /dev/null +++ b/src/imperazim/command/constraint/RequireConsoleConstraint.php @@ -0,0 +1,33 @@ +sendMessage(TextFormat::RED . 'You need to be in the console to use this command.'); + } + + /** + * Checks if sender is console (not player) + * + * @param CommandSender $sender Command executor + * @return bool True if console, false if player + */ + public function isSatisfiedBy(CommandSender $sender): bool { + return (!$sender instanceof Player); + } +} \ No newline at end of file diff --git a/src/imperazim/command/constraint/WorldConstraint.php b/src/imperazim/command/constraint/WorldConstraint.php new file mode 100644 index 0000000..ba80999 --- /dev/null +++ b/src/imperazim/command/constraint/WorldConstraint.php @@ -0,0 +1,47 @@ +allowedWorlds = (array) $worldNames; + } + + /** + * Notifies sender about invalid world + * + * @param CommandSender $sender Command executor + */ + public function onFailure(CommandSender $sender): void { + $worlds = implode(', ', $this->allowedWorlds); + $sender->sendMessage(TextFormat::RED . "You must be in one of these worlds: $worlds"); + } + + /** + * Checks if sender is in allowed world + * + * @param CommandSender $sender Command executor + * @return bool True if in allowed world, false otherwise + */ + public function isSatisfiedBy(CommandSender $sender): bool { + return ($sender instanceof Player) && + in_array($sender->getWorld()->getFolderName(), $this->allowedWorlds, true); + } +} \ No newline at end of file diff --git a/src/imperazim/command/dynamic/DynamicCommand.php b/src/imperazim/command/dynamic/DynamicCommand.php new file mode 100644 index 0000000..989b553 --- /dev/null +++ b/src/imperazim/command/dynamic/DynamicCommand.php @@ -0,0 +1,201 @@ + '', + 'description' => '', + 'aliases' => [], + 'permission' => DefaultPermissions::ROOT_USER, + 'constraints' => [], + 'arguments' => [], + 'subcommands' => [], + ]; + + /** @var Closure|null Success execution callback */ + private ?Closure $executeCallback = null; + + /** @var Closure|null Failure handling callback */ + private ?Closure $failureCallback = null; + + /** + * Constructs a dynamic command + * + * @param Plugin $plugin Owning plugin instance + * @param string $name Command name + */ + public function __construct(Plugin $plugin, string $name) { + $this->config['name'] = $name; + parent::__construct($plugin); + } + + /** + * Factory method for command creation + * + * @param Plugin $plugin Owning plugin instance + * @param string $name Command name + * @return self New command instance + */ + public static function create(Plugin $plugin, string $name): self { + return new self($plugin, $name); + } + + /** + * Sets the command description + * + * @param string $description Command description + * @return $this + */ + public function setDescription(string $description): self { + $this->config['description'] = $description; + return $this; + } + + /** + * Sets command aliases + * + * @param string[] $aliases Alternative command names + * @return $this + */ + public function setAliases(array $aliases): self { + $this->config['aliases'] = $aliases; + return $this; + } + + /** + * Sets required permission + * + * @param string $permission Permission node + * @return $this + */ + public function setPermission(string $permission): self { + $this->config['permission'] = $permission; + return $this; + } + + /** + * Adds a validation constraint + * + * @param Constraint $constraint Constraint instance + * @return $this + */ + public function addConstraint(Constraint $constraint): self { + $this->config['constraints'][] = $constraint; + return $this; + } + + /** + * Adds a command argument + * + * @param Argument $argument Argument instance + * @return $this + */ + public function addArgument(Argument $argument): self { + $this->config['arguments'][] = $argument; + return $this; + } + + /** + * Adds a subcommand + * + * @param SubCommand $subCommand Subcommand instance + * @return $this + */ + public function addSubCommand(SubCommand $subCommand): self { + $this->config['subcommands'][] = $subCommand; + return $this; + } + + /** + * Sets the success execution handler + * + * @param Closure $callback Callback with signature `function(CommandResult $result): void` + * @return $this + */ + public function setOnExecute(Closure $callback): self { + $this->executeCallback = $callback; + return $this; + } + + /** + * Sets the failure handler + * + * @param Closure $callback Callback with signature `function(CommandFailure $failure): void` + * @return $this + */ + public function setOnFailure(Closure $callback): self { + $this->failureCallback = $callback; + return $this; + } + + /** + * Builds the final command configuration + * + * @return array Command configuration structure + */ + public function onBuild(): array { + return $this->config; + } + + /** + * Executes the command success handler + * + * @param CommandResult $result Command execution result + */ + public function onExecute(CommandResult $result): void { + if ($this->executeCallback !== null) { + ($this->executeCallback)($result); + } + } + + /** + * Executes the command failure handler + * + * @param CommandFailure $failure Command failure details + */ + public function onFailure(CommandFailure $failure): void { + if ($this->failureCallback !== null) { + ($this->failureCallback)($failure); + } + } + + /** + * Registers the command with PocketMine's command map + */ + public function register(): void { + $cmdMap = $this->plugin->getServer()->getCommandMap(); + $namespace = $this->plugin->getDescription()->getName(); + $cmdMap->register($namespace, $this->plugin); + } +} \ No newline at end of file diff --git a/src/imperazim/command/dynamic/DynamicSubCommand.php b/src/imperazim/command/dynamic/DynamicSubCommand.php new file mode 100644 index 0000000..0957fe4 --- /dev/null +++ b/src/imperazim/command/dynamic/DynamicSubCommand.php @@ -0,0 +1,191 @@ + '', + 'description' => '', + 'aliases' => [], + 'permission' => DefaultPermissions::ROOT_USER, + 'constraints' => [], + 'arguments' => [], + 'subcommands' => [], + ]; + + /** @var Closure|null Success execution callback */ + private ?Closure $executeCallback = null; + + /** @var Closure|null Failure handling callback */ + private ?Closure $failureCallback = null; + + /** + * Constructs a dynamic subcommand + * + * @param Command|SubCommand $parent Parent command or subcommand + * @param string $name Subcommand name + */ + public function __construct(Command|SubCommand $parent, string $name) { + $this->config['name'] = $name; + parent::__construct($parent); + } + + /** + * Factory method for subcommand creation + * + * @param Command|SubCommand $parent Parent command instance + * @param string $name Subcommand name + * @return self New subcommand instance + */ + public static function create(Command|SubCommand $parent, string $name): self { + return new self($parent, $name); + } + + /** + * Sets the subcommand description + * + * @param string $description Subcommand description + * @return $this + */ + public function setDescription(string $description): self { + $this->config['description'] = $description; + return $this; + } + + /** + * Sets subcommand aliases + * + * @param string[] $aliases Alternative subcommand names + * @return $this + */ + public function setAliases(array $aliases): self { + $this->config['aliases'] = $aliases; + return $this; + } + + /** + * Sets required permission + * + * @param string $permission Permission node + * @return $this + */ + public function setPermission(string $permission): self { + $this->config['permission'] = $permission; + return $this; + } + + /** + * Adds a validation constraint + * + * @param Constraint $constraint Constraint instance + * @return $this + */ + public function addConstraint(Constraint $constraint): self { + $this->config['constraints'][] = $constraint; + return $this; + } + + /** + * Adds a subcommand argument + * + * @param Argument $argument Argument instance + * @return $this + */ + public function addArgument(Argument $argument): self { + $this->config['arguments'][] = $argument; + return $this; + } + + /** + * Adds a child subcommand + * + * @param SubCommand $subCommand Subcommand instance + * @return $this + */ + public function addSubCommand(SubCommand $subCommand): self { + $this->config['subcommands'][] = $subCommand; + return $this; + } + + /** + * Sets the success execution handler + * + * @param Closure $callback Callback with signature `function(CommandResult $result): void` + * @return $this + */ + public function setOnExecute(Closure $callback): self { + $this->executeCallback = $callback; + return $this; + } + + /** + * Sets the failure handler + * + * @param Closure $callback Callback with signature `function(CommandFailure $failure): void` + * @return $this + */ + public function setOnFailure(Closure $callback): self { + $this->failureCallback = $callback; + return $this; + } + + /** + * Builds the final subcommand configuration + * + * @return array Subcommand configuration structure + */ + public function onBuild(): array { + return $this->config; + } + + /** + * Executes the subcommand success handler + * + * @param CommandResult $result Command execution result + */ + public function onExecute(CommandResult $result): void { + if ($this->executeCallback !== null) { + ($this->executeCallback)($result); + } + } + + /** + * Executes the subcommand failure handler + * + * @param CommandFailure $failure Command failure details + */ + public function onFailure(CommandFailure $failure): void { + if ($this->failureCallback !== null) { + ($this->failureCallback)($failure); + } + } +} \ No newline at end of file diff --git a/src/imperazim/command/enum/CommandEnumManager.php b/src/imperazim/command/enum/CommandEnumManager.php new file mode 100644 index 0000000..0f0d7ba --- /dev/null +++ b/src/imperazim/command/enum/CommandEnumManager.php @@ -0,0 +1,116 @@ +getName(); + + if (isset(self::$enums[$name])) { + throw new CommandException("Soft enum '{$name}' already exists"); + } + + self::$enums[$name] = $enum; + self::broadcastUpdate($enum, UpdateSoftEnumPacket::TYPE_ADD); + } + + /** + * Updates an existing soft enum with new values + * + * @param string $enumName Name of enum to update + * @param string[] $values New enum values + * @throws CommandException If specified enum doesn't exist + */ + public static function updateEnum(string $enumName, array $values): void { + if (!isset(self::$enums[$enumName])) { + throw new CommandException("Unknown enum '{$enumName}'"); + } + + $enum = new CommandEnum($enumName, $values); + self::$enums[$enumName] = $enum; + self::broadcastUpdate($enum, UpdateSoftEnumPacket::TYPE_SET); + } + + /** + * Removes a soft enum + * + * @param string $enumName Name of enum to remove + * @throws CommandException If specified enum doesn't exist + */ + public static function removeEnum(string $enumName): void { + if (!isset(self::$enums[$enumName])) { + throw new CommandException("Unknown enum '{$enumName}'"); + } + + $enum = self::$enums[$enumName]; + unset(self::$enums[$enumName]); + self::broadcastUpdate($enum, UpdateSoftEnumPacket::TYPE_REMOVE); + } + + /** + * Broadcasts enum changes to all online players + * + * @param CommandEnum $enum Enum instance to broadcast + * @param int $updateType Update type (add/set/remove) + */ + private static function broadcastUpdate(CommandEnum $enum, int $updateType): void { + $packet = new UpdateSoftEnumPacket(); + $packet->enumName = $enum->getName(); + $packet->values = $enum->getValues(); + $packet->type = $updateType; + + self::broadcastToPlayers($packet); + } + + /** + * Broadcasts a packet to all online players + * + * @param ClientboundPacket $packet Packet to broadcast + */ + private static function broadcastToPlayers(ClientboundPacket $packet): void { + $players = Server::getInstance()->getOnlinePlayers(); + NetworkBroadcastUtils::broadcastPackets($players, [$packet]); + } +} \ No newline at end of file diff --git a/src/imperazim/command/exception/ArgumentException.php b/src/imperazim/command/exception/ArgumentException.php new file mode 100644 index 0000000..78b0f04 --- /dev/null +++ b/src/imperazim/command/exception/ArgumentException.php @@ -0,0 +1,17 @@ +sender; + } + + /** + * Gets the failure reason identifier + * + * @return int One of the class failure constants + */ + public function getReason(): int { + return $this->reasonId; + } + + /** + * Gets the failure reason as a human-readable name + * + * @return string Failure type name + */ + public function getReasonName(): string { + return match($this->reasonId) { + self::INVALID_ARGUMENT => 'INVALID_ARGUMENT', + self::MISSING_ARGUMENT => 'MISSING_ARGUMENT', + self::CONSTRAINT_FAILED => 'CONSTRAINT_FAILED', + self::EXECUTION_ERROR => 'EXECUTION_ERROR', + default => 'UNKNOWN_ERROR' + }; + } + + /** + * Gets contextual failure data + * + * Structure varies by failure type: + * - INVALID_ARGUMENT: May contain 'argument_errors' + * - MISSING_ARGUMENT: May contain 'missing_arguments' + * - CONSTRAINT_FAILED: May contain 'failed_constraints' + * - EXECUTION_ERROR: May contain exception or error details + * + * @return array Failure-specific contextual data + */ + public function getData(): array { + return $this->data; + } + + /** + * Gets failed constraints (for CONSTRAINT_FAILED errors) + * + * @return Constraint[] Array of failed constraint objects + */ + public function getFailedConstraints(): array { + return $this->data['failed_constraints'] ?? []; + } + + /** + * Gets the failure message + * + * Returns custom message if provided in data, otherwise default message + * + * @return string Human-readable error message + */ + public function getMessage(): string { + return $this->data['message'] ?? $this->getDefaultMessage(); + } + + /** + * Generates a default message based on failure type + * + * @return string Default error message for the failure type + */ + private function getDefaultMessage(): string { + return match($this->reasonId) { + self::INVALID_ARGUMENT => 'Invalid argument provided', + self::MISSING_ARGUMENT => 'Missing required arguments', + self::CONSTRAINT_FAILED => 'Command constraints not satisfied', + self::EXECUTION_ERROR => 'An error occurred during command execution', + default => 'Unknown command error' + }; + } + } \ No newline at end of file diff --git a/src/imperazim/command/result/CommandResult.php b/src/imperazim/command/result/CommandResult.php new file mode 100644 index 0000000..812e3f2 --- /dev/null +++ b/src/imperazim/command/result/CommandResult.php @@ -0,0 +1,56 @@ +sender; + } + + /** + * Gets the parsed command arguments + * + * @return CommandArguments Structured argument container + */ + public function getArgumentsList(): CommandArguments { + return $this->arguments; + } + + /** + * Gets the actual command label used + * + * @return string Command label (e.g., "help" in "/help") + */ + public function getLabel(): string { + return $this->label; + } +} \ No newline at end of file diff --git a/src/imperazim/command/traits/ArgumentableTrait.php b/src/imperazim/command/traits/ArgumentableTrait.php new file mode 100644 index 0000000..481af6e --- /dev/null +++ b/src/imperazim/command/traits/ArgumentableTrait.php @@ -0,0 +1,233 @@ +arguments[] = $argument; + return $this; + } + + /** + * Gets all registered arguments + * + * @return Argument[] + */ + public function getArguments(): array { + return $this->arguments; + } + + /** + * Retrieves an argument by its name + * + * @param string $name Name of the argument to find + * @return Argument|null Argument instance or null if not found + */ + public function getArgument(string $name): ?Argument { + foreach ($this->arguments as $argument) { + if ($argument->getName() === $name) { + return $argument; + } + } + return null; + } + + /** + * Builds the command usage string from configuration + * + * @param array $config Command configuration array + * @return string Formatted usage string + */ + private function buildUsage(array $config): string { + $parts = ['/' . ($config['name'] ?? 'command')]; + + foreach ($config['arguments'] ?? [] as $argument) { + $parts[] = $argument->getUsageFormatted(); + } + + return implode(' ', $parts); + } + + /** + * Parses raw command arguments according to defined argument types + * + * Handles special cases like StringArgument consuming multiple tokens + * + * @param string[] $rawArgs Raw arguments from command input + * @return array Processed arguments with correct grouping + */ + private function parseRawArgs(array $rawArgs): array { + $processed = []; + $argDefs = $this->arguments; + $rawCount = count($rawArgs); + $argCount = count($argDefs); + $i = 0; + $j = 0; + + while ($i < $argCount && $j < $rawCount) { + $argDef = $argDefs[$i]; + $remainingRaw = $rawCount - $j; + $remainingArgs = $argCount - $i; + + if ($argDef instanceof StringArgument && $remainingRaw > $remainingArgs) { + $toConsume = $remainingRaw - ($remainingArgs - 1); + $value = implode(' ', array_slice($rawArgs, $j, $toConsume)); + $processed[] = $value; + $j += $toConsume; + $i++; + continue; + } + + $processed[] = $rawArgs[$j]; + $j++; + $i++; + } + + return $processed; + } + + /** + * Validates command arguments against defined requirements + * + * @param CommandSender $sender Command executor + * @param string[] $rawArgs Raw arguments from command input + * @return array Validation result with status and potential errors + */ + private function validateArguments(CommandSender $sender, array $rawArgs): array { + $providedCount = count($rawArgs); + $requiredCount = count($this->getRequiredArguments()); + + if ($providedCount < $requiredCount) { + return $this->getMissingArgsErr($requiredCount, $providedCount); + } + + if ($providedCount > count($this->arguments)) { + return $this->getExcessArgsErr(); + } + + return $this->validateArgumentTypes($sender, $rawArgs); + } + + /** + * Gets all required (non-optional) arguments + * + * @return Argument[] Required arguments + */ + private function getRequiredArguments(): array { + $arguments = []; + foreach ($this->arguments as $argument) { + if (!$argument->isOptional()) { + $arguments[] = $argument; + } + } + return $arguments; + } + + /** + * Generates missing arguments error structure + * + * @param int $required Number of required arguments + * @param int $provided Number of provided arguments + * @return array Error details including missing argument names + */ + private function getMissingArgsErr(int $required, int $provided): array { + $missing = []; + for ($i = $provided; $i < $required; $i++) { + $missing[] = $this->arguments[$i]->getName(); + } + return [ + 'valid' => false, + 'type' => CommandFailure::MISSING_ARGUMENT, + 'message' => 'Missing required arguments: ' . implode(', ', $missing), + 'missing_arguments' => $missing + ]; + } + + /** + * Generates excess arguments error structure + * + * @return array Error details including maximum allowed arguments + */ + private function getExcessArgsErr(): array { + return [ + 'valid' => false, + 'type' => CommandFailure::INVALID_ARGUMENT, + 'message' => 'Too many arguments (max: ' . count($this->arguments) . ')', + 'max_arguments' => count($this->arguments) + ]; + } + + /** + * Validates argument values against their defined types + * + * @param CommandSender $sender Command executor + * @param string[] $rawArgs Processed raw arguments + * @return array Validation result with potential argument errors + */ + private function validateArgumentTypes(CommandSender $sender, array $rawArgs): array { + $errors = []; + + foreach ($rawArgs as $index => $value) { + if (!isset($this->arguments[$index])) continue; + $argument = $this->arguments[$index]; + + if (!$argument->canParse($value, $sender)) { + $errors[] = [ + 'argument' => $argument->getName(), + 'value' => $value, + 'message' => "Invalid value for argument '{$argument->getName()}'" + ]; + } + } + + if (!empty($errors)) { + return [ + 'valid' => false, + 'type' => CommandFailure::INVALID_ARGUMENT, + 'message' => 'Invalid arguments provided', + 'argument_errors' => $errors + ]; + } + + return ['valid' => true]; + } + + /** + * Parses and packages arguments into a CommandArguments object + * + * @param CommandSender $sender Command executor + * @param string[] $rawArgs Raw arguments from command input + * @return CommandArguments Structured argument container + */ + private function parseArguments(CommandSender $sender, array $rawArgs): CommandArguments { + $processedArgs = $this->parseRawArgs($rawArgs); + + $parsed = []; + foreach ($this->arguments as $index => $argDef) { + $rawValue = $processedArgs[$index] ?? null; + $parsed[$argDef->getName()] = $rawValue === null ? null : $argDef->parse($rawValue, $sender); + } + + return new CommandArguments($sender, $parsed); + } +} \ No newline at end of file diff --git a/src/imperazim/command/traits/ConstraintableTrait.php b/src/imperazim/command/traits/ConstraintableTrait.php new file mode 100644 index 0000000..ecbbb8b --- /dev/null +++ b/src/imperazim/command/traits/ConstraintableTrait.php @@ -0,0 +1,74 @@ +constraints[] = $constraint; + return $this; + } + + /** + * Gets all constraints applied to the command. + * + * @return Constraint[] Array of constraints + */ + public function getConstraints(): array { + return $this->constraints; + } + + /** + * Gets constraints of a specific type. + * + * @param string $type The class name of the constraint type + * @return Constraint[] Constraints of the specified type + */ + public function getConstraintsByType(string $type): array { + return array_filter($this->constraints, fn($c) => $c instanceof $type); + } + + /** + * Tests all constraints against a command sender. + * + * @param CommandSender $sender The command sender to test + * @return array Test results with keys: + * - 'success': Whether all constraints were satisfied + * - 'failed_constraints': Array of failed constraints + */ + private function testConstraints(CommandSender $sender): array { + $failed = []; + + foreach ($this->constraints as $constraint) { + if (!$constraint->isSatisfiedBy($sender)) { + $constraint->onFailure($sender); + $failed[] = $constraint; + } else { + $constraint->onSuccess($sender); + } + } + + return [ + 'success' => empty($failed), + 'failed_constraints' => $failed + ]; + } +} \ No newline at end of file diff --git a/src/imperazim/command/traits/SubCommandTrait.php b/src/imperazim/command/traits/SubCommandTrait.php new file mode 100644 index 0000000..bbf996c --- /dev/null +++ b/src/imperazim/command/traits/SubCommandTrait.php @@ -0,0 +1,51 @@ +subcommands[] = $subcommand; + return $this; + } + + /** + * Gets all registered subcommands + * + * @return SubCommand[] Array of subcommand instances + */ + public function getSubCommands(): array { + return $this->subcommands; + } + + /** + * Retrieves a subcommand by its name + * + * @param string $name Name of the subcommand to search for + * @return SubCommand|null Subcommand instance if found, null otherwise + */ + public function getSubCommand(string $name): ?SubCommand { + foreach ($this->subcommands as $subcommand) { + if ($subcommand->getName() === $name) { + return $subcommand; + } + } + return null; + } + +} \ No newline at end of file diff --git a/src/imperazim/components/command/Command.php b/src/imperazim/components/command/Command.php deleted file mode 100644 index d849c53..0000000 --- a/src/imperazim/components/command/Command.php +++ /dev/null @@ -1,78 +0,0 @@ -build = $this->build($plugin); - parent::__construct( - plugin: $plugin, - names: $this->build->getNames(), - description: $this->build->getDescription() - ); - } - - /** - * Set up the command. - * @return CommandBuilder - */ - protected abstract function build(PluginToolkit $plugin): CommandBuilder; - - /** - * Executes the command logic when triggered. - * @param PluginToolkit $plugin The plugin toolkit instance. - * @param mixed $sender The sender of the command (could be a player, console, etc.). - * @param string $aliasUsed The alias of the command that was used. - * @param array $args The arguments passed with the command. - */ - protected abstract function run(PluginToolkit $plugin, mixed $sender, string $aliasUsed, array $args): void; - - /** - * Prepares the command for execution. - */ - protected function prepare(): void { - if ($this->build instanceof CommandBuilder) { - $builder = $this->build; - if ($builder->getPermission()) { - $this->setPermission($builder->getPermission()); - } - $this->registerArguments($builder->getArguments()); - $this->addConstraints($builder->getConstraints()); - if ($builder->getSubcommands()) { - $subcommands = []; - foreach ($builder->getSubcommands() as $subcommand) { - $subcommands[] = new $subcommand($this->getOwningPlugin()); - } - $this->registerSubcommands($subcommands); - } - } - } - - /** - * Executes the command. - * @param mixed $sender - * @param string $aliasUsed - * @param array $args - */ - public function onRun(mixed $sender, string $aliasUsed, array $args): void { - $this->run($this->getOwningPlugin(), $sender, $aliasUsed, $args); - } -} \ No newline at end of file diff --git a/src/imperazim/components/command/CommandBuilder.php b/src/imperazim/components/command/CommandBuilder.php deleted file mode 100644 index 190d8cd..0000000 --- a/src/imperazim/components/command/CommandBuilder.php +++ /dev/null @@ -1,80 +0,0 @@ -names; - } - - /** - * Get command description. - * - * @return string - */ - public function getDescription(): string { - return $this->description; - } - - /** - * Get subcommands directory. - * - * @return array - */ - public function getSubcommands(): array { - return $this->subcommands; - } - - /** - * Get command permission. - * - * @return string|null - */ - public function getPermission(): ?string { - return $this->permission; - } - - /** - * Get command arguments. - * - * @return array - */ - public function getArguments(): array { - return $this->arguments; - } - - /** - * Get command constraints. - * - * @return array - */ - public function getConstraints(): array { - return $this->constraints; - } -} \ No newline at end of file diff --git a/src/imperazim/components/command/CommandManager.php b/src/imperazim/components/command/CommandManager.php deleted file mode 100644 index 55aa1a8..0000000 --- a/src/imperazim/components/command/CommandManager.php +++ /dev/null @@ -1,34 +0,0 @@ -overwriteCommands($plugin, [ - 'version' => VersionCommand::class, - 'genplugin' => GeneratePluginCommand::class, - ]); - return []; - } - -} \ No newline at end of file diff --git a/src/imperazim/components/command/defaults/GeneratePluginCommand.php b/src/imperazim/components/command/defaults/GeneratePluginCommand.php deleted file mode 100644 index 32dab02..0000000 --- a/src/imperazim/components/command/defaults/GeneratePluginCommand.php +++ /dev/null @@ -1,97 +0,0 @@ - new RawStringArgument('PluginName', true), - 1 => new RawStringArgument('PluginVersion', true), - 2 => new RawStringArgument('PluginApiVersion', true), - 3 => new RawStringArgument('PluginAuthor', true), - 4 => new RawStringArgument('AuthorInDir_Y/N', true) - ] - ); - } - - /** - * Executes the command. - * @param PluginToolkit $plugin - * @param mixed $sender - * @param string $aliasUsed - * @param array $args - */ - public function run(PluginToolkit $plugin, mixed $sender, string $aliasUsed, array $args): void { - $pluginName = $args['PluginName'] ?? "Plugin"; - $pluginVersion = $args['PluginVersion'] ?? "1.0.0"; - $pluginApiVersion = $args['PluginApiVersion'] ?? "5.0.0"; - $pluginAuthor = $args['PluginAuthor'] ?? "Unknown"; - $authorInDir = strtolower($args['AuthorInDir_Y/N'] ?? "n") === "y" ? "y" : "n"; - $useEasyLibrary = "y"; - - $currentDir = $plugin->getServer()->getDataPath() . "plugins"; - $dirPath = $currentDir . "/" . $pluginName; - - if (is_dir($dirPath)) { - $sender->sendMessage("§cError: The directory '$pluginName' already exists."); - return; - } - - mkdir($dirPath, 0777, true); - - $pluginNamespace = ($authorInDir === "y") ? "$pluginAuthor\\$pluginName" : $pluginName; - - $pluginYmlContent = "name: $pluginName\nversion: $pluginVersion\napi: $pluginApiVersion\nmain: $pluginNamespace\n"; - if ($pluginAuthor !== "Unknown") { - $pluginYmlContent .= "author: $pluginAuthor\n"; - } - $pluginYmlContent .= "depend:\n - EasyLibrary\n"; - - file_put_contents($dirPath . "/plugin.yml", $pluginYmlContent); - - $srcDir = ($authorInDir === "y") ? "$dirPath/src/$pluginAuthor" : "$dirPath/src"; - mkdir($srcDir, 0777, true); - - $classContent = "saveRecursiveResources();\n"; - $classContent .= " \$this->getLogger()->info(\"$pluginName with EasyLibrary enabled!\");\n"; - $classContent .= " }\n"; - $classContent .= "}\n"; - - file_put_contents("$srcDir/$pluginName.php", $classContent); - - $sender->sendMessage("§aPlugin $pluginName successfully created!"); - - return; - } -} \ No newline at end of file diff --git a/src/imperazim/components/command/defaults/VersionCommand.php b/src/imperazim/components/command/defaults/VersionCommand.php deleted file mode 100644 index 3321a3f..0000000 --- a/src/imperazim/components/command/defaults/VersionCommand.php +++ /dev/null @@ -1,130 +0,0 @@ -sendMessage(TextFormat::BOLD . TextFormat::GOLD . "===== SERVER INFORMATION ====="); - $sender->sendMessage(TextFormat::YELLOW . "Server Software: " . TextFormat::GREEN . VersionInfo::NAME); - $sender->sendMessage(TextFormat::YELLOW . "Library: " . TextFormat::GREEN . $plugin->getName() . TextFormat::GRAY . " (v" . $plugin->getDescription()->getVersion() . ")"); - $sender->sendMessage(TextFormat::YELLOW . "Enabled Components: " . TextFormat::GREEN . count($plugin->componentsManager->enabledComponents)); - $sender->sendMessage(TextFormat::YELLOW . "Third-party Components: " . TextFormat::GREEN . count(\LibraryComponents::$customComponents)); - - $versionColor = VersionInfo::IS_DEVELOPMENT_BUILD ? TextFormat::RED : TextFormat::GREEN; - $sender->sendMessage(TextFormat::YELLOW . "Server Version: " . $versionColor . VersionInfo::VERSION()->getFullVersion()); - $sender->sendMessage(TextFormat::YELLOW . "Git Hash: " . TextFormat::AQUA . VersionInfo::GIT_HASH()); - - $sender->sendMessage(TextFormat::YELLOW . "Minecraft Version: " . TextFormat::GREEN . ProtocolInfo::MINECRAFT_VERSION_NETWORK); - $sender->sendMessage(TextFormat::YELLOW . "Network Protocol: " . TextFormat::GREEN . ProtocolInfo::CURRENT_PROTOCOL); - $sender->sendMessage(TextFormat::YELLOW . "PHP Version: " . TextFormat::GREEN . PHP_VERSION); - - $jitMode = Utils::getOpcacheJitMode(); - if ($jitMode !== null) { - $jitStatus = $jitMode !== 0 - ? "Enabled (Mode: " . $jitMode . ")" - : "Disabled"; - } else { - $jitStatus = "Not Supported"; - } - $sender->sendMessage(TextFormat::YELLOW . "PHP JIT: " . TextFormat::GREEN . $jitStatus); - - $sender->sendMessage(TextFormat::YELLOW . "Operating System: " . TextFormat::GREEN . Utils::getOS()); - $sender->sendMessage(TextFormat::BOLD . TextFormat::GOLD . "==============================="); - } else { - $pluginName = implode(" ", $args); - $exactPlugin = $sender->getServer()->getPluginManager()->getPlugin($pluginName); - - if ($exactPlugin instanceof Plugin) { - $this->describeToSender($exactPlugin, $sender); - - return; - } - - $found = false; - $pluginName = strtolower($pluginName); - foreach ($sender->getServer()->getPluginManager()->getPlugins() as $plugin) { - if (stripos($plugin->getName(), $pluginName) !== false) { - $this->describeToSender($plugin, $sender); - $found = true; - } - } - - if (!$found) { - $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_noSuchPlugin()); - } - } - } catch (\Throwable $e) { - new \crashdump($e); - } - } - - private function describeToSender(Plugin $plugin, mixed $sender) : void { - $desc = $plugin->getDescription(); - $sender->sendMessage(TextFormat::DARK_GREEN . $desc->getName() . TextFormat::RESET . " version " . TextFormat::DARK_GREEN . $desc->getVersion()); - - if ($desc->getDescription() !== "") { - $sender->sendMessage($desc->getDescription()); - } - - if ($desc->getWebsite() !== "") { - $sender->sendMessage("Website: " . $desc->getWebsite()); - } - - if (count($authors = $desc->getAuthors()) > 0) { - if (count($authors) === 1) { - $sender->sendMessage("Author: " . implode(", ", $authors)); - } else { - $sender->sendMessage("Authors: " . implode(", ", $authors)); - } - } - } - -} \ No newline at end of file diff --git a/src/imperazim/components/command/subcommand/Subcommand.php b/src/imperazim/components/command/subcommand/Subcommand.php deleted file mode 100644 index 22b17e5..0000000 --- a/src/imperazim/components/command/subcommand/Subcommand.php +++ /dev/null @@ -1,69 +0,0 @@ -build = $this->build($plugin); - parent::__construct( - plugin: $plugin, - names: $this->build->getNames(), - description: $this->build->getDescription() - ); - } - - /** - * Set up the command. - * @return SubcommandBuilder - */ - protected abstract function build(PluginToolkit $plugin): SubcommandBuilder; - - /** - * Executes the command logic when triggered. - * @param PluginToolkit $plugin The plugin toolkit instance. - * @param mixed $sender The sender of the command (could be a player, console, etc.). - * @param string $aliasUsed The alias of the command that was used. - * @param array $args The arguments passed with the command. - */ - protected abstract function run(PluginToolkit $plugin, mixed $sender, string $aliasUsed, array $args): void; - - /** - * Prepares the command for execution. - */ - protected function prepare(): void { - if ($this->build instanceof SubcommandBuilder) { - $builder = $this->build; - if ($builder->getPermission()) { - $this->setPermission($builder->getPermission()); - } - $this->registerArguments($builder->getArguments()); - $this->addConstraints($builder->getConstraints()); - } - } - - /** - * Executes the command. - * @param mixed $sender - * @param string $aliasUsed - * @param array $args - */ - public function onRun(mixed $sender, string $aliasUsed, array $args): void { - $this->run($this->getOwningPlugin(), $sender, $aliasUsed, $args); - } -} \ No newline at end of file diff --git a/src/imperazim/components/command/subcommand/SubcommandBuilder.php b/src/imperazim/components/command/subcommand/SubcommandBuilder.php deleted file mode 100644 index 0e153b9..0000000 --- a/src/imperazim/components/command/subcommand/SubcommandBuilder.php +++ /dev/null @@ -1,70 +0,0 @@ -names; - } - - /** - * Get command description. - * - * @return string - */ - public function getDescription(): string { - return $this->description; - } - - /** - * Get command permission. - * - * @return string|null - */ - public function getPermission(): ?string { - return $this->permission; - } - - /** - * Get command arguments. - * - * @return array - */ - public function getArguments(): array { - return $this->arguments; - } - - /** - * Get command constraints. - * - * @return array - */ - public function getConstraints(): array { - return $this->constraints; - } -} \ No newline at end of file diff --git a/src/imperazim/components/commands/PluginsCommand.php b/src/imperazim/components/commands/PluginsCommand.php new file mode 100644 index 0000000..cd6f313 --- /dev/null +++ b/src/imperazim/components/commands/PluginsCommand.php @@ -0,0 +1,143 @@ + 'plugins', + 'aliases' => ['pl'], + 'description' => 'List all server plugins.', + 'permission' => DefaultPermissionNames::COMMAND_PLUGINS + ]; + } + + /** + * Executes the command logic. + * + * @param CommandResult $result The command execution result + */ + public function onExecute(CommandResult $result): void { + $sender = $result->getSender(); + $args = $result->getArgumentsList(); + $pluginManager = $sender->getServer()->getPluginManager(); + if (count($args) === 0) { + $plugins = $pluginManager->getPlugins(); + $enabled = count(array_filter($plugins, fn(Plugin $p) => $p->isEnabled())); + + // Cabeçalho dinâmico + $header = "===== PLUGINS (" . count($plugins) . ") ====="; + $footer = str_repeat("=", strlen($header)); + + $sender->sendMessage(TextFormat::BOLD . TextFormat::GOLD . $header); + $sender->sendMessage(TextFormat::GRAY . "Enabled: " . TextFormat::GREEN . $enabled . TextFormat::GRAY . " | Disabled: " . TextFormat::RED . (count($plugins) - $enabled)); + + $list = array_map(function(Plugin $plugin): string { + return ($plugin->isEnabled() ? TextFormat::GREEN : TextFormat::RED) . $plugin->getDescription()->getFullName(); + }, $plugins); + sort($list, SORT_STRING); + + $sender->sendMessage(TextFormat::RESET . implode("\n", $list)); + $sender->sendMessage(TextFormat::BOLD . TextFormat::GOLD . $footer); + } else { + $pluginName = implode(" ", $args->toArray()); + $exactPlugin = $pluginManager->getPlugin($pluginName); + + if ($exactPlugin instanceof Plugin) { + $this->describePlugin($exactPlugin, $sender); + return; + } + + $found = false; + $lowerName = strtolower($pluginName); + foreach ($pluginManager->getPlugins() as $p) { + if (stripos($p->getName(), $lowerName) !== false) { + $this->describePlugin($p, $sender); + $found = true; + } + } + + if (!$found) { + $sender->sendMessage(TextFormat::RED . "No plugin found matching '" . $pluginName . "'."); + } + } + } + + /** + * Handles command execution failures. + * + * @param CommandFailure $failure The failure details + */ + public function onFailure(CommandFailure $failure): void { + $sender = $failure->getSender(); + if ($failure->getReason() === CommandFailure::EXECUTION_ERROR) { + $data = $failure->getData(); + $sender->sendMessage("Fatal error: " . $failure->getMessage()); + $sender->sendMessage("Location: " . $data['file'] . ": " . $data['line']); + return; + } + $sender->sendMessage($failure->getMessage()); + } + + /** + * Describes the description of a plugin + * + * @param Plugin $plugin The plugin. + * @param mixed $sender The sender. + */ + private function describePlugin(Plugin $plugin, mixed $sender): void { + $desc = $plugin->getDescription(); + $status = $plugin->isEnabled() ? TextFormat::GREEN . "Enabled" : TextFormat::RED . "Disabled"; + + // Cabeçalho dinâmico para plugin individual + $header = "===== " . $desc->getName() . " ====="; + $footer = "======================" . str_repeat("=", strlen($desc->getName())); + + $sender->sendMessage(TextFormat::BOLD . TextFormat::DARK_AQUA . $header); + $sender->sendMessage(TextFormat::YELLOW . "Version: " . TextFormat::WHITE . $desc->getVersion()); + $sender->sendMessage(TextFormat::YELLOW . "Status: " . $status); + + if ($desc->getDescription() !== "") { + $sender->sendMessage(TextFormat::YELLOW . "Description: " . TextFormat::GRAY . $desc->getDescription()); + } + if ($desc->getWebsite() !== "") { + $sender->sendMessage(TextFormat::YELLOW . "Website: " . TextFormat::BLUE . $desc->getWebsite()); + } + if (count($authors = $desc->getAuthors()) > 0) { + $sender->sendMessage(TextFormat::YELLOW . "Authors: " . TextFormat::GOLD . implode(", ", $authors)); + } + $sender->sendMessage(TextFormat::BOLD . TextFormat::DARK_AQUA . $footer); + } + +} \ No newline at end of file diff --git a/src/imperazim/components/commands/VersionCommand.php b/src/imperazim/components/commands/VersionCommand.php new file mode 100644 index 0000000..76f3479 --- /dev/null +++ b/src/imperazim/components/commands/VersionCommand.php @@ -0,0 +1,227 @@ + 'version', + 'aliases' => ['ver', + 'about'], + 'description' => 'See detailed information about the server', + 'permission' => DefaultPermissionNames::COMMAND_VERSION + ]; + } + + /** + * Executes the version command logic. + * + * Displays server information when no arguments are provided, or searches + * for plugins when arguments are given. + * + * @param CommandResult $result The command execution result container + */ + public function onExecute(CommandResult $result): void { + $sender = $result->getSender(); + $args = $result->getArgumentsList(); + $pluginManager = $sender->getServer()->getPluginManager(); + + if (count($args) === 0) { + $plugin = $this->getPlugin(); + $server = $sender->getServer(); + + $header = "§l§6»§r §lSERVER INFORMATION§r §6«"; + $divider = "§8§l»§r§7" . str_repeat("─", 38) . "§8§l«"; + $footer = "§8§l»§r§7" . str_repeat("─", 38) . "§8§l«"; + + $message = []; + + $message[] = $header; + $message[] = $divider; + $message[] = "§e§l»§r §aSOFTWARE"; + $message[] = " §8▪ §7Server: §f" . VersionInfo::NAME; + $message[] = " §8▪ §7Library: §f" . $plugin->getName() . " §8(v" . $plugin->getDescription()->getVersion() . ")"; + + $versionColor = VersionInfo::IS_DEVELOPMENT_BUILD ? "§c" : "§a"; + $message[] = " §8▪ §7Version: " . $versionColor . VersionInfo::VERSION()->getFullVersion(); + $message[] = " §8▪ §7Git: §3" . VersionInfo::GIT_HASH(); + + $message[] = $divider; + $message[] = "§e§l»§r §aMINECRAFT"; + $message[] = " §8▪ §7Version: §f" . ProtocolInfo::MINECRAFT_VERSION_NETWORK; + $message[] = " §8▪ §7Protocol: §f" . ProtocolInfo::CURRENT_PROTOCOL; + + $message[] = $divider; + $message[] = "§e§l»§r §aPERFORMANCE"; + $message[] = " §8▪ §7Uptime: §f" . $this->formatUptime($server->getTick()); + $message[] = " §8▪ §7TPS: §f" . round($server->getTicksPerSecond(), 1); + $message[] = " §8▪ §7Load: §f" . round($server->getTickUsage(), 1) . "%"; + $message[] = " §8▪ §7Memory: §f" . $this->formatMemory(memory_get_usage()); + + $message[] = $divider; + $message[] = "§e§l»§r §aSYSTEM"; + $message[] = " §8▪ §7PHP: §f" . PHP_VERSION; + + $jitMode = Utils::getOpcacheJitMode(); + if ($jitMode !== null) { + $jitStatus = $jitMode !== 0 + ? "§aEnabled §8(Mode: " . $jitMode . ")" + : "§cDisabled"; + } else { + $jitStatus = "§cNot Supported"; + } + $message[] = " §8▪ §7PHP JIT: " . $jitStatus; + + $message[] = " §8▪ §7OS: §f" . Utils::getOS(); + + $message[] = $divider; + $message[] = "§e§l»§r §aCOMPONENTS"; + $message[] = " §8▪ §7Enabled: §a" . count($plugin->componentsManager->enabledComponents); + $message[] = " §8▪ §7Custom: §b" . count(\LibraryComponents::$customComponents) - count($plugin->componentsManager->enabledComponents); + + $message[] = $footer; + + $sender->sendMessage(implode("\n", $message)); + } else { + $pluginName = implode(" ", $args); + $exactPlugin = $pluginManager->getPlugin($pluginName); + + if ($exactPlugin instanceof Plugin) { + $this->describeToSender($exactPlugin, $sender); + return; + } + + $found = false; + $pluginName = strtolower($pluginName); + foreach ($pluginManager->getPlugins() as $plugin) { + if (stripos($plugin->getName(), $pluginName) !== false) { + $this->describeToSender($plugin, $sender); + $found = true; + } + } + + if (!$found) { + $sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_noSuchPlugin()); + } + } + } + + /** + * Formats memory bytes into human-readable string. + * + * @param int $bytes Memory usage in bytes + * @return string Formatted memory string (e.g. "128.54 MB") + */ + private function formatMemory(int $bytes): string { + $units = ['B', + 'KB', + 'MB', + 'GB']; + $index = 0; + + while ($bytes >= 1024 && $index < 3) { + $bytes /= 1024; + $index++; + } + + return round($bytes, 2) . ' ' . $units[$index]; + } + + /** + * Formats server uptime from ticks into human-readable duration. + * + * @param int $ticks Server uptime in ticks + * @return string Formatted duration (e.g. "2h 15m") + */ + private function formatUptime(int $ticks): string { + $seconds = $ticks / 20; + $minutes = $seconds / 60; + $hours = $minutes / 60; + + if ($hours >= 1) { + return floor($hours) . 'h ' . floor($minutes % 60) . 'm'; + } + if ($minutes >= 1) { + return floor($minutes) . 'm ' . floor($seconds % 60) . 's'; + } + return floor($seconds) . 's'; + } + + /** + * Handles command execution failures. + * + * @param CommandFailure $failure Container with failure details + */ + public function onFailure(CommandFailure $failure): void { + $sender = $failure->getSender(); + if ($failure->getReason() === CommandFailure::EXECUTION_ERROR) { + $data = $failure->getData(); + $sender->sendMessage("Fatal error: " . $failure->getMessage()); + $sender->sendMessage("Location: " . $data['file'] . ": " . $data['line']); + return; + } + $sender->sendMessage($failure->getMessage()); + } + + /** + * Describes plugin details to the sender. + * + * @param Plugin $plugin Plugin to describe + * @param mixed $sender Command sender + */ + private function describeToSender(Plugin $plugin, mixed $sender) : void { + $desc = $plugin->getDescription(); + $sender->sendMessage(TextFormat::DARK_GREEN . $desc->getName() . TextFormat::RESET . " version " . TextFormat::DARK_GREEN . $desc->getVersion()); + + if ($desc->getDescription() !== "") { + $sender->sendMessage($desc->getDescription()); + } + + if ($desc->getWebsite() !== "") { + $sender->sendMessage("Website: " . $desc->getWebsite()); + } + + if (count($authors = $desc->getAuthors()) > 0) { + if (count($authors) === 1) { + $sender->sendMessage("Author: " . implode(", ", $authors)); + } else { + $sender->sendMessage("Authors: " . implode(", ", $authors)); + } + } + } +} \ No newline at end of file diff --git a/src/imperazim/components/filesystem/File.php b/src/imperazim/components/filesystem/File.php index a507a0c..fcde41e 100644 --- a/src/imperazim/components/filesystem/File.php +++ b/src/imperazim/components/filesystem/File.php @@ -43,9 +43,6 @@ public function __construct( } else { $directory = str_replace('//', '/', $directoryOrConfig . '/'); } - if (!in_array($fileType, self::getTypes())) { - throw new FileSystemException("Invalid file type: $fileType"); - } $this->directoryOrConfig = $directory; $this->fileName = $fileName ?? ''; $this->fileType = $fileType ?? self::TYPE_YML; @@ -61,6 +58,27 @@ public function __construct( } } + // Add these new methods to support Island management + public function read(): array { + return $this->get(null, []); + } + + public function write(array $data): void { + $extension = self::getExtensionByType($this->getFileType()); + $content = self::serializeContent($extension, $data); + $this->writeFile($content); + } + + public function getFileName(): string { + return $this->fileName; + } + + public function getFolder(): string { + return $this->directoryOrConfig instanceof Config + ? dirname($this->directoryOrConfig->getPath()) + : $this->directoryOrConfig; + } + /** * Clone the File object with empty data. * @return self @@ -136,8 +154,9 @@ public static function serializeContent(string $extension, array $data): string 'txt' => self::writeList(array_keys($data)), 'ini' => self::writeIniFile($data) }; + } else { + return self::writeList(array_keys($data)); } - throw new FileSystemException("Unsupported file type: {$extension}"); } /** @@ -156,8 +175,9 @@ public static function deserializeContent(string $extension, string $fileContent 'txt' => array_fill_keys(self::parseList($fileContent), true), 'ini' => parse_ini_string($fileContent, true) }; + } else { + return array_fill_keys(self::parseList($fileContent), true); } - throw new FileSystemException("Unsupported file type: {$extension}"); } /** diff --git a/src/imperazim/components/filesystem/Path.php b/src/imperazim/components/filesystem/Path.php index baff19b..b381503 100644 --- a/src/imperazim/components/filesystem/Path.php +++ b/src/imperazim/components/filesystem/Path.php @@ -49,6 +49,53 @@ public function getFolder(): string { return $this->sourceFolder; } + /** + * Checks if the directory exists. + * @return bool + */ + public function exists(): bool { + return is_dir($this->sourceFolder); + } + + /** + * Gets all files in the directory as File objects. + * @return File[] + */ + public function getFiles(): array { + if (!$this->exists()) { + return []; + } + + $files = []; + $iterator = new DirectoryIterator($this->sourceFolder); + + foreach ($iterator as $file) { + if ($file->isDot() || $file->isDir()) continue; + + $fileName = $file->getBasename('.' . $file->getExtension()); + $extension = $file->getExtension(); + $fileType = File::getTypeByExtension($extension); + + $files[] = new File( + directoryOrConfig: $this->sourceFolder, + fileName: $fileName, + fileType: $fileType + ); + } + + return $files; + } + + /** + * Gets all file base names (without extension) in the directory. + * @return string[] + */ + public function getBaseNames(): array { + return array_map( + fn(File $file) => $file->getFileName(), + $this->getFiles() + ); + } /** * Adds a new file to the directory using the File class. @@ -270,11 +317,13 @@ public static function getRecursiveFiles(string $folder): array { foreach ($files as $file) { if ($file->isFile()) { + $extension = $file->getExtension(); + $fileType = File::getTypeByExtension($extension); $filesInfo[] = [ 'directory' => $file->getPath(), - 'fileName' => $file->getBasename(), - 'fileType' => File::getTypeByExtension($file->getExtension()), - 'content' => File::deserializeContent($file->getExtension(), file_get_contents($file->getRealPath())) + 'fileName' => $file->getBasename('.' . $extension), + 'fileType' => $fileType, + 'content' => File::deserializeContent($extension, file_get_contents($file->getRealPath())) ]; } } diff --git a/src/imperazim/components/filesystem/traits/FileExtensionTypes.php b/src/imperazim/components/filesystem/traits/FileExtensionTypes.php index 8a41b92..8fce184 100644 --- a/src/imperazim/components/filesystem/traits/FileExtensionTypes.php +++ b/src/imperazim/components/filesystem/traits/FileExtensionTypes.php @@ -58,7 +58,10 @@ public static function getExtensions(): array { public static function getTypeByExtension(string $extension): ?string { $extension = strtolower($extension); $type = array_search($extension, self::$typeToExtension, true); - return $type !== false ? $type : null; + if ($type !== false) { + return $type; + } + return 'file:' . $extension; } /** @@ -68,8 +71,15 @@ public static function getTypeByExtension(string $extension): ?string { * @return string|null */ public static function getExtensionByType(string $type, bool $withPoint = false): ?string { - $extension = self::$typeToExtension[$type] ?? null; - return $extension !== null ? ($withPoint ? '.' . $extension : $extension) : null; + if (isset(self::$typeToExtension[$type])) { + $extension = self::$typeToExtension[$type]; + return $withPoint ? '.' . $extension : $extension; + } + if (str_starts_with($type, 'file:')) { + $extension = substr($type, 5); + return $withPoint ? '.' . $extension : $extension; + } + return null; } - + } \ No newline at end of file diff --git a/src/imperazim/components/item/traits/ItemRarityTrait.php b/src/imperazim/components/item/traits/ItemRarityTrait.php deleted file mode 100644 index cb0cf13..0000000 --- a/src/imperazim/components/item/traits/ItemRarityTrait.php +++ /dev/null @@ -1,32 +0,0 @@ -rarity; - } - - /** @param string|null $rarity */ - public function setRarity(?string $rarity): void { - $this->rarity = $rarity; - } - -} \ No newline at end of file diff --git a/src/imperazim/components/plugin/PluginToolkit.php b/src/imperazim/components/plugin/PluginToolkit.php index 526365a..c961f8e 100644 --- a/src/imperazim/components/plugin/PluginToolkit.php +++ b/src/imperazim/components/plugin/PluginToolkit.php @@ -351,7 +351,7 @@ public function saveRecursiveResources(?string $loadType = '--merge'): ?array { private function processFile(array $file, ?string $loadType): ?File { try { $fileName = $file['fileName'] ?? null; - $fileType = $file['fileType'] ?? null; + $fileType = $file['fileType'] ?? 'file:unknown'; $fileContent = $file['content'] ?? null; $fileDirectory = $file['directory'] ?? null; diff --git a/src/imperazim/components/trigger/ItemTrigger.php b/src/imperazim/components/trigger/ItemTrigger.php new file mode 100644 index 0000000..98f196e --- /dev/null +++ b/src/imperazim/components/trigger/ItemTrigger.php @@ -0,0 +1,42 @@ +getInventory(); + + // Functional approach: Check if any item matches the class + $hasItem = count(array_filter( + $inventory->getContents(), + fn(Item $item) => $item instanceof $itemClass + )) > 0; + + if ($hasItem) { + $callback($player, $inventory); // Pass both player and inventory + return true; + } + return false; + }; + + parent::__construct($condition, fn(Player $player) => $callback($player, $player->getInventory())); + } +} \ No newline at end of file diff --git a/src/imperazim/components/trigger/TriggerTypes.php b/src/imperazim/components/trigger/TriggerTypes.php index 6c44ba7..4bc902f 100644 --- a/src/imperazim/components/trigger/TriggerTypes.php +++ b/src/imperazim/components/trigger/TriggerTypes.php @@ -12,5 +12,6 @@ final class TriggerTypes { const GLOBAL = 0; const PER_PLAYER = 1; + const ITEM = 2; } \ No newline at end of file diff --git a/src/imperazim/components/ui/Form.php b/src/imperazim/components/ui/Form.php deleted file mode 100644 index a1c1160..0000000 --- a/src/imperazim/components/ui/Form.php +++ /dev/null @@ -1,30 +0,0 @@ -title = $title; + } + + /** + * Gets form title. + * + * @return Title Title element + */ + public function getTitle(): Title { + return $this->title; + } + + /** + * Sets form title. + * + * @param Title $title New title + */ + public function setTitle(Title $title): void { + $this->title = $title; + } + + /** + * Sends form to player. + * + * @param Player $player Target player + */ + public function sendTo(Player $player): void { + $player->sendForm($this); + } + + /** + * Serializes form for JSON. + * + * @return array Serialized data + */ + final public function jsonSerialize(): array { + return [ + 'type' => $this->getFormType(), + 'title' => $this->title->getText(), + ...$this->serializeFormData() + ]; + } + + /** + * Serializes form-specific data. + * + * @return array Form data + */ + abstract protected function serializeFormData(): array; + + /** + * Gets form type identifier. + * + * @return string Form type + */ + abstract protected function getFormType(): string; +} \ No newline at end of file diff --git a/src/imperazim/form/FormData.php b/src/imperazim/form/FormData.php new file mode 100644 index 0000000..fea7ace --- /dev/null +++ b/src/imperazim/form/FormData.php @@ -0,0 +1,109 @@ + +* @implements IteratorAggregate +*/ +final class FormData implements ArrayAccess, IteratorAggregate, Countable { + /** @var array Data storage */ + private array $data; + + /** + * @param array $data Initial data + */ + public function __construct(array $data = []) { + $this->data = $data; + } + + /** + * Checks if offset exists. + * + * @param mixed $offset Offset to check + * @return bool Existence status + */ + public function offsetExists(mixed $offset): bool { + return isset($this->data[$offset]); + } + + /** + * Gets value by offset. + * + * @param mixed $offset Data key + * @return mixed Value or null + */ + public function offsetGet(mixed $offset): mixed { + return $this->data[$offset] ?? null; + } + + /** + * Sets value by offset. + * + * @param mixed $offset Data key + * @param mixed $value Value to set + */ + public function offsetSet(mixed $offset, mixed $value): void { + if ($offset === null) { + $this->data[] = $value; + } else { + $this->data[$offset] = $value; + } + } + + /** + * Unsets value by offset. + * + * @param mixed $offset Data key + */ + public function offsetUnset(mixed $offset): void { + unset($this->data[$offset]); + } + + /** + * Gets data iterator. + * + * @return Traversable Data iterator + */ + public function getIterator(): Traversable { + return new ArrayIterator($this->data); + } + + /** + * Counts data items. + * + * @return int Item count + */ + public function count(): int { + return count($this->data); + } + + /** + * Gets value by key. + * + * @param string|int $key Data key + * @return mixed Value or null + */ + public function get(string|int $key): mixed { + return $this->offsetGet($key); + } + + /** + * Converts data to array. + * + * @return array Data array + */ + public function toArray(): array { + return $this->data; + } +} \ No newline at end of file diff --git a/src/imperazim/form/FormResult.php b/src/imperazim/form/FormResult.php new file mode 100644 index 0000000..9736ea7 --- /dev/null +++ b/src/imperazim/form/FormResult.php @@ -0,0 +1,16 @@ +text; + } + + /** + * Serializes the element to a string. + * + * @return string Serialized text + */ + public function jsonSerialize(): string { + return $this->text; + } +} \ No newline at end of file diff --git a/src/imperazim/form/base/elements/Title.php b/src/imperazim/form/base/elements/Title.php new file mode 100644 index 0000000..8f1fa6f --- /dev/null +++ b/src/imperazim/form/base/elements/Title.php @@ -0,0 +1,17 @@ +player = $player; + $this->force = $force; + $this->formData = $data instanceof FormData ? $data : new FormData($data); + + parent::__construct($this->title($player, $this->formData)); + + if ($force) { + $this->sendTo($player); + } + } + + /** + * Gets form title. + * + * @param Player $player Viewing player + * @param FormData $data Form data + * @return Title Title element + */ + abstract protected function title(Player $player, FormData $data): Title; + + /** + * Gets form elements. + * + * @param Player $player Viewing player + * @param FormData $data Form data + * @return ElementCollection Elements collection + */ + abstract protected function elements(Player $player, FormData $data): ElementCollection; + + /** + * Handles form submission. + * + * @param Player $player Submitting player + * @param CustomResponse $response Form response + * @return FormResult Action result + */ + abstract protected function onSubmit( + Player $player, + CustomResponse $response + ): FormResult; + + /** + * Handles form closure. + * + * @param Player $player Closing player + * @param FormData $data Form data + * @return FormResult Action result + */ + abstract protected function onClose( + Player $player, + FormData $data + ): FormResult; + + /** + * Handles form response. + * + * @param Player $player Responding player + * @param mixed $raw Raw response data + */ + public function handleResponse(Player $player, $raw): void { + if ($raw !== null && !is_array($raw)) { + throw new InvalidArgumentException("Response data must be array or null."); + } + + $elements = $this->getCachedElements($player, $this->formData); + $idMap = []; + $elementMap = []; + + if (is_array($raw)) { + foreach ($elements as $idx => $element) { + $elementMap[$element->getId()] = $element; + + if ($element->hasValue() && array_key_exists($idx, $raw)) { + $idMap[$element->getId()] = $raw[$idx]; + } + } + } + + if ($raw === null) { + $result = $this->onClose($player, $this->formData); + } else { + $response = new CustomResponse( + $this->formData, + $raw, + $idMap, + $elementMap + ); + + if (!$this->force && count($response->getElementsRaw()) !== count($elements)) { + throw new InvalidArgumentException("Invalid number of fields."); + } + + $result = $this->onSubmit($player, $response); + } + + if ($result === FormResult::KEEP) { + $this->sendTo($player); + } + } + + /** + * Serializes form data. + * + * @return array Serialized data + */ + protected function serializeFormData(): array { + $elements = $this->getCachedElements($this->player, $this->formData); + return [ + 'content' => array_map( + fn(JsonSerializable $e) => $e->jsonSerialize(), + $elements + ) + ]; + } + + /** + * Gets form type. + * + * @return string Form type identifier + */ + protected function getFormType(): string { + return 'custom_form'; + } + + /** + * Gets and validates cached elements. + * + * @param Player $player Viewing player + * @param FormData $data Form data + * @return array Validated elements + * @throws InvalidArgumentException If elements lack IDs + */ + private function getCachedElements(Player $player, FormData $data): array { + if ($this->elementsCache === null) { + $this->elementsCache = $this->elements($player, $data); + + foreach ($this->elementsCache as $element) { + if (!$element->hasId()) { + throw new InvalidArgumentException("All elements must have an ID."); + } + } + } + return iterator_to_array($this->elementsCache); + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/DynamicCustomForm.php b/src/imperazim/form/custom/DynamicCustomForm.php new file mode 100644 index 0000000..a669a1f --- /dev/null +++ b/src/imperazim/form/custom/DynamicCustomForm.php @@ -0,0 +1,153 @@ +elements = new ElementCollection(); + $this->onSubmit = fn(Player $p, CustomResponse $r): FormResult => FormResult::CLOSE; + $this->onClose = fn(Player $p): FormResult => FormResult::CLOSE; + } + + /** + * Creates a new form instance. + * + * @param string|Title $title Form title + * @return self New instance + */ + final public static function create(string|Title $title): self { + return new self($title instanceof Title ? $title : new Title($title)); + } + + /** + * Builds a form using configuration closure. + * + * @param string|Title $title Form title + * @param Closure $configurator Configuration callback + * @return self Configured instance + */ + final public static function build(string|Title $title, Closure $configurator): self { + $form = self::create($title); + $configurator($form); + return $form; + } + + /** + * Adds an element to the form. + * + * @param Element $element Form element + * @return self + */ + public function addElement(Element $element): self { + $this->elements->add($element); + return $this; + } + + /** + * Sets submission handler. + * + * @param Closure $handler Submission handler + * @return self + */ + public function setOnSubmit(Closure $handler): self { + $this->onSubmit = $handler; + return $this; + } + + /** + * Sets close handler. + * + * @param Closure $handler Close handler + * @return self + */ + public function setOnClose(Closure $handler): self { + $this->onClose = $handler; + return $this; + } + + /** + * Serializes form data. + * + * @return array Serialized data + */ + protected function serializeFormData(): array { + return [ + 'content' => array_map( + fn(JsonSerializable $e) => $e->jsonSerialize(), + iterator_to_array($this->elements) + ) + ]; + } + + /** + * Gets form type. + * + * @return string Form type identifier + */ + protected function getFormType(): string { + return 'custom_form'; + } + + /** + * Handles form response. + * + * @param Player $player Responding player + * @param mixed $raw Raw response data + */ + public function handleResponse(Player $player, $raw): void { + if ($raw === null) { + $result = ($this->onClose)($player); + } elseif (is_array($raw)) { + $idMap = []; + $elementMap = []; + + foreach ($this->elements as $idx => $element) { + $elementMap[$element->getId()] = $element; + + if ($element->hasValue() && array_key_exists($idx, $raw)) { + $idMap[$element->getId()] = $raw[$idx]; + } + } + + $response = new CustomResponse(new FormData([]), $raw, $idMap, $elementMap); + $result = ($this->onSubmit)($player, $response); + } else { + throw new InvalidArgumentException("Invalid response data type"); + } + + if ($result === FormResult::KEEP) { + $this->sendTo($player); + } + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/Dropdown.php b/src/imperazim/form/custom/elements/Dropdown.php new file mode 100644 index 0000000..0ed7505 --- /dev/null +++ b/src/imperazim/form/custom/elements/Dropdown.php @@ -0,0 +1,120 @@ +options = $this->normalizeOptions($options); + $this->defaultIndex = $this->findOptionIndex($default); + } + + /** + * Normalizes options array. + * + * @param array $options Raw options + * @return Option[] Normalized options + */ + private function normalizeOptions(array $options): array { + $normalized = []; + foreach ($options as $key => $value) { + if ($value instanceof Option) { + $normalized[] = $value; + } elseif (is_array($value)) { + $normalized[] = new Option( + (string)($value['id'] ?? $key), + (string)($value['text'] ?? '') + ); + } else { + $normalized[] = new Option((string)$key, (string)$value); + } + } + return $normalized; + } + + /** + * Finds option index by ID or value. + * + * @param string|int $default Default identifier + * @return int Option index + */ + private function findOptionIndex(string|int $default): int { + if (is_int($default)) { + return $default; + } + + foreach ($this->options as $index => $option) { + if ($option->getId() === $default) { + return $index; + } + } + return 0; + } + + /** + * Serializes element for JSON. + * + * @return array Serialized data + */ + public function jsonSerialize(): array { + return [ + 'type' => 'dropdown', + 'text' => $this->text, + 'options' => array_map(fn(Option $o) => $o->getText(), $this->options), + 'default' => $this->defaultIndex + ]; + } + + /** + * Gets option by index. + * + * @param int $index Option position + * @return Option|null Option or null + */ + public function getOptionByIndex(int $index): ?Option { + return $this->options[$index] ?? null; + } + + /** + * Gets selected option. + * + * @param int $index Selected index + * @return SelectedOption|null Selected option or null + */ + public function getSelectedOption(int $index): ?SelectedOption { + $option = $this->getOptionByIndex($index); + if ($option === null) { + return null; + } + + return new SelectedOption( + $index, + $option->getId(), + $option->getText() + ); + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/Element.php b/src/imperazim/form/custom/elements/Element.php new file mode 100644 index 0000000..9fca74e --- /dev/null +++ b/src/imperazim/form/custom/elements/Element.php @@ -0,0 +1,56 @@ +id === null) { + throw new InvalidArgumentException("Element has no ID."); + } + return $this->id; + } + + /** + * Checks if element has an ID. + * + * @return bool True if has ID + */ + public function hasId(): bool { + return $this->id !== null; + } + + /** + * Checks if element holds a value. + * + * @return bool True if element has value + */ + public function hasValue(): bool { + return true; + } + + /** + * Serializes element for JSON. + * + * @return array Serialized data + */ + abstract public function jsonSerialize(): array; +} \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/ElementCollection.php b/src/imperazim/form/custom/elements/ElementCollection.php new file mode 100644 index 0000000..3937124 --- /dev/null +++ b/src/imperazim/form/custom/elements/ElementCollection.php @@ -0,0 +1,106 @@ +elements[] = $element; + } + + /** + * Creates collection from array. + * + * @param Element[] $elements Elements array + * @return self New collection + */ + public static function fromArray(array $elements): self { + $collection = new self(); + $collection->elements = $elements; + return $collection; + } + + /** + * Gets element by index. + * + * @param int $index Element position + * @return Element|null Element or null + */ + public function getByIndex(int $index): ?Element { + return $this->elements[$index] ?? null; + } + + /** + * Counts elements in collection. + * + * @return int Element count + */ + public function count(): int { + return count($this->elements); + } + + /** + * Gets iterator for elements. + * + * @return Traversable Element iterator + */ + public function getIterator(): Traversable { + yield from $this->elements; + } + + /** + * Checks if offset exists. + * + * @param mixed $offset Offset to check + * @return bool Existence status + */ + public function offsetExists(mixed $offset): bool { + return isset($this->elements[$offset]); + } + + /** + * Gets element by offset. + * + * @param mixed $offset Element position + * @return Element|null Element or null + */ + public function offsetGet(mixed $offset): ?Element { + return $this->elements[$offset] ?? null; + } + + /** + * Disabled setter (immutable). + * + * @throws RuntimeException Always + */ + public function offsetSet(mixed $offset, mixed $value): void { + throw new RuntimeException('ElementCollection is immutable'); + } + + /** + * Disabled unsetter (immutable). + * + * @throws RuntimeException Always + */ + public function offsetUnset(mixed $offset): void { + throw new RuntimeException('ElementCollection is immutable'); + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/Input.php b/src/imperazim/form/custom/elements/Input.php new file mode 100644 index 0000000..d6f15bf --- /dev/null +++ b/src/imperazim/form/custom/elements/Input.php @@ -0,0 +1,39 @@ + 'input', + 'text' => $this->text, + 'placeholder' => $this->placeholder, + 'default' => $this->default + ]; + } + } \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/Label.php b/src/imperazim/form/custom/elements/Label.php new file mode 100644 index 0000000..4407a47 --- /dev/null +++ b/src/imperazim/form/custom/elements/Label.php @@ -0,0 +1,40 @@ +toString()); + } + + /** + * Checks if element holds a value. + * + * @return bool Always false for labels + */ + public function hasValue(): bool { + return false; + } + + /** + * Serializes element for JSON. + * + * @return array Serialized data + */ + public function jsonSerialize(): array { + return [ + 'type' => 'label', + 'text' => $this->text + ]; + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/Option.php b/src/imperazim/form/custom/elements/Option.php new file mode 100644 index 0000000..c7a25fd --- /dev/null +++ b/src/imperazim/form/custom/elements/Option.php @@ -0,0 +1,37 @@ +id; + } + + /** + * Gets display text. + * + * @return string Display text + */ + public function getText(): string { + return $this->text; + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/Slider.php b/src/imperazim/form/custom/elements/Slider.php new file mode 100644 index 0000000..3b63681 --- /dev/null +++ b/src/imperazim/form/custom/elements/Slider.php @@ -0,0 +1,45 @@ + 'slider', + 'text' => $this->text, + 'min' => $this->min, + 'max' => $this->max, + 'step' => $this->step, + 'default' => $this->default + ]; + } + } \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/StepSlider.php b/src/imperazim/form/custom/elements/StepSlider.php new file mode 100644 index 0000000..aa0c1fb --- /dev/null +++ b/src/imperazim/form/custom/elements/StepSlider.php @@ -0,0 +1,120 @@ +steps = $this->normalizeSteps($steps); + $this->defaultIndex = $this->findStepIndex($default); + } + + /** + * Normalizes steps array. + * + * @param array $steps Raw steps + * @return Option[] Normalized steps + */ + private function normalizeSteps(array $steps): array { + $normalized = []; + foreach ($steps as $key => $value) { + if ($value instanceof Option) { + $normalized[] = $value; + } elseif (is_array($value)) { + $normalized[] = new Option( + (string)($value['id'] ?? $key), + (string)($value['text'] ?? '') + ); + } else { + $normalized[] = new Option((string)$key, (string)$value); + } + } + return $normalized; + } + + /** + * Finds step index by ID or value. + * + * @param string|int $default Default identifier + * @return int Step index + */ + private function findStepIndex(string|int $default): int { + if (is_int($default)) { + return $default; + } + + foreach ($this->steps as $index => $step) { + if ($step->getId() === $default) { + return $index; + } + } + return 0; + } + + /** + * Serializes element for JSON. + * + * @return array Serialized data + */ + public function jsonSerialize(): array { + return [ + 'type' => 'step_slider', + 'text' => $this->text, + 'steps' => array_map(fn(Option $s) => $s->getText(), $this->steps), + 'default' => $this->defaultIndex + ]; + } + + /** + * Gets step by index. + * + * @param int $index Step position + * @return Option|null Step or null + */ + public function getStepByIndex(int $index): ?Option { + return $this->steps[$index] ?? null; + } + + /** + * Gets selected step. + * + * @param int $index Selected index + * @return SelectedOption|null Selected step or null + */ + public function getSelectedStep(int $index): ?SelectedOption { + $step = $this->getStepByIndex($index); + if ($step === null) { + return null; + } + + return new SelectedOption( + $index, + $step->getId(), + $step->getText() + ); + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/elements/Toggle.php b/src/imperazim/form/custom/elements/Toggle.php new file mode 100644 index 0000000..88ed47d --- /dev/null +++ b/src/imperazim/form/custom/elements/Toggle.php @@ -0,0 +1,36 @@ + 'toggle', + 'text' => $this->text, + 'default' => $this->default + ]; + } + } \ No newline at end of file diff --git a/src/imperazim/form/custom/response/CustomResponse.php b/src/imperazim/form/custom/response/CustomResponse.php new file mode 100644 index 0000000..99c9aaf --- /dev/null +++ b/src/imperazim/form/custom/response/CustomResponse.php @@ -0,0 +1,223 @@ + $raw Raw response data + * @param array $idMap ID-value mapping + * @param array $elementMap Element mapping + */ + public function __construct( + private FormData $data, + private array $raw, + private array $idMap = [], + private array $elementMap = [] + ) { + if (empty($raw)) { + throw new InvalidArgumentException("Raw data must be array"); + } + } + + /** + * Gets form data. + * + * @return FormData Requested data + */ + public function getFormData(): FormData { + return $this->data; + } + + /** + * Gets element value by ID. + * + * @param string $id Element ID + * @return mixed Element value + * @throws InvalidArgumentException If ID not found + */ + public function getElement(string $id): mixed { + if (!array_key_exists($id, $this->idMap)) { + throw new InvalidArgumentException("ID '{$id}' not found."); + } + return $this->idMap[$id]; + } + + /** + * Gets raw response data. + * + * @return array Raw data + */ + public function getElementsRaw(): array { + return $this->raw; + } + + /** + * Gets string value. + * + * @param string $id Element ID + * @param string|null $default Default value + * @return string|null String value or default + */ + public function getString(string $id, ?string $default = null): ?string { + try { + $value = $this->getElement($id); + return $value === null ? $default : (string)$value; + } catch (InvalidArgumentException) { + return $default; + } + } + + /** + * Gets integer value. + * + * @param string $id Element ID + * @param int|null $default Default value + * @return int|null Integer value or default + */ + public function getInt(string $id, ?int $default = null): ?int { + try { + $value = $this->getElement($id); + return $value === null ? $default : (int)$value; + } catch (InvalidArgumentException) { + return $default; + } + } + + /** + * Gets boolean value. + * + * @param string $id Element ID + * @param bool|null $default Default value + * @return bool|null Boolean value or default + */ + public function getBool(string $id, ?bool $default = null): ?bool { + try { + $value = $this->getElement($id); + return $value === null ? $default : (bool)$value; + } catch (InvalidArgumentException) { + return $default; + } + } + + /** + * Gets float value. + * + * @param string $id Element ID + * @param float|null $default Default value + * @return float|null Float value or default + */ + public function getFloat(string $id, ?float $default = null): ?float { + try { + $value = $this->getElement($id); + return $value === null ? $default : (float)$value; + } catch (InvalidArgumentException) { + return $default; + } + } + + /** + * Gets array value. + * + * @param string $id Element ID + * @param array|null $default Default value + * @return array|null Array value or default + */ + public function getArray(string $id, ?array $default = null): ?array { + try { + $value = $this->getElement($id); + return is_array($value) ? $value : $default; + } catch (InvalidArgumentException) { + return $default; + } + } + + /** + * Gets toggle value. + * + * @param string $id Element ID + * @return bool Toggle value + */ + public function getToggle(string $id): bool { + return (bool) $this->getElement($id); + } + + /** + * Gets input value. + * + * @param string $id Element ID + * @return string Input value + */ + public function getInput(string $id): string { + return (string) $this->getElement($id); + } + + /** + * Gets slider value. + * + * @param string $id Element ID + * @return float Slider value + */ + public function getSlider(string $id): float { + return (float) $this->getElement($id); + } + + /** + * Gets dropdown selection. + * + * @param string $id Element ID + * @return SelectedOption Selected option + * @throws InvalidArgumentException If element not dropdown + */ + public function getDropdown(string $id): SelectedOption { + $index = (int) $this->getElement($id); + $element = $this->elementMap[$id] ?? null; + + if (!$element instanceof Dropdown) { + throw new InvalidArgumentException("Element with ID {$id} is not a Dropdown."); + } + + $selected = $element->getSelectedOption($index); + return $selected ?? new SelectedOption($index, (string)$index, 'Unknown'); + } + + /** + * Gets step slider selection. + * + * @param string $id Element ID + * @return SelectedOption Selected step + * @throws InvalidArgumentException If element not step slider + */ + public function getStepSlider(string $id): SelectedOption { + $index = (int) $this->getElement($id); + $element = $this->elementMap[$id] ?? null; + + if (!$element instanceof StepSlider) { + throw new InvalidArgumentException("Element with ID {$id} is not a StepSlider."); + } + + $selected = $element->getSelectedStep($index); + return $selected ?? new SelectedOption($index, (string)$index, 'Unknown'); + } + + /** + * Checks if element exists in response. + * + * @param string $id Element ID + * @return bool True if exists + */ + public function has(string $id): bool { + return array_key_exists($id, $this->idMap); + } +} \ No newline at end of file diff --git a/src/imperazim/form/custom/response/SelectedOption.php b/src/imperazim/form/custom/response/SelectedOption.php new file mode 100644 index 0000000..b87942c --- /dev/null +++ b/src/imperazim/form/custom/response/SelectedOption.php @@ -0,0 +1,48 @@ +index; + } + + /** + * Gets option ID. + * + * @return string Option ID + */ + public function getId(): string { + return $this->id; + } + + /** + * Gets display text. + * + * @return string Display text + */ + public function getText(): string { + return $this->text; + } +} \ No newline at end of file diff --git a/src/imperazim/form/long/DynamicLongForm.php b/src/imperazim/form/long/DynamicLongForm.php new file mode 100644 index 0000000..e165ce8 --- /dev/null +++ b/src/imperazim/form/long/DynamicLongForm.php @@ -0,0 +1,219 @@ +content = new Content(""); + $this->buttons = new ButtonCollection(); + $this->onClose = fn(Player $player) => FormResult::CLOSE; + } + + /** + * Creates a new form instance. + * + * @param string|Title $title Form title + * @return self New form instance + */ + final public static function create(string|Title $title): self { + return new self($title instanceof Title ? $title : new Title($title)); + } + + /** + * Builds a form using a configuration closure. + * + * @param string|Title $title Form title + * @param Closure $configurator Configuration callback + * @return self Configured form instance + */ + final public static function build(string|Title $title, Closure $configurator): self { + $form = self::create($title); + $configurator($form); + return $form; + } + + /** + * Sets form content. + * + * @param string|Content $content Form content + * @return self + */ + public function setContent(string|Content $content): self { + $this->content = $content instanceof Content ? $content : new Content($content); + return $this; + } + + /** + * Gets current content. + * + * @return Content Current content + */ + public function getContent(): Content { + return $this->content; + } + + /** + * Sets button collection. + * + * @param ButtonCollection $buttons Button collection + * @return self + */ + public function setButtons(ButtonCollection $buttons): self { + $this->buttons = $buttons; + return $this; + } + + /** + * Gets button collection. + * + * @return ButtonCollection Current buttons + */ + public function getButtons(): ButtonCollection { + return $this->buttons; + } + + /** + * Adds a button to the form. + * + * @param Button|string $button Button instance or text + * @param ButtonTexture|string|array|null $image Button image + * @param Closure|null $handler Click handler + * @return self + */ + public function addButton( + Button|string $button, + ButtonTexture|string|array|null $image = null, + ?Closure $handler = null + ): self { + if ($button instanceof Button) { + $this->buttons->add($button); + return $this; + } + + $img = $this->processImage($image); + $response = new ButtonResponse($handler ?? fn(Player $p) => FormResult::CLOSE); + $this->buttons->add(new Button($button, $img, $response)); + + return $this; + } + + /** + * Processes image parameter. + * + * @param ButtonTexture|string|array|null $image Image data + * @return ButtonTexture Processed texture + */ + private function processImage(ButtonTexture|string|array|null $image): ButtonTexture { + if ($image instanceof ButtonTexture) { + return $image; + } + + $type = ButtonTexture::PATH; + $data = ''; + + if (is_array($image)) { + $type = $image['type'] ?? $type; + $data = $image['data'] ?? $data; + } elseif (is_string($image)) { + $data = $image; + $type = filter_var($data, FILTER_VALIDATE_URL) ? ButtonTexture::URL : ButtonTexture::PATH; + } + + return new ButtonTexture($data, $type); + } + + /** + * Sets close handler. + * + * @param Closure $handler Close handler + * @return self + */ + public function setOnClose(Closure $handler): self { + $this->onClose = $handler; + return $this; + } + + /** + * Gets close handler. + * + * @return Closure Current handler + */ + public function getOnClose(): Closure { + return $this->onClose; + } + + /** + * Gets form type. + * + * @return string Form type identifier + */ + protected function getFormType(): string { + return "form"; + } + + /** + * Serializes form data. + * + * @return array Serialized form data + */ + protected function serializeFormData(): array { + return [ + 'content' => $this->content->getText(), + 'buttons' => array_map( + fn($button) => $button->jsonSerialize(), + iterator_to_array($this->buttons) + ) + ]; + } + + /** + * Handles form response. + * + * @param Player $player Responding player + * @param mixed $data Response data + */ + public function handleResponse(Player $player, $data): void { + if ($data === null) { + $result = ($this->onClose)($player); + } elseif (is_int($data) && ($button = $this->buttons[$data] ?? null)) { + $result = $button->getResponse()->handle($player); + } else { + $result = FormResult::CLOSE; + } + + if ($result === FormResult::KEEP) { + $this->sendTo($player); + } + } +} \ No newline at end of file diff --git a/src/imperazim/form/long/LongForm.php b/src/imperazim/form/long/LongForm.php new file mode 100644 index 0000000..60c9995 --- /dev/null +++ b/src/imperazim/form/long/LongForm.php @@ -0,0 +1,124 @@ +formData = $data instanceof FormData ? $data : new FormData($data); + parent::__construct($this->title($player, $this->formData)); + + if ($force) { + $this->sendTo($player); + } + } + + /** + * Gets the form title. + * + * @param Player $player The player viewing the form. + * @param FormData $data Form data container. + * @return Title The title element. + */ + abstract protected function title(Player $player, FormData $data): Title; + + /** + * Gets the form content. + * + * @param Player $player The player viewing the form. + * @param FormData $data Form data container. + * @return Content The content element. + */ + abstract protected function content(Player $player, FormData $data): Content; + + /** + * Gets the button collection. + * + * @param Player $player The player viewing the form. + * @param FormData $data Form data container. + * @return ButtonCollection The button collection. + */ + abstract protected function buttons(Player $player, FormData $data): ButtonCollection; + + /** + * Handles form closure. + * + * @param Player $player The player who closed the form. + * @param FormData $data Form data container. + * @return FormResult The result controlling form behavior. + */ + protected function onClose(Player $player, FormData $data): FormResult { + return FormResult::CLOSE; + } + + /** + * Returns the form type. + * + * @return string The form type identifier. + */ + protected function getFormType(): string { + return "form"; + } + + /** + * Serializes form data for UI rendering. + * + * @return array The serialized form data. + */ + protected function serializeFormData(): array { + return [ + 'content' => $this->content($this->player, $this->formData)->getText(), + 'buttons' => array_map( + fn($button) => $button->jsonSerialize(), + iterator_to_array($this->buttons($this->player, $this->formData)) + ) + ]; + } + + /** + * Handles form responses. + * + * @param Player $player The responding player. + * @param mixed $raw The raw response data. + */ + public function handleResponse(Player $player, $raw): void { + if ($raw === null) { + $result = $this->onClose($player, $this->formData); + } elseif (is_int($raw)) { + $button = $this->buttons($player, $this->formData)[$raw] ?? null; + $result = $button ? $button->getResponse()->handle($player) : FormResult::CLOSE; + } else { + return; + } + + if ($result === FormResult::KEEP) { + $this->sendTo($player); + } + } +} \ No newline at end of file diff --git a/src/imperazim/form/long/elements/Button.php b/src/imperazim/form/long/elements/Button.php new file mode 100644 index 0000000..7b93b2b --- /dev/null +++ b/src/imperazim/form/long/elements/Button.php @@ -0,0 +1,64 @@ +text; + } + + /** + * Gets button texture. + * + * @return ButtonTexture Button image + */ + public function getTexture(): ButtonTexture { + return $this->image; + } + + /** + * Gets button response handler. + * + * @return ButtonResponse Response handler + */ + public function getResponse(): ButtonResponse { + return $this->response; + } + + /** + * Serializes button for JSON. + * + * @return array Serialized button data + */ + public function jsonSerialize(): array { + return [ + 'text' => $this->text, + 'image' => $this->image->jsonSerialize() + ]; + } +} \ No newline at end of file diff --git a/src/imperazim/form/long/elements/ButtonCollection.php b/src/imperazim/form/long/elements/ButtonCollection.php new file mode 100644 index 0000000..4097ad2 --- /dev/null +++ b/src/imperazim/form/long/elements/ButtonCollection.php @@ -0,0 +1,107 @@ +buttons[] = $button; + } + + /** + * Creates collection from array. + * + * @param Button[] $buttons Button array + * @return self New collection + */ + public static function fromArray(array $buttons): self { + $collection = new self(); + $collection->buttons = $buttons; + return $collection; + } + + /** + * Gets button by index. + * + * @param int $index Button position + * @return Button|null Button or null + */ + public function getByIndex(int $index): ?Button { + return $this->buttons[$index] ?? null; + } + + /** + * Counts buttons in collection. + * + * @return int Number of buttons + */ + public function count(): int { + return count($this->buttons); + } + + /** + * Gets iterator for buttons. + * + * @return Traversable