Pin individual mods, resourcepacks, shaderpacks or datapacks from
Modrinth or CurseForge. Combine with modpacks.md for
the full pack flow.
.with_mod()
.with_modrinth_mods(vec![
("sodium", None), // mod
("sodium-extra", None), // resourcepack — routed automatically
("complementary-reimagined", None) // shader — routed automatically
])
.with_curseforge_mods(vec![
(238222, None), // JEI (mod)
(393402, None), // Pixel Daydream (resourcepack) — routed automatically
])
.done()Both methods take a list of tuples. The second element is an optional
pin — None lets the resolver pick the latest release compatible with
the instance's (minecraft_version, loader). Required dependencies are
followed transitively and deduplicated by (source, project).
Mods.path is qualified by sub-folder and used verbatim by the
installer: runtime_dir.join(mods.path) with no extra prefix. The
provider clients compute the sub-folder at fetch time so each asset
lands in its idiomatic place under the instance.
path emitted by the client |
Final location |
|---|---|
mods/sodium-fabric-0.5.8.jar |
<runtime>/mods/sodium-fabric-0.5.8.jar |
resourcepacks/sodium-extra.zip |
<runtime>/resourcepacks/sodium-extra.zip |
shaderpacks/iris.zip |
<runtime>/shaderpacks/iris.zip |
datapacks/foo.zip |
<runtime>/datapacks/foo.zip |
Before each fetch, the client does a GET /project/{slug} lookup to
read project_type, then maps:
Modrinth project_type |
Sub-folder |
|---|---|
mod |
mods |
resourcepack |
resourcepacks |
shader |
shaderpacks |
datapack |
datapacks (top-level — see warning below) |
| anything else | hard error QueryError::UnsupportedFormat |
The result is memoized in PROJECT_TYPE_CACHE (process-wide,
Cache<String, Arc<String>>, same TTL as the main MODRINTH_CACHE)
so the extra round-trip is paid once per project regardless of how many
versions are pulled.
Datapacks: Minecraft datapacks live in
<world>/datapacks/(per-world). The client routes them to a top-leveldatapacks/directory and emits atrace_warn!— proper per-world install requires manual handling and is only safe through a modpack's overrides targeting a specific world.
Same pattern via GET /mods/{mod_id} reading classId:
CurseForge classId |
Sub-folder |
|---|---|
6 (mod) |
mods |
12 (resourcepack) |
resourcepacks |
6552 (shaderpack) |
shaderpacks |
| anything else | hard error QueryError::UnsupportedFormat |
Cached in CLASS_ID_CACHE (Cache<u32, Arc<u32>>). The helper
lighty_modsloader::curseforge::client::install_subdir_for(mod_id, ttl)
is exported for use by the modpack pipeline.
CurseForge has no datapack class — datapacks distributed there are
either bundled into a mod or shipped via World (classId 17), which is
out of scope.
- Open
https://modrinth.com/mod/<slug>/versions. - Click the target version.
- The URL becomes
.../mod/<slug>/version/<version_id>. - The trailing segment is
version_id— an opaque string ("PpRTuoEh"), not the human-readable version"0.5.8".
.with_modrinth_mods(vec![
("sodium", Some("PpRTuoEh".into())), // pinned
("lithium", None), // latest compatible
])API alternative (read-only): GET https://api.modrinth.com/v2/project/<slug>/version
returns the list of versions with their id.
mod_idis the "Project ID" shown in the About sidebar ofhttps://www.curseforge.com/minecraft/mc-mods/<slug>. E.g. JEI =238222.file_idis the trailing URL segment after clicking a file under the Files tab:https://www.curseforge.com/minecraft/mc-mods/<slug>/files/<file_id>.
.with_curseforge_mods(vec![
(238222, Some(5234567)), // JEI pinned
(238222, None), // JEI latest
])API alternative: GET /v1/mods/<mod_id>/files (requires x-api-key) returns the file list.
CurseForge requires an API key. Set it once before any launch:
lighty_launcher::mods::curseforge::set_api_key(
std::env::var("CURSEFORGE_API_KEY")?
);(Implemented in crates/modsloader/src/curseforge/api.rs, re-exported
from the curseforge module.) Get a key at
https://console.curseforge.com/?#/api-keys.
lighty_modsloader::resolver::resolve is a BFS over the user request
list:
- Pop a request from the queue.
- Skip if its
ModKey(source + project id, version-agnostic) is already invisited. - Fetch the pivot
Modsentry via the appropriate API client (modrinth::fetchorcurseforge::fetch). The sub-folder lookup happens here. - Enqueue every
requireddependency the response declared. - Repeat until the queue is empty.
Output: Vec<Mods> ready for the standard mods installer.
- Modrinth runs against the public Labrinth API
(
https://api.modrinth.com/v2). No key required; uses a customUser-Agentper Modrinth's guidance (defined inmodrinth/api.rs). - CurseForge runs against the Core API
(
https://api.curseforge.com/v1) withx-api-key. Some projects disable third-party distribution — those returndownload_url: null, surfaced asQueryError::ModDistributionForbidden.
| Loader | Modrinth | CurseForge |
|---|---|---|
| Fabric | ✓ | ✓ |
| Forge | ✓ | ✓ |
| NeoForge | ✓ | ✓ |
| Quilt | ✓ | ✓ |
| Vanilla / OptiFine / LightyUpdater | rejected (UnsupportedLoader) |
rejected |
| Variant | Meaning |
|---|---|
QueryError::ModNotFound { provider, id } |
Project / version not found by ID |
QueryError::ModIncompatible { provider, id, mc, loader } |
No release compatible with (mc, loader) |
QueryError::ModDistributionForbidden { id } |
CurseForge download_url is null |
QueryError::UnsupportedFormat { what, expected, found } |
project_type / classId not in the routing table |
QueryError::UnsupportedLoader(...) |
Vanilla / OptiFine / LightyUpdater used with a mod source |
QueryError::Network(...) |
Underlying HTTP failure |
All flow through lighty_core::QueryError (shared with loader-side errors).