feat: modpack-bundled sorting rules + B41/B42 build pair
- data/modpack_rules/helldrinx.txt: bundled rules for HellDrinx FULL/LITE - app.py auto-injects modpack rules when a trigger wsid is in input; user-supplied rules are appended after and override on conflict - MANUAL_BUILD_PAIRS: betterLockpicking (B41) ↔ NFsBetterLockpicking (B42) - mlos_sort.py: minor adjustments (kept in lockstep across api/worker)
This commit is contained in:
154
api/app.py
154
api/app.py
@@ -14,7 +14,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -290,9 +290,107 @@ MANUAL_BUILD_PAIRS: Dict[str, str] = {
|
|||||||
# builds; B42 wsid bundles a TMMumble addon as a second mod_id.
|
# builds; B42 wsid bundles a TMMumble addon as a second mod_id.
|
||||||
"2613146550": "3632610172",
|
"2613146550": "3632610172",
|
||||||
"3632610172": "2613146550",
|
"3632610172": "2613146550",
|
||||||
|
# betterLockpicking (B41) ↔ NFsBetterLockpicking (B42). Different author
|
||||||
|
# picked up the B42 port and renamed the mod_id, so the mod_parsed-based
|
||||||
|
# cross-reference can't find the pair.
|
||||||
|
"2368058459": "3440867775",
|
||||||
|
"3440867775": "2368058459",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Modpack-bundled sorting_rules.txt files: when a trigger wsid is in the
|
||||||
|
# user's input, we prepend the matching file's contents to the user's rules
|
||||||
|
# text before parse_sorting_rules runs. User-supplied rules come last so they
|
||||||
|
# override modpack defaults on conflicting keys.
|
||||||
|
_MODPACK_RULES_DIR = Path(__file__).resolve().parent.parent / "data" / "modpack_rules"
|
||||||
|
_MODPACK_RULES_TRIGGERS: Dict[str, str] = {
|
||||||
|
# HellDrinx FULL + LITE both bundle the same sorting_rules.txt.
|
||||||
|
"3672556207": "helldrinx.txt", # HellDrinx FULL
|
||||||
|
"3662909244": "helldrinx.txt", # HellDrinx LITE
|
||||||
|
}
|
||||||
|
# Human-readable trigger labels for the warning message.
|
||||||
|
_MODPACK_RULES_LABELS: Dict[str, str] = {
|
||||||
|
"3672556207": "HellDrinx FULL",
|
||||||
|
"3662909244": "HellDrinx LITE",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _modpack_rules_for(input_wsids: List[str]) -> Tuple[str, List[Tuple[str, str]]]:
|
||||||
|
"""Return (rules_text, triggers) where triggers is a list of (wsid, label)
|
||||||
|
tuples that fired and rules_text is the concatenated content of every
|
||||||
|
distinct file referenced. Each rules file is included at most once even
|
||||||
|
if multiple trigger wsids share it (HellDrinx FULL + LITE case)."""
|
||||||
|
files_seen: set = set()
|
||||||
|
parts: List[str] = []
|
||||||
|
triggers: List[Tuple[str, str]] = []
|
||||||
|
for wsid in input_wsids:
|
||||||
|
fname = _MODPACK_RULES_TRIGGERS.get(wsid)
|
||||||
|
if not fname:
|
||||||
|
continue
|
||||||
|
triggers.append((wsid, _MODPACK_RULES_LABELS.get(wsid, wsid)))
|
||||||
|
if fname in files_seen:
|
||||||
|
continue
|
||||||
|
files_seen.add(fname)
|
||||||
|
try:
|
||||||
|
parts.append((_MODPACK_RULES_DIR / fname).read_text(encoding="utf-8"))
|
||||||
|
except OSError as e:
|
||||||
|
log.warning("modpack rules load failed for %s (%s): %s", wsid, fname, e)
|
||||||
|
return ("\n\n".join(parts), triggers)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rules_with_modpacks(
|
||||||
|
rules_raw: Optional[str],
|
||||||
|
input_wsids: List[str],
|
||||||
|
) -> Tuple[Dict[str, Any], List[Tuple[str, str]]]:
|
||||||
|
"""Parse user-supplied rules with any auto-detected modpack rules
|
||||||
|
prepended. Returns (parsed_rules, triggers). Caller emits a warning per
|
||||||
|
trigger so the user knows the auto-injection happened."""
|
||||||
|
modpack_text, triggers = _modpack_rules_for(input_wsids)
|
||||||
|
combined = (
|
||||||
|
(modpack_text + "\n\n" + (rules_raw or "")).strip()
|
||||||
|
if modpack_text else (rules_raw or "")
|
||||||
|
)
|
||||||
|
if not combined:
|
||||||
|
return ({}, triggers)
|
||||||
|
try:
|
||||||
|
return (parse_sorting_rules(combined), triggers)
|
||||||
|
except Exception:
|
||||||
|
log.warning("failed to parse modpack+user rules; ignoring")
|
||||||
|
return ({}, triggers)
|
||||||
|
|
||||||
|
|
||||||
|
def _emit_modpack_rules_warnings(
|
||||||
|
payload: Dict[str, Any],
|
||||||
|
triggers: List[Tuple[str, str]],
|
||||||
|
) -> None:
|
||||||
|
"""Append a `modpack-rules-applied` warning per modpack trigger so users
|
||||||
|
know the modpack's bundled sorting_rules.txt was auto-injected. Skips
|
||||||
|
triggers already flagged (idempotent across resort cycles)."""
|
||||||
|
if not triggers:
|
||||||
|
return
|
||||||
|
existing = payload.get("WARNINGS") or []
|
||||||
|
already_flagged = {
|
||||||
|
w.get("wsid") for w in existing
|
||||||
|
if w.get("tag") == "modpack-rules-applied" and w.get("wsid")
|
||||||
|
}
|
||||||
|
new_warnings: List[Dict[str, Any]] = []
|
||||||
|
for wsid, label in triggers:
|
||||||
|
if wsid in already_flagged:
|
||||||
|
continue
|
||||||
|
new_warnings.append({
|
||||||
|
"tag": "modpack-rules-applied",
|
||||||
|
"level": "amber",
|
||||||
|
"wsid": wsid,
|
||||||
|
"msg": (
|
||||||
|
f"{label} ({wsid}) detected — auto-applied its bundled "
|
||||||
|
f"sorting_rules.txt. Your manually-entered rules (if any) "
|
||||||
|
f"override modpack rules."
|
||||||
|
),
|
||||||
|
})
|
||||||
|
if new_warnings:
|
||||||
|
payload["WARNINGS"] = list(existing) + new_warnings
|
||||||
|
|
||||||
|
|
||||||
async def _find_swap_candidate(
|
async def _find_swap_candidate(
|
||||||
conn,
|
conn,
|
||||||
wsid: str,
|
wsid: str,
|
||||||
@@ -405,10 +503,13 @@ async def _emit_build_mismatch_warnings(
|
|||||||
Build tags don't include the requested build.
|
Build tags don't include the requested build.
|
||||||
|
|
||||||
`workshop_meta.tags` is Steam's controlled vocabulary — `Build 41` /
|
`workshop_meta.tags` is Steam's controlled vocabulary — `Build 41` /
|
||||||
`Build 42` are the canonical signal. Three cases per wsid:
|
`Build 42` are the canonical signal. Cases per wsid:
|
||||||
- both builds tagged → multi-build, no warn
|
- both builds tagged → multi-build, no warn
|
||||||
- only the OPPOSITE build tagged → wrong build, emit warn
|
- only the OPPOSITE build tagged → wrong build, emit warn
|
||||||
- neither build tagged → author didn't mark, can't tell, no warn
|
- neither build tagged BUT a MANUAL_BUILD_PAIRS partner is tagged for
|
||||||
|
the target build → infer wrong build, emit warn (covers untagged
|
||||||
|
wsids whose author never set a Steam build tag)
|
||||||
|
- neither build tagged AND no paired partner → can't tell, no warn
|
||||||
- only the TARGET build tagged → fine, no warn
|
- only the TARGET build tagged → fine, no warn
|
||||||
|
|
||||||
Multi-branch wsids already get their own `build-mismatch` from
|
Multi-branch wsids already get their own `build-mismatch` from
|
||||||
@@ -417,14 +518,22 @@ async def _emit_build_mismatch_warnings(
|
|||||||
"""
|
"""
|
||||||
if not input_wsids or pz_build not in ("B41", "B42"):
|
if not input_wsids or pz_build not in ("B41", "B42"):
|
||||||
return
|
return
|
||||||
|
# Expand the lookup set to include MANUAL_BUILD_PAIRS partners so we can
|
||||||
|
# check the partner's Steam tags in the same query (no N+1).
|
||||||
|
lookup_wsids = set(input_wsids)
|
||||||
|
for w in input_wsids:
|
||||||
|
if w in MANUAL_BUILD_PAIRS:
|
||||||
|
lookup_wsids.add(MANUAL_BUILD_PAIRS[w])
|
||||||
rows = await conn.fetch(
|
rows = await conn.fetch(
|
||||||
"""
|
"""
|
||||||
SELECT workshop_id, title, tags
|
SELECT workshop_id, title, tags
|
||||||
FROM workshop_meta
|
FROM workshop_meta
|
||||||
WHERE workshop_id = ANY($1::text[])
|
WHERE workshop_id = ANY($1::text[])
|
||||||
""",
|
""",
|
||||||
list(input_wsids),
|
list(lookup_wsids),
|
||||||
)
|
)
|
||||||
|
tags_by_wsid: Dict[str, List[str]] = {r["workshop_id"]: list(r["tags"] or []) for r in rows}
|
||||||
|
titles_by_wsid: Dict[str, str] = {r["workshop_id"]: r["title"] for r in rows}
|
||||||
target_tag = "Build 41" if pz_build == "B41" else "Build 42"
|
target_tag = "Build 41" if pz_build == "B41" else "Build 42"
|
||||||
other_tag = "Build 42" if pz_build == "B41" else "Build 41"
|
other_tag = "Build 42" if pz_build == "B41" else "Build 41"
|
||||||
existing = payload.get("WARNINGS") or []
|
existing = payload.get("WARNINGS") or []
|
||||||
@@ -433,16 +542,24 @@ async def _emit_build_mismatch_warnings(
|
|||||||
if w.get("tag") == "build-mismatch" and w.get("wsid")
|
if w.get("tag") == "build-mismatch" and w.get("wsid")
|
||||||
}
|
}
|
||||||
new_warnings: List[Dict[str, Any]] = []
|
new_warnings: List[Dict[str, Any]] = []
|
||||||
for r in rows:
|
for wsid in input_wsids:
|
||||||
wsid = r["workshop_id"]
|
|
||||||
if wsid in already_flagged:
|
if wsid in already_flagged:
|
||||||
continue
|
continue
|
||||||
tags = list(r["tags"] or [])
|
tags = tags_by_wsid.get(wsid)
|
||||||
|
if tags is None:
|
||||||
|
continue # no workshop_meta row cached
|
||||||
if target_tag in tags:
|
if target_tag in tags:
|
||||||
continue # supports the picked build
|
continue # supports the picked build
|
||||||
if other_tag not in tags:
|
# Two paths to "this is the wrong build":
|
||||||
continue # author didn't tag a build, can't tell
|
# (a) Steam directly tags it as the OPPOSITE build.
|
||||||
title = r["title"] or wsid
|
# (b) MANUAL_BUILD_PAIRS partner is tagged for our target build.
|
||||||
|
partner_supports_target = (
|
||||||
|
wsid in MANUAL_BUILD_PAIRS
|
||||||
|
and target_tag in tags_by_wsid.get(MANUAL_BUILD_PAIRS[wsid], [])
|
||||||
|
)
|
||||||
|
if other_tag not in tags and not partner_supports_target:
|
||||||
|
continue # untagged with no helpful partner — can't tell
|
||||||
|
title = titles_by_wsid.get(wsid) or wsid
|
||||||
warn: Dict[str, Any] = {
|
warn: Dict[str, Any] = {
|
||||||
"tag": "build-mismatch",
|
"tag": "build-mismatch",
|
||||||
"level": "amber",
|
"level": "amber",
|
||||||
@@ -696,12 +813,7 @@ async def _build_result_for_job(
|
|||||||
wsids,
|
wsids,
|
||||||
)
|
)
|
||||||
mods = [_row_to_modinfo(r) for r in rows]
|
mods = [_row_to_modinfo(r) for r in rows]
|
||||||
rules: Dict[str, Any] = {}
|
rules, modpack_triggers = _parse_rules_with_modpacks(rules_raw, wsids)
|
||||||
if rules_raw:
|
|
||||||
try:
|
|
||||||
rules = parse_sorting_rules(rules_raw)
|
|
||||||
except Exception:
|
|
||||||
log.warning("job result: failed to parse sorting_rules")
|
|
||||||
_inject_addon_loadafter(mods)
|
_inject_addon_loadafter(mods)
|
||||||
sort_result = sort_mods(mods, rules)
|
sort_result = sort_mods(mods, rules)
|
||||||
cached_ids = {r["workshop_id"] for r in rows}
|
cached_ids = {r["workshop_id"] for r in rows}
|
||||||
@@ -724,6 +836,7 @@ async def _build_result_for_job(
|
|||||||
await _augment_with_required_items(conn, payload, wsids)
|
await _augment_with_required_items(conn, payload, wsids)
|
||||||
if pz_build:
|
if pz_build:
|
||||||
await _emit_build_mismatch_warnings(conn, payload, wsids, pz_build)
|
await _emit_build_mismatch_warnings(conn, payload, wsids, pz_build)
|
||||||
|
_emit_modpack_rules_warnings(payload, modpack_triggers)
|
||||||
_reorder_warnings(payload)
|
_reorder_warnings(payload)
|
||||||
# Spec A §8 ownership: WORKSHOP_ITEMS_LINE preserves the SET of input
|
# Spec A §8 ownership: WORKSHOP_ITEMS_LINE preserves the SET of input
|
||||||
# wsids regardless of which are cached/non-mod/unknown. Within that set,
|
# wsids regardless of which are cached/non-mod/unknown. Within that set,
|
||||||
@@ -1017,13 +1130,7 @@ async def sort_endpoint(req: SortRequest, request: Request) -> Dict[str, Any]:
|
|||||||
|
|
||||||
mods: List[ModInfo] = [_row_to_modinfo(r) for r in rows]
|
mods: List[ModInfo] = [_row_to_modinfo(r) for r in rows]
|
||||||
|
|
||||||
rules: Dict[str, Any] = {}
|
rules, modpack_triggers = _parse_rules_with_modpacks(req.rules, input_ids)
|
||||||
if req.rules:
|
|
||||||
try:
|
|
||||||
rules = parse_sorting_rules(req.rules)
|
|
||||||
except Exception:
|
|
||||||
log.warning("failed to parse sorting_rules; ignoring")
|
|
||||||
rules = {}
|
|
||||||
|
|
||||||
_inject_addon_loadafter(mods)
|
_inject_addon_loadafter(mods)
|
||||||
sort_result = sort_mods(mods, rules)
|
sort_result = sort_mods(mods, rules)
|
||||||
@@ -1057,6 +1164,7 @@ async def sort_endpoint(req: SortRequest, request: Request) -> Dict[str, Any]:
|
|||||||
await _emit_build_mismatch_warnings(
|
await _emit_build_mismatch_warnings(
|
||||||
conn, payload, input_ids, req.pz_build or "B42",
|
conn, payload, input_ids, req.pz_build or "B42",
|
||||||
)
|
)
|
||||||
|
_emit_modpack_rules_warnings(payload, modpack_triggers)
|
||||||
_reorder_warnings(payload)
|
_reorder_warnings(payload)
|
||||||
# Surface ghost / non-mod IDs separately from real pending so the UI can
|
# Surface ghost / non-mod IDs separately from real pending so the UI can
|
||||||
# distinguish "indexing 2 mods" from "2 of your IDs are deleted/typo'd"
|
# distinguish "indexing 2 mods" from "2 of your IDs are deleted/typo'd"
|
||||||
|
|||||||
@@ -32,7 +32,26 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
# Constants (mirrors MLOS_sorting.lua)
|
# Constants (mirrors MLOS_sorting.lua)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
PREORDER: Dict[str, int] = {"ModManager": 1, "ModManagerServer": 2, "modoptions": 3}
|
PREORDER: Dict[str, int] = {
|
||||||
|
# B41 in-game MLOS sorter (wsid 3180893708) + server companion + vanilla
|
||||||
|
# mod options panel — slot at the very front by historical MLOS convention.
|
||||||
|
"ModManager": 1,
|
||||||
|
"ModManagerServer": 2,
|
||||||
|
"modoptions": 3,
|
||||||
|
# Project-specific forced order: tsarslib is a foundational lib that many
|
||||||
|
# vehicle/map mods require, so it must precede them. AquatsarYachtClub +
|
||||||
|
# AquatsarRVAddon + ProjectRVInterior42 form an authored cluster whose
|
||||||
|
# interior overlays only render correctly in this exact order. Slots 4-7
|
||||||
|
# land them immediately after the management tools, before any category-
|
||||||
|
# sorted content.
|
||||||
|
"tsarslib": 4,
|
||||||
|
"AquatsarYachtClubB42": 5,
|
||||||
|
"AquatsarRVAddon": 6,
|
||||||
|
"PROJECTRVInterior42": 7,
|
||||||
|
# damnlib (wsid 3171167894) — same nature as tsarslib: foundational lib
|
||||||
|
# consumed by many B42 mods. Slots after the Aquatsar block.
|
||||||
|
"damnlib": 8,
|
||||||
|
}
|
||||||
|
|
||||||
RAW_CATEGORY_ORDER: List[str] = [
|
RAW_CATEGORY_ORDER: List[str] = [
|
||||||
"coreRequirement",
|
"coreRequirement",
|
||||||
|
|||||||
71
data/modpack_rules/helldrinx.txt
Normal file
71
data/modpack_rules/helldrinx.txt
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
; HellDrinx modpack — bundled sorting_rules.txt
|
||||||
|
; Triggers: HellDrinx FULL (3672556207), HellDrinx LITE (3662909244).
|
||||||
|
; Auto-injected by app.py:_modpack_rules_for() when either trigger wsid is in
|
||||||
|
; the user's input. User-supplied rules are appended afterward and override
|
||||||
|
; these on conflicting keys.
|
||||||
|
;
|
||||||
|
; Source: HellDrinx FULL/LITE — keep this file in sync with the modpack's
|
||||||
|
; upstream sorting_rules.txt when it updates.
|
||||||
|
|
||||||
|
[PROJECTRVInterior42]
|
||||||
|
loadFirst=on
|
||||||
|
[TombBody]
|
||||||
|
loadAfter=SPNCC,SPNCCDetails,SPNCCDetailsHD,SpnHairAPI,SpnCloth,SpnOpenCloth,SPNCCFaces,SpnHair,FH,KATTAJ1_ClothesCore,KATTAJ1_Military,[J&G] Black Multicam Uniform,[J&G] British DPM Uniform,[J&G] Caution Pack,[J&G] Firefighter Uniform,[J&G] Neon Vandals Uniform,[J&G] SWAT Uniform,[J&G] Trauma Responder Uniform,[J&G] Umbrella Corp Uniform,[J&G] Alpine Multicam Uniform,[J&G] Desert Multicam Uniform,[J&G] Flecktarn Uniform,[J&G] Forest Multicam Uniform,[J&G] Jordan's Tailoring Buff,[J&G] MARPAT Camo Uniform,[J&G] XKU Uniform,AliceGear,CamisetasSeleccion,CerberusPack,DarkWpnSlings,EFTBP,GanydeBielovzki's Frockin Shirts n Ties,GanydeBielovzki's Frockin Splendor!,GanydeBielovzki's Frockin Splendor! Vol.2,GanydeBielovzki's Frockin Splendor! Vol.3,GanydeBielovzki's Frockin Splendor! Vol.4,GanydeBielovzki's Frockin Splendor! Vol.5,GanydeBielovzki's Frockin Wiseguys,H_E_C_U,HardwoodsTurnoutGear,KITTYOWO,LynxPack,MilitaryPonchosRELOADED,newclothesmodels,TwoWeaponsB,VanillaGearExpanded,zReApoPonchosB42,zReArmorPackBYKB42,ArmoredVests,ArmoredVestsPatch
|
||||||
|
loadLast=on
|
||||||
|
[TombBodyCustom]
|
||||||
|
loadAfter=TombBody
|
||||||
|
loadLast=on
|
||||||
|
[TombBodyTexDOLL]
|
||||||
|
loadAfter=TombBodyCustom
|
||||||
|
incompatibleMods=TombBodyTexNUDE,TombBodyTex
|
||||||
|
loadLast=on
|
||||||
|
[TombGothTexDOLL]
|
||||||
|
loadAfter=TombBodyTexDOLL
|
||||||
|
incompatibleMods=TombGothTexNUDE,TombBodyTexNUDE,TombBodyTex
|
||||||
|
loadLast=on
|
||||||
|
[TombGothHair]
|
||||||
|
loadAfter=TombBody,FH
|
||||||
|
[TombBodyCompat]
|
||||||
|
loadAfter=TombBody
|
||||||
|
loadLast=on
|
||||||
|
[TombWardrobeALT]
|
||||||
|
loadAfter=TombBody
|
||||||
|
loadLast=on
|
||||||
|
[Optimal]
|
||||||
|
loadLast=on
|
||||||
|
[Performance]
|
||||||
|
loadLast=on
|
||||||
|
[ProjectSummerCar]
|
||||||
|
loadAfter=VehicleRepairOverhaul,VehicleSalvageOverhaulB42
|
||||||
|
incompatibleMods=Ivmakk_RestoreEngineQuality
|
||||||
|
loadLast=on
|
||||||
|
category=undefined
|
||||||
|
[ItemretexturePSC]
|
||||||
|
loadLast=on
|
||||||
|
category=undefined
|
||||||
|
[VehicleRepairOverhaul]
|
||||||
|
loadBefore=ProjectSummerCar,Ivmakk_RestoreEngineQuality
|
||||||
|
incompatibleMods=SimpleAutoParts
|
||||||
|
loadLast=on
|
||||||
|
[VehicleSalvageOverhaulB42]
|
||||||
|
loadBefore=ProjectSummerCar,Ivmakk_RestoreEngineQuality
|
||||||
|
loadLast=on
|
||||||
|
[MoreDescriptionForTraits4213]
|
||||||
|
loadBefore=FIX_DetailedDescriptionsForOccupationsAndTraits
|
||||||
|
[HellDrinxJSONFix]
|
||||||
|
loadAfter=ItemretexturePSC,TombBodyCompat
|
||||||
|
loadLast=on
|
||||||
|
[HellDrinxTranslations]
|
||||||
|
loadAfter=ItemretexturePSC
|
||||||
|
loadLast=on
|
||||||
|
[HellDrinxBugFixes]
|
||||||
|
loadAfter=ItemretexturePSC,TombBodyCompat,TombWardrobeALT,HellDrinxStrangers,SpnCloth
|
||||||
|
loadLast=on
|
||||||
|
[HellDrinxEssentials]
|
||||||
|
loadLast=on
|
||||||
|
[HellDrinxBugFixesFirst]
|
||||||
|
loadFirst=on
|
||||||
|
[TACPDM2]
|
||||||
|
loadLast=on
|
||||||
|
[damnlib]
|
||||||
|
loadFirst=on
|
||||||
@@ -32,7 +32,26 @@ from typing import Dict, List, Optional, Tuple
|
|||||||
# Constants (mirrors MLOS_sorting.lua)
|
# Constants (mirrors MLOS_sorting.lua)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
PREORDER: Dict[str, int] = {"ModManager": 1, "ModManagerServer": 2, "modoptions": 3}
|
PREORDER: Dict[str, int] = {
|
||||||
|
# B41 in-game MLOS sorter (wsid 3180893708) + server companion + vanilla
|
||||||
|
# mod options panel — slot at the very front by historical MLOS convention.
|
||||||
|
"ModManager": 1,
|
||||||
|
"ModManagerServer": 2,
|
||||||
|
"modoptions": 3,
|
||||||
|
# Project-specific forced order: tsarslib is a foundational lib that many
|
||||||
|
# vehicle/map mods require, so it must precede them. AquatsarYachtClub +
|
||||||
|
# AquatsarRVAddon + ProjectRVInterior42 form an authored cluster whose
|
||||||
|
# interior overlays only render correctly in this exact order. Slots 4-7
|
||||||
|
# land them immediately after the management tools, before any category-
|
||||||
|
# sorted content.
|
||||||
|
"tsarslib": 4,
|
||||||
|
"AquatsarYachtClubB42": 5,
|
||||||
|
"AquatsarRVAddon": 6,
|
||||||
|
"PROJECTRVInterior42": 7,
|
||||||
|
# damnlib (wsid 3171167894) — same nature as tsarslib: foundational lib
|
||||||
|
# consumed by many B42 mods. Slots after the Aquatsar block.
|
||||||
|
"damnlib": 8,
|
||||||
|
}
|
||||||
|
|
||||||
RAW_CATEGORY_ORDER: List[str] = [
|
RAW_CATEGORY_ORDER: List[str] = [
|
||||||
"coreRequirement",
|
"coreRequirement",
|
||||||
|
|||||||
Reference in New Issue
Block a user