From 8d949d517411400410c7417055442f61f7cb10ab Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 11:31:51 +0100 Subject: [PATCH 01/15] remove gameq and use "own" implementation for queries --- .../database/Seeders/PlayerCounterSeeder.php | 103 ++++++++++++++---- player-counter/plugin.json | 3 +- player-counter/src/Enums/GameQueryType.php | 58 ---------- .../Query/QueryTypeSchemaInterface.php | 13 +++ .../src/Extensions/Query/QueryTypeService.php | 29 +++++ .../Schemas/CitizenFXQueryTypeSchema.php | 69 ++++++++++++ .../Schemas/GoldSourceQueryTypeSchema.php | 24 ++++ .../MinecraftBedrockQueryTypeSchema.php | 45 ++++++++ .../Schemas/MinecraftJavaQueryTypeSchema.php | 81 ++++++++++++++ .../Query/Schemas/SourceQueryTypeSchema.php | 53 +++++++++ .../GameQueries/GameQueryResource.php | 8 +- .../src/Filament/Server/Pages/PlayersPage.php | 42 +++---- .../Server/Widgets/ServerPlayerWidget.php | 8 +- .../Servers/PlayerCounterController.php | 13 +-- player-counter/src/Models/GameQuery.php | 45 ++------ .../Providers/PlayerCounterPluginProvider.php | 19 ++++ 16 files changed, 454 insertions(+), 159 deletions(-) delete mode 100644 player-counter/src/Enums/GameQueryType.php create mode 100644 player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php create mode 100644 player-counter/src/Extensions/Query/QueryTypeService.php create mode 100644 player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php create mode 100644 player-counter/src/Extensions/Query/Schemas/GoldSourceQueryTypeSchema.php create mode 100644 player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php create mode 100644 player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php create mode 100644 player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php diff --git a/player-counter/database/Seeders/PlayerCounterSeeder.php b/player-counter/database/Seeders/PlayerCounterSeeder.php index a16bdde5..5aec7009 100644 --- a/player-counter/database/Seeders/PlayerCounterSeeder.php +++ b/player-counter/database/Seeders/PlayerCounterSeeder.php @@ -5,46 +5,101 @@ use App\Models\Egg; use Boy132\PlayerCounter\Models\EggGameQuery; use Boy132\PlayerCounter\Models\GameQuery; +use Exception; use Illuminate\Database\Seeder; class PlayerCounterSeeder extends Seeder { + public const MAPPINGS = [ + [ + 'names' => 'Squad', + 'query_type' => 'source', + 'query_port_offset' => 19378, + ], + [ + 'names' => 'Barotrauma', + 'query_type' => 'source', + 'query_port_offset' => 1, + ], + [ + 'names' => 'Valheim', + 'query_type' => 'source', + 'query_port_offset' => 1, + ], + [ + 'names' => ['V Rising', 'V-Rising', 'VRising'], + 'query_type' => 'source', + 'query_port_offset' => 1, + ], + [ + 'names' => ['The Forrest', 'TheForrest'], + 'query_type' => 'source', + 'query_port_offset' => 1, + ], + [ + 'names' => ['Arma 3', 'Arma3'], + 'query_type' => 'source', + 'query_port_offset' => 1, + ], + [ + 'names' => ['ARK: Survival Evolved', 'ARK: SurvivalEvolved', 'ARK Survival Evolved', 'ARK SurvivalEvolved', 'ARKSurvivalEvolved'], + 'query_type' => 'source', + 'query_port_offset' => 19238, + ], + [ + 'names' => 'Unturned', + 'query_type' => 'source', + 'query_port_offset' => 1, + ], + [ + 'names' => ['Insurgency: Sandstorm', 'Insurgency Sandstorm', 'InsurgencySandstorm'], + 'query_type' => 'source', + 'query_port_offset' => 29, + ], + [ + 'tag' => 'bedrock', + 'query_type' => 'minecraft_bedrock', + 'query_port_offset' => null, + ], + [ + 'tag' => 'minecraft', + 'query_type' => 'minecraft_java', + 'query_port_offset' => null, + ], + [ + 'tag' => 'source', + 'query_type' => 'source', + 'query_port_offset' => null, + ], + ]; + public function run(): void { - $minecraftQuery = GameQuery::firstOrCreate(['query_type' => 'minecraft']); - $sourceQuery = GameQuery::firstOrCreate(['query_type' => 'source']); - foreach (Egg::all() as $egg) { $tags = $egg->tags ?? []; - if (in_array('minecraft', $tags)) { - EggGameQuery::firstOrCreate([ - 'egg_id' => $egg->id, - ], [ - 'game_query_id' => $minecraftQuery->id, - ]); - } elseif (in_array('source', $tags)) { - if ($egg->name === 'Rust') { - $rustQuery = GameQuery::firstOrCreate(['query_type' => 'rust']); + 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'], + ]); - EggGameQuery::firstOrCreate([ - 'egg_id' => $egg->id, - ], [ - 'game_query_id' => $rustQuery->id, - ]); - } else { - EggGameQuery::firstOrCreate([ - 'egg_id' => $egg->id, - ], [ - 'game_query_id' => $sourceQuery->id, - ]); + EggGameQuery::firstOrCreate([ + 'egg_id' => $egg->id, + ], [ + 'game_query_id' => $query->id, + ]); + } catch (Exception) { + } } } } // @phpstan-ignore if.alwaysTrue if ($this->command) { - $this->command->info('Created game query types for minecraft and source'); + $this->command->info('Created game query types for existing eggs'); } } } diff --git a/player-counter/plugin.json b/player-counter/plugin.json index 86082193..1e8ad72a 100644 --- a/player-counter/plugin.json +++ b/player-counter/plugin.json @@ -15,6 +15,7 @@ ], "panel_version": null, "composer_packages": { - "krymosoftware/gameq": "^4.0" + "xpaw/php-minecraft-query": "^5.0.0", + "xpaw/php-source-query-class": "^5.0.0" } } \ No newline at end of file diff --git a/player-counter/src/Enums/GameQueryType.php b/player-counter/src/Enums/GameQueryType.php deleted file mode 100644 index 159f8696..00000000 --- a/player-counter/src/Enums/GameQueryType.php +++ /dev/null @@ -1,58 +0,0 @@ -name)->headline(); - } - - public function isMinecraft(): bool - { - return $this === self::MinecraftJava || $this === self::MinecraftBedrock; - } -} diff --git a/player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php b/player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php new file mode 100644 index 00000000..59111062 --- /dev/null +++ b/player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php @@ -0,0 +1,13 @@ +} */ + public function process(string $ip, int $port): ?array; +} diff --git a/player-counter/src/Extensions/Query/QueryTypeService.php b/player-counter/src/Extensions/Query/QueryTypeService.php new file mode 100644 index 00000000..c2c92bd3 --- /dev/null +++ b/player-counter/src/Extensions/Query/QueryTypeService.php @@ -0,0 +1,29 @@ +schemas, $id); + } + + public function register(QueryTypeSchemaInterface $schema): void + { + if (array_key_exists($schema->getId(), $this->schemas)) { + return; + } + + $this->schemas[$schema->getId()] = $schema; + } + + /** @return array */ + public function getMappings(): array + { + return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all(); + } +} diff --git a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php new file mode 100644 index 00000000..d35d84dd --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php @@ -0,0 +1,69 @@ +} */ + public function process(string $ip, int $port): ?array + { + try { + $this->resolveSRV($ip, $port); + + $http = Http::acceptJson() + ->connectTimeout(5) + ->throw() + ->baseUrl("http://$ip:$port/"); + + $info = $http->get('dynamic.json')->json(); + $players = $http->get('players.json')->json(); + + return [ + 'hostname' => $info['hostname'], + 'map' => $info['mapname'], + 'current_players' => $info['clients'], + 'max_players' => $info['sv_maxclients'], + 'players' => array_map(fn ($player) => ['id' => (string) $player['id'], 'name' => (string) $player['name']], $players), + ]; + } catch (Exception $exception) { + report($exception); + } + + return null; + } + + private function resolveSRV(string &$ip, int &$port): void + { + if (is_ip($ip)) { + return; + } + + $record = dns_get_record('_cfx._udp.' . $ip, DNS_SRV); + + if (!$record) { + return; + } + + if ($record[0]['target']) { + $ip = $record[0]['target']; + } + + if ($record[0]['port']) { + $port = (int) $record[0]['port']; + } + } +} diff --git a/player-counter/src/Extensions/Query/Schemas/GoldSourceQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/GoldSourceQueryTypeSchema.php new file mode 100644 index 00000000..19295cfe --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/GoldSourceQueryTypeSchema.php @@ -0,0 +1,24 @@ +} */ + public function process(string $ip, int $port): ?array + { + return $this->run($ip, $port, SourceQuery::GOLDSOURCE); + } +} diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php new file mode 100644 index 00000000..b9ab84f8 --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php @@ -0,0 +1,45 @@ +} */ + public function process(string $ip, int $port): ?array + { + $query = new MinecraftQuery(); + + try { + $query->ConnectBedrock($ip, $port, 5, true); + + $info = $query->GetInfo(); + $players = $query->GetPlayers(); + + return [ + 'hostname' => $info['HostName'], + 'map' => $info['Map'], + 'current_players' => $info['Players'], + 'max_players' => $info['MaxPlayers'], + 'players' => array_map(fn ($player) => ['id' => (string) $player['Id'], 'name' => (string) $player['Name']], $players), + ]; + } catch (Exception $exception) { + report($exception); + } + + return null; + } +} diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php new file mode 100644 index 00000000..e1d7875d --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -0,0 +1,81 @@ +} */ + public function process(string $ip, int $port): ?array + { + $query = $this->tryQuery($ip, $port); + if ($query) { + return $query; + } + + try { + $ping = new MinecraftPing($ip, $port, 5, true); + + $data = $ping->Query(); + + if (!$data) { + throw new Exception('Query function returned false'); + } + + return [ + 'hostname' => $data['description']['text'], + 'map' => 'world', // No map from MinecraftPing + 'current_players' => $data['players']['online'], + 'max_players' => $data['players']['max'], + 'players' => $data['players']['sample'], + ]; + } catch (Exception $exception) { + report($exception); + } finally { + if (isset($ping)) { + $ping->Close(); + } + } + + return null; + } + + /** @return false|array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + protected function tryQuery(string $ip, int $port): false|array + { + $query = new MinecraftQuery(); + + try { + $query->Connect($ip, $port, 5, true); + + $info = $query->GetInfo(); + $players = $query->GetPlayers(); + + return [ + 'hostname' => $info['HostName'], + 'map' => $info['Map'], + 'current_players' => $info['Players'], + 'max_players' => $info['MaxPlayers'], + 'players' => array_map(fn ($player) => ['id' => $player['Id'], 'name' => $player['Name']], $players), + ]; + } catch (Exception $exception) { + report($exception); + } + + return false; + } +} diff --git a/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php new file mode 100644 index 00000000..0a20cee0 --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php @@ -0,0 +1,53 @@ +} */ + public function process(string $ip, int $port): ?array + { + return $this->run($ip, $port, SourceQuery::SOURCE); + } + + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + protected function run(string $ip, int $port, int $engine): ?array + { + $query = new SourceQuery(); + + try { + $query->Connect($ip, $port, 5, $engine); + + $info = $query->GetInfo(); + $players = $query->GetPlayers(); + + return [ + 'hostname' => $info['HostName'], + 'map' => $info['Map'], + 'current_players' => $info['Players'], + 'max_players' => $info['MaxPlayers'], + 'players' => array_map(fn ($player) => ['id' => (string) $player['Id'], 'name' => (string) $player['Name']], $players), + ]; + } catch (Exception $exception) { + report($exception); + } finally { + $query->Disconnect(); + } + + return null; + } +} diff --git a/player-counter/src/Filament/Admin/Resources/GameQueries/GameQueryResource.php b/player-counter/src/Filament/Admin/Resources/GameQueries/GameQueryResource.php index 83c54e98..ef9070f1 100644 --- a/player-counter/src/Filament/Admin/Resources/GameQueries/GameQueryResource.php +++ b/player-counter/src/Filament/Admin/Resources/GameQueries/GameQueryResource.php @@ -2,7 +2,7 @@ namespace Boy132\PlayerCounter\Filament\Admin\Resources\GameQueries; -use Boy132\PlayerCounter\Enums\GameQueryType; +use Boy132\PlayerCounter\Extensions\Query\QueryTypeService; use Boy132\PlayerCounter\Filament\Admin\Resources\GameQueries\Pages\ManageGameQueries; use Boy132\PlayerCounter\Models\GameQuery; use Filament\Actions\DeleteAction; @@ -48,7 +48,8 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('query_type') ->label(trans('player-counter::query.type')) - ->badge(), + ->badge() + ->formatStateUsing(fn ($state, QueryTypeService $service) => $service->getMappings()[$state] ?? $state), TextColumn::make('query_port_offset') ->label(trans('player-counter::query.port_offset')) ->placeholder(trans('player-counter::query.no_offset')), @@ -76,8 +77,7 @@ public static function form(Schema $schema): Schema Select::make('query_type') ->label(trans('player-counter::query.type')) ->required() - ->options(GameQueryType::class) - ->disableOptionWhen(fn (string $value) => $value === GameQueryType::FiveMRedM->value) // see https://github.com/pelican-dev/plugins/issues/48 + ->options(fn (QueryTypeService $service) => $service->getMappings()) ->selectablePlaceholder(false) ->preload() ->searchable(), diff --git a/player-counter/src/Filament/Server/Pages/PlayersPage.php b/player-counter/src/Filament/Server/Pages/PlayersPage.php index 31b1c96d..92dc2dde 100644 --- a/player-counter/src/Filament/Server/Pages/PlayersPage.php +++ b/player-counter/src/Filament/Server/Pages/PlayersPage.php @@ -83,7 +83,7 @@ public function table(Table $table): Table /** @var ?GameQuery $gameQuery */ $gameQuery = $server->egg->gameQuery; // @phpstan-ignore property.notFound - $isMinecraft = $gameQuery?->query_type->isMinecraft(); + $isMinecraft = $gameQuery?->query_type === 'minecraft_java'; $whitelist = []; $ops = []; @@ -117,12 +117,12 @@ public function table(Table $table): Table $gameQuery = $server->egg->gameQuery; // @phpstan-ignore property.notFound if ($gameQuery) { - $data = $gameQuery->runQuery($server->allocation); + $data = $gameQuery->runQuery($server->allocation) ?? []; $players = $data['players']; } if ($search) { - $players = array_filter($players, fn ($player) => str($player['gq_name'])->contains($search, true)); + $players = array_filter($players, fn ($player) => str($player['name'])->contains($search, true)); } return new LengthAwarePaginator(array_slice($players, ($page - 1) * $recordsPerPage, $recordsPerPage), count($players), $recordsPerPage, $page); @@ -137,9 +137,9 @@ public function table(Table $table): Table Split::make([ ImageColumn::make('avatar') ->visible(fn () => $isMinecraft) - ->state(fn (array $record) => 'https://cravatar.eu/helmhead/' . $record['gq_name'] . '/256.png') + ->state(fn (array $record) => 'https://cravatar.eu/helmhead/' . $record['name'] . '/256.png') ->grow(false), - TextColumn::make('gq_name') + TextColumn::make('name') ->label('Name') ->tooltip(fn (array $record) => array_key_exists('id', $record) ? $record['id'] : null) ->searchable(), @@ -147,12 +147,12 @@ public function table(Table $table): Table ->visible(fn () => $isMinecraft) ->badge() ->grow(false) - ->state(fn (array $record) => in_array($record['gq_name'], $whitelist) ? trans('player-counter::query.whitelisted') : null), + ->state(fn (array $record) => in_array($record['name'], $whitelist) ? trans('player-counter::query.whitelisted') : null), TextColumn::make('is_op') ->visible(fn () => $isMinecraft) ->badge() ->grow(false) - ->state(fn (array $record) => in_array($record['gq_name'], $ops) ? trans('player-counter::query.op') : null), + ->state(fn (array $record) => in_array($record['name'], $ops) ? trans('player-counter::query.op') : null), TextColumn::make('time') ->hidden(fn () => $isMinecraft) ->badge() @@ -170,11 +170,11 @@ public function table(Table $table): Table $server = Filament::getTenant(); try { - $server->send('kick ' . $record['gq_name']); + $server->send('kick ' . $record['name']); Notification::make() ->title(trans('player-counter::query.notifications.player_kicked')) - ->body($record['gq_name']) + ->body($record['name']) ->success() ->send(); @@ -191,21 +191,21 @@ public function table(Table $table): Table }), Action::make('whitelist') ->visible(fn () => $isMinecraft) - ->label(fn (array $record) => in_array($record['gq_name'], $whitelist) ? trans('player-counter::query.remove_from_whitelist') : trans('player-counter::query.add_to_whitelist')) - ->icon(fn (array $record) => in_array($record['gq_name'], $whitelist) ? 'tabler-playlist-x' : 'tabler-playlist-add') - ->color(fn (array $record) => in_array($record['gq_name'], $whitelist) ? 'danger' : 'success') + ->label(fn (array $record) => in_array($record['name'], $whitelist) ? trans('player-counter::query.remove_from_whitelist') : trans('player-counter::query.add_to_whitelist')) + ->icon(fn (array $record) => in_array($record['name'], $whitelist) ? 'tabler-playlist-x' : 'tabler-playlist-add') + ->color(fn (array $record) => in_array($record['name'], $whitelist) ? 'danger' : 'success') ->action(function (array $record) use ($whitelist) { /** @var Server $server */ $server = Filament::getTenant(); try { - $action = in_array($record['gq_name'], $whitelist) ? 'remove' : 'add'; + $action = in_array($record['name'], $whitelist) ? 'remove' : 'add'; - $server->send('whitelist ' . $action . ' ' . $record['gq_name']); + $server->send('whitelist ' . $action . ' ' . $record['name']); Notification::make() ->title(trans('player-counter::query.notifications.player_whitelist_' . $action)) - ->body($record['gq_name']) + ->body($record['name']) ->success() ->send(); @@ -222,21 +222,21 @@ public function table(Table $table): Table }), Action::make('op') ->visible(fn () => $isMinecraft) - ->label(fn (array $record) => in_array($record['gq_name'], $ops) ? trans('player-counter::query.remove_from_ops') : trans('player-counter::query.add_to_ops')) - ->icon(fn (array $record) => in_array($record['gq_name'], $ops) ? 'tabler-shield-minus' : 'tabler-shield-plus') - ->color(fn (array $record) => in_array($record['gq_name'], $ops) ? 'warning' : 'success') + ->label(fn (array $record) => in_array($record['name'], $ops) ? trans('player-counter::query.remove_from_ops') : trans('player-counter::query.add_to_ops')) + ->icon(fn (array $record) => in_array($record['name'], $ops) ? 'tabler-shield-minus' : 'tabler-shield-plus') + ->color(fn (array $record) => in_array($record['name'], $ops) ? 'warning' : 'success') ->action(function (array $record) use ($ops) { /** @var Server $server */ $server = Filament::getTenant(); try { - $action = in_array($record['gq_name'], $ops) ? 'deop' : 'op'; + $action = in_array($record['name'], $ops) ? 'deop' : 'op'; - $server->send($action . ' ' . $record['gq_name']); + $server->send($action . ' ' . $record['name']); Notification::make() ->title(trans('player-counter::query.notifications.player_' . $action)) - ->body($record['gq_name']) + ->body($record['name']) ->success() ->send(); diff --git a/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php b/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php index 4856ae27..67caf261 100644 --- a/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php +++ b/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php @@ -45,12 +45,12 @@ protected function getStats(): array return []; } - $data = $gameQuery->runQuery($server->allocation); + $data = $gameQuery->runQuery($server->allocation) ?? []; return [ - SmallStatBlock::make(trans('player-counter::query.hostname'), $data['gq_hostname'] ?? trans('player-counter::query.unknown')), - SmallStatBlock::make(trans('player-counter::query.players'), ($data['gq_numplayers'] ?? '?') . ' / ' . ($data['gq_maxplayers'] ?? '?')), - SmallStatBlock::make(trans('player-counter::query.map'), $data['gq_mapname'] ?? trans('player-counter::query.unknown')), + SmallStatBlock::make(trans('player-counter::query.hostname'), $data['hostname'] ?? trans('player-counter::query.unknown')), + SmallStatBlock::make(trans('player-counter::query.players'), ($data['current_players'] ?? '?') . ' / ' . ($data['max_players'] ?? '?')), + SmallStatBlock::make(trans('player-counter::query.map'), $data['map'] ?? trans('player-counter::query.unknown')), ]; } } diff --git a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php index da16b6af..94bf2676 100644 --- a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php +++ b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php @@ -24,12 +24,7 @@ public function query(Server $server): JsonResponse { $data = $this->runQuery($server); - return response()->json([ - 'hostname' => (string) $data['gq_hostname'], - 'current_players' => (int) $data['gq_numplayers'], - 'max_players' => (int) $data['gq_maxplayers'], - 'map' => (string) $data['gq_mapname'], - ]); + return response()->json(array_except($data, 'players')); } /** @@ -44,13 +39,13 @@ public function players(Server $server): JsonResponse $data = $this->runQuery($server); /** @var string[] $players */ - $players = array_map(fn ($player) => $player['gq_name'], $data['players']); + $players = array_map(fn ($player) => $player['name'], $data['players']); return response()->json($players); } - /** @return array */ - private function runQuery(Server $server): array + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + private function runQuery(Server $server): ?array { if (!$server->allocation || $server->allocation->ip === '0.0.0.0' || $server->allocation->ip === '::') { abort(Response::HTTP_NOT_ACCEPTABLE, 'Server has invalid allocation'); diff --git a/player-counter/src/Models/GameQuery.php b/player-counter/src/Models/GameQuery.php index d79c7723..5d2d6546 100644 --- a/player-counter/src/Models/GameQuery.php +++ b/player-counter/src/Models/GameQuery.php @@ -4,16 +4,14 @@ use App\Models\Allocation; use App\Models\Egg; -use Boy132\PlayerCounter\Enums\GameQueryType; -use Exception; -use GameQ\GameQ; +use Boy132\PlayerCounter\Extensions\Query\QueryTypeService; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; /** * @property int $id - * @property GameQueryType $query_type + * @property string $query_type * @property ?int $query_port_offset * @property Collection|Egg[] $eggs * @property int|null $eggs_count @@ -29,49 +27,20 @@ class GameQuery extends Model 'query_port_offset' => null, ]; - protected function casts(): array - { - return [ - 'query_type' => GameQueryType::class, - ]; - } - public function eggs(): BelongsToMany { return $this->belongsToMany(Egg::class); } - /** @return array */ - public function runQuery(Allocation $allocation): array + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + public function runQuery(Allocation $allocation): ?array { $ip = config('player-counter.use_alias') && is_ip($allocation->alias) ? $allocation->alias : $allocation->ip; $ip = is_ipv6($ip) ? '[' . $ip . ']' : $ip; - $host = $ip . ':' . $allocation->port; - - try { - $data = [ - 'type' => $this->query_type->value, - 'host' => $host, - ]; - - if ($this->query_port_offset) { - $data['options'] = [ - 'query_port' => $allocation->port + $this->query_port_offset, - ]; - } - - $gameQ = new GameQ(); - - $gameQ->addServer($data); - - $gameQ->setOption('debug', config('app.debug')); - - return $gameQ->process()[$host] ?? []; - } catch (Exception $exception) { - report($exception); - } + /** @var QueryTypeService $service */ + $service = app(QueryTypeService::class); // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions - return []; + return $service->get($this->query_type)->process($ip, $allocation->port + ($this->query_port_offset ?? 0)); } } diff --git a/player-counter/src/Providers/PlayerCounterPluginProvider.php b/player-counter/src/Providers/PlayerCounterPluginProvider.php index b2199892..0f90c67d 100644 --- a/player-counter/src/Providers/PlayerCounterPluginProvider.php +++ b/player-counter/src/Providers/PlayerCounterPluginProvider.php @@ -6,6 +6,12 @@ use App\Filament\Server\Pages\Console; use App\Models\Egg; use App\Models\Role; +use Boy132\PlayerCounter\Extensions\Query\QueryTypeService; +use Boy132\PlayerCounter\Extensions\Query\Schemas\CitizenFXQueryTypeSchema; +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\SourceQueryTypeSchema; use Boy132\PlayerCounter\Filament\Server\Widgets\ServerPlayerWidget; use Boy132\PlayerCounter\Models\EggGameQuery; use Boy132\PlayerCounter\Models\GameQuery; @@ -19,6 +25,19 @@ public function register(): void Role::registerCustomModelIcon('game_query', 'tabler-device-desktop-search'); Console::registerCustomWidgets(ConsoleWidgetPosition::AboveConsole, [ServerPlayerWidget::class]); + + $this->app->singleton(QueryTypeService::class, function () { + $service = new QueryTypeService(); + + // Default Query types + $service->register(new SourceQueryTypeSchema()); + $service->register(new GoldSourceQueryTypeSchema()); + $service->register(new MinecraftJavaQueryTypeSchema()); + $service->register(new MinecraftBedrockQueryTypeSchema()); + $service->register(new CitizenFXQueryTypeSchema()); + + return $service; + }); } public function boot(): void From 488432392213f353af259ee6b1b8e3d3390bd6e7 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 12:03:34 +0100 Subject: [PATCH 02/15] update readme --- player-counter/README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/player-counter/README.md b/player-counter/README.md index 384dda7d..601b858a 100644 --- a/player-counter/README.md +++ b/player-counter/README.md @@ -4,17 +4,14 @@ Show the amount of connected players to game servers with real-time querying cap ## Setup -**IMPORTANT**: You need to have the bz2 php extension and zip/7zip installed! +Make sure your server has an allocation with a public ip. Alternatively, if you use local ips you can put the public ip in the allocation alias and enable "Use allocation alias?" in the plugin settings. -Make sure your server has an allocation with a public ip. - -For Minecraft servers you need to set `enable-query` to true and the `query-port` to your server port! (in `server.properties`) -Game query for FiveM/RedM is currently not available due to a [bug with GameQ](https://github.com/pelican-dev/plugins/issues/48). +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. ## Features - Real-time player count display for game servers -- Support for multiple game query protocols via [GameQ](https://github.com/krymosoftware/gameq) +- Support for multiple game query protocols - Link query protocols to specific eggs - Dashboard widget showing connected players - Dedicated players page for detailed information From be9204728f502238508145da3edc6b2aa794d2aa Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 12:04:31 +0100 Subject: [PATCH 03/15] remove gameq from workflow --- .github/workflows/lint.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 71bf5c8d..ddc6af4f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,7 +33,7 @@ jobs: phpstan: name: PHPStan runs-on: ubuntu-latest - + strategy: fail-fast: true matrix: @@ -61,7 +61,7 @@ jobs: - name: Install plugin dependencies run: | cd pelican - composer require "stripe/stripe-php:^18.0" "kovah/laravel-socialite-oidc:^0.6" "krymosoftware/gameq:^4.0" "socialiteproviders/pocketid:^5.0" + composer require "stripe/stripe-php:^18.0" "kovah/laravel-socialite-oidc:^0.6" "socialiteproviders/pocketid:^5.0" - name: Setup .env file run: cp pelican/.env.example pelican/.env @@ -76,4 +76,3 @@ jobs: run: | cd pelican vendor/bin/phpstan analyse --memory-limit=-1 --error-format=github - From ca5b66b9fa20ac56199907e75c3c0e5792ad6cd8 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 13:33:37 +0100 Subject: [PATCH 04/15] better error handling --- .../Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php | 4 ++++ .../Query/Schemas/MinecraftBedrockQueryTypeSchema.php | 4 ++++ .../Query/Schemas/MinecraftJavaQueryTypeSchema.php | 6 +++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php index d35d84dd..6b737210 100644 --- a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php @@ -32,6 +32,10 @@ public function process(string $ip, int $port): ?array $info = $http->get('dynamic.json')->json(); $players = $http->get('players.json')->json(); + if (!$info || !$players) { + return null; + } + return [ 'hostname' => $info['hostname'], 'map' => $info['mapname'], diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php index b9ab84f8..e7e92375 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php @@ -29,6 +29,10 @@ public function process(string $ip, int $port): ?array $info = $query->GetInfo(); $players = $query->GetPlayers(); + if (!$info || !$players) { + return null; + } + return [ 'hostname' => $info['HostName'], 'map' => $info['Map'], diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index e1d7875d..159b73fd 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -33,7 +33,7 @@ public function process(string $ip, int $port): ?array $data = $ping->Query(); if (!$data) { - throw new Exception('Query function returned false'); + return null; } return [ @@ -65,6 +65,10 @@ protected function tryQuery(string $ip, int $port): false|array $info = $query->GetInfo(); $players = $query->GetPlayers(); + if (!$info || !$players) { + return false; + } + return [ 'hostname' => $info['HostName'], 'map' => $info['Map'], From 2fae7865065d144c8bf1e24c6544f8486a861364 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 14:10:24 +0100 Subject: [PATCH 05/15] fix minecraft hostname --- .../Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index 159b73fd..a1e29ef8 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -36,8 +36,8 @@ public function process(string $ip, int $port): ?array return null; } - return [ - 'hostname' => $data['description']['text'], + return [ + 'hostname' => is_string($data['description']) ? $data['description'] : $data['description']['text'], 'map' => 'world', // No map from MinecraftPing 'current_players' => $data['players']['online'], 'max_players' => $data['players']['max'], From f280dfd2d395fb6f2b215c369d689d70731031d3 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 14:10:48 +0100 Subject: [PATCH 06/15] pint --- .../Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index a1e29ef8..1f5f5e80 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -36,7 +36,7 @@ public function process(string $ip, int $port): ?array return null; } - return [ + return [ 'hostname' => is_string($data['description']) ? $data['description'] : $data['description']['text'], 'map' => 'world', // No map from MinecraftPing 'current_players' => $data['players']['online'], From c2cba24dc37c425ecb249f753002cf0f3341af7d Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 14:30:41 +0100 Subject: [PATCH 07/15] more minecraft fixes --- .../Query/Schemas/MinecraftJavaQueryTypeSchema.php | 4 ++-- player-counter/src/Filament/Server/Pages/PlayersPage.php | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index 1f5f5e80..2a7d8a8c 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -41,7 +41,7 @@ public function process(string $ip, int $port): ?array 'map' => 'world', // No map from MinecraftPing 'current_players' => $data['players']['online'], 'max_players' => $data['players']['max'], - 'players' => $data['players']['sample'], + 'players' => $data['players']['sample'] ?? [], ]; } catch (Exception $exception) { report($exception); @@ -74,7 +74,7 @@ protected function tryQuery(string $ip, int $port): false|array 'map' => $info['Map'], 'current_players' => $info['Players'], 'max_players' => $info['MaxPlayers'], - 'players' => array_map(fn ($player) => ['id' => $player['Id'], 'name' => $player['Name']], $players), + 'players' => $players, ]; } catch (Exception $exception) { report($exception); diff --git a/player-counter/src/Filament/Server/Pages/PlayersPage.php b/player-counter/src/Filament/Server/Pages/PlayersPage.php index 92dc2dde..8bf55070 100644 --- a/player-counter/src/Filament/Server/Pages/PlayersPage.php +++ b/player-counter/src/Filament/Server/Pages/PlayersPage.php @@ -117,8 +117,11 @@ public function table(Table $table): Table $gameQuery = $server->egg->gameQuery; // @phpstan-ignore property.notFound if ($gameQuery) { - $data = $gameQuery->runQuery($server->allocation) ?? []; - $players = $data['players']; + $data = $gameQuery->runQuery($server->allocation); + + if ($data) { + $players = $data['players']; + } } if ($search) { From 082e40e6d8a42c689af11abf2f7687eefd241dc3 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 14:35:35 +0100 Subject: [PATCH 08/15] fix player mapping for minecraft query --- .../Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index 2a7d8a8c..5c33509b 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -74,7 +74,7 @@ protected function tryQuery(string $ip, int $port): false|array 'map' => $info['Map'], 'current_players' => $info['Players'], 'max_players' => $info['MaxPlayers'], - 'players' => $players, + 'players' => array_map(fn ($player) => ['id' => (string) $player, 'name' => (string) $player], $players), ]; } catch (Exception $exception) { report($exception); From 0aeab1d3e646377b955f14ffd5100bc27099526f Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 14:50:18 +0100 Subject: [PATCH 09/15] fix player list on bedrock --- .../src/Extensions/Query/QueryTypeSchemaInterface.php | 2 +- .../Query/Schemas/MinecraftBedrockQueryTypeSchema.php | 7 +++---- player-counter/src/Filament/Server/Pages/PlayersPage.php | 4 ++-- .../Api/Client/Servers/PlayerCounterController.php | 6 +++++- player-counter/src/Models/GameQuery.php | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php b/player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php index 59111062..439adb7f 100644 --- a/player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php +++ b/player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php @@ -8,6 +8,6 @@ public function getId(): string; public function getName(): string; - /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: ?array} */ public function process(string $ip, int $port): ?array; } diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php index e7e92375..c73e098e 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php @@ -18,7 +18,7 @@ public function getName(): string return 'Minecraft (Bedrock)'; } - /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: ?array} */ public function process(string $ip, int $port): ?array { $query = new MinecraftQuery(); @@ -27,9 +27,8 @@ public function process(string $ip, int $port): ?array $query->ConnectBedrock($ip, $port, 5, true); $info = $query->GetInfo(); - $players = $query->GetPlayers(); - if (!$info || !$players) { + if (!$info) { return null; } @@ -38,7 +37,7 @@ public function process(string $ip, int $port): ?array 'map' => $info['Map'], 'current_players' => $info['Players'], 'max_players' => $info['MaxPlayers'], - 'players' => array_map(fn ($player) => ['id' => (string) $player['Id'], 'name' => (string) $player['Name']], $players), + 'players' => null, ]; } catch (Exception $exception) { report($exception); diff --git a/player-counter/src/Filament/Server/Pages/PlayersPage.php b/player-counter/src/Filament/Server/Pages/PlayersPage.php index 8bf55070..3253504b 100644 --- a/player-counter/src/Filament/Server/Pages/PlayersPage.php +++ b/player-counter/src/Filament/Server/Pages/PlayersPage.php @@ -119,7 +119,7 @@ public function table(Table $table): Table if ($gameQuery) { $data = $gameQuery->runQuery($server->allocation); - if ($data) { + if ($data && $data['players']) { $players = $data['players']; } } @@ -134,7 +134,7 @@ public function table(Table $table): Table ->contentGrid([ 'default' => 1, 'lg' => 2, - 'xl' => 3, + 'xl' => $isMinecraft ? 2 : 3, ]) ->columns([ Split::make([ diff --git a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php index 94bf2676..4746c6c7 100644 --- a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php +++ b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php @@ -38,13 +38,17 @@ public function players(Server $server): JsonResponse { $data = $this->runQuery($server); + if (!$data['players']) { + abort(Response::HTTP_NOT_ACCEPTABLE, 'Server query has no player list'); + } + /** @var string[] $players */ $players = array_map(fn ($player) => $player['name'], $data['players']); return response()->json($players); } - /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: ?array} */ private function runQuery(Server $server): ?array { if (!$server->allocation || $server->allocation->ip === '0.0.0.0' || $server->allocation->ip === '::') { diff --git a/player-counter/src/Models/GameQuery.php b/player-counter/src/Models/GameQuery.php index 5d2d6546..4c91b662 100644 --- a/player-counter/src/Models/GameQuery.php +++ b/player-counter/src/Models/GameQuery.php @@ -32,7 +32,7 @@ public function eggs(): BelongsToMany return $this->belongsToMany(Egg::class); } - /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array} */ + /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: ?array} */ public function runQuery(Allocation $allocation): ?array { $ip = config('player-counter.use_alias') && is_ip($allocation->alias) ? $allocation->alias : $allocation->ip; From c61707389e4e4a7391a50aaad12b4863df410da3 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 14:56:11 +0100 Subject: [PATCH 10/15] better check for allocation ip --- .../src/Filament/Server/Pages/PlayersPage.php | 2 +- .../Server/Widgets/ServerPlayerWidget.php | 2 +- .../Client/Servers/PlayerCounterController.php | 2 +- player-counter/src/Models/GameQuery.php | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/player-counter/src/Filament/Server/Pages/PlayersPage.php b/player-counter/src/Filament/Server/Pages/PlayersPage.php index 3253504b..6edbd3b2 100644 --- a/player-counter/src/Filament/Server/Pages/PlayersPage.php +++ b/player-counter/src/Filament/Server/Pages/PlayersPage.php @@ -40,7 +40,7 @@ public static function canAccess(): bool /** @var Server $server */ $server = Filament::getTenant(); - if (!$server->allocation || $server->allocation->ip === '0.0.0.0' || $server->allocation->ip === '::') { + if (!GameQuery::canRunQuery($server->allocation)) { return false; } diff --git a/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php b/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php index 67caf261..9333c67a 100644 --- a/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php +++ b/player-counter/src/Filament/Server/Widgets/ServerPlayerWidget.php @@ -21,7 +21,7 @@ public static function canView(): bool return false; } - if (!$server->allocation || $server->allocation->ip === '0.0.0.0' || $server->allocation->ip === '::') { + if (!GameQuery::canRunQuery($server->allocation)) { return false; } diff --git a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php index 4746c6c7..8f9e2e60 100644 --- a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php +++ b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php @@ -51,7 +51,7 @@ public function players(Server $server): JsonResponse /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: ?array} */ private function runQuery(Server $server): ?array { - if (!$server->allocation || $server->allocation->ip === '0.0.0.0' || $server->allocation->ip === '::') { + if (!GameQuery::canRunQuery($server->allocation)) { abort(Response::HTTP_NOT_ACCEPTABLE, 'Server has invalid allocation'); } diff --git a/player-counter/src/Models/GameQuery.php b/player-counter/src/Models/GameQuery.php index 4c91b662..0fe9ab24 100644 --- a/player-counter/src/Models/GameQuery.php +++ b/player-counter/src/Models/GameQuery.php @@ -35,6 +35,10 @@ public function eggs(): BelongsToMany /** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: ?array} */ public function runQuery(Allocation $allocation): ?array { + if (!static::canRunQuery($allocation)) { + return null; + } + $ip = config('player-counter.use_alias') && is_ip($allocation->alias) ? $allocation->alias : $allocation->ip; $ip = is_ipv6($ip) ? '[' . $ip . ']' : $ip; @@ -43,4 +47,15 @@ public function runQuery(Allocation $allocation): ?array return $service->get($this->query_type)->process($ip, $allocation->port + ($this->query_port_offset ?? 0)); } + + public static function canRunQuery(?Allocation $allocation): bool + { + if (!$allocation) { + return false; + } + + $ip = config('player-counter.use_alias') && is_ip($allocation->alias) ? $allocation->alias : $allocation->ip; + + return !in_array($ip, ['0.0.0.0', '::']); + } } From 7f6a0d2e305c0ccc96ef041146b449a2589d4256 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 15:10:16 +0100 Subject: [PATCH 11/15] add new packages to workflow --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ddc6af4f..9b0ecf3c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -61,7 +61,7 @@ jobs: - name: Install plugin dependencies run: | cd pelican - composer require "stripe/stripe-php:^18.0" "kovah/laravel-socialite-oidc:^0.6" "socialiteproviders/pocketid:^5.0" + composer require "stripe/stripe-php:^18.0" "kovah/laravel-socialite-oidc:^0.6" "socialiteproviders/pocketid:^5.0" "xpaw/php-minecraft-query:^5.0.0" "xpaw/php-source-query-class:^5.0.0" - name: Setup .env file run: cp pelican/.env.example pelican/.env From a0fd2f3a05bf06f857c96b5b2bbbb79ce57fb1d1 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 15:11:57 +0100 Subject: [PATCH 12/15] fix phpstan --- player-counter/src/Models/GameQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player-counter/src/Models/GameQuery.php b/player-counter/src/Models/GameQuery.php index 0fe9ab24..6c7b523d 100644 --- a/player-counter/src/Models/GameQuery.php +++ b/player-counter/src/Models/GameQuery.php @@ -43,7 +43,7 @@ public function runQuery(Allocation $allocation): ?array $ip = is_ipv6($ip) ? '[' . $ip . ']' : $ip; /** @var QueryTypeService $service */ - $service = app(QueryTypeService::class); // @phpstan-ignore myCustomRules.forbiddenGlobalFunctions + $service = app(QueryTypeService::class); return $service->get($this->query_type)->process($ip, $allocation->port + ($this->query_port_offset ?? 0)); } From 8575cc72f4b44598cb1e8d2096df61c3f47e0d4c Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 15:30:43 +0100 Subject: [PATCH 13/15] better array checks --- .../src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php | 2 +- .../Query/Schemas/MinecraftBedrockQueryTypeSchema.php | 4 ++-- .../Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php | 2 +- player-counter/src/Models/GameQuery.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php index 6b737210..a0434a46 100644 --- a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php @@ -32,7 +32,7 @@ public function process(string $ip, int $port): ?array $info = $http->get('dynamic.json')->json(); $players = $http->get('players.json')->json(); - if (!$info || !$players) { + if (!is_array($info) || !is_array($players)) { return null; } diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php index c73e098e..6afe0560 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php @@ -28,7 +28,7 @@ public function process(string $ip, int $port): ?array $info = $query->GetInfo(); - if (!$info) { + if (!is_array($info)) { return null; } @@ -37,7 +37,7 @@ public function process(string $ip, int $port): ?array 'map' => $info['Map'], 'current_players' => $info['Players'], 'max_players' => $info['MaxPlayers'], - 'players' => null, + 'players' => null, // Bedrock has no player list ]; } catch (Exception $exception) { report($exception); diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index 5c33509b..8423635e 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -65,7 +65,7 @@ protected function tryQuery(string $ip, int $port): false|array $info = $query->GetInfo(); $players = $query->GetPlayers(); - if (!$info || !$players) { + if (!is_array($info) || !is_array($players)) { return false; } diff --git a/player-counter/src/Models/GameQuery.php b/player-counter/src/Models/GameQuery.php index 6c7b523d..29a880c8 100644 --- a/player-counter/src/Models/GameQuery.php +++ b/player-counter/src/Models/GameQuery.php @@ -45,7 +45,7 @@ public function runQuery(Allocation $allocation): ?array /** @var QueryTypeService $service */ $service = app(QueryTypeService::class); - return $service->get($this->query_type)->process($ip, $allocation->port + ($this->query_port_offset ?? 0)); + return $service->get($this->query_type)?->process($ip, $allocation->port + ($this->query_port_offset ?? 0)); } public static function canRunQuery(?Allocation $allocation): bool From a60890db787d0c40771a98fb23ab8e74ff837ec9 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 15:40:20 +0100 Subject: [PATCH 14/15] use timeout instead of connectTimeout --- .../src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php index a0434a46..ac4226c0 100644 --- a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php @@ -25,7 +25,7 @@ public function process(string $ip, int $port): ?array $this->resolveSRV($ip, $port); $http = Http::acceptJson() - ->connectTimeout(5) + ->timeout(5) ->throw() ->baseUrl("http://$ip:$port/"); From fd17f300db7686e778a9e924394a30746c06ddf1 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 27 Jan 2026 15:53:40 +0100 Subject: [PATCH 15/15] better null checks --- player-counter/src/Filament/Server/Pages/PlayersPage.php | 4 ++-- .../Api/Client/Servers/PlayerCounterController.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/player-counter/src/Filament/Server/Pages/PlayersPage.php b/player-counter/src/Filament/Server/Pages/PlayersPage.php index 6edbd3b2..b63b8569 100644 --- a/player-counter/src/Filament/Server/Pages/PlayersPage.php +++ b/player-counter/src/Filament/Server/Pages/PlayersPage.php @@ -119,8 +119,8 @@ public function table(Table $table): Table if ($gameQuery) { $data = $gameQuery->runQuery($server->allocation); - if ($data && $data['players']) { - $players = $data['players']; + if ($data) { + $players = $data['players'] ?? []; } } diff --git a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php index 8f9e2e60..0eb174aa 100644 --- a/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php +++ b/player-counter/src/Http/Controllers/Api/Client/Servers/PlayerCounterController.php @@ -38,7 +38,7 @@ public function players(Server $server): JsonResponse { $data = $this->runQuery($server); - if (!$data['players']) { + if (is_null($data['players'])) { abort(Response::HTTP_NOT_ACCEPTABLE, 'Server query has no player list'); }