feat: add migrate step to move mod folders between groups on preset change

Before step_fetch, scan all downloads/ subdirs and move any mod that
comparison.json now assigns to a different group. Matching uses steam_id
(via meta.cpp publishedid) first, normalized name as fallback.

Stale junctions in arma_dir are removed before the folder move so
step_link can re-create them pointing to the new location.

- New arma_modlist_tools/migrator.py: migrate_mod_groups()
- run.py: step_migrate(), --skip-migrate flag, wired into dispatch loop
- gui/app.py: step_migrate inserted as Step 3/5 between compare and fetch
- gui/locales.py: add step3/4/5 names (en + vi), renumber old 3->4, 4->5
- test_suite.py: 7 new migrator tests (158 total, 0 failed)
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-14 15:08:40 +07:00
parent 45cb023513
commit 48637ffe90
6 changed files with 341 additions and 12 deletions

24
run.py
View File

@@ -78,6 +78,22 @@ def step_compare(cfg) -> None:
print(f" -> {cfg.comparison}")
def step_migrate(cfg) -> None:
if not cfg.comparison.exists():
print(f" NOTE: {cfg.comparison} not found — skipping migration.")
return
if not cfg.downloads.is_dir():
print(f" NOTE: downloads dir missing ({cfg.downloads}) — nothing to migrate.")
return
from arma_modlist_tools.migrator import migrate_mod_groups
comparison = json.loads(cfg.comparison.read_text(encoding="utf-8"))
arma_dir = cfg.arma_dir if cfg.arma_dir.is_dir() else None
result = migrate_mod_groups(cfg.downloads, arma_dir, comparison)
if result["errors"]:
for name, err in result["errors"].items():
print(f" ERROR {name}: {err}")
def step_fetch(cfg) -> None:
if not cfg.comparison.exists():
print(f" ERROR: {cfg.comparison} not found. Run parse + compare first.")
@@ -178,6 +194,7 @@ def main() -> None:
parser = argparse.ArgumentParser(description="Run the full mod management pipeline.")
parser.add_argument("--skip-parse", action="store_true")
parser.add_argument("--skip-compare", action="store_true")
parser.add_argument("--skip-migrate", action="store_true")
parser.add_argument("--skip-fetch", action="store_true")
parser.add_argument("--skip-link", action="store_true")
parser.add_argument("--group", "-g", metavar="GROUP",
@@ -187,13 +204,15 @@ def main() -> None:
steps = [
(not args.skip_parse, "Parse presets"),
(not args.skip_compare, "Compare presets"),
(not args.skip_migrate, "Migrate mods"),
(not args.skip_fetch, "Fetch mods"),
(not args.skip_link, "Link mods"),
]
active_count = sum(1 for run, _ in steps if run)
step_num = 0
if args.skip_parse and args.skip_compare and args.skip_fetch and args.skip_link:
if (args.skip_parse and args.skip_compare and args.skip_migrate
and args.skip_fetch and args.skip_link):
print("All steps skipped — nothing to do.")
sys.exit(0)
@@ -201,6 +220,7 @@ def main() -> None:
(cfg,),
(cfg,),
(cfg,),
(cfg,),
None, # handled separately
]):
if not run:
@@ -221,6 +241,8 @@ def main() -> None:
step_parse(cfg)
elif name == "Compare presets":
step_compare(cfg)
elif name == "Migrate mods":
step_migrate(cfg)
elif name == "Fetch mods":
step_fetch(cfg)