From d17d8b08fdb83f09204e486dba15e5a951daea63 Mon Sep 17 00:00:00 2001 From: Rouven Date: Sat, 21 Mar 2026 00:04:23 +0100 Subject: [PATCH 1/4] add module system --- .../Modules/ExtendsPlanetService.php | 21 ++ .../Modules/ExtendsPlayerService.php | 21 ++ app/Contracts/Modules/ProvidesGameObjects.php | 23 ++ .../Modules/ProvidesHighscoreCategory.php | 27 ++ .../Modules/ProvidesQueueProcessor.php | 26 ++ .../Models/Abstracts/GameObject.php | 8 + .../Abstracts/ObjectPropertyService.php | 78 ++++-- .../Controllers/Admin/ModulesController.php | 90 ++++++ app/Modules/ModuleServiceProvider.php | 117 ++++++++ app/Providers/AppServiceProvider.php | 16 ++ app/Services/ModuleSlotService.php | 66 +++++ app/Services/ObjectService.php | 21 +- app/Services/PlanetService.php | 7 + config/modules.php | 18 ++ docs/modules.md | 261 ++++++++++++++++++ .../views/ingame/admin/modules.blade.php | 84 ++++++ .../views/ingame/layouts/admin-menu.blade.php | 3 + resources/views/ingame/layouts/main.blade.php | 4 + .../views/ingame/overview/index.blade.php | 3 + .../views/ingame/resources/index.blade.php | 4 + routes/web.php | 5 + 21 files changed, 885 insertions(+), 18 deletions(-) create mode 100644 app/Contracts/Modules/ExtendsPlanetService.php create mode 100644 app/Contracts/Modules/ExtendsPlayerService.php create mode 100644 app/Contracts/Modules/ProvidesGameObjects.php create mode 100644 app/Contracts/Modules/ProvidesHighscoreCategory.php create mode 100644 app/Contracts/Modules/ProvidesQueueProcessor.php create mode 100644 app/Http/Controllers/Admin/ModulesController.php create mode 100644 app/Modules/ModuleServiceProvider.php create mode 100644 app/Services/ModuleSlotService.php create mode 100644 config/modules.php create mode 100644 docs/modules.md create mode 100644 resources/views/ingame/admin/modules.blade.php diff --git a/app/Contracts/Modules/ExtendsPlanetService.php b/app/Contracts/Modules/ExtendsPlanetService.php new file mode 100644 index 000000000..42cdcedba --- /dev/null +++ b/app/Contracts/Modules/ExtendsPlanetService.php @@ -0,0 +1,21 @@ +tag() in bootModule(): + * app()->tag(MyPlanetExtension::class, 'module.planet_extensions'); + */ +interface ExtendsPlanetService +{ + /** + * Called during planet resource production calculation. + * Modify the planet state or accumulate production values as needed. + */ + public function extendResourceProduction(PlanetService $planet): void; +} diff --git a/app/Contracts/Modules/ExtendsPlayerService.php b/app/Contracts/Modules/ExtendsPlayerService.php new file mode 100644 index 000000000..c8cb6a128 --- /dev/null +++ b/app/Contracts/Modules/ExtendsPlayerService.php @@ -0,0 +1,21 @@ +tag() in bootModule(): + * app()->tag(MyPlayerExtension::class, 'module.player_extensions'); + */ +interface ExtendsPlayerService +{ + /** + * Called when the PlayerService is booted for a player. + * Attach module-specific data to the player as needed. + */ + public function extendPlayer(PlayerService $player): void; +} diff --git a/app/Contracts/Modules/ProvidesGameObjects.php b/app/Contracts/Modules/ProvidesGameObjects.php new file mode 100644 index 000000000..ea03eba8f --- /dev/null +++ b/app/Contracts/Modules/ProvidesGameObjects.php @@ -0,0 +1,23 @@ +getGameObjects()) in bootModule(). + * + * @see \OGame\Services\ObjectService::registerModuleObjects() + */ +interface ProvidesGameObjects +{ + /** + * Return the array of GameObject instances provided by this module. + * + * @return array + */ + public function getGameObjects(): array; +} diff --git a/app/Contracts/Modules/ProvidesHighscoreCategory.php b/app/Contracts/Modules/ProvidesHighscoreCategory.php new file mode 100644 index 000000000..8ebd8fd6a --- /dev/null +++ b/app/Contracts/Modules/ProvidesHighscoreCategory.php @@ -0,0 +1,27 @@ +tag() in bootModule(): + * app()->tag(MyHighscoreCategory::class, 'module.highscore_categories'); + */ +interface ProvidesHighscoreCategory +{ + /** + * Return the unique string identifier for this highscore category. + */ + public function getCategoryId(): string; + + /** + * Return the display name for this highscore category. + */ + public function getCategoryLabel(): string; + + /** + * Return the score value for the given user ID. + */ + public function getScoreForUser(int $userId): int; +} diff --git a/app/Contracts/Modules/ProvidesQueueProcessor.php b/app/Contracts/Modules/ProvidesQueueProcessor.php new file mode 100644 index 000000000..b93570d69 --- /dev/null +++ b/app/Contracts/Modules/ProvidesQueueProcessor.php @@ -0,0 +1,26 @@ +tag(MyQueueProcessor::class, 'module.queue_processors'); + * + * The implementation is responsible for retrieving and processing any finished + * queue items for the given planet. + */ +interface ProvidesQueueProcessor +{ + /** + * Process any finished module queue items for the given planet. + * Called on every page load during the normal queue processing cycle. + */ + public function processQueue(PlanetService $planet): void; +} diff --git a/app/GameObjects/Models/Abstracts/GameObject.php b/app/GameObjects/Models/Abstracts/GameObject.php index eea07b45e..9ae2ae22d 100644 --- a/app/GameObjects/Models/Abstracts/GameObject.php +++ b/app/GameObjects/Models/Abstracts/GameObject.php @@ -32,6 +32,14 @@ abstract class GameObject */ public string $class_name = ''; + /** + * Optional module-defined sub-type discriminator. + * Allows modules to semantically distinguish their objects while still + * routing through existing queue services via the primary $type. + * Example: 'lifeform_building', 'lifeform_tech' + */ + public ?string $subType = null; + public string $description; public string $description_long; diff --git a/app/GameObjects/Services/Properties/Abstracts/ObjectPropertyService.php b/app/GameObjects/Services/Properties/Abstracts/ObjectPropertyService.php index 90c6ff80a..1a6a1d2ba 100644 --- a/app/GameObjects/Services/Properties/Abstracts/ObjectPropertyService.php +++ b/app/GameObjects/Services/Properties/Abstracts/ObjectPropertyService.php @@ -20,10 +20,35 @@ abstract class ObjectPropertyService */ protected string $propertyName = ''; + /** + * Registry of module-provided bonus modifier callables, keyed by property name. + * Each callable receives (PlayerService $player, GameObject $object) and returns int percentage. + * Applied to base_value, additive alongside the research bonus. + * + * @var array> + */ + private static array $bonusModifiers = []; + public function __construct(protected GameObject $parent_object, protected int $base_value) { } + /** + * Register an additional bonus percentage for a named property. + * + * Usage in a module's bootModule(): + * ObjectPropertyService::registerBonusModifier('attack', function (PlayerService $player, GameObject $object): int { + * return $player->getResearchLevel('my_tech') * 5; + * }); + * + * @param string $property The property name: 'attack', 'shield', 'structural_integrity', 'capacity', 'fuel', etc. + * @param callable $fn Receives (PlayerService, GameObject), returns int percentage applied to base_value + */ + public static function registerBonusModifier(string $property, callable $fn): void + { + self::$bonusModifiers[$property][] = $fn; + } + /** * Get the bonus percentage for a property. * @@ -40,28 +65,47 @@ abstract protected function getBonusPercentage(PlayerService $player): int; */ public function calculateProperty(PlayerService $player): GameObjectPropertyDetails { - $bonusPercentage = $this->getBonusPercentage($player); - // Use integer arithmetic to avoid floating point precision issues - $bonusValue = intdiv($this->base_value * $bonusPercentage, 100); + // Research bonus applied to base_value + $researchBonus = $this->getBonusPercentage($player); + $researchBonusValue = intdiv($this->base_value * $researchBonus, 100); + + // Module bonus modifiers — each returns an int percentage applied to base_value, + // additive alongside the research bonus (same pattern as character class bonuses). + $moduleBonus = 0; + $moduleBreakdown = []; + foreach (self::$bonusModifiers[$this->propertyName] ?? [] as $fn) { + $pct = $fn($player, $this->parent_object); + if ($pct !== 0) { + $moduleBonus += $pct; + $moduleBreakdown[] = [ + 'type' => 'Module bonus', + 'value' => intdiv($this->base_value * $pct, 100), + 'percentage' => $pct, + ]; + } + } + $moduleBonusValue = intdiv($this->base_value * $moduleBonus, 100); + + $totalValue = $this->base_value + $researchBonusValue + $moduleBonusValue; - $totalValue = $this->base_value + $bonusValue; + $bonuses = []; + if ($researchBonus > 0) { + $bonuses[] = [ + 'type' => 'Research bonus', + 'value' => $researchBonusValue, + 'percentage' => $researchBonus, + ]; + } + foreach ($moduleBreakdown as $entry) { + $bonuses[] = $entry; + } - // Prepare the breakdown for future-proofing (assuming more components might be added) - // TODO: add model for breakdown - // TODO: Add more components to the breakdown if necessary like class bonuses, premium member - // bonuses, item bonuses etc. $breakdown = [ - 'rawValue' => $this->base_value, - 'bonuses' => [ - [ - 'type' => 'Research bonus', - 'value' => $bonusValue, - 'percentage' => $bonusPercentage, - ], - ], + 'rawValue' => $this->base_value, + 'bonuses' => $bonuses, 'totalValue' => $totalValue, ]; - return new GameObjectPropertyDetails($this->base_value, $bonusValue, $totalValue, $breakdown); + return new GameObjectPropertyDetails($this->base_value, $researchBonusValue + $moduleBonusValue, $totalValue, $breakdown); } } diff --git a/app/Http/Controllers/Admin/ModulesController.php b/app/Http/Controllers/Admin/ModulesController.php new file mode 100644 index 000000000..629159d67 --- /dev/null +++ b/app/Http/Controllers/Admin/ModulesController.php @@ -0,0 +1,90 @@ +disabledFile = storage_path('app/modules-disabled.json'); + } + + /** + * List all discovered modules (from config/modules.php and Composer auto-discovery) + * with their enabled/disabled state. + */ + public function index(): View + { + $disabled = $this->loadDisabled(); + $modules = []; + + foreach (ModuleServiceProvider::allDiscovered() as $providerClass) { + /** @var ModuleServiceProvider $provider */ + $provider = new $providerClass(app()); + $manifest = $provider->getModuleManifest(); + + $modules[] = [ + 'provider' => $providerClass, + 'id' => $provider->moduleId(), + 'name' => $manifest['name'] ?? $provider->moduleId(), + 'description' => $manifest['description'] ?? '', + 'version' => $manifest['version'] ?? '?', + 'enabled' => !in_array($providerClass, $disabled), + 'missing' => false, + ]; + } + + return view('ingame.admin.modules', compact('modules')); + } + + /** + * Toggle a module on or off. + * Changes take effect on the next request (no cache:clear needed). + */ + public function toggle(Request $request): RedirectResponse + { + $providerClass = $request->input('provider'); + + if (!is_string($providerClass) || !class_exists($providerClass)) { + return redirect()->back()->with('error', 'Invalid module provider class.'); + } + + $disabled = $this->loadDisabled(); + + if (in_array($providerClass, $disabled)) { + $disabled = array_values(array_filter($disabled, fn ($p) => $p !== $providerClass)); + $message = 'Module enabled. Reload the page to apply.'; + } else { + $disabled[] = $providerClass; + $message = 'Module disabled. Reload the page to apply.'; + } + + $this->saveDisabled($disabled); + + return redirect()->route('admin.modules.index')->with('success', $message); + } + + private function loadDisabled(): array + { + if (!file_exists($this->disabledFile)) { + return []; + } + return json_decode(file_get_contents($this->disabledFile), true) ?? []; + } + + private function saveDisabled(array $disabled): void + { + if (!is_dir(dirname($this->disabledFile))) { + mkdir(dirname($this->disabledFile), 0755, true); + } + file_put_contents($this->disabledFile, json_encode(array_values($disabled), JSON_PRETTY_PRINT)); + } +} diff --git a/app/Modules/ModuleServiceProvider.php b/app/Modules/ModuleServiceProvider.php new file mode 100644 index 000000000..73e8f03ca --- /dev/null +++ b/app/Modules/ModuleServiceProvider.php @@ -0,0 +1,117 @@ +modulePath('module.json'); + if (file_exists($path)) { + return json_decode(file_get_contents($path), true) ?? []; + } + return ['name' => $this->moduleId(), 'description' => '', 'version' => '?']; + } + + /** + * Boot the module: load routes, migrations, views, translations, + * then delegate to bootModule() for module-specific logic. + * Returns early without doing anything if the module is disabled. + */ + public function boot(): void + { + if ($this->isDisabled()) { + return; + } + + if (file_exists($routes = $this->modulePath('routes/web.php'))) { + Route::middleware('web')->group($routes); + } + + if (is_dir($migrations = $this->modulePath('database/migrations'))) { + $this->loadMigrationsFrom($migrations); + } + + if (is_dir($views = $this->modulePath('resources/views'))) { + $this->loadViewsFrom($views, $this->moduleId()); + } + + if (is_dir($lang = $this->modulePath('resources/lang'))) { + $this->loadTranslationsFrom($lang, $this->moduleId()); + } + + $this->bootModule(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 420c9a742..6317cc9e6 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace OGame\Providers; use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; use OGame\Exceptions\Handler; @@ -10,6 +11,7 @@ use OGame\Factories\PlayerServiceFactory; use OGame\Models\User; use OGame\Observers\UserObserver; +use OGame\Services\ModuleSlotService; use OGame\Services\SettingsService; class AppServiceProvider extends ServiceProvider @@ -30,6 +32,11 @@ final public function boot(): void // Register model observers User::observe(UserObserver::class); + + // Register @moduleSlot Blade directive for module view injection + Blade::directive('moduleSlot', function (string $expression): string { + return ""; + }); } /** @@ -55,5 +62,14 @@ final public function register(): void }); $this->app->singleton(ExceptionHandler::class, Handler::class); + + // Register bundled modules listed in config/modules.php. + // Composer-installed modules are auto-discovered via their composer.json. + // The ModuleServiceProvider base class handles the enabled/disabled check in boot(). + foreach (config('modules.enabled', []) as $providerClass) { + if (is_string($providerClass) && class_exists($providerClass)) { + $this->app->register($providerClass); + } + } } } diff --git a/app/Services/ModuleSlotService.php b/app/Services/ModuleSlotService.php new file mode 100644 index 000000000..77f7a6af1 --- /dev/null +++ b/app/Services/ModuleSlotService.php @@ -0,0 +1,66 @@ +render(); + * }); + * + * Available slot names: + * layout.resources_bar — after darkmatter tile in main layout resource bar + * layout.resources_bar_js — after resource JS vars in main layout + * resources.building_section — after building grid on resources page + * resources.production_box — after production boxes on resources page + * overview.planet_info — after planet stats on overview page + * admin.nav — after existing nav items in admin bar + */ +class ModuleSlotService +{ + /** @var array> */ + private static array $slots = []; + + /** + * Register a renderer callable for a named slot. + * + * @param string $slot The slot name, e.g. 'layout.resources_bar' + * @param callable $renderer Receives array $data, returns HTML string + */ + public static function register(string $slot, callable $renderer): void + { + self::$slots[$slot][] = $renderer; + } + + /** + * Render all registered callables for a slot and return concatenated HTML. + * + * @param string $slot + * @param array $data + * @return string + */ + public static function render(string $slot, array $data = []): string + { + $html = ''; + foreach (self::$slots[$slot] ?? [] as $renderer) { + $html .= $renderer($data); + } + + return $html; + } + + /** + * Returns true if at least one renderer is registered for the slot. + */ + public static function hasSlot(string $slot): bool + { + return !empty(self::$slots[$slot]); + } +} diff --git a/app/Services/ObjectService.php b/app/Services/ObjectService.php index 6ece9e917..8ac017ead 100644 --- a/app/Services/ObjectService.php +++ b/app/Services/ObjectService.php @@ -32,6 +32,24 @@ */ class ObjectService { + /** + * Module-registered game objects. Populated by modules via registerModuleObjects(). + * + * @var array<\OGame\GameObjects\Models\Abstracts\GameObject> + */ + private static array $moduleObjects = []; + + /** + * Register additional game objects provided by a module. + * Call this in your module's bootModule() method. + * + * @param array<\OGame\GameObjects\Models\Abstracts\GameObject> $objects + */ + public static function registerModuleObjects(array $objects): void + { + self::$moduleObjects = array_merge(self::$moduleObjects, $objects); + } + /** * Get all objects. * @@ -40,7 +58,8 @@ class ObjectService public static function getObjects(): array { return [...BuildingObjects::get(), ...StationObjects::get(), ...ResearchObjects::get(), - ...MilitaryShipObjects::get(), ...CivilShipObjects::get(), ...DefenseObjects::get()]; + ...MilitaryShipObjects::get(), ...CivilShipObjects::get(), ...DefenseObjects::get(), + ...self::$moduleObjects]; } /** diff --git a/app/Services/PlanetService.php b/app/Services/PlanetService.php index f8b9ef549..b4a30f401 100644 --- a/app/Services/PlanetService.php +++ b/app/Services/PlanetService.php @@ -1102,6 +1102,13 @@ public function update(): void // ------ $this->updateResourceStorageStats(false); + // ------ + // 6. Process module queue processors (e.g. lifeform building queues) + // ------ + foreach (app()->tagged('module.queue_processors') as $processor) { + $processor->processQueue($this); + } + // Save the planet manually here to prevent it from happening 5+ times in the methods above. $this->save(); } else { diff --git a/config/modules.php b/config/modules.php new file mode 100644 index 000000000..c140c7f0c --- /dev/null +++ b/config/modules.php @@ -0,0 +1,18 @@ + [ + // OGame\Modules\Lifeforms\LifeformsServiceProvider::class, + ], +]; diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 000000000..071a12b08 --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,261 @@ +# OGameX Module System + +Modules allow features that are not part of the core OGameX codebase to be distributed as standalone Composer packages and installed on demand. The core repository provides the infrastructure; all module code lives in separate repositories. + +--- + +## Enabling a Module + +Install the module via Composer: + +```bash +composer require ogamex-modules/my-module +``` + +Then add its ServiceProvider class to `config/modules.php`: + +```php +return [ + 'enabled' => [ + OGame\Modules\MyModule\MyModuleServiceProvider::class, + ], +]; +``` + +Modules are only activated when both the Composer package is installed **and** the class appears in the `enabled` list. This keeps production deployments intentional. + +--- + +## Module Package Structure + +``` +ogamex-modules/my-module/ +├── composer.json +├── module.json # Human-readable manifest +├── src/ +│ ├── MyModuleServiceProvider.php # Extends OGame\Modules\ModuleServiceProvider +│ ├── GameObjects/ # New GameObject definitions +│ ├── Services/ # Custom services (e.g. own queue service) +│ ├── Queue/ # ProvidesQueueProcessor implementations +│ ├── Models/ # Eloquent models +│ └── Http/ +│ ├── Controllers/ +│ └── ViewComposers/ +├── database/ +│ └── migrations/ +├── resources/ +│ ├── views/ +│ └── lang/ +├── routes/ +│ └── web.php +└── tests/ +``` + +### `module.json` + +```json +{ + "name": "My Module", + "id": "my-module", + "description": "Description of what this module adds.", + "version": "1.0.0", + "ogamex_core_min": "1.0.0", + "requires": [] +} +``` + +### `composer.json` + +```json +{ + "name": "ogamex-modules/my-module", + "require": { + "php": "^8.5", + "laravel/framework": "^12.0" + }, + "autoload": { + "psr-4": { + "OGame\\Modules\\MyModule\\": "src/" + } + } +} +``` + +Do not use Laravel package auto-discovery (`extra.laravel.providers`). Modules must be explicitly enabled in `config/modules.php`. + +--- + +## Creating a ServiceProvider + +All modules extend `OGame\Modules\ModuleServiceProvider` and implement three methods: + +```php +namespace OGame\Modules\MyModule; + +use OGame\Modules\ModuleServiceProvider; + +class MyModuleServiceProvider extends ModuleServiceProvider +{ + public function moduleId(): string + { + return 'my-module'; + } + + protected function modulePath(string $relative): string + { + return dirname(__DIR__) . '/' . $relative; + } + + public function bootModule(): void + { + // Register game objects, view slots, bonus modifiers, queue processors, etc. + } +} +``` + +The base class automatically loads routes, migrations, views, and translations from the module package root when `boot()` is called. + +--- + +## Registering Game Objects + +To add new ships, defense, buildings, or research objects, register them with `ObjectService`: + +```php +use OGame\Services\ObjectService; + +public function bootModule(): void +{ + ObjectService::registerModuleObjects([ + ...MyShipObjects::get(), + ...MyDefenseObjects::get(), + ]); +} +``` + +New ships using `GameObjectType::Ship` and defense using `GameObjectType::Defense` automatically participate in the battle engine, debris calculation, and unit queue — no further core changes needed. + +Planet storage uses a DB column per `machine_name` (e.g. `$planet->my_new_ship`). Provide a migration in `database/migrations/` to add these columns. + +Use `$subType` on `GameObject` to semantically distinguish module objects that share a core type: + +```php +$object->type = GameObjectType::Building; +$object->subType = 'lifeform_building'; +``` + +--- + +## Registering Property Bonus Modifiers + +To add bonuses to ship or unit properties (attack, shield, structural integrity, cargo capacity, fuel), register a callable with `ObjectPropertyService`: + +```php +use OGame\GameObjects\Services\Properties\Abstracts\ObjectPropertyService; +use OGame\GameObjects\Models\ShipObject; + +public function bootModule(): void +{ + // Add 3% attack bonus per level of my_combat_tech, ships only + ObjectPropertyService::registerBonusModifier('attack', + function (PlayerService $player, GameObject $object): int { + if (!($object instanceof ShipObject)) { + return 0; + } + return $player->getResearchLevel('my_combat_tech') * 3; + } + ); +} +``` + +The callable receives `(PlayerService $player, GameObject $object)` and returns an **int percentage**. The core applies it as `intdiv(base_value * percentage, 100)`, additive alongside the research bonus — matching the same pattern as character class bonuses. + +Available property names: `attack`, `shield`, `structural_integrity`, `capacity`, `fuel`, `fuel_capacity`. + +--- + +## Injecting into Core Views (`@moduleSlot`) + +Core Blade views contain `@moduleSlot(...)` directives at agreed extension points. Register a renderer callable to inject HTML into a slot: + +```php +use OGame\Services\ModuleSlotService; + +public function bootModule(): void +{ + ModuleSlotService::register('layout.resources_bar', function (array $data): string { + return view('my-module::layout.resource-tile', $data)->render(); + }); +} +``` + +### Available Slots + +| Slot name | View file | Data available | +|-----------|-----------|----------------| +| `layout.resources_bar` | `ingame/layouts/main.blade.php` | `currentPlanet`, `currentPlayer` | +| `layout.resources_bar_js` | `ingame/layouts/main.blade.php` | `currentPlanet` | +| `resources.building_section` | `ingame/resources/index.blade.php` | `planet`, `buildings` | +| `resources.production_box` | `ingame/resources/index.blade.php` | `planet` | +| `overview.planet_info` | `ingame/overview/index.blade.php` | _(none)_ | +| `admin.nav` | `ingame/layouts/admin-menu.blade.php` | _(none)_ | + +--- + +## Bringing Your Own Queue Service + +For modules with complex queue behavior (custom costs, cooldowns, slot selection), bring a fully self-contained queue service rather than routing through the core queues. + +The core calls `processQueue()` on every page load during the normal queue processing cycle. Tag your implementation in `bootModule()`: + +```php +use OGame\Contracts\Modules\ProvidesQueueProcessor; + +// In bootModule(): +app()->tag(MyBuildingQueueProcessor::class, 'module.queue_processors'); +``` + +Implement the contract: + +```php +use OGame\Contracts\Modules\ProvidesQueueProcessor; +use OGame\Services\PlanetService; + +class MyBuildingQueueProcessor implements ProvidesQueueProcessor +{ + public function processQueue(PlanetService $planet): void + { + // Retrieve and process finished queue items for this planet + } +} +``` + +For modules that only need pre-validation before an item enters a core queue (e.g. a cost check), implement `processQueue()` as a no-op and handle validation in your controller before calling the core queue service. + +--- + +## Available Hook Contracts + +All contracts live under `OGame\Contracts\Modules\`. + +| Interface | Tag | Purpose | +|-----------|-----|---------| +| `ProvidesGameObjects` | _(call directly)_ | Register GameObject instances with ObjectService | +| `ExtendsPlanetService` | `module.planet_extensions` | Planet-level production calculations | +| `ExtendsPlayerService` | `module.player_extensions` | Player-level data injection | +| `ProvidesQueueProcessor` | `module.queue_processors` | Own queue processing cycle | +| `ProvidesHighscoreCategory` | `module.highscore_categories` | Add highscore categories | + +--- + +## Admin Panel Modules + +A module can provide a fully independent admin panel with its own layout, route group, and controllers. The only core integration needed is injecting nav links via the `admin.nav` slot: + +```php +ModuleSlotService::register('admin.nav', function (): string { + return view('my-module::admin.nav-links')->render(); +}); +``` + +Routes should be grouped under a prefix like `/admin-panel/...` and protected with admin middleware. diff --git a/resources/views/ingame/admin/modules.blade.php b/resources/views/ingame/admin/modules.blade.php new file mode 100644 index 000000000..966f075f6 --- /dev/null +++ b/resources/views/ingame/admin/modules.blade.php @@ -0,0 +1,84 @@ +@extends('ingame.layouts.main') + +@section('content') +
+
+

@lang('Modules')

+
+ +
+
+

@lang('Installed Modules')

+
+ + @if (session('success')) +
+ {{ session('success') }} +
+ @endif + @if (session('error')) +
+ {{ session('error') }} +
+ @endif + + @if (empty($modules)) +

No modules are declared in config/modules.php.

+ @else + + + + + + + + + + + + @foreach ($modules as $module) + + + + + + + + @endforeach + +
ModuleVersionDescriptionStatusAction
+ {{ $module['name'] }} + @if ($module['missing']) +
Class not found: {{ $module['provider'] }} + @else +
{{ $module['id'] }} + @endif +
{{ $module['version'] }}{{ $module['description'] ?: '—' }} + @if ($module['missing']) + ✗ Missing + @elseif ($module['enabled']) + ✓ Enabled + @else + ✗ Disabled + @endif + + @unless ($module['missing']) +
+ @csrf + + +
+ @endunless +
+ +

+ Changes take effect on the next page load. To permanently add or remove modules, edit config/modules.php. +

+ @endif +
+
+@endsection diff --git a/resources/views/ingame/layouts/admin-menu.blade.php b/resources/views/ingame/layouts/admin-menu.blade.php index 428f4b363..276a7b3fd 100644 --- a/resources/views/ingame/layouts/admin-menu.blade.php +++ b/resources/views/ingame/layouts/admin-menu.blade.php @@ -83,6 +83,9 @@
  • Developer shortcuts
  • Server settings
  • Rules & Legal
  • +
  • Modules
  • + {{-- Module extension point: additional admin nav links --}} + @moduleSlot('admin.nav') @endif diff --git a/resources/views/ingame/layouts/main.blade.php b/resources/views/ingame/layouts/main.blade.php index e8f3cd688..63c169924 100644 --- a/resources/views/ingame/layouts/main.blade.php +++ b/resources/views/ingame/layouts/main.blade.php @@ -276,6 +276,8 @@ class="{{ $resources['deuterium']['storage_almost_full'] ? 'middlemark' : '' }}{ + {{-- Module extension point: additional resource tiles (e.g. population/food from Lifeforms) --}} + @moduleSlot('layout.resources_bar', ['currentPlanet' => $currentPlanet, 'currentPlayer' => $currentPlayer])