diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 71bf5c8d..9b0ecf3c 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" "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 @@ -76,4 +76,3 @@ jobs: run: | cd pelican vendor/bin/phpstan analyse --memory-limit=-1 --error-format=github - 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 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..439adb7f --- /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..ac4226c0 --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php @@ -0,0 +1,73 @@ +} */ + public function process(string $ip, int $port): ?array + { + try { + $this->resolveSRV($ip, $port); + + $http = Http::acceptJson() + ->timeout(5) + ->throw() + ->baseUrl("http://$ip:$port/"); + + $info = $http->get('dynamic.json')->json(); + $players = $http->get('players.json')->json(); + + if (!is_array($info) || !is_array($players)) { + return null; + } + + 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..6afe0560 --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php @@ -0,0 +1,48 @@ +} */ + public function process(string $ip, int $port): ?array + { + $query = new MinecraftQuery(); + + try { + $query->ConnectBedrock($ip, $port, 5, true); + + $info = $query->GetInfo(); + + if (!is_array($info)) { + return null; + } + + return [ + 'hostname' => $info['HostName'], + 'map' => $info['Map'], + 'current_players' => $info['Players'], + 'max_players' => $info['MaxPlayers'], + 'players' => null, // Bedrock has no player list + ]; + } 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..8423635e --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -0,0 +1,85 @@ +} */ + 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) { + return null; + } + + 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'], + '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(); + + if (!is_array($info) || !is_array($players)) { + return false; + } + + return [ + 'hostname' => $info['HostName'], + 'map' => $info['Map'], + 'current_players' => $info['Players'], + 'max_players' => $info['MaxPlayers'], + 'players' => array_map(fn ($player) => ['id' => (string) $player, 'name' => (string) $player], $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..b63b8569 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; } @@ -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 = []; @@ -118,11 +118,14 @@ public function table(Table $table): Table if ($gameQuery) { $data = $gameQuery->runQuery($server->allocation); - $players = $data['players']; + + if ($data) { + $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); @@ -131,15 +134,15 @@ public function table(Table $table): Table ->contentGrid([ 'default' => 1, 'lg' => 2, - 'xl' => 3, + 'xl' => $isMinecraft ? 2 : 3, ]) ->columns([ 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 +150,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 +173,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 +194,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 +225,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..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; } @@ -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..0eb174aa 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')); } /** @@ -43,16 +38,20 @@ public function players(Server $server): JsonResponse { $data = $this->runQuery($server); + if (is_null($data['players'])) { + abort(Response::HTTP_NOT_ACCEPTABLE, 'Server query has no player list'); + } + /** @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 === '::') { + 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 d79c7723..29a880c8 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,35 @@ 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 { + 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; - $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, - ]; - } + /** @var QueryTypeService $service */ + $service = app(QueryTypeService::class); - $gameQ = new GameQ(); - - $gameQ->addServer($data); - - $gameQ->setOption('debug', config('app.debug')); + return $service->get($this->query_type)?->process($ip, $allocation->port + ($this->query_port_offset ?? 0)); + } - return $gameQ->process()[$host] ?? []; - } catch (Exception $exception) { - report($exception); + public static function canRunQuery(?Allocation $allocation): bool + { + if (!$allocation) { + return false; } - return []; + $ip = config('player-counter.use_alias') && is_ip($allocation->alias) ? $allocation->alias : $allocation->ip; + + return !in_array($ip, ['0.0.0.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