diff --git a/src/main/core/mods.ts b/src/main/core/mods.ts index 1645bcc..9e162c8 100644 --- a/src/main/core/mods.ts +++ b/src/main/core/mods.ts @@ -116,9 +116,15 @@ const MR_LOADER: Partial> = { export async function searchModrinth(id: string, query: string): Promise { const server = getServer(id) if (!server) throw new Error('server-not-found') - const loader = MR_LOADER[server.type] + // Filter to the loaders this server can actually run. A single inner array is + // OR'd by Modrinth, so a plugin server matches the whole Bukkit family (incl. + // plugins tagged only `spigot`/`bukkit`); disjoint taxonomies then keep mods + // and plugins from mixing. Plugins are indexed as project_type "mod" on + // Modrinth, so we deliberately do NOT filter by project_type (it would drop + // every plugin) — the loader facet is the correct separator. + const loaders = searchLoaders(server.type) const facets: string[][] = [] - if (loader) facets.push([`categories:${loader}`]) + if (loaders.length) facets.push(loaders.map((l) => `categories:${l}`)) if (server.mcVersion && server.mcVersion !== 'unknown') facets.push([`versions:${server.mcVersion}`]) const url = `${MR}/search?limit=20&index=relevance&query=${encodeURIComponent(query)}` + @@ -154,6 +160,26 @@ export function loadersFor(type: ServerType): string[] { return single ? [single] : [] } +/** Mod loaders a hybrid (mohist/arclight) can run alongside Bukkit plugins. */ +const HYBRID_MOD_LOADERS = ['forge'] + +/** + * Modrinth loaders to *search* for a server type. Unlike loadersFor (which drives + * the hash-based update check), this decides what browse results a user should + * see, so a hybrid unions the plugin family with its mod loaders — mohist should + * show both plugins and Forge mods. Because the loader taxonomies are disjoint + * (nothing is tagged both `paper` and `fabric`), filtering to these loaders is + * what keeps a Fabric server from listing plugins and vice-versa. Pure. + */ +export function searchLoaders(type: ServerType): string[] { + const isPlugin = PLUGIN_TYPES.includes(type) + const isModded = MODDED_TYPES.includes(type) + if (isPlugin && isModded) return [...new Set([...PLUGIN_LOADER_FAMILY, ...HYBRID_MOD_LOADERS])] + if (isPlugin) return PLUGIN_LOADER_FAMILY + const single = MR_LOADER[type] + return single ? [single] : [] +} + function fileSha1(path: string): string { return createHash('sha1').update(readFileSync(path)).digest('hex') } diff --git a/src/main/smoke.ts b/src/main/smoke.ts index 52c885d..fbe8845 100644 --- a/src/main/smoke.ts +++ b/src/main/smoke.ts @@ -576,6 +576,25 @@ export async function runModUpdateSmoke(): Promise { if (modsMod.loadersFor('unknown').length !== 0) return fail('unknown type should not filter by loader') console.log('MODUPDATE-SMOKE: loader family OK (plugin servers widen, modded/proxy stay single)') + // Browse-search loaders (#47): what the Modrinth tab filters by, so mods and + // plugins don't mix. Plugin servers = Bukkit family, modded = single loader, + // hybrid (mohist) = plugin family UNION its mod loaders (both must show), + // proxy = single, vanilla/unknown = unfiltered. + const sl = (t: Parameters[0]): string[] => modsMod.searchLoaders(t) + for (const l of ['paper', 'spigot', 'bukkit', 'purpur', 'folia']) { + if (!sl('paper').includes(l)) return fail(`paper browse should search ${l}`) + } + if (sl('fabric').join() !== 'fabric') return fail('fabric browse should search only fabric') + if (sl('forge').join() !== 'forge') return fail('forge browse should search only forge') + if (sl('velocity').join() !== 'velocity') return fail('velocity browse should search only velocity') + // hybrid must surface BOTH plugins and Forge mods + const moh = sl('mohist') + for (const l of ['paper', 'spigot', 'bukkit', 'forge']) { + if (!moh.includes(l)) return fail(`mohist (hybrid) browse should search ${l}, got ${moh.join()}`) + } + if (sl('vanilla').length !== 0) return fail('vanilla browse should not filter by loader') + console.log('MODUPDATE-SMOKE: browse-search loaders OK (plugin family, modded single, hybrid unions both)') + console.log('MODUPDATE-SMOKE: PASS') app.exit(0) } catch (e) {