diff options
Diffstat (limited to 'libs/basic_games/games/openmw_support/openmw_cfg.py')
| -rw-r--r-- | libs/basic_games/games/openmw_support/openmw_cfg.py | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/libs/basic_games/games/openmw_support/openmw_cfg.py b/libs/basic_games/games/openmw_support/openmw_cfg.py new file mode 100644 index 0000000..d6d0d01 --- /dev/null +++ b/libs/basic_games/games/openmw_support/openmw_cfg.py @@ -0,0 +1,169 @@ +""" +openmw_cfg.py — generate the managed block of an ``openmw.cfg``. + +OpenMW does not use a separate VFS injector: it reads its mods directly from an +ordered list of ``data=`` directories and loads plugins in the order of the +``content=`` lines. This module rewrites *only* the keys we own, leaving every +other line (engine settings, comments, unrelated keys) untouched: + + data= asset/plugin search dirs; LATER entries override earlier + content= ordered plugin load list (.esp/.esm/.omwaddon/.omwscripts) + groundcover= grass/groundcover plugins (kept out of content= for perf) + fallback-archive= .bsa archives; later entries override earlier + +It is deliberately pure standard library (no Qt / mobase) so it can be unit +tested on its own and reused by the OpenMW game plugin. The caller decides what +goes into ``data_dirs`` — a single merged dir (Fluorine FUSE) or one entry per +mod (native OpenMW VFS) — this module stays agnostic about that choice. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Iterable + +# Morrowind masters, in canonical load order. +# +# Do NOT list ``builtin.omwscripts`` here. It is OpenMW's built-in Lua bundle, +# shipped inside the engine's ``resources/vfs-mw`` and loaded *implicitly* by the +# engine itself. If we also emit ``content=builtin.omwscripts`` it ends up +# specified twice across the config chain and OpenMW aborts on startup with +# "Content file specified more than once: builtin.omwscripts. Aborting...". +VANILLA_MASTERS: list[str] = [ + "Morrowind.esm", + "Tribunal.esm", + "Bloodmoon.esm", +] + +# Vanilla BSAs, always emitted as the lowest-priority fallback-archive entries. +VANILLA_BSAS: list[str] = [ + "Morrowind.bsa", + "Tribunal.bsa", + "Bloodmoon.bsa", +] + +# Keys this module fully owns (lowercase, exact match). +_MANAGED_KEYS = frozenset({"data", "content", "groundcover", "fallback-archive"}) + + +def escape_data_path(path: str) -> str: + """Quote/escape a path for a ``data=`` line. + + openmw.cfg uses boost::filesystem quoting: ``&`` and ``"`` are escaped with a + leading ``&`` and the whole value is wrapped in double quotes. This matches + AnyOldName3's MO2 exporter so paths containing spaces or quotes round-trip. + """ + out = ['"'] + for ch in path: + if ch in ("&", '"'): + out.append("&") + out.append(ch) + out.append('"') + return "".join(out) + + +def _is_managed(line: str) -> bool: + s = line.strip() + if not s or s.startswith("#") or "=" not in s: + return False + return s.split("=", 1)[0].strip().lower() in _MANAGED_KEYS + + +def _preserve_non_managed(cfg_path: Path) -> list[str]: + """Return existing cfg lines minus the keys we manage, trailing blanks trimmed.""" + kept: list[str] = [] + if cfg_path.is_file(): + for raw in cfg_path.read_text(encoding="utf-8", errors="replace").splitlines(): + if not _is_managed(raw): + kept.append(raw) + while kept and not kept[-1].strip(): + kept.pop() + return kept + + +def _dedup_preserving_order( + base: Iterable[str], extra: Iterable[str] +) -> list[str]: + """Concatenate ``base`` then ``extra``, dropping case-insensitive duplicates.""" + result: list[str] = [] + seen: set[str] = set() + for item in (*base, *extra): + key = item.lower() + if key not in seen: + seen.add(key) + result.append(item) + return result + + +def build_managed_block( + data_dirs: Iterable[Path | str], + content_plugins: Iterable[str], + groundcover_plugins: Iterable[str] = (), + fallback_archives: Iterable[str] = (), + *, + vanilla_masters: Iterable[str] = VANILLA_MASTERS, + vanilla_bsas: Iterable[str] = VANILLA_BSAS, +) -> list[str]: + """Build the managed cfg lines (no I/O), in the order OpenMW expects.""" + content = _dedup_preserving_order(vanilla_masters, content_plugins) + archives = _dedup_preserving_order(vanilla_bsas, fallback_archives) + + block: list[str] = [""] # blank separator from the preserved section + block += [f"data={escape_data_path(str(d))}" for d in data_dirs] + block += [f"content={c}" for c in content] + block += [f"groundcover={g}" for g in groundcover_plugins] + block += [f"fallback-archive={a}" for a in archives] + return block + + +def write_openmw_cfg( + cfg_path: Path, + data_dirs: Iterable[Path | str], + content_plugins: Iterable[str], + groundcover_plugins: Iterable[str] = (), + fallback_archives: Iterable[str] = (), + *, + vanilla_masters: Iterable[str] = VANILLA_MASTERS, + vanilla_bsas: Iterable[str] = VANILLA_BSAS, + log_fn=None, +) -> None: + """Rewrite the managed data=/content=/groundcover=/fallback-archive= block.""" + _log = log_fn or (lambda _: None) + data_dirs = list(data_dirs) # consumed twice (block + log); avoid generator exhaustion + kept = _preserve_non_managed(cfg_path) + block = build_managed_block( + data_dirs, + content_plugins, + groundcover_plugins, + fallback_archives, + vanilla_masters=vanilla_masters, + vanilla_bsas=vanilla_bsas, + ) + cfg_path.parent.mkdir(parents=True, exist_ok=True) + cfg_path.write_text("\n".join(kept + block) + "\n", encoding="utf-8") + _log(f" Wrote openmw.cfg: {len(data_dirs)} data dir(s) to {cfg_path}.") + + +def restore_openmw_cfg( + cfg_path: Path, + data_dirs: Iterable[Path | str], + *, + vanilla_masters: Iterable[str] = VANILLA_MASTERS, + vanilla_bsas: Iterable[str] = VANILLA_BSAS, + log_fn=None, +) -> None: + """Reset the managed block to vanilla-only (used on uninstall / 'clear').""" + _log = log_fn or (lambda _: None) + if not cfg_path.is_file(): + return + kept = _preserve_non_managed(cfg_path) + block = build_managed_block( + data_dirs, + (), + (), + (), + vanilla_masters=vanilla_masters, + vanilla_bsas=vanilla_bsas, + ) + cfg_path.write_text("\n".join(kept + block) + "\n", encoding="utf-8") + _log(f" Restored openmw.cfg to vanilla content at {cfg_path}.") |
