diff --git a/minecraft-modrinth/config/minecraft-modrinth.php b/minecraft-modrinth/config/minecraft-modrinth.php index 0b67a5fe..bb74b3b5 100644 --- a/minecraft-modrinth/config/minecraft-modrinth.php +++ b/minecraft-modrinth/config/minecraft-modrinth.php @@ -1,3 +1,5 @@ (bool) env('MINECRAFT_MODRINTH_ALWAYS_USE_LATEST_VERSION', false), +]; diff --git a/minecraft-modrinth/lang/cs/strings.php b/minecraft-modrinth/lang/cs/strings.php new file mode 100644 index 00000000..03d904b1 --- /dev/null +++ b/minecraft-modrinth/lang/cs/strings.php @@ -0,0 +1,74 @@ + 'Modrinth', + 'minecraft_mods' => 'Minecraft Módy', + 'minecraft_plugins' => 'Minecraft Pluginy', + + 'settings' => [ + 'always_use_latest_version' => 'Vždy použít nejnovější verzi', + 'always_use_latest_version_hint' => 'Přeskočí kontrolu kompatibility s verzí Minecraftu při hledání a instalaci modů/pluginů a vždy použije nejnovější dostupnou verzi pro detekovaný loader. Užitečné pro aktualizaci modů/pluginů ještě před povýšením serveru na novější verzi Minecraftu, protože se spoléhá na jejich obvyklou zpětnou kompatibilitu. Kompatibilita loaderu (Paper/Spigot/Fabric/...) se pořád kontroluje.', + 'settings_saved' => 'Nastavení uloženo', + ], + + 'page' => [ + 'open_folder' => 'Otevřít složku :folder', + 'minecraft_version' => 'Verze Minecraftu', + 'loader' => 'Loader', + 'installed' => 'Nainstalováno :type', + 'unknown' => 'Neznámé', + 'view_all' => 'Vše', + 'view_installed' => 'Nainstalované', + 'mod_unavailable' => 'Tento mod/plugin už není na Modrinthu dostupný', + ], + + 'table' => [ + 'columns' => [ + 'title' => 'Název', + 'author' => 'Autor', + 'downloads' => 'Stažení', + 'date_modified' => 'Upraveno', + ], + ], + + 'version' => [ + 'type' => 'Typ', + 'downloads' => 'Stažení', + 'published' => 'Publikováno', + 'changelog' => 'Seznam změn', + 'no_file_found' => 'Soubor nenalezen', + ], + + 'actions' => [ + 'install_latest' => 'Nainstalovat nejnovější verzi', + 'install' => 'Nainstalovat', + 'installed' => 'Nainstalováno', + 'update' => 'Aktualizovat', + 'uninstall' => 'Odinstalovat', + 'versions' => 'Výběr verze', + ], + + 'modals' => [ + 'update_heading' => 'Aktualizovat mod/plugin', + 'update_description' => 'Tímto se verze :old_version nahradí verzí :new_version. Starý soubor bude smazán.', + 'uninstall_heading' => 'Odinstalovat mod/plugin', + 'uninstall_description' => 'Opravdu chcete odinstalovat :name? Tímto se soubor trvale smaže z vašeho serveru.', + ], + + 'notifications' => [ + 'install_success' => 'Instalace dokončena', + 'install_success_body' => 'Úspěšně nainstalováno :name verze :version', + 'install_failed' => 'Instalace se nezdařila', + 'install_failed_body' => 'Při instalaci došlo k chybě. Zkuste to prosím znovu nebo kontaktujte podporu, pokud problém přetrvává.', + 'update_success' => 'Aktualizace dokončena', + 'update_success_body' => 'Úspěšně aktualizováno na verzi :version', + 'update_failed' => 'Aktualizace se nezdařila', + 'update_failed_body' => 'Při aktualizaci došlo k chybě. Zkuste to prosím znovu nebo kontaktujte podporu, pokud problém přetrvává.', + 'uninstall_success' => 'Odinstalace dokončena', + 'uninstall_success_body' => 'Úspěšně odinstalováno :name', + 'uninstall_partial' => 'Odinstalace neúplná', + 'uninstall_partial_body' => 'Soubor :name byl smazán, ale nepodařilo se ho odebrat ze seznamu nainstalovaných. Stále se může zobrazovat jako nainstalovaný.', + 'uninstall_failed' => 'Odinstalace se nezdařila', + 'uninstall_failed_body' => 'Při odinstalaci došlo k chybě. Zkuste to prosím znovu nebo kontaktujte podporu, pokud problém přetrvává.', + ], +]; diff --git a/minecraft-modrinth/lang/de/strings.php b/minecraft-modrinth/lang/de/strings.php index 83c42741..9490ce5d 100644 --- a/minecraft-modrinth/lang/de/strings.php +++ b/minecraft-modrinth/lang/de/strings.php @@ -6,7 +6,8 @@ 'minecraft_plugins' => 'Minecraft Plugins', 'settings' => [ - 'latest_minecraft_version' => 'Neueste Minecraft-Version', + 'always_use_latest_version' => 'Immer neueste Version verwenden', + 'always_use_latest_version_hint' => 'Überspringt die Prüfung der Minecraft-Versionskompatibilität bei der Suche und Installation von Mods/Plugins und verwendet immer die neueste verfügbare Version für den erkannten Loader. Nützlich, um Mods/Plugins schon vor einem Upgrade des Servers auf eine neuere Minecraft-Version zu aktualisieren, basierend auf deren üblicher Abwärtskompatibilität. Die Loader-Kompatibilität (Paper/Spigot/Fabric/...) wird weiterhin geprüft.', 'settings_saved' => 'Einstellungen gespeichert', ], diff --git a/minecraft-modrinth/lang/en/strings.php b/minecraft-modrinth/lang/en/strings.php index c2d359c5..36165347 100644 --- a/minecraft-modrinth/lang/en/strings.php +++ b/minecraft-modrinth/lang/en/strings.php @@ -6,7 +6,8 @@ 'minecraft_plugins' => 'Minecraft Plugins', 'settings' => [ - 'latest_minecraft_version' => 'Latest Minecraft Version', + 'always_use_latest_version' => 'Always Use Latest Version', + 'always_use_latest_version_hint' => 'Skip the Minecraft version compatibility check when searching for and installing mods/plugins, always using the newest available version for the detected loader. Useful for updating mods/plugins ahead of upgrading the server to a newer Minecraft version, relying on their usual backwards compatibility. Loader compatibility (Paper/Spigot/Fabric/...) is still enforced.', 'settings_saved' => 'Settings saved', ], diff --git a/minecraft-modrinth/src/MinecraftModrinthPlugin.php b/minecraft-modrinth/src/MinecraftModrinthPlugin.php index 320e1b40..800b8fca 100644 --- a/minecraft-modrinth/src/MinecraftModrinthPlugin.php +++ b/minecraft-modrinth/src/MinecraftModrinthPlugin.php @@ -2,11 +2,14 @@ namespace Boy132\MinecraftModrinth; +use App\Contracts\Plugins\HasPluginSettings; use App\Traits\EnvironmentWriterTrait; use Filament\Contracts\Plugin; +use Filament\Forms\Components\Toggle; +use Filament\Notifications\Notification; use Filament\Panel; -class MinecraftModrinthPlugin implements Plugin +class MinecraftModrinthPlugin implements HasPluginSettings, Plugin { use EnvironmentWriterTrait; @@ -23,4 +26,33 @@ public function register(Panel $panel): void } public function boot(Panel $panel): void {} + + public function getSettingsFormData(): array + { + return config('minecraft-modrinth'); + } + + public function getSettingsForm(): array + { + return [ + Toggle::make('always_use_latest_version') + ->label(trans('minecraft-modrinth::strings.settings.always_use_latest_version')) + ->hintIcon('tabler-question-mark') + ->hintIconTooltip(trans('minecraft-modrinth::strings.settings.always_use_latest_version_hint')) + ->inline(false) + ->default(fn () => config('minecraft-modrinth.always_use_latest_version')), + ]; + } + + public function saveSettings(array $data): void + { + $this->writeToEnvironment([ + 'MINECRAFT_MODRINTH_ALWAYS_USE_LATEST_VERSION' => $data['always_use_latest_version'], + ]); + + Notification::make() + ->title(trans('minecraft-modrinth::strings.settings.settings_saved')) + ->success() + ->send(); + } } diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 7db2b7f5..cdcfe3d3 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -25,13 +25,7 @@ class MinecraftModrinthService public function getMinecraftVersion(Server $server): ?string { - $version = $server->variables()->where(fn ($builder) => $builder->where('env_variable', 'MINECRAFT_VERSION')->orWhere('env_variable', 'MC_VERSION'))->first()?->server_value; - - if (!$version || $version === 'latest') { - return $this->getLatestMinecraftVersion(); - } - - return $version; + return $this->getConfiguredMinecraftVersion($server) ?? $this->getLatestMinecraftVersion(); } public function getLatestMinecraftVersion(): ?string @@ -55,6 +49,71 @@ public function getLatestMinecraftVersion(): ?string }); } + /** + * The most recent Modrinth release tags, newest first. Used instead of a single exact + * "latest" version when a server has no explicit Minecraft version configured: plugin + * authors often lag behind re-tagging support for the very newest release even though + * nothing in the plugin actually changed, so a single exact match hides plugins that + * work fine on it. This has no effect once a server sets an explicit version, since that + * is treated as an exact requirement instead. + * + * @return string[] + */ + protected function getRecentMinecraftVersions(): array + { + return cache()->remember('modrinth:recent_minecraft_versions', now()->addHour(), function () { + try { + /** @var array $versions */ + $versions = Http::asJson() + ->timeout(5) + ->connectTimeout(5) + ->throw() + ->get('https://api.modrinth.com/v2/tag/game_version') + ->json(); + + return collect($versions) + ->filter(fn ($version) => $version['version_type'] === 'release') + ->take(5) + ->pluck('version') + ->all(); + } catch (Exception $exception) { + report($exception); + + return []; + } + }); + } + + protected function getConfiguredMinecraftVersion(Server $server): ?string + { + $version = $server->variables()->where(fn ($builder) => $builder->where('env_variable', 'MINECRAFT_VERSION')->orWhere('env_variable', 'MC_VERSION'))->first()?->server_value; + + return ($version && $version !== 'latest') ? $version : null; + } + + /** + * Minecraft versions to filter search/version-list results by. An explicit server + * version is an exact requirement (a single value), but a server without one falls + * back to a small window of the most recent releases rather than only the single + * newest, for the reason explained on getRecentMinecraftVersions(). + * + * @return string[] + */ + protected function getMinecraftVersionsForFiltering(Server $server): array + { + $configured = $this->getConfiguredMinecraftVersion($server); + if ($configured) { + return [$configured]; + } + + $recent = $this->getRecentMinecraftVersions(); + if (!empty($recent)) { + return $recent; + } + + return array_filter([$this->getLatestMinecraftVersion()]); + } + /** @return array{icon: string, name: string, supported_project_types: string[], display_name: string}|null */ public function getLoaderFromServer(Server $server): ?array { @@ -104,7 +163,75 @@ public function getLoaders(): array }); } - /** @return array{hits: array>, total_hits: int} */ + /** + * Loaders that can also run plugins/mods published only for an upstream loader they + * are backwards-compatible with, e.g. a Paper server can run plain Spigot/Bukkit plugins. + * Only one direction: a plugin published for the fork isn't guaranteed to run on the + * upstream loader, so the reverse mapping is intentionally not added. + * + * @return string[] + */ + protected function getCompatibleLoaders(string $loader): array + { + return match ($loader) { + 'spigot' => ['spigot', 'bukkit'], + 'paper' => ['paper', 'spigot', 'bukkit'], + 'purpur' => ['purpur', 'paper', 'spigot', 'bukkit'], + 'folia' => ['folia', 'paper', 'spigot', 'bukkit'], + 'waterfall' => ['waterfall', 'bungeecord'], + 'quilt' => ['quilt', 'fabric'], + default => [$loader], + }; + } + + /** + * Proxy loaders aren't tied to a specific Minecraft version the way a server is: they + * relay the protocol for whatever version the backend servers run, so a plugin's declared + * Minecraft game versions on Modrinth mostly just reflect whenever it was last published, + * not what it's actually compatible with. Filtering those by an exact game version hides + * older but still working proxy plugins, so the version filter is skipped for them. + */ + protected function isProxyLoader(string $loader): bool + { + return in_array($loader, ['velocity', 'bungeecord', 'waterfall'], true); + } + + /** + * Whether search/version-list results should be restricted to a Minecraft version at + * all. Always false for proxy loaders (see isProxyLoader()), and also false when the + * "Always Use Latest Version" plugin setting is on, for admins who'd rather always get + * the newest available mod/plugin version - e.g. to update everything ahead of upgrading + * the server itself to a newer Minecraft version - and accept the (usual) backwards + * compatibility risk themselves instead of waiting on Modrinth authors to re-tag support. + */ + protected function shouldFilterByMinecraftVersion(string $loader): bool + { + return !$this->isProxyLoader($loader) && !(bool) config('minecraft-modrinth.always_use_latest_version'); + } + + /** + * Cache keys below only vary by loader/version/project, so without this, flipping the + * "Always Use Latest Version" setting would keep serving whatever was cached under the + * same key before the flip for up to its TTL, since the setting itself isn't part of it. + */ + protected function versionFilterCacheSuffix(string $loader): string + { + return $this->shouldFilterByMinecraftVersion($loader) ? 'exact' : 'latest'; + } + + /** + * Modrinth's own "project_type" field (mod/plugin/resourcepack/...) is whatever the + * author picked when they first created the project; a Bukkit-family project can be + * stored as "mod" even though it only has paper/spigot/purpur versions and Modrinth's + * own site lists it under /plugin/ (it decides that split by loader, not this field). + * Filtering strictly by our own Mod/Plugin enum value against that field hid projects + * like https://modrinth.com/plugin/excellenteconomy this way. The loader/category facet + * already discriminates mod-loader projects (fabric/forge/...) from plugin-loader ones + * (paper/spigot/...), so project_type is only kept as a loose safety net excluding + * unrelated types like resourcepacks/shaders/datapacks, not as the mod/plugin split. + * + * @return array{hits: array>, total_hits: int} + */ public function getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null): array { $modrinthProjectType = $modrinthProjectType->value; @@ -120,13 +247,22 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject $minecraftVersion = $this->getMinecraftVersion($server); $minecraftLoader = $minecraftLoader['name']; + $loaderFacets = implode(',', array_map(fn ($loader) => "\"categories:$loader\"", $this->getCompatibleLoaders($minecraftLoader))); + + $facetGroups = ["[$loaderFacets]"]; + if ($this->shouldFilterByMinecraftVersion($minecraftLoader)) { + $versionFacets = implode(',', array_map(fn ($version) => "\"versions:$version\"", $this->getMinecraftVersionsForFiltering($server))); + $facetGroups[] = "[$versionFacets]"; + } + $facetGroups[] = '["project_type:mod","project_type:plugin"]'; + $data = [ 'offset' => ($page - 1) * 20, 'limit' => 20, - 'facets' => "[[\"categories:$minecraftLoader\"],[\"versions:$minecraftVersion\"],[\"project_type:{$modrinthProjectType}\"]]", + 'facets' => '['.implode(',', $facetGroups).']', ]; - $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:$page"; + $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:".$this->versionFilterCacheSuffix($minecraftLoader).":$page"; if ($search) { $data['query'] = $search; @@ -258,16 +394,24 @@ public function getInstalledModsFromModrinth(array $installedMods, int $page = 1 protected function getVersionsCacheKey(string $projectId, ?string $minecraftVersion, string $minecraftLoader): string { - return "modrinth_versions:$projectId:$minecraftVersion:$minecraftLoader"; + return "modrinth_versions:$projectId:$minecraftVersion:$minecraftLoader:".$this->versionFilterCacheSuffix($minecraftLoader); } - /** @return array{game_versions: string, loaders: string} */ - protected function getVersionsQuery(?string $minecraftVersion, string $minecraftLoader): array + /** @return array{game_versions?: string, loaders: string} */ + protected function getVersionsQuery(Server $server, string $minecraftLoader): array { - return [ - 'game_versions' => "[\"$minecraftVersion\"]", - 'loaders' => "[\"$minecraftLoader\"]", + $loaders = implode(',', array_map(fn ($loader) => "\"$loader\"", $this->getCompatibleLoaders($minecraftLoader))); + + $query = [ + 'loaders' => "[$loaders]", ]; + + if ($this->shouldFilterByMinecraftVersion($minecraftLoader)) { + $versions = implode(',', array_map(fn ($version) => "\"$version\"", $this->getMinecraftVersionsForFiltering($server))); + $query['game_versions'] = "[$versions]"; + } + + return $query; } /** @param array $versions */ @@ -311,7 +455,7 @@ public function getProjectVersions(string $projectId, Server $server): array ->timeout(5) ->connectTimeout(5) ->throw() - ->get("https://api.modrinth.com/v2/project/$projectId/version", $this->getVersionsQuery($minecraftVersion, $minecraftLoader)) + ->get("https://api.modrinth.com/v2/project/$projectId/version", $this->getVersionsQuery($server, $minecraftLoader)) ->json(); } catch (Exception $exception) { report($exception); @@ -350,7 +494,7 @@ public function getProjectVersionsBulk(array $projectIds, Server $server): array $minecraftVersion = $this->getMinecraftVersion($server); $minecraftLoader = $minecraftLoader['name']; - $query = $this->getVersionsQuery($minecraftVersion, $minecraftLoader); + $query = $this->getVersionsQuery($server, $minecraftLoader); $results = []; $missing = []; diff --git a/player-counter/README.md b/player-counter/README.md index 65298820..21f16435 100644 --- a/player-counter/README.md +++ b/player-counter/README.md @@ -12,6 +12,14 @@ 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 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. 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 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 +36,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/database/Seeders/PlayerCounterSeeder.php b/player-counter/database/Seeders/PlayerCounterSeeder.php index 8d2055d4..96e58e60 100644 --- a/player-counter/database/Seeders/PlayerCounterSeeder.php +++ b/player-counter/database/Seeders/PlayerCounterSeeder.php @@ -83,6 +83,31 @@ class PlayerCounterSeeder extends Seeder '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', + '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' => 'minecraft', 'query_type' => 'minecraft_java', @@ -102,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) { } } diff --git a/player-counter/lang/cs/query.php b/player-counter/lang/cs/query.php new file mode 100644 index 00000000..28b47f61 --- /dev/null +++ b/player-counter/lang/cs/query.php @@ -0,0 +1,57 @@ + 'Žádné herní dotazy', + 'query' => 'Herní dotaz|Herní dotazy', + 'type' => 'Typ', + 'port_offset' => 'Offset portu dotazu', + 'no_offset' => 'Bez offsetu', + 'port_offset_hint' => 'Offset, který bude přičten k portu alokace. Obvykle prázdné/0 nebo 1.', + 'port_variable' => 'Proměnná portu dotazu', + 'no_variable' => 'Bez proměnné', + 'port_variable_hint' => 'Název proměnné prostředí spouštěcí proměnné, která se použije k získání portu dotazu, např. "QUERY_PORT". Pokud je hodnota nastavena, offset portu dotazu bude ignorován! Ponechte prázdné pro použití portu alokace a offsetu.', + 'eggs' => 'Vejce', + 'no_eggs' => 'Žádné vejce', + 'hostname' => 'Název serveru', + 'players' => 'Hráči', + 'map' => 'Mapa', + 'unknown' => 'Neznámé', + + 'kick' => 'Kicknout', + 'ban' => 'Zabanovat', + + 'whitelisted' => 'Na whitelistu', + 'add_to_whitelist' => 'Přidat na whitelist', + 'remove_from_whitelist' => 'Odebrat z whitelistu', + + 'op' => 'OP', + 'add_to_ops' => 'Udělit OP', + 'remove_from_ops' => 'Odebrat OP', + + 'use_alias' => 'Použít alias alokace?', + 'use_alias_hint' => 'Pokud je zaškrtnuto, dotazy budou místo IP adresy používat alias alokace', + + 'table' => [ + 'no_players' => 'Nenalezeni žádní hráči', + 'no_players_description' => 'Buď na serveru nikdo není online, nebo je na něm vypnutý query', + 'server_offline' => 'Server je offline', + ], + + 'notifications' => [ + 'settings_saved' => 'Nastavení uloženo', + + 'player_kicked' => 'Hráč byl vyhozen ze serveru', + 'player_kick_failed' => 'Hráče se nepodařilo vyhodit', + + 'player_banned' => 'Hráč byl na serveru zabanován', + 'player_ban_failed' => 'Hráče se nepodařilo zabanovat', + + 'player_whitelist_add' => 'Hráč přidán na whitelist', + 'player_whitelist_remove' => 'Hráč odebrán z whitelistu', + 'player_whitelist_failed' => 'Whitelist se nepodařilo změnit', + + 'player_op' => 'Hráči byl udělen OP', + 'player_deop' => 'Hráči byl odebrán OP', + 'player_op_failed' => 'OP se nepodařilo změnit', + ], +]; diff --git a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php index 63828d52..d2f2c5fd 100644 --- a/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/CitizenFXQueryTypeSchema.php @@ -44,8 +44,9 @@ public function process(Server $server, string $ip, int $port): ?array 'max_players' => $info['sv_maxclients'], 'players' => array_map(fn ($player) => ['id' => (string) $player['id'], 'name' => (string) $player['name']], $players), ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. } return null; diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php index 946dcdf5..43d7226c 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftBedrockQueryTypeSchema.php @@ -40,8 +40,9 @@ public function process(Server $server, string $ip, int $port): ?array 'max_players' => $info['MaxPlayers'], 'players' => null, // Bedrock has no player list ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. } return null; diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php index ea4d6857..a353915f 100644 --- a/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftJavaQueryTypeSchema.php @@ -58,8 +58,10 @@ protected function tryQuery(string $ip, int $port): false|array 'max_players' => $info['MaxPlayers'], 'players' => array_map(fn ($player) => ['id' => (string) $player, 'name' => (string) $player], $players), ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. It falls back to + // tryPing() below, and the UI already reflects an unreachable server on its own. } return false; @@ -84,8 +86,8 @@ protected function tryPing(string $ip, int $port): false|array 'max_players' => $data['players']['max'], 'players' => $data['players']['sample'] ?? [], ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported, see tryQuery() above - same reasoning applies to the ping fallback. } finally { if (isset($ping)) { $ping->Close(); diff --git a/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php new file mode 100644 index 00000000..1ee00423 --- /dev/null +++ b/player-counter/src/Extensions/Query/Schemas/MinecraftProxyQueryTypeSchema.php @@ -0,0 +1,29 @@ +} */ + public function process(Server $server, string $ip, int $port): ?array + { + $ping = $this->tryPing($ip, $port); + if ($ping) { + return $ping; + } + + return null; + } +} diff --git a/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php index 97105f84..b55b8403 100644 --- a/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/PalworldQueryTypeSchema.php @@ -50,9 +50,9 @@ public function process(Server $server, string $ip, int $port): ?array 'players' => array_map(fn ($player) => ['id' => (string) ($player['playerId'] ?? $player['userId']), 'name' => (string) $player['name']], $players), ]; - } catch (Exception $exception) { - report($exception); - + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. return null; } } diff --git a/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php b/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php index 5d4902fd..8d95586c 100644 --- a/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php +++ b/player-counter/src/Extensions/Query/Schemas/SourceQueryTypeSchema.php @@ -43,8 +43,9 @@ protected function run(string $ip, int $port, int $engine): ?array 'max_players' => $info['MaxPlayers'], 'players' => array_map(fn ($player) => ['id' => (string) $player['Id'], 'name' => (string) $player['Name']], $players), ]; - } catch (Exception $exception) { - report($exception); + } catch (Exception) { + // Not reported: a failed query almost always just means the server is offline, + // starting or otherwise unreachable, not an application error. } finally { $query->Disconnect(); } diff --git a/player-counter/src/Filament/Server/Pages/PlayersPage.php b/player-counter/src/Filament/Server/Pages/PlayersPage.php index 53ebadbc..83c9290e 100644 --- a/player-counter/src/Filament/Server/Pages/PlayersPage.php +++ b/player-counter/src/Filament/Server/Pages/PlayersPage.php @@ -44,6 +44,8 @@ class PlayersPage extends Page implements HasTable public bool $isMinecraft = false; + public bool $isProxy = false; + /** @var array */ 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), @@ -193,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') @@ -222,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') 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());