# Mods

Pin individual mods, resourcepacks, shaderpacks or datapacks from Modrinth or CurseForge. Combine with [`modpacks.md`](/lightylauncher/crates/modsloader/docs/modpacks.md) for the full pack flow.

## API surface

```rust
.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)`.

## Asset routing — where the file lands

`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`              |

### Modrinth — `project_type` → sub-folder

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-level `datapacks/` directory and emits a `trace_warn!` — proper per-world install requires manual handling and is only safe through a modpack's overrides targeting a specific world.

### CurseForge — `classId` → sub-folder

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.

## Pinning a specific version

### Modrinth — `version_id`

1. Open `https://modrinth.com/mod/<slug>/versions`.
2. Click the target version.
3. The URL becomes `.../mod/<slug>/version/<version_id>`.
4. The trailing segment is `version_id` — an **opaque string** (`"PpRTuoEh"`), **not** the human-readable version `"0.5.8"`.

```rust
.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`.

### CurseForge — `mod_id` + `file_id`

* **`mod_id`** is the "Project ID" shown in the About sidebar of `https://www.curseforge.com/minecraft/mc-mods/<slug>`. E.g. JEI = `238222`.
* **`file_id`** is the trailing URL segment after clicking a file under the Files tab: `https://www.curseforge.com/minecraft/mc-mods/<slug>/files/<file_id>`.

```rust
.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.

### Setting the CurseForge API key

CurseForge requires an API key. Set it once before any launch:

```rust
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>.

## How resolution works

`lighty_modsloader::resolver::resolve` is a BFS over the user request list:

1. Pop a request from the queue.
2. Skip if its `ModKey` (source + project id, version-agnostic) is already in `visited`.
3. Fetch the pivot `Mods` entry via the appropriate API client (`modrinth::fetch` or `curseforge::fetch`). The sub-folder lookup happens here.
4. Enqueue every `required` dependency the response declared.
5. Repeat until the queue is empty.

Output: `Vec<Mods>` ready for the standard mods installer.

### Provider notes

* **Modrinth** runs against the public Labrinth API (`https://api.modrinth.com/v2`). No key required; uses a custom `User-Agent` per Modrinth's guidance (defined in `modrinth/api.rs`).
* **CurseForge** runs against the Core API (`https://api.curseforge.com/v1`) with `x-api-key`. Some projects disable third-party distribution — those return `download_url: null`, surfaced as `QueryError::ModDistributionForbidden`.

### Loader compatibility

| Loader                             | Modrinth                       | CurseForge |
| ---------------------------------- | ------------------------------ | ---------- |
| Fabric                             | ✓                              | ✓          |
| Forge                              | ✓                              | ✓          |
| NeoForge                           | ✓                              | ✓          |
| Quilt                              | ✓                              | ✓          |
| Vanilla / OptiFine / LightyUpdater | rejected (`UnsupportedLoader`) | rejected   |

## Errors you might see

| 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).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://hamadi.gitbook.io/lightylauncher/crates/modsloader/docs/mods.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
