Skip to content

Milestones

List view

  • # EasyLibrary 2.0 Roadmap > \[!IMPORTANT] > **Overview:** EasyLibrary 2.0 is a major refactoring that modernizes the core framework. It replaces third-party dependencies with in-house solutions, consolidates previously fragmented APIs, and flattens the namespace structure for cleaner organization. The key changes include: * **LibCommand:** A new modular, class-based command system with clear separation of definition, execution, and error handling. * **LibForm:** A unified form-building API that merges old UI and vendor form logic, offering both class-based (LongForm) and fluent (DynamicLongForm) builders. * **Namespace Restructuring:** A simplified directory layout, moving all code under the `imperazim` root (with external libraries under `vendor`) and eliminating legacy `components/` and nested paths. These changes improve maintainability, consistency, and developer control by making every component fully internal and clearly structured. ## LibCommand: Modular In-House Command System For EasyLibrary 2.0, we replaced any third-party command libraries with a fully in-house solution. The new **LibCommand** framework is a self-contained, class-based command system that enforces a **separation of concerns**. > \[!IMPORTANT] > **In-House Architecture:** All commands are now defined as classes under the `imperazim\command` namespace. This eliminates external dependencies and ensures every command implementation is internal. > \[!TIP] > **Modular Design:** Each command is a self-contained class (extending `Command` or `SubCommand`), making it easy to add, remove, or extend functionality without affecting other commands. > \[!NOTE] > **Separation of Concerns:** Command definition (name, description, arguments) is declared in a single `onBuild()` method, execution logic resides in the `onExecute()` method, and error/failure handling is done in `onFailure()`. This clear structure keeps command components distinct. > \[!TIP] > **Typed Arguments & Constraints:** Arguments are defined with types (e.g., `IntegerArgument`, `PlayerArgument`) and validated before execution. Permissions and context checks (such as “in-game only”) are handled by dedicated constraint classes, keeping validation logic separate. > \[!TIP] > **Subcommand Support:** Commands can include nested subcommands by specifying them in the `subcommands` array. Each subcommand is also a class (`SubCommand`) with its own `onBuild()`, `onExecute()`, and `onFailure()` methods, enabling organized command hierarchies. Below are simplified examples illustrating the class-based command structure: ### Command Class Example ```php <?php use imperazim\command\Command; use imperazim\command\constraint\InGameConstraint; use imperazim\command\result\CommandResult; use imperazim\command\result\CommandFailure; use imperazim\command\argument\IntegerArgument; class MyInfoCommand extends Command { public function onBuild(): array { return [ 'name' => 'serverinfo', 'aliases' => ['sinfo', 'server'], 'description' => 'Displays server information', 'permission' => DefaultPermissions::ROOT_USER, 'arguments' => [ new IntegerArgument( name: 'page', optional: true, min: 1, max: 3 ) ], 'subcommands' => [new PlayerSubCommand($this)], 'constraints' => [new InGameConstraint()] ]; } public function onExecute(CommandResult $result): void { $sender = $result->getSender(); $page = $result->getArgumentsList()->get('page', 1); $server = Server::getInstance(); $tps = $server->getTicksPerSecond(); // Generate server information based on the requested page $serverInfo = match($page) { 1 => [ "Players: §6" . count($server->getOnlinePlayers()) . "§r/" . $server->getMaxPlayers(), "World: §6" . ($server->getWorldManager()->getDefaultWorld()?->getDisplayName() ?? "Unknown"), "TPS: " . match(true) { $tps > 18 => "§a" . number_format($tps, 1) . "§r", $tps > 15 => "§e" . number_format($tps, 1) . "§r", default => "§c" . number_format($tps, 1) . "§r" } ], 2 => [ "Version: §6" . $server->getVersion(), "API: §6" . $server->getApiVersion(), "Plugins: §6" . count($server->getPluginManager()->getPlugins()) ], 3 => [ "IP: §6" . $server->getIp(), "Port: §6" . $server->getPort(), "View Distance: §6" . $server->getViewDistance() ], default => ['§cInvalid page! Use 1, 2 or 3'] }; // Send formatted information to the player $sender->sendMessage("§l§3=== §r§aServer Information§3 (Page $page) §l==="); $sender->sendMessage(implode("\n§7", $serverInfo)); } public function onFailure(CommandFailure $failure): void { $sender = $failure->getSender(); switch ($failure->getReason()) { case CommandFailure::CONSTRAINT_FAILED: // Handle constraint failures (e.g., player not in-game) // $failedConstraints = $failure->getData()['failed_constraints'] ?? []; break; case CommandFailure::MISSING_ARGUMENT: // Handle missing required arguments $sender->sendMessage("§eUsage: /serverinfo [page: 1-3]"); break; case CommandFailure::INVALID_ARGUMENT: // Handle invalid argument values $sender->sendMessage("§eUsage: /serverinfo [page: 1-3]"); break; case CommandFailure::EXECUTION_ERROR: // Handle unexpected execution errors $sender->sendMessage("§cAn error occurred while executing the command"); break; } } } ?> ``` ### SubCommand Class Example ```php <?php use imperazim\command\SubCommand; use imperazim\command\result\CommandResult; use imperazim\command\result\CommandFailure; use imperazim\command\argument\PlayerArgument; class PlayerSubCommand extends SubCommand { public function onBuild(): array { return [ 'name' => 'player', 'description' => 'Displays information about a player', 'arguments' => [new PlayerArgument('target', false)] ]; } public function onExecute(CommandResult $result): void { $sender = $result->getSender(); $target = $result->getArgumentsList()->get('target'); if (!$target instanceof Player) { $sender->sendMessage(TextFormat::RED . "Player not found or not online"); return; } $this->displayPlayerInfo($sender, $target); } private function displayPlayerInfo(CommandSender $sender, Player $target): void { $health = $target->getHealth(); $maxHealth = $target->getMaxHealth(); $position = $target->getPosition(); $world = $target->getWorld()->getDisplayName(); $sender->sendMessage(TextFormat::GOLD . "=== Player Information ==="); $sender->sendMessage(TextFormat::YELLOW . "Name: " . TextFormat::WHITE . $target->getName()); $sender->sendMessage(TextFormat::YELLOW . "Health: " . $this->formatHealthBar($health, $maxHealth)); $sender->sendMessage(TextFormat::YELLOW . "Position: " . TextFormat::WHITE . "X: {$position->getX()}, Y: {$position->getY()}, Z: {$position->getZ()}"); $sender->sendMessage(TextFormat::YELLOW . "World: " . TextFormat::WHITE . $world); $sender->sendMessage(TextFormat::YELLOW . "Gamemode: " . TextFormat::WHITE . $this->getGamemodeName($target->getGamemode())); } private function formatHealthBar(float $current, float $max): string { $percentage = $current / $max; $color = match(true) { $percentage < 0.25 => TextFormat::RED, $percentage < 0.5 => TextFormat::GOLD, default => TextFormat::GREEN }; return $color . number_format($current, 1) . TextFormat::GRAY . "/" . TextFormat::WHITE . number_format($max, 1) . "❤"; } private function getGamemodeName(int $mode): string { return match($mode) { Player::SURVIVAL => "Survival", Player::CREATIVE => "Creative", Player::ADVENTURE => "Adventure", Player::SPECTATOR => "Spectator", default => "Unknown" }; } public function onFailure(CommandFailure $failure): void { $sender = $failure->getSender(); switch ($failure->getReason()) { case CommandFailure::MISSING_ARGUMENT: $sender->sendMessage(TextFormat::RED . "Missing player name"); $sender->sendMessage(TextFormat::YELLOW . "Usage: /info player <playerName>"); break; case CommandFailure::INVALID_ARGUMENT: $errors = $failure->getData()['argument_errors'] ?? []; $firstError = $errors[0] ?? null; if ($firstError && $firstError['argument'] === 'target') { $sender->sendMessage(TextFormat::RED . "Invalid player: " . $firstError['message']); } $sender->sendMessage(TextFormat::YELLOW . "Usage: /info player <playerName>"); break; case CommandFailure::EXECUTION_ERROR: $sender->sendMessage(TextFormat::RED . "An error occurred while fetching player information"); break; } } } ?> ``` ## LibForm: Unified Form-Building API In EasyLibrary 2.0, all form-related functionality is consolidated under a single `imperazim/form` directory. We merged the old `imperazim/vendor/libform` and `imperazim/components/ui/form` packages into this unified API. The new LibForm system offers **two powerful approaches** to create in-game forms: a class-based builder and a chainable (fluent) builder. > \[!IMPORTANT] > **Unified Directory:** All form-building logic now resides in `imperazim/form`. This merge eliminates duplicate code and streamlines maintenance by using a single namespace for forms. > \[!TIP] > **Dual API Styles:** Developers can choose between a *LongForm* (class-based) approach or a dynamic, fluent API (using `DynamicLongForm`). Both approaches produce equivalent forms and can be used interchangeably according to preference. ### Class-Based LongForm Example ```php <?php use imperazim\form\long\LongForm; use imperazim\form\long\elements\Button; use imperazim\form\long\elements\ButtonTexture; use imperazim\form\long\elements\ButtonCollection; use imperazim\form\long\response\ButtonResponse; use imperazim\form\base\Title; use imperazim\form\base\Content; class MyListForm extends LongForm { protected function title(Player $player, FormData $formData): Title { return new Title('Main Menu'); } protected function content(Player $player, FormData $formData): Content { return new Content('Select an option:'); } protected function buttons(Player $player, FormData $formData): ButtonCollection { return ButtonCollection::fromArray([ new Button( 'Option 1', new ButtonTexture('textures/ui/icon_recipe', ButtonTexture::PATH), new ButtonResponse(function(Player $player): FormResult { $player->sendMessage("You clicked Option 1!"); return FormResult::CLOSE; }) ), new Button( 'Option 2', null, new ButtonResponse(function(Player $player): FormResult { $player->sendMessage("Option 2 selected."); return FormResult::CLOSE; }) ), ]); } } ?> ``` #### Sending the Form After defining the form class, you can display it to a player with: ```php new MyListForm($player, $formData, true); ``` ### Fluent DynamicLongForm Example ```php <?php use imperazim\form\long\DynamicLongForm; DynamicLongForm::create("Quick Menu") ->setContent("What do you want to do?") ->addButton( "Status", 'textures/blocks/diamond_block', function(Player $player): FormResult { $player->sendMessage("Status chosen!"); return FormResult::CLOSE; } ) ->addButton( "Settings", null, function(Player $player): FormResult { $player->sendMessage("Opening settings..."); return FormResult::CLOSE; } ) ->sendTo($player); ?> ``` > \[!NOTE] > **Usage:** Modal and Custom form types follow the same logic as above. The fluent API uses chained method calls (`create()`, `setContent()`, `addButton()`, and finally `->sendTo($player)`) to build and send the form in one go. ## Namespace and Directory Restructuring EasyLibrary 2.0 simplifies the project layout by flattening the namespace hierarchy. We removed the old nested directories (`imperazim/components`, `imperazim/vendor`, etc.) and placed all library code directly under `imperazim`. This change makes the namespace and folder structure much cleaner. > \[!IMPORTANT] > **Flat Namespace:** There are no longer separate `components` or `vendor` subdirectories within `imperazim`. All Imperazim modules and packages are now at the `imperazim/*` level, while external libraries reside under the top-level `vendor/` directory. The directory mappings for existing code are: * `imperazim/components/*` → `imperazim/*` * `imperazim/vendor/*` → `vendor/*` * `imperazim/bugfixes/*` → `loader/*` This restructuring ensures that namespace declarations directly mirror the directory structure, improving clarity and reducing confusion over where classes are located. Each component’s namespace now starts with `imperazim\` (or `vendor\` for external packages), reflecting a straightforward, flat architecture.

    No due date
    0/3 issues closed