From 115d85de179a028ba28426a4e41b015cd720bbdd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 12:48:01 +0000 Subject: [PATCH 01/15] player-counter: add Czech (cs) translation for query lang file Mirrors the existing en/de/ru lang/query.php files key-for-key with natural Czech phrasing for Minecraft/game server admins. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- player-counter/lang/cs/query.php | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 player-counter/lang/cs/query.php diff --git a/player-counter/lang/cs/query.php b/player-counter/lang/cs/query.php new file mode 100644 index 00000000..f065c2c0 --- /dev/null +++ b/player-counter/lang/cs/query.php @@ -0,0 +1,57 @@ + 'Žádné herní dotazy', + 'query' => 'Herní dotaz|Herní dotazy', + 'type' => 'Typ', + 'port_offset' => 'Offset portu dotazu', + 'no_offset' => 'Bez offsetu', + 'port_offset_hint' => 'Offset, který bude přičten k portu alokace. Obvykle prázdné/0 nebo 1.', + 'port_variable' => 'Proměnná portu dotazu', + 'no_variable' => 'Bez proměnné', + 'port_variable_hint' => 'Název proměnné prostředí spouštěcí proměnné, která se použije k získání portu dotazu, např. "QUERY_PORT". Pokud je hodnota nastavena, offset portu dotazu bude ignorován! Ponechte prázdné pro použití portu alokace a offsetu.', + 'eggs' => 'Eggy', + 'no_eggs' => 'Žádné eggy', + 'hostname' => 'Název serveru', + 'players' => 'Hráči', + 'map' => 'Mapa', + 'unknown' => 'Neznámé', + + 'kick' => 'Kicknout', + 'ban' => 'Bannout', + + 'whitelisted' => 'Na whitelistu', + 'add_to_whitelist' => 'Přidat na whitelist', + 'remove_from_whitelist' => 'Odebrat z whitelistu', + + 'op' => 'OP', + 'add_to_ops' => 'Udělit OP', + 'remove_from_ops' => 'Odebrat OP', + + 'use_alias' => 'Použít alias alokace?', + 'use_alias_hint' => 'Pokud je zaškrtnuto, dotazy budou místo IP adresy používat alias alokace', + + 'table' => [ + 'no_players' => 'Nenalezeni žádní hráči', + 'no_players_description' => 'Buď na serveru nikdo není online, nebo je na něm vypnutý query', + 'server_offline' => 'Server je offline', + ], + + 'notifications' => [ + 'settings_saved' => 'Nastavení uloženo', + + 'player_kicked' => 'Hráč byl vyhozen ze serveru', + 'player_kick_failed' => 'Hráče se nepodařilo vyhodit', + + 'player_banned' => 'Hráč byl na serveru zabanován', + 'player_ban_failed' => 'Hráče se nepodařilo zabanovat', + + 'player_whitelist_add' => 'Hráč přidán na whitelist', + 'player_whitelist_remove' => 'Hráč odebrán z whitelistu', + 'player_whitelist_failed' => 'Whitelist se nepodařilo změnit', + + 'player_op' => 'Hráči byl udělen OP', + 'player_deop' => 'Hráči byl odebrán OP', + 'player_op_failed' => 'OP se nepodařilo změnit', + ], +]; From 22616b18f7c3205df6b1c3984808ab3789e146bd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 12:48:57 +0000 Subject: [PATCH 02/15] player-counter: add Minecraft Proxy (Velocity/BungeeCord/Waterfall) query type Velocity, BungeeCord and Waterfall all speak the same Java Edition status/ping protocol as a vanilla server, so the existing Java query schema can be reused as-is for a proxy target. Adds a distinct "Minecraft (Proxy)" query type so it shows up separately in the game query type selector, and disables the whitelist, OP list and avatar features on the players page for this type since a proxy has no whitelist.json/ops.json or player data files of its own. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- player-counter/README.md | 8 +++++++- .../Schemas/MinecraftProxyQueryTypeSchema.php | 16 ++++++++++++++++ .../src/Filament/Server/Pages/PlayersPage.php | 5 ++++- .../Providers/PlayerCounterPluginProvider.php | 2 ++ 4 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php diff --git a/player-counter/README.md b/player-counter/README.md index 65298820..76571127 100644 --- a/player-counter/README.md +++ b/player-counter/README.md @@ -12,6 +12,12 @@ For each game you need to create a Game Query in the admin area. Minecraft servers will first try the query (which requires you to set `enable-query` to true and `query-port` to your server port in `server.properties`) and will fallback to ping. It is recommended to enable query. +### Minecraft Proxy (Velocity/BungeeCord/Waterfall) + +Use the `Minecraft (Proxy)` query type when the server you are querying is actually a Velocity, BungeeCord or Waterfall proxy sitting in front of one or more backend servers, instead of a standalone Minecraft server. All three proxies speak the same Java Edition status/ping protocol as a vanilla server, so the query works the same way and returns the aggregated player count/list across all backend servers behind the proxy. + +Since a proxy has no `whitelist.json`, `ops.json` or player data files of its own, the whitelist, OP list and player avatar features on the players page are disabled for this query type. Whitelist/OP management still works normally when applied directly to the backend servers using the regular `Minecraft (Java)` query type. + ### Palworld For Palworld servers you need to set `RESTAPIEnabled` to `true` and `RESTAPIPort` to your server port in `PalWorldSettings.ini`. You also need to set an admin password via the `ADMIN_PASSWORD` startup variable. @@ -28,7 +34,7 @@ For Palworld servers you need to set `RESTAPIEnabled` to `true` and `RESTAPIPort ### Supported Games -- Minecraft (Java/Bedrock) +- Minecraft (Java/Bedrock), including Velocity/BungeeCord/Waterfall proxies - FiveM/RedM - Palworld - Any game server that uses [Valve's A2S query protocol](https://developer.valvesoftware.com/wiki/Server_queries), e.g. Garry's Mod, Rust, Barotrauma, Valheim, V Rising, The Forest, Arma 3, Arma Reforger, ARK: SE (ARK: SA will _NOT_ work), Unturned, Insurgency, Insurgency: Sandstorm + many more. diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php new file mode 100644 index 00000000..1c7b499e --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php @@ -0,0 +1,16 @@ + */ public array $players = []; @@ -106,6 +108,7 @@ protected function loadPlayersData(): void $gameQuery = $server->egg->gameQuery; // @phpstan-ignore property.notFound $this->isMinecraft = $gameQuery?->query_type === 'minecraft_java'; + $this->isProxy = $gameQuery?->query_type === 'minecraft_proxy'; $this->whitelist = []; $this->ops = []; @@ -185,7 +188,7 @@ public function table(Table $table): Table ->grow(false) ->state(fn (array $record) => in_array($record['name'], $this->ops) ? trans('player-counter::query.op') : null), TextColumn::make('time') - ->hidden(fn () => $this->isMinecraft) + ->hidden(fn () => $this->isMinecraft || $this->isProxy) ->badge() ->grow(false) ->formatStateUsing(fn ($state) => $state ? CarbonInterval::seconds($state)->cascade()->forHumans() : null), diff --git a/player-counter/src/Providers/PlayerCounterPluginProvider.php b/player-counter/src/Providers/PlayerCounterPluginProvider.php index 5ee32eab..a8a9c458 100644 --- a/player-counter/src/Providers/PlayerCounterPluginProvider.php +++ b/player-counter/src/Providers/PlayerCounterPluginProvider.php @@ -11,6 +11,7 @@ use Boy132\PlayerCounter\Extensions\Query\Schemas\GoldSourceQueryTypeSchema; use Boy132\PlayerCounter\Extensions\Query\Schemas\MinecraftBedrockQueryTypeSchema; use Boy132\PlayerCounter\Extensions\Query\Schemas\MinecraftJavaQueryTypeSchema; +use Boy132\PlayerCounter\Extensions\Query\Schemas\MinecraftProxyQueryTypeSchema; use Boy132\PlayerCounter\Extensions\Query\Schemas\PalworldQueryTypeSchema; use Boy132\PlayerCounter\Extensions\Query\Schemas\SourceQueryTypeSchema; use Boy132\PlayerCounter\Filament\Server\Widgets\ServerPlayerWidget; @@ -35,6 +36,7 @@ public function register(): void $service->register(new GoldSourceQueryTypeSchema()); $service->register(new MinecraftJavaQueryTypeSchema()); $service->register(new MinecraftBedrockQueryTypeSchema()); + $service->register(new MinecraftProxyQueryTypeSchema()); $service->register(new CitizenFXQueryTypeSchema()); $service->register(new PalworldQueryTypeSchema()); From afb99f325ef89942d1925dfbe4bd726536e13544 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 12:58:13 +0000 Subject: [PATCH 03/15] player-counter: fix Minecraft Proxy query crashing on legacy query attempt Velocity/BungeeCord/Waterfall don't reliably support the legacy enable-query/query-port GameSpot query protocol, and the exception thrown by the query library on failure was propagating past the generic Exception catch in MinecraftJavaQueryTypeSchema::tryQuery(), crashing the players widget instead of falling back to ping. Override process() in MinecraftProxyQueryTypeSchema to always use the ping/status protocol only, skipping the legacy query attempt entirely for proxy targets. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- player-counter/README.md | 4 +++- .../Query/Schemas/MinecraftProxyQueryTypeSchema.php | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/player-counter/README.md b/player-counter/README.md index 76571127..2270e324 100644 --- a/player-counter/README.md +++ b/player-counter/README.md @@ -14,7 +14,9 @@ Minecraft servers will first try the query (which requires you to set `enable-qu ### Minecraft Proxy (Velocity/BungeeCord/Waterfall) -Use the `Minecraft (Proxy)` query type when the server you are querying is actually a Velocity, BungeeCord or Waterfall proxy sitting in front of one or more backend servers, instead of a standalone Minecraft server. All three proxies speak the same Java Edition status/ping protocol as a vanilla server, so the query works the same way and returns the aggregated player count/list across all backend servers behind the proxy. +Use the `Minecraft (Proxy)` query type when the server you are querying is actually a Velocity, BungeeCord or Waterfall proxy sitting in front of one or more backend servers, instead of a standalone Minecraft server. All three proxies speak the same Java Edition status/ping protocol as a vanilla server, so this query type returns the aggregated player count/list across all backend servers behind the proxy. + +Unlike the `Minecraft (Java)` type, this always uses the ping/status protocol only and never attempts the legacy `enable-query`/`query-port` query, since proxy software does not support that legacy query protocol reliably (it caused connection errors during testing). No proxy-side query configuration is needed. Since a proxy has no `whitelist.json`, `ops.json` or player data files of its own, the whitelist, OP list and player avatar features on the players page are disabled for this query type. Whitelist/OP management still works normally when applied directly to the backend servers using the regular `Minecraft (Java)` query type. diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php index 1c7b499e..1ee00423 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php @@ -2,6 +2,8 @@ namespace Boy132\PlayerCounter\Extensions\Query\Schemas; +use App\Models\Server; + class MinecraftProxyQueryTypeSchema extends MinecraftJavaQueryTypeSchema { public function getId(): string @@ -13,4 +15,15 @@ public function getName(): string { return 'Minecraft (Proxy)'; } + + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + public function process(Server $server, string $ip, int $port): ?array + { + $ping = $this->tryPing($ip, $port); + if ($ping) { + return $ping; + } + + return null; + } } From 8ed61f38ee2279e101b576db20d26807fcd478e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:03:29 +0000 Subject: [PATCH 04/15] player-counter: improve Czech translation wording for ban/eggs Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- player-counter/lang/cs/query.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/player-counter/lang/cs/query.php b/player-counter/lang/cs/query.php index f065c2c0..28b47f61 100644 --- a/player-counter/lang/cs/query.php +++ b/player-counter/lang/cs/query.php @@ -10,15 +10,15 @@ 'port_variable' => 'Proměnná portu dotazu', 'no_variable' => 'Bez proměnné', 'port_variable_hint' => 'Název proměnné prostředí spouštěcí proměnné, která se použije k získání portu dotazu, např. "QUERY_PORT". Pokud je hodnota nastavena, offset portu dotazu bude ignorován! Ponechte prázdné pro použití portu alokace a offsetu.', - 'eggs' => 'Eggy', - 'no_eggs' => 'Žádné eggy', + 'eggs' => 'Vejce', + 'no_eggs' => 'Žádné vejce', 'hostname' => 'Název serveru', 'players' => 'Hráči', 'map' => 'Mapa', 'unknown' => 'Neznámé', 'kick' => 'Kicknout', - 'ban' => 'Bannout', + 'ban' => 'Zabanovat', 'whitelisted' => 'Na whitelistu', 'add_to_whitelist' => 'Přidat na whitelist', From 201203ab3575274db3ffd2ed63d1e350a7c20caf Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:14:03 +0000 Subject: [PATCH 05/15] player-counter: auto-assign Minecraft Proxy query type in seeder Matches eggs named/tagged Velocity, BungeeCord or Waterfall to the new minecraft_proxy query type, same as the existing minecraft/bedrock/source tag mappings, so proxy eggs get a sensible default query type without manual admin setup. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../database/Seeders/PlayerCounterSeeder.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/player-counter/database/Seeders/PlayerCounterSeeder.php b/player-counter/database/Seeders/PlayerCounterSeeder.php index 8d2055d4..928651f1 100644 --- a/player-counter/database/Seeders/PlayerCounterSeeder.php +++ b/player-counter/database/Seeders/PlayerCounterSeeder.php @@ -89,6 +89,27 @@ class PlayerCounterSeeder extends Seeder 'query_port_offset' => null, 'query_port_variable' => null, ], + [ + 'names' => 'Velocity', + 'tag' => 'velocity', + 'query_type' => 'minecraft_proxy', + 'query_port_offset' => null, + 'query_port_variable' => null, + ], + [ + 'names' => 'BungeeCord', + 'tag' => 'bungeecord', + 'query_type' => 'minecraft_proxy', + 'query_port_offset' => null, + 'query_port_variable' => null, + ], + [ + 'names' => 'Waterfall', + 'tag' => 'waterfall', + 'query_type' => 'minecraft_proxy', + 'query_port_offset' => null, + 'query_port_variable' => null, + ], [ 'tag' => 'source', 'query_type' => 'source', From 3afae57bded1f97edfc205d2724c4ccedac209a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:28:21 +0000 Subject: [PATCH 06/15] minecraft-modrinth: add Czech (cs) translation Mirrors the existing en/de lang/strings.php files key-for-key with natural Czech phrasing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- minecraft-modrinth/lang/cs/strings.php | 73 ++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 minecraft-modrinth/lang/cs/strings.php diff --git a/minecraft-modrinth/lang/cs/strings.php b/minecraft-modrinth/lang/cs/strings.php new file mode 100644 index 00000000..80f92afa --- /dev/null +++ b/minecraft-modrinth/lang/cs/strings.php @@ -0,0 +1,73 @@ + 'Modrinth', + 'minecraft_mods' => 'Minecraft Módy', + 'minecraft_plugins' => 'Minecraft Pluginy', + + 'settings' => [ + 'latest_minecraft_version' => 'Nejnovější verze Minecraftu', + 'settings_saved' => 'Nastavení uloženo', + ], + + 'page' => [ + 'open_folder' => 'Otevřít složku :folder', + 'minecraft_version' => 'Verze Minecraftu', + 'loader' => 'Loader', + 'installed' => 'Nainstalováno :type', + 'unknown' => 'Neznámé', + 'view_all' => 'Vše', + 'view_installed' => 'Nainstalované', + 'mod_unavailable' => 'Tento mod/plugin už není na Modrinthu dostupný', + ], + + 'table' => [ + 'columns' => [ + 'title' => 'Název', + 'author' => 'Autor', + 'downloads' => 'Stažení', + 'date_modified' => 'Upraveno', + ], + ], + + 'version' => [ + 'type' => 'Typ', + 'downloads' => 'Stažení', + 'published' => 'Publikováno', + 'changelog' => 'Seznam změn', + 'no_file_found' => 'Soubor nenalezen', + ], + + 'actions' => [ + 'install_latest' => 'Nainstalovat nejnovější verzi', + 'install' => 'Nainstalovat', + 'installed' => 'Nainstalováno', + 'update' => 'Aktualizovat', + 'uninstall' => 'Odinstalovat', + 'versions' => 'Výběr verze', + ], + + 'modals' => [ + 'update_heading' => 'Aktualizovat mod/plugin', + 'update_description' => 'Tímto se verze :old_version nahradí verzí :new_version. Starý soubor bude smazán.', + 'uninstall_heading' => 'Odinstalovat mod/plugin', + 'uninstall_description' => 'Opravdu chcete odinstalovat :name? Tímto se soubor trvale smaže z vašeho serveru.', + ], + + 'notifications' => [ + 'install_success' => 'Instalace dokončena', + 'install_success_body' => 'Úspěšně nainstalováno :name verze :version', + 'install_failed' => 'Instalace se nezdařila', + 'install_failed_body' => 'Při instalaci došlo k chybě. Zkuste to prosím znovu nebo kontaktujte podporu, pokud problém přetrvává.', + 'update_success' => 'Aktualizace dokončena', + 'update_success_body' => 'Úspěšně aktualizováno na verzi :version', + 'update_failed' => 'Aktualizace se nezdařila', + 'update_failed_body' => 'Při aktualizaci došlo k chybě. Zkuste to prosím znovu nebo kontaktujte podporu, pokud problém přetrvává.', + 'uninstall_success' => 'Odinstalace dokončena', + 'uninstall_success_body' => 'Úspěšně odinstalováno :name', + 'uninstall_partial' => 'Odinstalace neúplná', + 'uninstall_partial_body' => 'Soubor :name byl smazán, ale nepodařilo se ho odebrat ze seznamu nainstalovaných. Stále se může zobrazovat jako nainstalovaný.', + 'uninstall_failed' => 'Odinstalace se nezdařila', + 'uninstall_failed_body' => 'Při odinstalaci došlo k chybě. Zkuste to prosím znovu nebo kontaktujte podporu, pokud problém přetrvává.', + ], +]; From 94d1acbf21d63037c6cb8fafefafa108c362bf29 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:28:30 +0000 Subject: [PATCH 07/15] minecraft-modrinth: fix search/version filters excluding compatible plugins getProjects()/getProjectVersions()/getProjectVersionsBulk() filtered strictly by the single loader detected from the egg's tags (e.g. "categories:paper" or "loaders":["paper"]). Plugins on Modrinth that only declare the "spigot" or "bukkit" category (never re-tagged as "paper" even though they work fine there, since Paper is backwards compatible with the Spigot/Bukkit API) were silently excluded from search results and from the available-versions list, even though they are installable and run correctly. Add getCompatibleLoaders() to expand the detected loader into its upstream-compatible loaders (paper -> paper/spigot/bukkit, purpur -> purpur/paper/spigot/bukkit, folia -> folia/paper/spigot/bukkit, waterfall -> waterfall/bungeecord, quilt -> quilt/fabric) and OR them together in both the search facets and the version-list loader filter. The mapping is one-directional: a plugin published only for a fork isn't guaranteed to run on the upstream loader, so the reverse isn't added. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 7db2b7f5..ee81ca27 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -104,6 +104,27 @@ public function getLoaders(): array }); } + /** + * Loaders that can also run plugins/mods published only for an upstream loader they + * are backwards-compatible with, e.g. a Paper server can run plain Spigot/Bukkit plugins. + * Only one direction: a plugin published for the fork isn't guaranteed to run on the + * upstream loader, so the reverse mapping is intentionally not added. + * + * @return string[] + */ + protected function getCompatibleLoaders(string $loader): array + { + return match ($loader) { + 'spigot' => ['spigot', 'bukkit'], + 'paper' => ['paper', 'spigot', 'bukkit'], + 'purpur' => ['purpur', 'paper', 'spigot', 'bukkit'], + 'folia' => ['folia', 'paper', 'spigot', 'bukkit'], + 'waterfall' => ['waterfall', 'bungeecord'], + 'quilt' => ['quilt', 'fabric'], + default => [$loader], + }; + } + /** @return array{hits: array>, total_hits: int} */ public function getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null): array { @@ -120,10 +141,12 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject $minecraftVersion = $this->getMinecraftVersion($server); $minecraftLoader = $minecraftLoader['name']; + $loaderFacets = implode(',', array_map(fn ($loader) => "\"categories:$loader\"", $this->getCompatibleLoaders($minecraftLoader))); + $data = [ 'offset' => ($page - 1) * 20, 'limit' => 20, - 'facets' => "[[\"categories:$minecraftLoader\"],[\"versions:$minecraftVersion\"],[\"project_type:{$modrinthProjectType}\"]]", + 'facets' => "[[$loaderFacets],[\"versions:$minecraftVersion\"],[\"project_type:{$modrinthProjectType}\"]]", ]; $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:$page"; @@ -264,9 +287,11 @@ protected function getVersionsCacheKey(string $projectId, ?string $minecraftVers /** @return array{game_versions: string, loaders: string} */ protected function getVersionsQuery(?string $minecraftVersion, string $minecraftLoader): array { + $loaders = implode(',', array_map(fn ($loader) => "\"$loader\"", $this->getCompatibleLoaders($minecraftLoader))); + return [ 'game_versions' => "[\"$minecraftVersion\"]", - 'loaders' => "[\"$minecraftLoader\"]", + 'loaders' => "[$loaders]", ]; } From cef3e4c4795062cf79075ecb0b334d223c0a9c61 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:36:04 +0000 Subject: [PATCH 08/15] minecraft-modrinth: don't filter proxy plugins by Minecraft version For Velocity/BungeeCord/Waterfall, the plugin's declared Modrinth game versions mostly just reflect whenever it was last published, not what it actually supports: a proxy relays the protocol for whatever version the backend servers run and isn't itself tied to one Minecraft version. Filtering search results and version lists by an exact game version match was hiding older but still working proxy plugins. Skip the "versions" search facet and the game_versions query param for these loaders; the loader/category filter (already OR'd across compatible loaders) is what actually determines compatibility here. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index ee81ca27..548451e8 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -125,6 +125,18 @@ protected function getCompatibleLoaders(string $loader): array }; } + /** + * Proxy loaders aren't tied to a specific Minecraft version the way a server is: they + * relay the protocol for whatever version the backend servers run, so a plugin's declared + * Minecraft game versions on Modrinth mostly just reflect whenever it was last published, + * not what it's actually compatible with. Filtering those by an exact game version hides + * older but still working proxy plugins, so the version filter is skipped for them. + */ + protected function isProxyLoader(string $loader): bool + { + return in_array($loader, ['velocity', 'bungeecord', 'waterfall'], true); + } + /** @return array{hits: array>, total_hits: int} */ public function getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null): array { @@ -143,10 +155,16 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject $loaderFacets = implode(',', array_map(fn ($loader) => "\"categories:$loader\"", $this->getCompatibleLoaders($minecraftLoader))); + $facetGroups = ["[$loaderFacets]"]; + if (!$this->isProxyLoader($minecraftLoader)) { + $facetGroups[] = "[\"versions:$minecraftVersion\"]"; + } + $facetGroups[] = "[\"project_type:{$modrinthProjectType}\"]"; + $data = [ 'offset' => ($page - 1) * 20, 'limit' => 20, - 'facets' => "[[$loaderFacets],[\"versions:$minecraftVersion\"],[\"project_type:{$modrinthProjectType}\"]]", + 'facets' => '['.implode(',', $facetGroups).']', ]; $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:$page"; @@ -284,15 +302,20 @@ protected function getVersionsCacheKey(string $projectId, ?string $minecraftVers return "modrinth_versions:$projectId:$minecraftVersion:$minecraftLoader"; } - /** @return array{game_versions: string, loaders: string} */ + /** @return array{game_versions?: string, loaders: string} */ protected function getVersionsQuery(?string $minecraftVersion, string $minecraftLoader): array { $loaders = implode(',', array_map(fn ($loader) => "\"$loader\"", $this->getCompatibleLoaders($minecraftLoader))); - return [ - 'game_versions' => "[\"$minecraftVersion\"]", + $query = [ 'loaders' => "[$loaders]", ]; + + if (!$this->isProxyLoader($minecraftLoader)) { + $query['game_versions'] = "[\"$minecraftVersion\"]"; + } + + return $query; } /** @param array $versions */ From c8fd562f20e5fc81f3436a19bf053496acff5a5d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:46:22 +0000 Subject: [PATCH 09/15] minecraft-modrinth: stop filtering search by our mod/plugin enum's project_type Modrinth's "project_type" field is whatever the author picked when the project was created, not what it's actually compatible with. A Bukkit-family project can be stored as project_type "mod" while only having paper/spigot/purpur versions, and Modrinth's own site still lists it under /plugin/ since it decides that split by loader, not this field (e.g. https://modrinth.com/plugin/excellenteconomy). Filtering search strictly by our own Mod/Plugin enum value against this field hid such projects entirely, even on an exact loader match. The loader/category facet already discriminates mod-loader projects (fabric/forge/...) from plugin-loader ones (paper/spigot/...), so project_type is now OR'd across both values, kept only as a loose safety net against unrelated types like resourcepacks/shaders/ datapacks rather than as the actual mod vs. plugin split. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 548451e8..9e2babf7 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -137,7 +137,19 @@ protected function isProxyLoader(string $loader): bool return in_array($loader, ['velocity', 'bungeecord', 'waterfall'], true); } - /** @return array{hits: array>, total_hits: int} */ + /** + * Modrinth's own "project_type" field (mod/plugin/resourcepack/...) is whatever the + * author picked when they first created the project; a Bukkit-family project can be + * stored as "mod" even though it only has paper/spigot/purpur versions and Modrinth's + * own site lists it under /plugin/ (it decides that split by loader, not this field). + * Filtering strictly by our own Mod/Plugin enum value against that field hid projects + * like https://modrinth.com/plugin/excellenteconomy this way. The loader/category facet + * already discriminates mod-loader projects (fabric/forge/...) from plugin-loader ones + * (paper/spigot/...), so project_type is only kept as a loose safety net excluding + * unrelated types like resourcepacks/shaders/datapacks, not as the mod/plugin split. + * + * @return array{hits: array>, total_hits: int} + */ public function getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null): array { $modrinthProjectType = $modrinthProjectType->value; @@ -159,7 +171,7 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject if (!$this->isProxyLoader($minecraftLoader)) { $facetGroups[] = "[\"versions:$minecraftVersion\"]"; } - $facetGroups[] = "[\"project_type:{$modrinthProjectType}\"]"; + $facetGroups[] = '["project_type:mod","project_type:plugin"]'; $data = [ 'offset' => ($page - 1) * 20, From c5b95776e7fca7208d018f5ad17f98c67265dbc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:55:53 +0000 Subject: [PATCH 10/15] minecraft-modrinth: widen version fallback so unretagged plugins still show Verified against the live Modrinth API: the newest release tag right now is 26.3, but excellenteconomy (and similarly many other plugins) only declares support up to 26.1.2 - two releases behind, even though it's a plain Bukkit-API economy plugin with no reason to actually break on newer patches. Servers without an explicit MINECRAFT_VERSION/ MC_VERSION variable fell back to that single newest tag and filtered search/version-list results by an exact match against it, hiding any plugin whose author hasn't re-tagged support for it yet. Add getRecentMinecraftVersions() (the last 5 release tags) and use that as an OR'd window instead of the single newest tag whenever no explicit version is configured. An explicit server version is still treated as an exact requirement, since that's a real constraint rather than a guess. Confirmed against the live API that excellenteconomy now appears in search and has an installable version file with this window. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 85 ++++++++++++++++--- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 9e2babf7..2810a150 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -25,13 +25,7 @@ class MinecraftModrinthService public function getMinecraftVersion(Server $server): ?string { - $version = $server->variables()->where(fn ($builder) => $builder->where('env_variable', 'MINECRAFT_VERSION')->orWhere('env_variable', 'MC_VERSION'))->first()?->server_value; - - if (!$version || $version === 'latest') { - return $this->getLatestMinecraftVersion(); - } - - return $version; + return $this->getConfiguredMinecraftVersion($server) ?? $this->getLatestMinecraftVersion(); } public function getLatestMinecraftVersion(): ?string @@ -55,6 +49,71 @@ public function getLatestMinecraftVersion(): ?string }); } + /** + * The most recent Modrinth release tags, newest first. Used instead of a single exact + * "latest" version when a server has no explicit Minecraft version configured: plugin + * authors often lag behind re-tagging support for the very newest release even though + * nothing in the plugin actually changed, so a single exact match hides plugins that + * work fine on it. This has no effect once a server sets an explicit version, since that + * is treated as an exact requirement instead. + * + * @return string[] + */ + protected function getRecentMinecraftVersions(): array + { + return cache()->remember('modrinth:recent_minecraft_versions', now()->addHour(), function () { + try { + /** @var array $versions */ + $versions = Http::asJson() + ->timeout(5) + ->connectTimeout(5) + ->throw() + ->get('https://api.modrinth.com/v2/tag/game_version') + ->json(); + + return collect($versions) + ->filter(fn ($version) => $version['version_type'] === 'release') + ->take(5) + ->pluck('version') + ->all(); + } catch (Exception $exception) { + report($exception); + + return []; + } + }); + } + + protected function getConfiguredMinecraftVersion(Server $server): ?string + { + $version = $server->variables()->where(fn ($builder) => $builder->where('env_variable', 'MINECRAFT_VERSION')->orWhere('env_variable', 'MC_VERSION'))->first()?->server_value; + + return ($version && $version !== 'latest') ? $version : null; + } + + /** + * Minecraft versions to filter search/version-list results by. An explicit server + * version is an exact requirement (a single value), but a server without one falls + * back to a small window of the most recent releases rather than only the single + * newest, for the reason explained on getRecentMinecraftVersions(). + * + * @return string[] + */ + protected function getMinecraftVersionsForFiltering(Server $server): array + { + $configured = $this->getConfiguredMinecraftVersion($server); + if ($configured) { + return [$configured]; + } + + $recent = $this->getRecentMinecraftVersions(); + if (!empty($recent)) { + return $recent; + } + + return array_filter([$this->getLatestMinecraftVersion()]); + } + /** @return array{icon: string, name: string, supported_project_types: string[], display_name: string}|null */ public function getLoaderFromServer(Server $server): ?array { @@ -169,7 +228,8 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject $facetGroups = ["[$loaderFacets]"]; if (!$this->isProxyLoader($minecraftLoader)) { - $facetGroups[] = "[\"versions:$minecraftVersion\"]"; + $versionFacets = implode(',', array_map(fn ($version) => "\"versions:$version\"", $this->getMinecraftVersionsForFiltering($server))); + $facetGroups[] = "[$versionFacets]"; } $facetGroups[] = '["project_type:mod","project_type:plugin"]'; @@ -315,7 +375,7 @@ protected function getVersionsCacheKey(string $projectId, ?string $minecraftVers } /** @return array{game_versions?: string, loaders: string} */ - protected function getVersionsQuery(?string $minecraftVersion, string $minecraftLoader): array + protected function getVersionsQuery(Server $server, string $minecraftLoader): array { $loaders = implode(',', array_map(fn ($loader) => "\"$loader\"", $this->getCompatibleLoaders($minecraftLoader))); @@ -324,7 +384,8 @@ protected function getVersionsQuery(?string $minecraftVersion, string $minecraft ]; if (!$this->isProxyLoader($minecraftLoader)) { - $query['game_versions'] = "[\"$minecraftVersion\"]"; + $versions = implode(',', array_map(fn ($version) => "\"$version\"", $this->getMinecraftVersionsForFiltering($server))); + $query['game_versions'] = "[$versions]"; } return $query; @@ -371,7 +432,7 @@ public function getProjectVersions(string $projectId, Server $server): array ->timeout(5) ->connectTimeout(5) ->throw() - ->get("https://api.modrinth.com/v2/project/$projectId/version", $this->getVersionsQuery($minecraftVersion, $minecraftLoader)) + ->get("https://api.modrinth.com/v2/project/$projectId/version", $this->getVersionsQuery($server, $minecraftLoader)) ->json(); } catch (Exception $exception) { report($exception); @@ -410,7 +471,7 @@ public function getProjectVersionsBulk(array $projectIds, Server $server): array $minecraftVersion = $this->getMinecraftVersion($server); $minecraftLoader = $minecraftLoader['name']; - $query = $this->getVersionsQuery($minecraftVersion, $minecraftLoader); + $query = $this->getVersionsQuery($server, $minecraftLoader); $results = []; $missing = []; From 5966d69f36004e45d5bee1a12f88a8dbe46f54e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 14:04:50 +0000 Subject: [PATCH 11/15] minecraft-modrinth: add "Always Use Latest Version" plugin setting Even with the recent-versions fallback window, a server with an explicit MINECRAFT_VERSION/MC_VERSION variable set still filtered search/version-list results by an exact match against it, so plugins whose author hasn't re-tagged support for that specific point release yet stayed hidden regardless of the window (e.g. the panel showed "26.2" as the detected version while excellenteconomy only declares up to 26.1.2). Add a plugin setting (persisted as MINECRAFT_MODRINTH_ALWAYS_USE_LATEST_VERSION, following the same HasPluginSettings/EnvironmentWriterTrait pattern player-counter already uses) that, when enabled, skips the Minecraft version filter entirely for both search and version listing - loader compatibility is still enforced, only the game-version check is skipped. This covers the case an admin explicitly wants: always install the newest available mod/plugin version, relying on their usual backwards compatibility, e.g. to update everything ahead of upgrading the server itself to a newer Minecraft version. Repurposes the previously-unused 'latest_minecraft_version' lang key (dead scaffolding with no code ever reading it) into this toggle's label/hint across en/de/cs. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../config/minecraft-modrinth.php | 4 ++- minecraft-modrinth/lang/cs/strings.php | 3 +- minecraft-modrinth/lang/de/strings.php | 3 +- minecraft-modrinth/lang/en/strings.php | 3 +- .../src/MinecraftModrinthPlugin.php | 34 ++++++++++++++++++- .../src/Services/MinecraftModrinthService.php | 17 ++++++++-- 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/minecraft-modrinth/config/minecraft-modrinth.php b/minecraft-modrinth/config/minecraft-modrinth.php index 0b67a5fe..bb74b3b5 100644 --- a/minecraft-modrinth/config/minecraft-modrinth.php +++ b/minecraft-modrinth/config/minecraft-modrinth.php @@ -1,3 +1,5 @@ (bool) env('MINECRAFT_MODRINTH_ALWAYS_USE_LATEST_VERSION', false), +]; diff --git a/minecraft-modrinth/lang/cs/strings.php b/minecraft-modrinth/lang/cs/strings.php index 80f92afa..03d904b1 100644 --- a/minecraft-modrinth/lang/cs/strings.php +++ b/minecraft-modrinth/lang/cs/strings.php @@ -6,7 +6,8 @@ 'minecraft_plugins' => 'Minecraft Pluginy', 'settings' => [ - 'latest_minecraft_version' => 'Nejnovější verze Minecraftu', + 'always_use_latest_version' => 'Vždy použít nejnovější verzi', + 'always_use_latest_version_hint' => 'Přeskočí kontrolu kompatibility s verzí Minecraftu při hledání a instalaci modů/pluginů a vždy použije nejnovější dostupnou verzi pro detekovaný loader. Užitečné pro aktualizaci modů/pluginů ještě před povýšením serveru na novější verzi Minecraftu, protože se spoléhá na jejich obvyklou zpětnou kompatibilitu. Kompatibilita loaderu (Paper/Spigot/Fabric/...) se pořád kontroluje.', 'settings_saved' => 'Nastavení uloženo', ], diff --git a/minecraft-modrinth/lang/de/strings.php b/minecraft-modrinth/lang/de/strings.php index 83c42741..9490ce5d 100644 --- a/minecraft-modrinth/lang/de/strings.php +++ b/minecraft-modrinth/lang/de/strings.php @@ -6,7 +6,8 @@ 'minecraft_plugins' => 'Minecraft Plugins', 'settings' => [ - 'latest_minecraft_version' => 'Neueste Minecraft-Version', + 'always_use_latest_version' => 'Immer neueste Version verwenden', + 'always_use_latest_version_hint' => 'Überspringt die Prüfung der Minecraft-Versionskompatibilität bei der Suche und Installation von Mods/Plugins und verwendet immer die neueste verfügbare Version für den erkannten Loader. Nützlich, um Mods/Plugins schon vor einem Upgrade des Servers auf eine neuere Minecraft-Version zu aktualisieren, basierend auf deren üblicher Abwärtskompatibilität. Die Loader-Kompatibilität (Paper/Spigot/Fabric/...) wird weiterhin geprüft.', 'settings_saved' => 'Einstellungen gespeichert', ], diff --git a/minecraft-modrinth/lang/en/strings.php b/minecraft-modrinth/lang/en/strings.php index c2d359c5..36165347 100644 --- a/minecraft-modrinth/lang/en/strings.php +++ b/minecraft-modrinth/lang/en/strings.php @@ -6,7 +6,8 @@ 'minecraft_plugins' => 'Minecraft Plugins', 'settings' => [ - 'latest_minecraft_version' => 'Latest Minecraft Version', + 'always_use_latest_version' => 'Always Use Latest Version', + 'always_use_latest_version_hint' => 'Skip the Minecraft version compatibility check when searching for and installing mods/plugins, always using the newest available version for the detected loader. Useful for updating mods/plugins ahead of upgrading the server to a newer Minecraft version, relying on their usual backwards compatibility. Loader compatibility (Paper/Spigot/Fabric/...) is still enforced.', 'settings_saved' => 'Settings saved', ], diff --git a/minecraft-modrinth/src/MinecraftModrinthPlugin.php b/minecraft-modrinth/src/MinecraftModrinthPlugin.php index 320e1b40..800b8fca 100644 --- a/minecraft-modrinth/src/MinecraftModrinthPlugin.php +++ b/minecraft-modrinth/src/MinecraftModrinthPlugin.php @@ -2,11 +2,14 @@ namespace Boy132\MinecraftModrinth; +use App\Contracts\Plugins\HasPluginSettings; use App\Traits\EnvironmentWriterTrait; use Filament\Contracts\Plugin; +use Filament\Forms\Components\Toggle; +use Filament\Notifications\Notification; use Filament\Panel; -class MinecraftModrinthPlugin implements Plugin +class MinecraftModrinthPlugin implements HasPluginSettings, Plugin { use EnvironmentWriterTrait; @@ -23,4 +26,33 @@ public function register(Panel $panel): void } public function boot(Panel $panel): void {} + + public function getSettingsFormData(): array + { + return config('minecraft-modrinth'); + } + + public function getSettingsForm(): array + { + return [ + Toggle::make('always_use_latest_version') + ->label(trans('minecraft-modrinth::strings.settings.always_use_latest_version')) + ->hintIcon('tabler-question-mark') + ->hintIconTooltip(trans('minecraft-modrinth::strings.settings.always_use_latest_version_hint')) + ->inline(false) + ->default(fn () => config('minecraft-modrinth.always_use_latest_version')), + ]; + } + + public function saveSettings(array $data): void + { + $this->writeToEnvironment([ + 'MINECRAFT_MODRINTH_ALWAYS_USE_LATEST_VERSION' => $data['always_use_latest_version'], + ]); + + Notification::make() + ->title(trans('minecraft-modrinth::strings.settings.settings_saved')) + ->success() + ->send(); + } } diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 2810a150..fad82d01 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -196,6 +196,19 @@ protected function isProxyLoader(string $loader): bool return in_array($loader, ['velocity', 'bungeecord', 'waterfall'], true); } + /** + * Whether search/version-list results should be restricted to a Minecraft version at + * all. Always false for proxy loaders (see isProxyLoader()), and also false when the + * "Always Use Latest Version" plugin setting is on, for admins who'd rather always get + * the newest available mod/plugin version - e.g. to update everything ahead of upgrading + * the server itself to a newer Minecraft version - and accept the (usual) backwards + * compatibility risk themselves instead of waiting on Modrinth authors to re-tag support. + */ + protected function shouldFilterByMinecraftVersion(string $loader): bool + { + return !$this->isProxyLoader($loader) && !(bool) config('minecraft-modrinth.always_use_latest_version'); + } + /** * Modrinth's own "project_type" field (mod/plugin/resourcepack/...) is whatever the * author picked when they first created the project; a Bukkit-family project can be @@ -227,7 +240,7 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject $loaderFacets = implode(',', array_map(fn ($loader) => "\"categories:$loader\"", $this->getCompatibleLoaders($minecraftLoader))); $facetGroups = ["[$loaderFacets]"]; - if (!$this->isProxyLoader($minecraftLoader)) { + if ($this->shouldFilterByMinecraftVersion($minecraftLoader)) { $versionFacets = implode(',', array_map(fn ($version) => "\"versions:$version\"", $this->getMinecraftVersionsForFiltering($server))); $facetGroups[] = "[$versionFacets]"; } @@ -383,7 +396,7 @@ protected function getVersionsQuery(Server $server, string $minecraftLoader): ar 'loaders' => "[$loaders]", ]; - if (!$this->isProxyLoader($minecraftLoader)) { + if ($this->shouldFilterByMinecraftVersion($minecraftLoader)) { $versions = implode(',', array_map(fn ($version) => "\"$version\"", $this->getMinecraftVersionsForFiltering($server))); $query['game_versions'] = "[$versions]"; } From 18c2bfa791d0eee92dbd52604f8798d1a9041078 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 14:14:59 +0000 Subject: [PATCH 12/15] minecraft-modrinth: fix search/version cache key ignoring the latest-version toggle Confirmed live: searching "excell" on Paper 26.2 with the search cache warm only returned the 4 hits that explicitly declare 26.2 support, even after enabling "Always Use Latest Version" - excellenteconomy (max 26.1.2) was still missing. getProjects()'s cache key and getVersionsCacheKey() were built from project type/version/loader/project id only, none of which change when the setting is flipped, so a search or version list cached under the strict-filter facets kept being served for up to its TTL (30 minutes) after switching to latest-only. Add versionFilterCacheSuffix() and include it in both cache keys so toggling the setting is reflected immediately instead of waiting out the old cache entry. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index fad82d01..cdcfe3d3 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -209,6 +209,16 @@ protected function shouldFilterByMinecraftVersion(string $loader): bool return !$this->isProxyLoader($loader) && !(bool) config('minecraft-modrinth.always_use_latest_version'); } + /** + * Cache keys below only vary by loader/version/project, so without this, flipping the + * "Always Use Latest Version" setting would keep serving whatever was cached under the + * same key before the flip for up to its TTL, since the setting itself isn't part of it. + */ + protected function versionFilterCacheSuffix(string $loader): string + { + return $this->shouldFilterByMinecraftVersion($loader) ? 'exact' : 'latest'; + } + /** * Modrinth's own "project_type" field (mod/plugin/resourcepack/...) is whatever the * author picked when they first created the project; a Bukkit-family project can be @@ -252,7 +262,7 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject 'facets' => '['.implode(',', $facetGroups).']', ]; - $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:$page"; + $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:".$this->versionFilterCacheSuffix($minecraftLoader).":$page"; if ($search) { $data['query'] = $search; @@ -384,7 +394,7 @@ public function getInstalledModsFromModrinth(array $installedMods, int $page = 1 protected function getVersionsCacheKey(string $projectId, ?string $minecraftVersion, string $minecraftLoader): string { - return "modrinth_versions:$projectId:$minecraftVersion:$minecraftLoader"; + return "modrinth_versions:$projectId:$minecraftVersion:$minecraftLoader:".$this->versionFilterCacheSuffix($minecraftLoader); } /** @return array{game_versions?: string, loaders: string} */ From 9ff3fd9f181c7f3e4ab98cc59546316fcdbafbae Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 14:49:44 +0000 Subject: [PATCH 13/15] player-counter: give proxy mappings precedence over generic minecraft tag An egg tagged both 'minecraft' and a proxy tag (e.g. 'velocity') hit the generic 'minecraft' -> minecraft_java mapping first in the old MAPPINGS order. Since EggGameQuery::firstOrCreate() only matched on egg_id, the association created by that first match was never revisited once a later proxy mapping matched the same egg, so the egg kept minecraft_java instead of minecraft_proxy. Move the proxy mappings before the generic 'minecraft' one, and resolve a single highest-priority mapping per egg explicitly instead of relying on iteration order plus firstOrCreate's create-only semantics. Also correct the one known bad state this ordering bug could already have produced on an existing install: a proxy egg whose association still points to minecraft_java gets updated to minecraft_proxy. Any other existing association (including a manually customized one) is left untouched, so re-running the seeder can't clobber intentional admin changes to unrelated eggs. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../database/Seeders/PlayerCounterSeeder.php | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/player-counter/database/Seeders/PlayerCounterSeeder.php b/player-counter/database/Seeders/PlayerCounterSeeder.php index 928651f1..96e58e60 100644 --- a/player-counter/database/Seeders/PlayerCounterSeeder.php +++ b/player-counter/database/Seeders/PlayerCounterSeeder.php @@ -83,12 +83,10 @@ class PlayerCounterSeeder extends Seeder 'query_port_offset' => null, 'query_port_variable' => null, ], - [ - 'tag' => 'minecraft', - 'query_type' => 'minecraft_java', - 'query_port_offset' => null, - 'query_port_variable' => null, - ], + // Proxy mappings must come before the generic 'minecraft' tag mapping below: an egg + // can carry both tags (e.g. a Velocity egg also tagged 'minecraft'), and only the + // first match in this list is applied per egg, so the more specific proxy mapping + // has to win the tie instead of being shadowed by the generic Java one. [ 'names' => 'Velocity', 'tag' => 'velocity', @@ -110,6 +108,12 @@ class PlayerCounterSeeder extends Seeder 'query_port_offset' => null, 'query_port_variable' => null, ], + [ + 'tag' => 'minecraft', + 'query_type' => 'minecraft_java', + 'query_port_offset' => null, + 'query_port_variable' => null, + ], [ 'tag' => 'source', 'query_type' => 'source', @@ -123,23 +127,50 @@ public function run(): void foreach (Egg::all() as $egg) { $tags = $egg->tags ?? []; - foreach (self::MAPPINGS as $mapping) { - if ((array_key_exists('names', $mapping) && in_array($egg->name, array_wrap($mapping['names']))) || (array_key_exists('tag', $mapping) && in_array($mapping['tag'], $tags))) { - try { - $query = GameQuery::firstOrCreate([ - 'query_type' => $mapping['query_type'], - 'query_port_offset' => $mapping['query_port_offset'], - 'query_port_variable' => $mapping['query_port_variable'], - ]); + // Only the first (highest-priority) match in MAPPINGS applies per egg: an egg can + // match more than one mapping (e.g. a Velocity egg also tagged 'minecraft'), and + // MAPPINGS is ordered so the more specific one wins that tie. + $mapping = null; + foreach (self::MAPPINGS as $candidate) { + if ((array_key_exists('names', $candidate) && in_array($egg->name, array_wrap($candidate['names']))) || (array_key_exists('tag', $candidate) && in_array($candidate['tag'], $tags))) { + $mapping = $candidate; + + break; + } + } + + if (!$mapping) { + continue; + } + + try { + $query = GameQuery::firstOrCreate([ + 'query_type' => $mapping['query_type'], + 'query_port_offset' => $mapping['query_port_offset'], + 'query_port_variable' => $mapping['query_port_variable'], + ]); + + /** @var ?EggGameQuery $existing */ + $existing = EggGameQuery::where('egg_id', $egg->id)->first(); - EggGameQuery::firstOrCreate([ - 'egg_id' => $egg->id, - ], [ - 'game_query_id' => $query->id, - ]); - } catch (Exception) { + if ($existing) { + // Correct the one known bad state an older version of this seeder could + // produce: a proxy egg (also tagged 'minecraft') mis-assigned minecraft_java + // because the generic mapping used to be checked before the proxy ones. + // Any other existing association is left alone, so manual admin changes + // to unrelated eggs survive a re-run of this seeder. + if ($mapping['query_type'] === 'minecraft_proxy' && GameQuery::find($existing->game_query_id)?->query_type === 'minecraft_java') { + $existing->update(['game_query_id' => $query->id]); } + + continue; } + + EggGameQuery::create([ + 'egg_id' => $egg->id, + 'game_query_id' => $query->id, + ]); + } catch (Exception) { } } From 863348b3ef87b8b2f90d8038ea70149705f4e9d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 14:57:59 +0000 Subject: [PATCH 14/15] player-counter: hide kick/ban actions for proxy queries Both actions were only gated on the active tab, not on isProxy, so they showed up on the online tab for a minecraft_proxy query too and sent 'kick '/'ban ' to the proxy's console. Stock Velocity, BungeeCord and Waterfall don't provide those commands out of the box, so the actions couldn't do anything on a proxy unless a plugin added them. Gate both on !isProxy, same as the whitelist/OP/avatar features. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- player-counter/README.md | 2 +- player-counter/src/Filament/Server/Pages/PlayersPage.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/player-counter/README.md b/player-counter/README.md index 2270e324..21f16435 100644 --- a/player-counter/README.md +++ b/player-counter/README.md @@ -18,7 +18,7 @@ Use the `Minecraft (Proxy)` query type when the server you are querying is actua Unlike the `Minecraft (Java)` type, this always uses the ping/status protocol only and never attempts the legacy `enable-query`/`query-port` query, since proxy software does not support that legacy query protocol reliably (it caused connection errors during testing). No proxy-side query configuration is needed. -Since a proxy has no `whitelist.json`, `ops.json` or player data files of its own, the whitelist, OP list and player avatar features on the players page are disabled for this query type. Whitelist/OP management still works normally when applied directly to the backend servers using the regular `Minecraft (Java)` query type. +Since a proxy has no `whitelist.json`, `ops.json` or player data files of its own, the whitelist, OP list and player avatar features on the players page are disabled for this query type. Kick and ban are disabled too, since stock Velocity/BungeeCord/Waterfall don't provide those console commands out of the box. Whitelist/OP/kick/ban management still works normally when applied directly to the backend servers using the regular `Minecraft (Java)` query type. ### Palworld diff --git a/player-counter/src/Filament/Server/Pages/PlayersPage.php b/player-counter/src/Filament/Server/Pages/PlayersPage.php index f22209bd..83c9290e 100644 --- a/player-counter/src/Filament/Server/Pages/PlayersPage.php +++ b/player-counter/src/Filament/Server/Pages/PlayersPage.php @@ -196,7 +196,7 @@ public function table(Table $table): Table ]) ->recordActions([ Action::make('exclude_kick') - ->visible(fn () => !$this->activeTab || $this->activeTab === 'online') + ->visible(fn () => (!$this->activeTab || $this->activeTab === 'online') && !$this->isProxy) ->label(trans('player-counter::query.kick')) ->icon('tabler-door-exit') ->color('danger') @@ -225,7 +225,7 @@ public function table(Table $table): Table } }), Action::make('exclude_ban') - ->visible(fn () => !$this->activeTab || $this->activeTab === 'online') + ->visible(fn () => (!$this->activeTab || $this->activeTab === 'online') && !$this->isProxy) ->label(trans('player-counter::query.ban')) ->icon('tabler-hammer') ->color('danger') From fcf079d93bd5df689814c2eed973d0d29f12451c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 15:01:46 +0000 Subject: [PATCH 15/15] player-counter: stop logging failed queries as application errors Every query schema called report(\$exception) whenever a query attempt failed, which fires on any restart, boot-up window, or brief network hiccup - not just genuine bugs. With the players widget polling every 30s, this spammed the log on something completely routine: the queried server (or proxy) not answering yet. The UI already reflects an unreachable server as offline/unknown without needing a log entry for it. Drop the report() calls in all six query schemas (Java query+ping, Bedrock, Source/GoldSource, CitizenFX, Palworld); the proxy schema inherits the Java ones, so this covers it too. The exceptions are still caught and swallowed exactly as before, just without logging. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../Query/Schemas/CitizenFXQueryTypeSchema.php | 5 +++-- .../Query/Schemas/MinecraftBedrockQueryTypeSchema.php | 5 +++-- .../Query/Schemas/MinecraftJavaQueryTypeSchema.php | 10 ++++++---- .../Query/Schemas/PalworldQueryTypeSchema.php | 6 +++--- .../Extensions/Query/Schemas/SourceQueryTypeSchema.php | 5 +++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php index 63828d52..d2f2c5fd 100644 --- a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php @@ -44,8 +44,9 @@ public function process(Server $server, string $ip, int $port): ?array 'max_players' => $info['sv_maxclients'], 'players' => array_map(fn ($player) => ['id' => (string) $player['id'], 'name' => (string) $player['name']], $players), ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. } return null; diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php index 946dcdf5..43d7226c 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php @@ -40,8 +40,9 @@ public function process(Server $server, string $ip, int $port): ?array 'max_players' => $info['MaxPlayers'], 'players' => null, // Bedrock has no player list ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. } return null; diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index ea4d6857..a353915f 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -58,8 +58,10 @@ protected function tryQuery(string $ip, int $port): false|array 'max_players' => $info['MaxPlayers'], 'players' => array_map(fn ($player) => ['id' => (string) $player, 'name' => (string) $player], $players), ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. It falls back to + // tryPing() below, and the UI already reflects an unreachable server on its own. } return false; @@ -84,8 +86,8 @@ protected function tryPing(string $ip, int $port): false|array 'max_players' => $data['players']['max'], 'players' => $data['players']['sample'] ?? [], ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported, see tryQuery() above - same reasoning applies to the ping fallback. } finally { if (isset($ping)) { $ping->Close(); diff --git a/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php index 97105f84..b55b8403 100644 --- a/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php @@ -50,9 +50,9 @@ public function process(Server $server, string $ip, int $port): ?array 'players' => array_map(fn ($player) => ['id' => (string) ($player['playerId'] ?? $player['userId']), 'name' => (string) $player['name']], $players), ]; - } catch (Exception $exception) { - report($exception); - + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. return null; } } diff --git a/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php index 5d4902fd..8d95586c 100644 --- a/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php @@ -43,8 +43,9 @@ protected function run(string $ip, int $port, int $engine): ?array 'max_players' => $info['MaxPlayers'], 'players' => array_map(fn ($player) => ['id' => (string) $player['Id'], 'name' => (string) $player['Name']], $players), ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. } finally { $query->Disconnect(); }