6.9 KiB
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:
- Explicit:
mod.infocontainscategory=patch(new value added toRAW_CATEGORY_ORDER). - Author-tagged via sorting_rules.txt: user-supplied
[modId]\ncategory=patchoverrides anything else (existing mechanism, no change). - Name heuristic (conservative):
mod.namematches 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"toRAW_CATEGORY_ORDER(so it's a validmod.categoryvalue 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 otherwiseundefined. - Modify
_initial_sort_key: prepend1 if mod.category == "patch" else 0as the new tuple element.
- Append
adapters.py: extendCAT_MAPwith"patch": "patch"so the frontend pill key is preserved (see §6).worker.py: no change.mod.infoparsing already accepts arbitrarycategory=…values; once"patch"is inCATEGORY_ORDER, existing parser code passes it through unchanged.- No schema migration.
mod_parsed.categoryis alreadyTEXT NOT NULL DEFAULT 'undefined'-"patch"fits without alteration.
6. Frontend changes
- New pill
patchin the mod-table category column. Recommended palette: muted mauve / pale grey to distinguish fromgameplay(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 -
MiscandFrameworkare the closest, both too noisy to map). - Multiple patch sub-tiers (e.g., "patches-of-patches"). YAGNI; the existing
loadAftermechanism handles ordering between two patches when needed. - A
mod_parsed.is_patchboolean column. Derived fromcategoryis 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_keyreturns an 8-element tuple withis_patch(0 or 1) at index 0.RAW_CATEGORY_ORDERincludes"patch".derive_categoryreturns"patch"when the name regex\b(patch|compat|compatibility)\b(case-insensitive) matches andmod.info'scategory=is unset orundefined.- Explicit
category=patchinmod.infois honored by the existing parser (no parser change required). sorting_rules.txtcategory=patchoverride forces a mod into the patch tier.- In a sort with one
loadLast=onmap mod and one patch, the patch sorts after the map mod inSORTED_ORDER. - In a sort with two patches, alphabetical ordering applies between them (existing alpha tiebreaker preserved).
- In a sort with no patches,
SORTED_ORDERis bit-identical to pre-spec output (is_patch=0for all rows preserves existing total ordering). MOD_DBrows for patches carrycat: "patch"onceadapters.CAT_MAPis extended.
9. Test cases
- Explicit patch via mod.info - wsid X has
category=patch. Expect: sorts last regardless ofloadLast.MOD_DB.cat = "patch". - Heuristic match - mod named
BB Compatibility Patch, no explicit category. Expect: detected as patch, sorted last. - Heuristic miss (intentional) - mods named
BugFixes,LittleTweaks,BalanceFix. Expect: NOT in patch tier. - Patch + loadLast map mod - input: a
loadLast=onmap mod (Eerie_County) and a patch (Eerie_County - Brita Compat). Expect:Eerie_Countyprecedes the patch inSORTED_ORDER. - Two patches -
AAA-CompatibilityandZZZ-Patch. Expect: alphabetical order preserved within the tier. - No patches in input - sort identical to current behavior; regression test against a saved canonical fixture (e.g.
2169435993;2392709985;2487022075). sorting_rules.txtoverride - user supplies[Some_Mod]\ncategory=patch; expected to force into tier even if name doesn't match heuristic andmod.infodoesn't declare it.- 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.