diff --git a/api/adapters.py b/api/adapters.py index 1c59097..4924d28 100644 --- a/api/adapters.py +++ b/api/adapters.py @@ -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, diff --git a/api/app.py b/api/app.py index 7fb0341..60829ba 100644 --- a/api/app.py +++ b/api/app.py @@ -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