Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
115d85d
player-counter: add Czech (cs) translation for query lang file
claude Sep 18, 2026
22616b1
player-counter: add Minecraft Proxy (Velocity/BungeeCord/Waterfall) q…
claude Sep 18, 2026
f420aaa
Merge pull request #1 from Martindob/i18n/czech-locale-player-counter
Martindob Sep 18, 2026
0bf02bb
Merge pull request #2 from Martindob/feature/player-counter-velocity-…
Martindob Sep 18, 2026
afb99f3
player-counter: fix Minecraft Proxy query crashing on legacy query at…
claude Sep 18, 2026
8ed61f3
player-counter: improve Czech translation wording for ban/eggs
claude Sep 18, 2026
201203a
player-counter: auto-assign Minecraft Proxy query type in seeder
claude Sep 18, 2026
3afae57
minecraft-modrinth: add Czech (cs) translation
claude Sep 18, 2026
94d1acb
minecraft-modrinth: fix search/version filters excluding compatible p…
claude Sep 18, 2026
cef3e4c
minecraft-modrinth: don't filter proxy plugins by Minecraft version
claude Sep 18, 2026
c8fd562
minecraft-modrinth: stop filtering search by our mod/plugin enum's pr…
claude Sep 18, 2026
c5b9577
minecraft-modrinth: widen version fallback so unretagged plugins stil…
claude Sep 18, 2026
5966d69
minecraft-modrinth: add "Always Use Latest Version" plugin setting
claude Sep 18, 2026
18c2bfa
minecraft-modrinth: fix search/version cache key ignoring the latest-…
claude Sep 18, 2026
9ff3fd9
player-counter: give proxy mappings precedence over generic minecraft…
claude Sep 18, 2026
863348b
player-counter: hide kick/ban actions for proxy queries
claude Sep 18, 2026
fcf079d
player-counter: stop logging failed queries as application errors
claude Sep 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion minecraft-modrinth/config/minecraft-modrinth.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<?php

return [];
return [
'always_use_latest_version' => (bool) env('MINECRAFT_MODRINTH_ALWAYS_USE_LATEST_VERSION', false),
];
74 changes: 74 additions & 0 deletions minecraft-modrinth/lang/cs/strings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

return [
'plugin_name' => '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á.',
],
];
3 changes: 2 additions & 1 deletion minecraft-modrinth/lang/de/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],

Expand Down
3 changes: 2 additions & 1 deletion minecraft-modrinth/lang/en/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],

Expand Down
34 changes: 33 additions & 1 deletion minecraft-modrinth/src/MinecraftModrinthPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
}
}
180 changes: 162 additions & 18 deletions minecraft-modrinth/src/Services/MinecraftModrinthService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<int, mixed> $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
{
Expand Down Expand Up @@ -104,7 +163,75 @@ public function getLoaders(): array
});
}

/** @return array{hits: array<int, array<string, mixed>>, 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<int, array<string, mixed>>, total_hits: int}
*/
public function getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null): array
{
$modrinthProjectType = $modrinthProjectType->value;
Expand All @@ -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;
Expand Down Expand Up @@ -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<int, mixed> $versions */
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 = [];
Expand Down
Loading
Loading