From ba4260c5ed3bcc503897b4edbcd88388e8c6f425 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 12:48:57 +0000 Subject: [PATCH 1/5] 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 7b2213be963fde2ce681e6294f3d864e3d5c012c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 12:58:13 +0000 Subject: [PATCH 2/5] 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 a8e05984410898a20e43b578276b0dc9eccd6690 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:14:03 +0000 Subject: [PATCH 3/5] 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 8ee5cd8828f9b879cd0f8e43ea0d3ee4f9cadd32 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 14:49:44 +0000 Subject: [PATCH 4/5] 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 92b3f225b5f9d95b6dd65bdfd948871e4fbd472a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 14:57:59 +0000 Subject: [PATCH 5/5] 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')