Files
HRiggs 7ac541def5
Build and Publish Container / Publish Linux container (push) Failing after 1m52s
public site and containers for that.
2026-10-02 12:57:22 -04:00

111 lines
3.9 KiB
Python

from __future__ import annotations
import io
import xml.etree.ElementTree as ET
from collections.abc import Iterable
from dataclasses import dataclass
DLC = {
"royalty": ("ludeon.rimworld.royalty", "Royalty"),
"ideology": ("ludeon.rimworld.ideology", "Ideology"),
"biotech": ("ludeon.rimworld.biotech", "Biotech"),
"anomaly": ("ludeon.rimworld.anomaly", "Anomaly"),
"odyssey": ("ludeon.rimworld.odyssey", "Odyssey"),
}
BASE_GAME = ("ludeon.rimworld", "Core")
@dataclass(frozen=True)
class ModListItem:
package_id: str
workshop_id: str
name: str
def core_items(selected_dlc: Iterable[str]) -> list[ModListItem]:
result = [ModListItem(BASE_GAME[0], "0", BASE_GAME[1])]
normalized = {value.strip().lower() for value in selected_dlc}
for key, (package_id, name) in DLC.items():
if key in normalized:
result.append(ModListItem(package_id, "0", name))
return result
def workshop_items(rows: Iterable[dict]) -> list[ModListItem]:
result: list[ModListItem] = []
for row in rows:
if row.get("status") != "ready" or not row.get("package_id"):
continue
# The Workshop id is the correct RimWorld modSteamIds value. The old loader
# sometimes substituted About.xml's steamAppId, which can be the game app id.
result.append(
ModListItem(
package_id=str(row["package_id"]),
workshop_id=str(row["workshop_id"]),
name=str(
row.get("display_name") or row.get("title") or row["package_id"]
),
)
)
return result
def deduplicate(items: Iterable[ModListItem]) -> list[ModListItem]:
result: list[ModListItem] = []
seen_packages: set[str] = set()
seen_workshop_ids: set[str] = set()
for item in items:
package_key = item.package_id.lower()
if package_key in seen_packages:
continue
if item.workshop_id != "0" and item.workshop_id in seen_workshop_ids:
continue
seen_packages.add(package_key)
if item.workshop_id != "0":
seen_workshop_ids.add(item.workshop_id)
result.append(item)
return result
def parse_active_mods(xml_bytes: bytes) -> list[ModListItem]:
if len(xml_bytes) > 5 * 1024 * 1024:
raise ValueError("ModsConfig.xml is larger than 5 MB")
try:
root = ET.fromstring(xml_bytes)
except ET.ParseError as error:
raise ValueError(f"ModsConfig.xml is invalid: {error}") from error
active = root.find("activeMods")
if active is None:
raise ValueError("ModsConfig.xml has no activeMods section")
items: list[ModListItem] = []
for entry in active.findall("li"):
package_id = (entry.text or "").strip()
if package_id:
items.append(ModListItem(package_id, "0", package_id))
return items
def _append_list(parent: ET.Element, name: str, values: Iterable[str]) -> None:
element = ET.SubElement(parent, name)
for value in values:
ET.SubElement(element, "li").text = value
def generate_rml(items: Iterable[ModListItem], game_version: str) -> bytes:
normalized = deduplicate(items)
root = ET.Element("savedModList")
meta = ET.SubElement(root, "meta")
ET.SubElement(meta, "gameVersion").text = game_version
_append_list(meta, "modIds", (item.package_id for item in normalized))
_append_list(meta, "modSteamIds", (item.workshop_id for item in normalized))
_append_list(meta, "modNames", (item.name for item in normalized))
mod_list = ET.SubElement(root, "modList")
_append_list(mod_list, "ids", (item.package_id for item in normalized))
_append_list(mod_list, "names", (item.name for item in normalized))
ET.indent(root, space="\t")
output = io.BytesIO()
ET.ElementTree(root).write(output, encoding="utf-8", xml_declaration=True)
return output.getvalue()