-
Notifications
You must be signed in to change notification settings - Fork 14
arma-reforger-workshop - Plugin for managing Arma Reforger mods. #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThis pull request introduces a new Arma Reforger Workshop Plugin for Filament, enabling server administrators to manage and browse Arma Reforger workshop mods. The implementation includes a plugin manifest, localization strings, a core service with mod management and workshop integration, a Facade for easy access, and two Filament pages for browsing and managing installed mods. Changes
Sequence DiagramssequenceDiagram
participant Admin as Admin User
participant Page as ArmaReforgerWorkshop<br/>Page
participant Service as Workshop<br/>Service
participant FileRepo as Daemon File<br/>Repository
participant Workshop as Bohemia<br/>Workshop
rect rgb(200, 220, 255)
Note over Admin,Workshop: Add Mod Flow
Admin->>Page: Click "Add Mod" + Submit Form
Page->>Service: addMod(server, modId, name, version)
Service->>FileRepo: get config.json
FileRepo-->>Service: file contents
Service->>Service: Parse & update mods array
Service->>FileRepo: write updated config.json
FileRepo-->>Service: success
Service-->>Page: bool result
Page->>Admin: Show success notification
end
rect rgb(200, 220, 255)
Note over Admin,Workshop: Browse & View Details
Admin->>Page: Browse Workshop
Page->>Service: browseWorkshop(search, page)
Service->>Workshop: fetch search results (15 min cache)
Workshop-->>Service: HTML with embedded JSON
Service->>Service: Parse assets & extract mod data
Service-->>Page: LengthAwarePaginator [mods]
Page->>Admin: Display mods with details
Admin->>Page: View mod details
Page->>Service: getModDetails(modId)
alt Cache Hit (6 hours)
Service-->>Page: cached details
else Cache Miss
Service->>Workshop: fetch mod page
Workshop-->>Service: HTML + embedded data
Service->>Service: Parse & cache
Service-->>Page: details array
end
Page->>Admin: Display mod info
end
sequenceDiagram
participant Admin as Admin User
participant BrowsePage as BrowseWorkshop<br/>Page
participant Service as Workshop<br/>Service
participant FileRepo as Daemon File<br/>Repository
rect rgb(220, 200, 255)
Note over Admin,FileRepo: Install from Browse
Admin->>BrowsePage: Click Install on mod row
BrowsePage->>BrowsePage: Show confirmation modal
Admin->>BrowsePage: Confirm installation
BrowsePage->>Service: addMod(server, modId, name, version)
Service->>FileRepo: get & parse config.json
Service->>FileRepo: write mod entry to config.json
alt Success
FileRepo-->>Service: ok
Service-->>BrowsePage: true
BrowsePage->>Admin: Success notification + disable install action
else Failure
FileRepo-->>Service: error
Service-->>BrowsePage: false
BrowsePage->>Admin: Danger notification
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas requiring extra attention:
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (6)
arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (2)
169-177: Consider using a generic error message instead of exposing exception details.Displaying
$exception->getMessage()directly to users could expose internal implementation details or sensitive information. Consider using a generic error message and logging the full exception for debugging.🔎 Proposed fix
} catch (Exception $exception) { report($exception); Notification::make() ->title('Failed to add mod') - ->body($exception->getMessage()) + ->body('Could not update the server configuration. Please try again.') ->danger() ->send(); }
156-168: Use localization strings instead of hardcoded text.The localization file defines notification strings (
notifications.mod_added,notifications.mod_added_body, etc.) that are not being used here. This duplicates text and makes translations harder to maintain.🔎 Proposed fix
if ($success) { Notification::make() - ->title('Mod added') - ->body("'{$record['name']}' has been added to your server configuration.") + ->title(__('arma-reforger-workshop::arma-reforger-workshop.notifications.mod_added')) + ->body(__('arma-reforger-workshop::arma-reforger-workshop.notifications.mod_added_body', ['name' => $record['name']])) ->success() ->send(); } else { Notification::make() - ->title('Failed to add mod') - ->body('Could not update the server configuration.') + ->title(__('arma-reforger-workshop::arma-reforger-workshop.notifications.failed_to_add')) + ->body(__('arma-reforger-workshop::arma-reforger-workshop.notifications.config_update_failed')) ->danger() ->send(); }arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (1)
76-90: Unused$searchparameter.The
$searchparameter is declared but never used. Consider either implementing client-side filtering on the installed mods or removing the parameter if Filament allows.Additionally,
getModDetails()is called for each installed mod. While results are cached for 6 hours, the initial load with many mods could be slow due to sequential HTTP requests.🔎 Proposed fix to use the search parameter
$mods = ArmaReforgerWorkshop::getInstalledMods($server, $fileRepository); +// Filter by search if provided +if (!empty($search)) { + $mods = collect($mods)->filter(function ($mod) use ($search) { + return str_contains(strtolower($mod['name']), strtolower($search)) + || str_contains(strtolower($mod['modId']), strtolower($search)); + })->values()->toArray(); +} + // Enrich with workshop details $enrichedMods = collect($mods)->map(function ($mod) {arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (3)
92-97: Case-sensitive comparison inconsistency.The duplicate check here uses case-sensitive comparison (
=== $modId), butisModInstalled()(line 318) uses case-insensitive comparison viastrtoupper(). While the UI normalizes to uppercase before callingaddMod, this inconsistency could cause issues if the service is called directly with mixed-case IDs.Consider normalizing the modId to uppercase within this method for consistency:
🔎 Proposed fix
+ $modId = strtoupper($modId); + // Check if mod already exists foreach ($config['game']['mods'] as $existingMod) { - if (($existingMod['modId'] ?? '') === $modId) { + if (strtoupper($existingMod['modId'] ?? '') === $modId) { return true; // Already installed } }
138-141: Same case-sensitivity inconsistency in removeMod.The filter uses case-sensitive comparison. For consistency with
isModInstalled(), consider using case-insensitive comparison:🔎 Proposed fix
+ $modId = strtoupper($modId); + $config['game']['mods'] = collect($config['game']['mods']) - ->filter(fn ($mod) => ($mod['modId'] ?? '') !== $modId) + ->filter(fn ($mod) => strtoupper($mod['modId'] ?? '') !== $modId) ->values() ->toArray();
170-214: Error results may be cached for 6 hours.If an exception occurs or the HTTP request fails inside the
cache()->remember()callback, the minimal fallback result['modId' => $modId]will be cached for 6 hours, preventing retry until the cache expires.Consider caching only successful results:
🔎 Proposed fix using flexible() or manual caching
public function getModDetails(string $modId): array { - return cache()->remember("arma_reforger_mod:$modId", now()->addHours(6), function () use ($modId) { + $cacheKey = "arma_reforger_mod:$modId"; + $cached = cache()->get($cacheKey); + if ($cached !== null) { + return $cached; + } + + $result = (function () use ($modId) { try { $response = Http::timeout(10) ->connectTimeout(5) ->get(self::WORKSHOP_URL . '/' . $modId); if (!$response->successful()) { return ['modId' => $modId]; } // ... rest of parsing logic ... return array_filter($details, fn ($v) => $v !== null); } catch (Exception $exception) { report($exception); - return ['modId' => $modId]; + return null; // Don't cache errors } - }); + })(); + + if ($result !== null && count($result) > 1) { + cache()->put($cacheKey, $result, now()->addHours(6)); + return $result; + } + + return ['modId' => $modId]; }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
arma-reforger-workshop/README.md(1 hunks)arma-reforger-workshop/lang/en/arma-reforger-workshop.php(1 hunks)arma-reforger-workshop/plugin.json(1 hunks)arma-reforger-workshop/src/ArmaReforgerWorkshopPlugin.php(1 hunks)arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php(1 hunks)arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php(1 hunks)arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php(1 hunks)arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (3)
arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)
ArmaReforgerWorkshop(23-29)arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (9)
canAccess(38-48)getNavigationLabel(50-53)getModelLabel(55-58)getPluralModelLabel(60-63)getTitle(65-68)table(73-173)getHeaderActions(175-253)ArmaReforgerWorkshopPage(27-285)content(255-284)arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (5)
isArmaReforgerServer(14-22)browseWorkshop(222-285)getModWorkshopUrl(158-161)isModInstalled(313-324)addMod(73-122)
arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (1)
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (8)
ArmaReforgerWorkshopService(10-325)isArmaReforgerServer(14-22)getInstalledMods(29-55)getModDetails(168-215)getModWorkshopUrl(158-161)removeMod(127-153)addMod(73-122)getConfigPath(60-68)
arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (1)
ArmaReforgerWorkshopService(10-325)
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (3)
arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)
ArmaReforgerWorkshop(23-29)arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (1)
content(255-284)arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (1)
content(209-219)
🪛 markdownlint-cli2 (0.18.1)
arma-reforger-workshop/README.md
61-61: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🪛 PHPMD (2.15.0)
arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php
76-76: Avoid unused parameters such as '$search'. (undefined)
(UnusedFormalParameter)
arma-reforger-workshop/src/ArmaReforgerWorkshopPlugin.php
22-22: Avoid unused parameters such as '$panel'. (undefined)
(UnusedFormalParameter)
🔇 Additional comments (19)
arma-reforger-workshop/plugin.json (1)
1-17: LGTM!The plugin manifest is well-structured with all required metadata fields properly defined.
arma-reforger-workshop/src/ArmaReforgerWorkshopPlugin.php (1)
15-22: LGTM!The plugin registration correctly uses Filament's page discovery mechanism. The empty
boot()method with unused$panelparameter is required by thePlugininterface contract, so the static analysis warning is a false positive.arma-reforger-workshop/README.md (1)
1-61: LGTM!The README provides clear and comprehensive documentation covering features, requirements, configuration, and caching behavior. The bold styling for the author section is a minor style choice flagged by markdownlint, but it's acceptable for this context.
arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (3)
35-45: LGTM!The access control correctly validates both tenant existence and server type before granting access.
70-83: LGTM!The table configuration correctly integrates with the workshop service, using deferred loading and matching pagination to the service's per-page count.
195-219: LGTM!Header actions and content structure are properly implemented with clear navigation between pages and an accessible link to the external workshop.
arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)
23-29: Facade implementation is correct with comprehensive PHPDoc annotations.The @method annotations (lines 11-19) accurately document all service methods with proper type hints and return types. The getFacadeAccessor returns the service class name, which Laravel will auto-resolve from the container.
Verify that the
ArmaReforgerWorkshopServicebinding is registered in the application's service provider (typically in a service provider's boot method) to ensure proper container resolution at runtime.arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (5)
38-48: LGTM!The access control logic properly validates the tenant exists before checking if it's an Arma Reforger server, and combines with parent access checks.
103-128: LGTM!Good column configuration with appropriate placeholders, icons, and toggleable visibility.
130-172: LGTM!The remove action has proper confirmation, error handling, and user feedback via notifications.
175-253: LGTM!Good validation for mod ID (16-character hex), proper error handling, and consistent uppercase normalization before storing.
255-284: LGTM!Good defensive error handling in the content display, gracefully falling back to 'Unknown' on failure.
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (7)
14-22: LGTM!Clean implementation with proper null coalescing for missing features/tags.
29-55: LGTM!Robust implementation with proper error handling and filtering of invalid entries.
60-68: LGTM!Clean implementation with sensible default fallback.
158-161: LGTM!Simple and correct URL construction.
222-285: LGTM!Good implementation with appropriate timeouts and 15-minute cache TTL. The same caching-of-failures concern applies here but is less impactful due to the shorter cache duration.
290-308: LGTM!Good defensive implementation with proper fallbacks for missing thumbnail data.
313-324: LGTM!Correct case-insensitive comparison for mod ID matching.
| 'mod_added_body' => '\':name\' has been added to your server configuration.', | ||
| 'mod_removed' => 'Mod removed', | ||
| 'mod_removed_body' => '\':name\' has been removed from your server configuration.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary escape sequences in single-quoted strings.
In PHP single-quoted strings, single quotes don't need backslash escaping unless followed by another backslash. The \' here will display literal backslashes. If the intent is to wrap the mod name in quotes, consider using double quotes inside the string.
🔎 Proposed fix
- 'mod_added_body' => '\':name\' has been added to your server configuration.',
+ 'mod_added_body' => '":name" has been added to your server configuration.',
'mod_removed' => 'Mod removed',
- 'mod_removed_body' => '\':name\' has been removed from your server configuration.',
+ 'mod_removed_body' => '":name" has been removed from your server configuration.',📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 'mod_added_body' => '\':name\' has been added to your server configuration.', | |
| 'mod_removed' => 'Mod removed', | |
| 'mod_removed_body' => '\':name\' has been removed from your server configuration.', | |
| 'mod_added_body' => '":name" has been added to your server configuration.', | |
| 'mod_removed' => 'Mod removed', | |
| 'mod_removed_body' => '":name" has been removed from your server configuration.', |
🤖 Prompt for AI Agents
In arma-reforger-workshop/lang/en/arma-reforger-workshop.php around lines 34-36,
the strings use escaped single quotes like '\':name\'' which is
unnecessary/confusing; update those entries to either remove the backslashes and
use a double-quoted outer string with single quotes inside (e.g. "':name' has
been added..."/"':name' has been removed...") or simply include the name without
extra quotes (':name has been added...'/':name has been removed...') so the
displayed text does not contain literal backslashes.
| ->visible(function (array $record) { | ||
| /** @var Server $server */ | ||
| $server = Filament::getTenant(); | ||
| /** @var DaemonFileRepository $fileRepository */ | ||
| $fileRepository = app(DaemonFileRepository::class); | ||
|
|
||
| return !ArmaReforgerWorkshop::isModInstalled($server, $fileRepository, $record['modId']); | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Performance issue: N+1 calls to check mod installation status.
The visible() callbacks for both install and installed actions call isModInstalled() for every row. Since isModInstalled() internally calls getInstalledMods() which reads from the daemon file repository, this results in 24+ remote calls per page load. Consider caching the installed mods list once and checking against it.
🔎 Suggested approach
Add a property to cache installed mod IDs and compute it once:
protected ?array $installedModIds = null;
protected function getInstalledModIds(): array
{
if ($this->installedModIds === null) {
/** @var Server $server */
$server = Filament::getTenant();
/** @var DaemonFileRepository $fileRepository */
$fileRepository = app(DaemonFileRepository::class);
$installedMods = ArmaReforgerWorkshop::getInstalledMods($server, $fileRepository);
$this->installedModIds = array_map(
fn($mod) => strtoupper($mod['modId']),
$installedMods
);
}
return $this->installedModIds;
}Then update the visible callbacks:
->visible(function (array $record) {
- /** @var Server $server */
- $server = Filament::getTenant();
- /** @var DaemonFileRepository $fileRepository */
- $fileRepository = app(DaemonFileRepository::class);
-
- return !ArmaReforgerWorkshop::isModInstalled($server, $fileRepository, $record['modId']);
+ return !in_array(strtoupper($record['modId']), $this->getInstalledModIds(), true);
})Also applies to: 184-191
🤖 Prompt for AI Agents
In arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php
around lines 132-139 and 184-191, the visible() callbacks call
ArmaReforgerWorkshop::isModInstalled() per-row causing N+1 remote reads; add a
protected ?array $installedModIds property and a getInstalledModIds() method
that on first call retrieves ArmaReforgerWorkshop::getInstalledMods($server,
$fileRepository), maps mod IDs (uppercased) into the property, then return it,
and update both visible() callbacks to check existence against
getInstalledModIds() (use the same casing normalization) instead of calling
isModInstalled() per row.
Boy132
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix pint and what rabbit mentioned.
Hi, i created plugin for managing mods for Arma Reforger. The plugin is mainly vibe-coded (because of my poor php knowledge), so free to suggest any improvements.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.