Skip to content
Merged
5 changes: 2 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
phpstan:
name: PHPStan
runs-on: ubuntu-latest

strategy:
fail-fast: true
matrix:
Expand Down Expand Up @@ -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
Expand All @@ -76,4 +76,3 @@ jobs:
run: |
cd pelican
vendor/bin/phpstan analyse --memory-limit=-1 --error-format=github

9 changes: 3 additions & 6 deletions player-counter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 79 additions & 24 deletions player-counter/database/Seeders/PlayerCounterSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
}
3 changes: 2 additions & 1 deletion player-counter/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
58 changes: 0 additions & 58 deletions player-counter/src/Enums/GameQueryType.php

This file was deleted.

13 changes: 13 additions & 0 deletions player-counter/src/Extensions/Query/QueryTypeSchemaInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Boy132\PlayerCounter\Extensions\Query;

interface QueryTypeSchemaInterface
{
public function getId(): string;

public function getName(): string;

/** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: ?array<array{id: string, name: string}>} */
public function process(string $ip, int $port): ?array;
}
29 changes: 29 additions & 0 deletions player-counter/src/Extensions/Query/QueryTypeService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Boy132\PlayerCounter\Extensions\Query;

class QueryTypeService
{
/** @var QueryTypeSchemaInterface[] */
private array $schemas = [];

public function get(string $id): ?QueryTypeSchemaInterface
{
return array_get($this->schemas, $id);
}

public function register(QueryTypeSchemaInterface $schema): void
{
if (array_key_exists($schema->getId(), $this->schemas)) {
return;
}

$this->schemas[$schema->getId()] = $schema;
}

/** @return array<string, string> */
public function getMappings(): array
{
return collect($this->schemas)->mapWithKeys(fn ($schema) => [$schema->getId() => $schema->getName()])->all();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Boy132\PlayerCounter\Extensions\Query\Schemas;

use Boy132\PlayerCounter\Extensions\Query\QueryTypeSchemaInterface;
use Exception;
use Illuminate\Support\Facades\Http;

class CitizenFXQueryTypeSchema implements QueryTypeSchemaInterface
{
public function getId(): string
{
return 'cfx';
}

public function getName(): string
{
return 'CitizenFX';
}

/** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array<array{id: string, name: string}>} */
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'];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Boy132\PlayerCounter\Extensions\Query\Schemas;

use xPaw\SourceQuery\SourceQuery;

class GoldSourceQueryTypeSchema extends SourceQueryTypeSchema
{
public function getId(): string
{
return 'goldsrc';
}

public function getName(): string
{
return 'GoldSrc';
}

/** @return ?array{hostname: string, map: string, current_players: int, max_players: int, players: array<array{id: string, name: string}>} */
public function process(string $ip, int $port): ?array
{
return $this->run($ip, $port, SourceQuery::GOLDSOURCE);
}
}
Loading