Initial release: full Arma 3 mod management toolchain

Pipeline: parse HTML presets, compare modlists, download from Caddy
file server, create junctions/symlinks to Arma 3 Server directory.
Includes update/sync flows, missing-mod reporting, OS compat layer,
shared config, dep checker, comprehensive test suite (71 tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
revernomad17
2026-04-07 16:04:36 +07:00
commit 91a38b269b
24 changed files with 4976 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
"""
arma_modlist_tools.compare
~~~~~~~~~~~~~~~~~~~~~~~~~~
Compare two or more Arma 3 mod presets (parsed by :mod:`arma_modlist_tools.parser`)
and produce a breakdown of shared and preset-unique mods.
Typical usage::
from arma_modlist_tools.parser import parse_modlist_dir
from arma_modlist_tools.compare import compare_presets
presets = parse_modlist_dir("modlist_html")
result = compare_presets(*presets)
"""
from __future__ import annotations
def _mod_key(mod: dict) -> str:
"""Return the identity key for a mod.
Uses ``steam_id`` when available (canonical Workshop identifier),
falls back to ``name`` for local mods that have no Workshop ID.
"""
return mod["steam_id"] or mod["name"]
def compare_presets(*presets: dict) -> dict:
"""
Compare two or more preset dicts and return a comparison dict.
:param presets: Two or more preset dicts as returned by
:func:`~arma_modlist_tools.parser.parse_modlist_html`.
:returns: Dict with keys:
- ``compared_presets`` — list of preset names that were compared
- ``shared`` — mods present in **every** preset
- ``mod_count`` — number of shared mods
- ``mods`` — list of mod entry dicts
- ``unique`` — per-preset mods not present in any other preset
- keyed by ``preset_name``
- each value has ``mod_count`` and ``mods``
:raises ValueError: If fewer than two presets are provided.
"""
if len(presets) < 2:
raise ValueError("compare_presets requires at least two presets")
# Build per-preset {identity_key -> mod_entry} mappings
preset_maps: list[dict[str, dict]] = [
{_mod_key(mod): mod for mod in preset["mods"]}
for preset in presets
]
# Shared keys = intersection across ALL presets
shared_keys: set[str] = set(preset_maps[0].keys())
for pm in preset_maps[1:]:
shared_keys &= pm.keys()
# Shared mods: take entries from the first preset (identical across all)
shared_mods = [preset_maps[0][k] for k in preset_maps[0] if k in shared_keys]
# Unique mods per preset: entries whose key is not in the shared set
unique: dict[str, dict] = {}
for preset, pm in zip(presets, preset_maps):
unique_mods = [mod for k, mod in pm.items() if k not in shared_keys]
unique[preset["preset_name"]] = {
"mod_count": len(unique_mods),
"mods": unique_mods,
}
return {
"compared_presets": [p["preset_name"] for p in presets],
"shared": {
"mod_count": len(shared_mods),
"mods": shared_mods,
},
"unique": unique,
}