fix: missing-dep [add] button respects MOD_ID_ALIASES on build mismatch

When user is on B42 and a mod requires the B41 mod_id (e.g.
truemusic_mixtape_megapack require=truemusic), the canonical wsid
(2613146550, B41-only) is rightly filtered out by the build guard —
but we were silently dropping the suggestion. Now we fall back to
MOD_ID_ALIASES: truemusic → TrueMoozic, suggest wsid 3632610172.

- _lookup_wsids_for_missing returns Dict[str, Tuple[wsid, suggested_mod_id]]
- build_warnings unpacks tuple, label clarifies rename when alias used
- adapters.build_response signature updated for new shape
This commit is contained in:
2026-05-04 16:08:43 +00:00
parent 75728e6b28
commit 591f4608d4
2 changed files with 72 additions and 28 deletions

View File

@@ -84,17 +84,20 @@ def _resolve_satisfied_via_alias(input_modids: Set[str]) -> Set[str]:
def build_warnings(
mlos_warnings: Dict[str, Any],
wsid_lookup: Dict[str, str] | None = None,
wsid_lookup: Dict[str, Tuple[str, str]] | None = None,
source_wsids: Dict[str, str] | None = None,
input_modids: Set[str] | None = None,
) -> List[Dict[str, Any]]:
"""Translate mlos_sort warnings to the SORTOF_DATA WARNINGS shape.
wsid_lookup: optional {mod_id -> workshop_id} map of *deps* we already
have cached. When provided, each 'missing' warning is augmented with
`actions: [{type: "add-wsid", wsid, label}]` so the frontend can render
a click-to-add button. Unknown deps fall back to a Steam Workshop search
link the user follows manually.
wsid_lookup: optional {missing_mod_id -> (workshop_id, suggested_mod_id)}
map produced by app._lookup_wsids_for_missing. The `suggested_mod_id`
differs from the missing mod_id when a MOD_ID_ALIASES alternative was
used because the canonical mod_id's only cached wsid is wrong-build
(e.g. user on B42 needs `truemusic` → suggest `TrueMoozic`'s wsid).
When provided, each 'missing' warning is augmented with `actions:
[{type: "add-wsid", wsid, modId, label}]`. Unknown deps fall back to a
Steam Workshop search link the user follows manually.
source_wsids: optional {mod_id -> workshop_id} map of *every cached mod
in the current sort*. Used to attach the source mod's wsid onto
@@ -120,13 +123,19 @@ def build_warnings(
continue
actions: List[Dict[str, str]] = []
for dep in deps:
wsid = lookup.get(dep)
if wsid:
entry = lookup.get(dep)
if entry:
wsid, suggested = entry
# If MOD_ID_ALIASES rewrote the suggestion (B42 user needing
# B41 mod_id `truemusic` gets `TrueMoozic` instead), surface
# both names so the user understands what's being added.
label = (f"add {suggested}" if suggested == dep
else f"add {suggested} (renamed from {dep})")
actions.append({
"type": "add-wsid",
"wsid": wsid,
"modId": dep,
"label": f"add {dep}",
"modId": suggested,
"label": label,
})
else:
# No cache hit -> link to Steam Workshop search so the user
@@ -558,7 +567,7 @@ def build_response(
mods: List[ModInfo],
sort_result: Dict[str, Any],
status: str,
wsid_lookup: Dict[str, str] | None = None,
wsid_lookup: Dict[str, Tuple[str, str]] | None = None,
pz_build: str = "B42",
is_resort: bool = False,
selected_modids: Optional[Set[str]] = None,

View File

@@ -228,20 +228,29 @@ async def _lookup_wsids_for_missing(
conn,
mlos_warnings: Dict[str, Any],
pz_build: Optional[str] = None,
) -> Dict[str, str]:
) -> Dict[str, Tuple[str, str]]:
"""Resolve missing-requirement mod_ids to wsids via mod_parsed cache.
Returns {mod_id -> workshop_id} for any missing dep we've previously
cached. Used to render an [add to list] action button next to the
'missing' warning. Unknown deps just get no button.
Returns {missing_mod_id -> (workshop_id, suggested_mod_id)} for any
missing dep we've previously cached. `suggested_mod_id` matches the
missing mod_id by default, but when the canonical wsid is wrong-build
AND `MOD_ID_ALIASES` (adapters.py) has an alternative whose cached
wsid IS build-correct, we redirect to the alias. Example: user on B42
has `truemusic_mixtape_megapack` (require=truemusic). truemusic only
exists as a B41 wsid; the alias `TrueMoozic` exists as a B42 wsid.
Result: {"truemusic": ("3632610172", "TrueMoozic")} — frontend renders
[add TrueMoozic] pointing at the correct build's wsid.
pz_build filters out wrong-build resolutions: if the user is on B42 and
the only cached wsid for `dep` is tagged `Build 41` only, we DROP the
suggestion. Adding it would just trigger a build-mismatch warning;
worse, in cases like `TacHold requires modoptions` where the author's
`require=` is over-declared, the suggestion misleads the user toward
a B41 mod that doesn't actually need to be in the load order.
the only cached wsid for `dep` is tagged `Build 41` only, the canonical
suggestion is DROPPED (and the alias path may rescue it). Adding the
wrong-build dep would just trigger a build-mismatch warning; worse, in
cases like `TacHold requires modoptions` where `require=` is over-
declared, the suggestion misleads the user toward a B41 mod that
doesn't actually need to be in the load order.
"""
from adapters import MOD_ID_ALIASES
missing = mlos_warnings.get("missing_requirements") or {}
if not missing:
return {}
@@ -252,6 +261,11 @@ async def _lookup_wsids_for_missing(
wanted.add(d)
if not wanted:
return {}
# Expand the fetch set with alias targets so we can resolve in one round.
for dep in list(wanted):
for alias_target in MOD_ID_ALIASES.get(dep, []):
wanted.add(alias_target)
rows = await conn.fetch(
"""
SELECT DISTINCT ON (mp.mod_id) mp.mod_id, mp.workshop_id, wm.tags
@@ -268,15 +282,36 @@ async def _lookup_wsids_for_missing(
other_tag = "Build 41" if target_tag == "Build 42" else (
"Build 42" if target_tag == "Build 41" else None
)
out: Dict[str, str] = {}
def _is_build_correct(tags: List[str]) -> bool:
if not target_tag or not other_tag:
return True
return not (other_tag in tags and target_tag not in tags)
# candidates: mod_id -> (wsid, build_correct)
candidates: Dict[str, Tuple[str, bool]] = {}
for r in rows:
if target_tag and other_tag:
tags = list(r["tags"] or [])
if other_tag in tags and target_tag not in tags:
# Wrong-build only — adding it would just trigger a
# build-mismatch. Skip the suggestion.
continue
out[r["mod_id"]] = r["workshop_id"]
tags = list(r["tags"] or [])
candidates[r["mod_id"]] = (r["workshop_id"], _is_build_correct(tags))
deps_seen: set = set()
for deps in missing.values():
for d in deps:
if d:
deps_seen.add(d)
out: Dict[str, Tuple[str, str]] = {}
for dep in deps_seen:
cand = candidates.get(dep)
if cand and cand[1]:
out[dep] = (cand[0], dep)
continue
# Canonical missing or wrong-build; try aliases in declaration order.
for alias_target in MOD_ID_ALIASES.get(dep, []):
alt = candidates.get(alias_target)
if alt and alt[1]:
out[dep] = (alt[0], alias_target)
break
return out