From f52a11d8570f220a73b9758ca33fddfe36161333 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:28:30 +0000 Subject: [PATCH 1/4] minecraft-modrinth: fix search/version filters excluding compatible plugins getProjects()/getProjectVersions()/getProjectVersionsBulk() filtered strictly by the single loader detected from the egg's tags (e.g. "categories:paper" or "loaders":["paper"]). Plugins on Modrinth that only declare the "spigot" or "bukkit" category (never re-tagged as "paper" even though they work fine there, since Paper is backwards compatible with the Spigot/Bukkit API) were silently excluded from search results and from the available-versions list, even though they are installable and run correctly. Add getCompatibleLoaders() to expand the detected loader into its upstream-compatible loaders (paper -> paper/spigot/bukkit, purpur -> purpur/paper/spigot/bukkit, folia -> folia/paper/spigot/bukkit, waterfall -> waterfall/bungeecord, quilt -> quilt/fabric) and OR them together in both the search facets and the version-list loader filter. The mapping is one-directional: a plugin published only for a fork isn't guaranteed to run on the upstream loader, so the reverse isn't added. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 7db2b7f5..ee81ca27 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -104,6 +104,27 @@ public function getLoaders(): array }); } + /** + * 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], + }; + } + /** @return array{hits: array>, total_hits: int} */ public function getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null): array { @@ -120,10 +141,12 @@ 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))); + $data = [ 'offset' => ($page - 1) * 20, 'limit' => 20, - 'facets' => "[[\"categories:$minecraftLoader\"],[\"versions:$minecraftVersion\"],[\"project_type:{$modrinthProjectType}\"]]", + 'facets' => "[[$loaderFacets],[\"versions:$minecraftVersion\"],[\"project_type:{$modrinthProjectType}\"]]", ]; $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:$page"; @@ -264,9 +287,11 @@ protected function getVersionsCacheKey(string $projectId, ?string $minecraftVers /** @return array{game_versions: string, loaders: string} */ protected function getVersionsQuery(?string $minecraftVersion, string $minecraftLoader): array { + $loaders = implode(',', array_map(fn ($loader) => "\"$loader\"", $this->getCompatibleLoaders($minecraftLoader))); + return [ 'game_versions' => "[\"$minecraftVersion\"]", - 'loaders' => "[\"$minecraftLoader\"]", + 'loaders' => "[$loaders]", ]; } From fb951b8ec6e7b6d1f9640d4b2b6e01ff6bbc4be2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:36:04 +0000 Subject: [PATCH 2/4] minecraft-modrinth: don't filter proxy plugins by Minecraft version For Velocity/BungeeCord/Waterfall, the plugin's declared Modrinth game versions mostly just reflect whenever it was last published, not what it actually supports: a proxy relays the protocol for whatever version the backend servers run and isn't itself tied to one Minecraft version. Filtering search results and version lists by an exact game version match was hiding older but still working proxy plugins. Skip the "versions" search facet and the game_versions query param for these loaders; the loader/category filter (already OR'd across compatible loaders) is what actually determines compatibility here. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index ee81ca27..548451e8 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -125,6 +125,18 @@ protected function getCompatibleLoaders(string $loader): array }; } + /** + * 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); + } + /** @return array{hits: array>, total_hits: int} */ public function getProjects(Server $server, ModrinthProjectType $modrinthProjectType, int $page = 1, ?string $search = null): array { @@ -143,10 +155,16 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject $loaderFacets = implode(',', array_map(fn ($loader) => "\"categories:$loader\"", $this->getCompatibleLoaders($minecraftLoader))); + $facetGroups = ["[$loaderFacets]"]; + if (!$this->isProxyLoader($minecraftLoader)) { + $facetGroups[] = "[\"versions:$minecraftVersion\"]"; + } + $facetGroups[] = "[\"project_type:{$modrinthProjectType}\"]"; + $data = [ 'offset' => ($page - 1) * 20, 'limit' => 20, - 'facets' => "[[$loaderFacets],[\"versions:$minecraftVersion\"],[\"project_type:{$modrinthProjectType}\"]]", + 'facets' => '['.implode(',', $facetGroups).']', ]; $key = "modrinth_projects:{$modrinthProjectType}:$minecraftVersion:$minecraftLoader:$page"; @@ -284,15 +302,20 @@ protected function getVersionsCacheKey(string $projectId, ?string $minecraftVers return "modrinth_versions:$projectId:$minecraftVersion:$minecraftLoader"; } - /** @return array{game_versions: string, loaders: string} */ + /** @return array{game_versions?: string, loaders: string} */ protected function getVersionsQuery(?string $minecraftVersion, string $minecraftLoader): array { $loaders = implode(',', array_map(fn ($loader) => "\"$loader\"", $this->getCompatibleLoaders($minecraftLoader))); - return [ - 'game_versions' => "[\"$minecraftVersion\"]", + $query = [ 'loaders' => "[$loaders]", ]; + + if (!$this->isProxyLoader($minecraftLoader)) { + $query['game_versions'] = "[\"$minecraftVersion\"]"; + } + + return $query; } /** @param array $versions */ From 452c47506781f07a54c1a3b6f1ace25390320ac1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:46:22 +0000 Subject: [PATCH 3/4] minecraft-modrinth: stop filtering search by our mod/plugin enum's project_type Modrinth's "project_type" field is whatever the author picked when the project was created, not what it's actually compatible with. A Bukkit-family project can be stored as project_type "mod" while only having paper/spigot/purpur versions, and Modrinth's own site still lists it under /plugin/ since it decides that split by loader, not this field (e.g. https://modrinth.com/plugin/excellenteconomy). Filtering search strictly by our own Mod/Plugin enum value against this field hid such projects entirely, even on an exact loader match. The loader/category facet already discriminates mod-loader projects (fabric/forge/...) from plugin-loader ones (paper/spigot/...), so project_type is now OR'd across both values, kept only as a loose safety net against unrelated types like resourcepacks/shaders/ datapacks rather than as the actual mod vs. plugin split. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 548451e8..9e2babf7 100644 --- a/minecraft-modrinth/src/Services/MinecraftModrinthService.php +++ b/minecraft-modrinth/src/Services/MinecraftModrinthService.php @@ -137,7 +137,19 @@ protected function isProxyLoader(string $loader): bool return in_array($loader, ['velocity', 'bungeecord', 'waterfall'], true); } - /** @return array{hits: array>, total_hits: int} */ + /** + * 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; @@ -159,7 +171,7 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject if (!$this->isProxyLoader($minecraftLoader)) { $facetGroups[] = "[\"versions:$minecraftVersion\"]"; } - $facetGroups[] = "[\"project_type:{$modrinthProjectType}\"]"; + $facetGroups[] = '["project_type:mod","project_type:plugin"]'; $data = [ 'offset' => ($page - 1) * 20, From 0f9285b5dd2dc654a39ff648408a6db80351a588 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:55:53 +0000 Subject: [PATCH 4/4] minecraft-modrinth: widen version fallback so unretagged plugins still show Verified against the live Modrinth API: the newest release tag right now is 26.3, but excellenteconomy (and similarly many other plugins) only declares support up to 26.1.2 - two releases behind, even though it's a plain Bukkit-API economy plugin with no reason to actually break on newer patches. Servers without an explicit MINECRAFT_VERSION/ MC_VERSION variable fell back to that single newest tag and filtered search/version-list results by an exact match against it, hiding any plugin whose author hasn't re-tagged support for it yet. Add getRecentMinecraftVersions() (the last 5 release tags) and use that as an OR'd window instead of the single newest tag whenever no explicit version is configured. An explicit server version is still treated as an exact requirement, since that's a real constraint rather than a guess. Confirmed against the live API that excellenteconomy now appears in search and has an installable version file with this window. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SA9aNschKoWLmWkWNC2fGZ --- .../src/Services/MinecraftModrinthService.php | 85 ++++++++++++++++--- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/minecraft-modrinth/src/Services/MinecraftModrinthService.php b/minecraft-modrinth/src/Services/MinecraftModrinthService.php index 9e2babf7..2810a150 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 { @@ -169,7 +228,8 @@ public function getProjects(Server $server, ModrinthProjectType $modrinthProject $facetGroups = ["[$loaderFacets]"]; if (!$this->isProxyLoader($minecraftLoader)) { - $facetGroups[] = "[\"versions:$minecraftVersion\"]"; + $versionFacets = implode(',', array_map(fn ($version) => "\"versions:$version\"", $this->getMinecraftVersionsForFiltering($server))); + $facetGroups[] = "[$versionFacets]"; } $facetGroups[] = '["project_type:mod","project_type:plugin"]'; @@ -315,7 +375,7 @@ protected function getVersionsCacheKey(string $projectId, ?string $minecraftVers } /** @return array{game_versions?: string, loaders: string} */ - protected function getVersionsQuery(?string $minecraftVersion, string $minecraftLoader): array + protected function getVersionsQuery(Server $server, string $minecraftLoader): array { $loaders = implode(',', array_map(fn ($loader) => "\"$loader\"", $this->getCompatibleLoaders($minecraftLoader))); @@ -324,7 +384,8 @@ protected function getVersionsQuery(?string $minecraftVersion, string $minecraft ]; if (!$this->isProxyLoader($minecraftLoader)) { - $query['game_versions'] = "[\"$minecraftVersion\"]"; + $versions = implode(',', array_map(fn ($version) => "\"$version\"", $this->getMinecraftVersionsForFiltering($server))); + $query['game_versions'] = "[$versions]"; } return $query; @@ -371,7 +432,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); @@ -410,7 +471,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 = [];