88 lines
6.9 KiB
Markdown
88 lines
6.9 KiB
Markdown
# Spec G-patch - Patch tier (Final Loads)
|
|
|
|
**Date:** 2026-04-30
|
|
**Status:** Draft (awaiting review)
|
|
**Sibling specs:** A (multi-branch picker), B (collection expansion + live progress), C (build context), D (dep "Add" button), E (precacher), F (folded into B), G (cleanups bundle - this spec carves a piece out)
|
|
|
|
## 1. Summary
|
|
|
|
Add a **"patch" tier** to the load-order calculation: mods explicitly authored or detected as patches sort *after* every non-patch mod, including those flagged `loadLast=on`. Implementation is a single new axis at the top of `mlos_sort._initial_sort_key` plus a heuristic in `derive_category`. No schema migration. No new endpoint. No backwards-incompat changes for existing mods.
|
|
|
|
## 2. Problem
|
|
|
|
The PZ load-order convention (and the user-supplied 37-bucket taxonomy, bucket 37 "Final Loads") treats compatibility patches and retextures-of-other-mods as a strictly-last tier - they have to load *after* `loadLast=on` map mods, because they intercept or override the things those mods install. Today our sort key has no such tier:
|
|
|
|
```
|
|
PREORDER → loadFirst → loadLast → category → in-category loadFirst → in-category loadLast → alpha
|
|
```
|
|
|
|
A `loadLast=on` map mod ends up in the same bucket as a patch, ordered alphabetically. Patches that need to override the map mod can land *before* it. Silent corruption - output looks valid, the wrong mod wins at runtime.
|
|
|
|
## 3. Detection rules
|
|
|
|
A mod is a patch iff **any** of these is true:
|
|
|
|
1. **Explicit:** `mod.info` contains `category=patch` (new value added to `RAW_CATEGORY_ORDER`).
|
|
2. **Author-tagged via sorting_rules.txt:** user-supplied `[modId]\ncategory=patch` overrides anything else (existing mechanism, no change).
|
|
3. **Name heuristic (conservative):** `mod.name` matches the case-insensitive regex `\b(patch|compat|compatibility)\b`. Examples that match: `BetterFlashlight Patch`, `BB Compatibility`, `RavenCreek - MoreSimpleClothing Compat`. Examples that **do not** match: `BugFixes`, `LittleTweaks`, `BalanceFix` - "fix" / "tweak" / "fixes" are too broad and would over-flag.
|
|
|
|
The first matching rule wins. The heuristic is intentionally narrow; mod authors who want to opt in should use rule 1.
|
|
|
|
## 4. Sort behavior
|
|
|
|
Insert a new axis **at position 0** (above `PREORDER`) of the sort tuple:
|
|
|
|
```
|
|
(is_patch, PREORDER, loadFirst, loadLast, category, in-cat loadFirst, in-cat loadLast, alpha)
|
|
```
|
|
|
|
`is_patch = 1` for patches, `0` otherwise. Tuple comparison guarantees patches sort after every non-patch mod regardless of every downstream axis. Within the patch group, the existing sub-keys still apply (a patch with `PREORDER=2`, e.g. `ModManagerServer-Patch`, still sorts second-among-patches).
|
|
|
|
## 5. Backend changes
|
|
|
|
- **`mlos_sort.py`:**
|
|
- Append `"patch"` to `RAW_CATEGORY_ORDER` (so it's a valid `mod.category` value and topo sort treats it like any other category).
|
|
- Extend `derive_category(mod)` with the §3 name heuristic, returning `"patch"` when matched and category is otherwise `undefined`.
|
|
- Modify `_initial_sort_key`: prepend `1 if mod.category == "patch" else 0` as the new tuple element.
|
|
- **`adapters.py`:** extend `CAT_MAP` with `"patch": "patch"` so the frontend pill key is preserved (see §6).
|
|
- **`worker.py`:** no change. `mod.info` parsing already accepts arbitrary `category=…` values; once `"patch"` is in `CATEGORY_ORDER`, existing parser code passes it through unchanged.
|
|
- **No schema migration.** `mod_parsed.category` is already `TEXT NOT NULL DEFAULT 'undefined'` - `"patch"` fits without alteration.
|
|
|
|
## 6. Frontend changes
|
|
|
|
- **New pill** `patch` in the mod-table category column. Recommended palette: muted mauve / pale grey to distinguish from `gameplay` (the current default for tweaks-shaped mods) without competing for attention.
|
|
- **Pill is descriptive only** - sort position already telegraphs "this is a patch" since patches cluster at the bottom of the table. The pill is a quick visual confirmation, not a signal the user has to learn.
|
|
- **CSS** addition only (one rule): `.cat.patch { background: …; color: …; }`. No layout or component changes.
|
|
|
|
If the user prefers to skip the pill (5 buckets stays cleaner), the spec is satisfied without it; sort behavior is the load-bearing change.
|
|
|
|
## 7. Out of scope
|
|
|
|
- Detecting patches from Steam workshop tags (Steam's vocabulary has no canonical "Patch" tag - `Misc` and `Framework` are the closest, both too noisy to map).
|
|
- Multiple patch sub-tiers (e.g., "patches-of-patches"). YAGNI; the existing `loadAfter` mechanism handles ordering between two patches when needed.
|
|
- A `mod_parsed.is_patch` boolean column. Derived from `category` is sufficient and avoids a migration.
|
|
- Auto-detecting patches via mod content inspection (Lua module overrides, file collisions). Heuristics only.
|
|
|
|
## 8. Acceptance criteria
|
|
|
|
- [ ] `mlos_sort._initial_sort_key` returns an 8-element tuple with `is_patch` (0 or 1) at index 0.
|
|
- [ ] `RAW_CATEGORY_ORDER` includes `"patch"`.
|
|
- [ ] `derive_category` returns `"patch"` when the name regex `\b(patch|compat|compatibility)\b` (case-insensitive) matches and `mod.info`'s `category=` is unset or `undefined`.
|
|
- [ ] Explicit `category=patch` in `mod.info` is honored by the existing parser (no parser change required).
|
|
- [ ] `sorting_rules.txt` `category=patch` override forces a mod into the patch tier.
|
|
- [ ] In a sort with one `loadLast=on` map mod and one patch, the patch sorts *after* the map mod in `SORTED_ORDER`.
|
|
- [ ] In a sort with two patches, alphabetical ordering applies between them (existing alpha tiebreaker preserved).
|
|
- [ ] In a sort with no patches, `SORTED_ORDER` is bit-identical to pre-spec output (`is_patch=0` for all rows preserves existing total ordering).
|
|
- [ ] `MOD_DB` rows for patches carry `cat: "patch"` once `adapters.CAT_MAP` is extended.
|
|
|
|
## 9. Test cases
|
|
|
|
1. **Explicit patch via mod.info** - wsid X has `category=patch`. Expect: sorts last regardless of `loadLast`. `MOD_DB.cat = "patch"`.
|
|
2. **Heuristic match** - mod named `BB Compatibility Patch`, no explicit category. Expect: detected as patch, sorted last.
|
|
3. **Heuristic miss (intentional)** - mods named `BugFixes`, `LittleTweaks`, `BalanceFix`. Expect: NOT in patch tier.
|
|
4. **Patch + loadLast map mod** - input: a `loadLast=on` map mod (`Eerie_County`) and a patch (`Eerie_County - Brita Compat`). Expect: `Eerie_County` precedes the patch in `SORTED_ORDER`.
|
|
5. **Two patches** - `AAA-Compatibility` and `ZZZ-Patch`. Expect: alphabetical order preserved within the tier.
|
|
6. **No patches in input** - sort identical to current behavior; regression test against a saved canonical fixture (e.g. `2169435993;2392709985;2487022075`).
|
|
7. **`sorting_rules.txt` override** - user supplies `[Some_Mod]\ncategory=patch`; expected to force into tier even if name doesn't match heuristic and `mod.info` doesn't declare it.
|
|
8. **Patch with PREORDER mod_id** - hypothetical `ModManagerServer-Patch` (mod_id matches PREORDER table). Expect: still sorts within the patch tier (last), but among patches uses PREORDER=2 sub-ordering.
|