From d12a5bbe03e8beb7c258684ce88f0e4da3dc40de Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Thu, 27 Aug 2026 16:00:25 +0200 Subject: [PATCH] visuals: track MPQ source revisions --- remote_packages.py | 86 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/remote_packages.py b/remote_packages.py index d14fba7..e78df09 100644 --- a/remote_packages.py +++ b/remote_packages.py @@ -527,22 +527,27 @@ def _managed_locations(target_dir, mod_id): return root, manifest, backups -def _load_managed_manifest(target_dir, mod_id): +def _load_managed_manifest_data(target_dir, mod_id): _, manifest_path, _ = _managed_locations(target_dir, mod_id) if not os.path.isfile(manifest_path): - return [] + return {} try: with open(manifest_path, "r", encoding="utf-8") as handle: data = json.load(handle) - files = data.get("files", []) - if not isinstance(files, list): - return [] - return [_safe_relative_path(item) for item in files if isinstance(item, str)] + return data if isinstance(data, dict) else {} except (OSError, json.JSONDecodeError, ValueError): + return {} + + +def _load_managed_manifest(target_dir, mod_id): + data = _load_managed_manifest_data(target_dir, mod_id) + files = data.get("files", []) + if not isinstance(files, list): return [] + return [_safe_relative_path(item) for item in files if isinstance(item, str)] -def _write_managed_manifest(target_dir, mod_id, relative_files): +def _write_managed_manifest(target_dir, mod_id, relative_files, revision=None): _, manifest_path, _ = _managed_locations(target_dir, mod_id) os.makedirs(os.path.dirname(manifest_path), exist_ok=True) temp_path = manifest_path + ".new" @@ -550,6 +555,8 @@ def _write_managed_manifest(target_dir, mod_id, relative_files): "mod_id": mod_id, "files": [str(path).replace(os.sep, "/") for path in relative_files], } + if revision is not None: + payload["revision"] = str(revision) with open(temp_path, "w", encoding="utf-8") as handle: json.dump(payload, handle, indent=2) os.replace(temp_path, manifest_path) @@ -591,6 +598,19 @@ def _restore_or_remove_managed_file(target_dir, mod_id, relative_path): +# Existing manifests created before revision tracking correspond to revision 1 +# of these visual sources. This lets current installations migrate without one +# unnecessary large re-download, while a future revision bump can still force +# a refresh automatically. +VISUAL_MOD_REVISIONS = { + "visual_darker_nights": "1", + "visual_pretty_night_sky": "1", + "visual_epoch_water": "1", + "visual_fog_pushback": "1", + "visual_pink_herbs": "1", +} + + def managed_mod_is_installed(target_dir, mod_id): """True only when a tool-managed manifest exists and all owned files remain present.""" files = _load_managed_manifest(target_dir, mod_id) @@ -599,6 +619,37 @@ def managed_mod_is_installed(target_dir, mod_id): return all(os.path.isfile(os.path.join(target_dir, rel)) for rel in files) +def managed_mod_is_current(target_dir, mod_id, revision): + """Check installed files plus the source revision recorded by the tool.""" + if not managed_mod_is_installed(target_dir, mod_id): + return False + + data = _load_managed_manifest_data(target_dir, mod_id) + installed_revision = data.get("revision") + + # Migrate pre-revision manifests for the visual sources that existed when + # revision tracking was introduced. + if installed_revision is None and mod_id in VISUAL_MOD_REVISIONS: + installed_revision = "1" + + return str(installed_revision) == str(revision) + + +def managed_mpq_is_current(target_dir, mod_id, revision): + """Cheap integrity check for an already installed managed MPQ.""" + if not managed_mod_is_current(target_dir, mod_id, revision): + return False + files = _load_managed_manifest(target_dir, mod_id) + if len(files) != 1: + return False + path = os.path.join(target_dir, files[0]) + try: + with open(path, "rb") as handle: + return handle.read(3) == b"MPQ" + except OSError: + return False + + def remove_managed_mod(target_dir, mod_id): """Remove only files previously installed by this tool, restoring backups.""" root, manifest_path, backup_root = _managed_locations(target_dir, mod_id) @@ -627,7 +678,7 @@ def remove_managed_mod(target_dir, mod_id): pass -def _install_managed_files(target_dir, mod_id, file_mappings): +def _install_managed_files(target_dir, mod_id, file_mappings, revision=None): """Install files while preserving any pre-existing user files for restore.""" root, _, backup_root = _managed_locations(target_dir, mod_id) del root @@ -657,7 +708,7 @@ def _install_managed_files(target_dir, mod_id, file_mappings): _atomic_replace_file(source_path, target) - _write_managed_manifest(target_dir, mod_id, new_files) + _write_managed_manifest(target_dir, mod_id, new_files, revision=revision) def _verify_mpq(path): @@ -670,7 +721,16 @@ def _verify_mpq(path): raise RemotePackageError("Downloaded file is not a valid MPQ archive.") -def _install_remote_mpq(target_dir, mod_id, url, destination, progress=None, label="Downloading visual mod", timeout=300): +def _install_remote_mpq( + target_dir, + mod_id, + url, + destination, + progress=None, + label="Downloading visual mod", + timeout=300, + revision=None, +): temp_path = _download( url, suffix=".mpq", @@ -685,6 +745,7 @@ def _install_remote_mpq(target_dir, mod_id, url, destination, progress=None, lab target_dir, mod_id, [(temp_path, destination)], + revision=revision, ) finally: try: @@ -708,6 +769,7 @@ def install_darker_nights(target_dir, progress=None): os.path.join("Data", "patch-N.mpq"), progress=progress, label="Downloading Darker Nights", + revision=VISUAL_MOD_REVISIONS["visual_darker_nights"], ) return "Project Reforged Patch-N" @@ -722,6 +784,7 @@ def install_pretty_night_sky(target_dir, progress=None): os.path.join("Data", "patch-Z.mpq"), progress=progress, label="Downloading Pretty Night Sky", + revision=VISUAL_MOD_REVISIONS["visual_pretty_night_sky"], ) return "RetroCro mirror" @@ -734,6 +797,7 @@ def install_epoch_water(target_dir, progress=None): os.path.join("Data", "patch-W.mpq"), progress=progress, label="Downloading Epoch Water", + revision=VISUAL_MOD_REVISIONS["visual_epoch_water"], ) return "RetroCro mirror" @@ -746,6 +810,7 @@ def install_fog_pushback(target_dir, progress=None): os.path.join("Data", "patch-Y.mpq"), progress=progress, label="Downloading Fog Pushback", + revision=VISUAL_MOD_REVISIONS["visual_fog_pushback"], ) return "RetroCro mirror" @@ -758,6 +823,7 @@ def install_pink_herbs(target_dir, progress=None): os.path.join("Data", "patch-H.mpq"), progress=progress, label="Downloading Pink Herbs", + revision=VISUAL_MOD_REVISIONS["visual_pink_herbs"], ) return "seacrabsam/patch-herb main"