Tran G. (Revernomad) Khoa b24828ac68 docs: update CLAUDE.md, README, and Vietnamese guide for migration step
- CLAUDE.md: document migrator.py algorithm and junction-removal rationale
- README.md: pipeline now 5 steps, add --skip-migrate flag, add migrator.py
  to folder structure, update test count 142 -> 158
- docs/huong-dan-su-dung.md: 5-step pipeline table, new glossary entry,
  updated footer version note
2026-04-14 15:11:39 +07:00
2026-04-08 15:33:58 +07:00
2026-04-08 15:33:58 +07:00
2026-04-08 15:33:58 +07:00
2026-04-08 15:33:58 +07:00
2026-04-08 15:33:58 +07:00

arma-modlist-tools

Python toolchain for managing Arma 3 mod presets: parse launcher exports, compare presets, download mods from a Caddy file server, and create junction/symlink links to an Arma 3 Server installation.


Quick Reference

# First time setup
cp config.template.json config.json   # fill in server URL + credentials + arma_dir
python check_deps.py                  # verify dependencies

# Day-to-day: full pipeline
python run.py                         # parse → compare → migrate → download → link

# GUI (recommended)
python gui.py

# Maintenance
python clean_orphans.py --dry-run     # find stale mod folders from old presets
python update_mods.py                 # re-download changed files (size-check)
python sync_missing.py                # retry mods that were absent from server
python check_names.py --fix           # fix folder name mismatches

# Testing
python test_suite.py                  # 158 tests (network tests auto-skip if offline)

Table of Contents

  1. Prerequisites
  2. Installation
  3. Configuration
  4. Quick Start — Full Pipeline
  5. Individual Scripts
  6. Migrating Existing Mods
  7. Folder Structure
  8. Moving to a New Device
  9. Running Tests

Prerequisites

Requirement Version Notes
Python >= 3.9 python --version
requests any pip install requests
tqdm any pip install tqdm
customtkinter any pip install customtkinter (GUI only)
Windows or Linux Windows uses junctions, Linux uses symlinks

Run the dep checker to confirm everything is ready:

python check_deps.py

Installation

# Clone the repo
git clone https://git.revoluxiant.io.vn/revernomad17/arma-modlist-tools.git
cd arma-modlist-tools

# Install dependencies
pip install -r requirements.txt

# Copy config template and fill in your credentials/paths
cp config.template.json config.json

Edit config.json — see Configuration below.


Configuration

All scripts read a single config.json in the project root. Copy the template and fill in your values:

{
  "server": {
    "base_url": "https://your-caddy-server/arma3mods/",
    "username": "your_username",
    "password": "your_password"
  },
  "paths": {
    "arma_dir":    "C:\\Path\\To\\Arma 3 Server",
    "downloads":   "downloads",
    "modlist_html": "modlist_html",
    "modlist_json": "modlist_json"
  }
}
Key Description
server.base_url Root URL of your Caddy file server. Must have a trailing slash.
server.username HTTP Basic Auth username.
server.password HTTP Basic Auth password.
paths.arma_dir Absolute path to the Arma 3 Server directory where links will be created.
paths.downloads Where mod files are downloaded locally. Relative to project root.
paths.modlist_html Folder containing exported Arma 3 Launcher preset .html files.
paths.modlist_json Folder where parsed/compared JSON files are saved.

Note: config.json is in .gitignore because it contains credentials. Never commit it.


Quick Start — Full Pipeline

Place your Arma 3 Launcher preset exports (.html) into the modlist_html/ folder, then run:

python run.py

This runs all five steps in sequence:

Step 1/5: Parse presets       — modlist_html/*.html  ->  modlist_json/*.json
Step 2/5: Compare presets     — produces modlist_json/comparison.json
Step 3/5: Migrate mod groups  — moves existing folders to match new group assignments
Step 4/5: Fetch mods          — downloads from server  ->  downloads/
Step 5/5: Link mods           — creates junctions/symlinks in Arma 3 Server dir

The migrate step avoids re-downloading mods that already exist on disk when you switch preset versions (e.g. AA_v1). It matches mods by steam ID (via meta.cpp) and moves the folder to the correct group, removing any stale junction first so the link step can re-create it at the new path.

Skip flags

python run.py --skip-fetch --skip-link                    # parse + compare + migrate only
python run.py --skip-parse --skip-compare --skip-fetch    # link only
python run.py --skip-parse --skip-compare --skip-fetch --group shared
python run.py --skip-migrate                              # skip auto-migration
Flag Skips
--skip-parse Step 1 (HTML parsing)
--skip-compare Step 2 (preset comparison)
--skip-migrate Step 3 (mod group migration)
--skip-fetch Step 4 (downloading)
--skip-link Step 5 (linking)
--group GROUP Link step: only link this one group (e.g. shared)

Safe to re-run. Every step is idempotent — existing files are skipped, already-linked mods are skipped.


Individual Scripts

check_deps.py

Verify Python version and required packages before running anything else.

python check_deps.py
  Python         3.9.2         OK
  OS             Windows Server

  requests       2.33.0        OK
  tqdm           4.67.3        OK

  All checks passed. Ready to run.

parse_modlist.py

Parse all .html preset exports in modlist_html/ and save each as JSON in modlist_json/.

How to get the HTML files: In Arma 3 Launcher → Mods → Preset → right-click → Export to HTML. Place the exported files in modlist_html/.

python parse_modlist.py
150th_MW_2026_v1.0.html  ->  modlist_json/150th_MW_2026_v1.0.json  (52 mods)
150th_WW2_2026_V1.0.html ->  modlist_json/150th_WW2_2026_V1.0.json (48 mods)

Output JSON per preset:

{
  "preset_name": "150th_MW_2026_v1.0",
  "source_file": "150th_MW_2026_v1.0.html",
  "mod_count": 52,
  "mods": [
    {
      "name": "CBA_A3",
      "source": "steam",
      "url": "https://steamcommunity.com/sharedfiles/filedetails/?id=450814997",
      "steam_id": "450814997"
    }
  ]
}

compare_modlists.py

Compare all presets in modlist_html/ and produce modlist_json/comparison.json with a shared/unique breakdown.

python compare_modlists.py
Compared: 150th_MW_2026_v1.0, 150th_WW2_2026_V1.0
  Shared mods           : 28
  Unique to 150th_MW... : 24
  Unique to 150th_WW2...: 20
  -> modlist_json/comparison.json

Output comparison.json structure:

{
  "compared_presets": ["Preset_A", "Preset_B"],
  "shared": {
    "mod_count": 28,
    "mods": [...]
  },
  "unique": {
    "Preset_A": { "mod_count": 24, "mods": [...] },
    "Preset_B": { "mod_count": 20, "mods": [...] }
  }
}

Requires: at least 2 .html files in modlist_html/.


fetch_mods.py

Download all mods from the Caddy file server into downloads/, organized by group.

python fetch_mods.py
Loaded comparison: 150th_MW_2026_v1.0, 150th_WW2_2026_V1.0
Total mods to consider: 72

Building server index...  87 mods indexed

[1/70] @ace  ->  downloads/shared/@ace/  (group: shared)
  addons/ace_common/...    5.2 MB  [################] 100%
  ...
  Done  42 downloaded   8.2 MB

Overwrite existing? [s]kip / [o]verwrite: s
  • Mods shared across all presets go into downloads/shared/
  • Preset-unique mods go into downloads/<preset_name>/
  • Saves modlist_json/missing_report.json listing any mods not found on server
  • Prompts once if destination folders already exist

Requires: modlist_json/comparison.json (run compare_modlists.py first).


Manage junction/symlink links between downloads/ and the Arma 3 Server directory.

Check status

python link_mods.py status --group shared
  Group  : shared
  Path   : downloads/shared
  Arma   : C:\...\Arma 3 Server

  Mod                                                  Status
  ----------------------------------------------------------
  @ace                                                 [LINKED]
  @cba_a3                                              [------]

  1 / 2 linked
python link_mods.py link --group shared
python link_mods.py unlink --group shared

Prompts for confirmation before removing links. Removing a link does not delete the mod files in downloads/.

List available groups

Omit --group to see what groups exist:

python link_mods.py status

Windows: Creates NTFS directory junctions (mklink /J). No administrator rights required. Linux: Creates standard symlinks (os.symlink).


report_missing.py

Check which mods from comparison.json are missing from the file server. Saves modlist_json/missing_report.json.

python report_missing.py
Checking server index...  87 mods indexed
Cross-referencing 72 required mods...

  Missing from server (2):

  steam_id        Group                         Name
  --------------- ----------------------------- ----------------------------------------
  2648308937      150th_WW2_2026_V1.0           IFA3 AIO
  463939057       shared                        ACE3

  70 / 72 found on server
  Report saved: modlist_json/missing_report.json

Requires: modlist_json/comparison.json.


sync_missing.py

Re-check the server for mods that were previously missing and download any that have since been added to the server.

python sync_missing.py
Loading missing report: 2 mods previously missing
Re-checking server index...  89 mods indexed

  Newly available: 1 mods
  Still missing  : 1 mods

  [+] @ace  ->  downloads/shared/
     Done  8.2 MB

  Missing report updated: 1 still missing

  Linking newly added mods...
    shared    1 new linked, 27 already linked

Flow:

  1. Loads modlist_json/missing_report.json
  2. Re-checks server index for newly available mods
  3. Downloads newly available mods to the correct group folder
  4. Updates missing_report.json (removes mods now downloaded)
  5. Runs linker for affected groups — existing links are safely skipped

Requires: modlist_json/missing_report.json (run report_missing.py or fetch_mods.py first).


update_mods.py

Re-download mod files that have changed on the server without changing the modlist structure (same mods, updated file versions).

Detection uses file size comparison: a file is re-downloaded if it is missing locally or its local size differs from the server-reported size. Use --force to re-download everything unconditionally.

python update_mods.py                       # check all groups and mods
python update_mods.py --group shared        # one group only
python update_mods.py --mod @ace            # one specific mod
python update_mods.py --force               # re-download all files
python update_mods.py --force --group shared
Building server index...  87 mods indexed

  Mode: size-check
  Checking 28 mod folder(s)...

  [=] @cba_a3     shared     4 files   up-to-date
  [+] @ace        shared     42 files  3 updated  (8.2 MB)
  [=] @rhsusaf    150th_MW   88 files  up-to-date

  Total: 134 files checked, 3 updated, 8.2 MB downloaded

No re-linking needed. Junctions already point at the downloads/ folders, so updated files are immediately visible to the Arma 3 Server.


check_names.py

Diagnostic tool that compares mod folder names in downloads/ against the server's canonical names, and optionally fixes mismatches.

Status codes:

Status Meaning
OK Disk folder name matches server name exactly
MISMATCH Disk name differs from server canonical name (rename needed)
ID_COLLISION Local meta.cpp has a wrong publishedid that belongs to a different mod
NOT_ON_SERVER No matching folder found on server

Report only

python check_names.py
python check_names.py --group shared    # limit to one group
  Disk name                                     Group                     Status / Server name
  --------------------------------------------  ------------------------  ---------------------------------------------------
  @ace                                          shared                    OK  (@ace)
  @CBA_A3                                       shared                    MISMATCH  ->  @cba_a3
  @NIArms All in One- ACE Compatibility         150th_MW_2026_v1.0        ID_COLLISION  @Realistic Ragdoll Physics (local id: 1234567)
  @150th Languard Zeus Tools                    150th_WW2_2026_V1.0       NOT_ON_SERVER

  80 OK,  1 mismatch,  1 id_collision,  1 not on server

  Run with --fix to rename mismatched folders and --fix-ids to correct wrong steam IDs in meta.cpp.

Fix mismatched folder names

Renames MISMATCH folders on disk to the server's canonical name and updates the arma_dir junction to match.

python check_names.py --fix
  [+] @CBA_A3  ->  @cba_a3

Fix wrong steam IDs

Corrects the publishedid in local meta.cpp files for ID_COLLISION entries. Uses comparison.json as the authoritative source of steam IDs (which came from the Steam Workshop URLs in your HTML presets).

python check_names.py --fix-ids
  [+] @NIArms All in One- ACE Compatibility   meta.cpp: 1234567 -> 9876543

Both fixes at once:

python check_names.py --fix --fix-ids

Requires: modlist_json/comparison.json for --fix-ids (run run.py --skip-fetch --skip-link first).


clean_orphans.py

Find and optionally delete orphaned mod folders — downloads/{group}/@ModName folders that are no longer referenced in comparison.json. These accumulate when you switch presets and re-run the pipeline without cleaning up old downloads.

python clean_orphans.py               # list orphans, prompt for confirmation
python clean_orphans.py --dry-run     # list orphans, do not delete
python clean_orphans.py --yes         # list and delete without prompting
  Group                         Folder                            Size
  ----------------------------  --------------------------------  ----------
  shared                        @OldMod                           124.5 MB
  150th_WW2_2026_V1.0           @SomeMod                          88.2 MB

  2 orphan(s) found — 212.7 MB total

  Delete all orphans? [y/N]
  • Matches by normalized name (same logic as the fetcher), so spacing/capitalization differences are handled correctly
  • Junction-safe: uses os.rmdir() on junctions rather than shutil.rmtree to avoid deleting target files

Requires: modlist_json/comparison.json (run run.py --skip-fetch --skip-link first).

The same functionality is available as the Clean Orphans tab in the GUI.


run.py

Orchestrator that chains all five pipeline steps. Described in Quick Start above.


gui.py

Launch the graphical interface for the toolchain.

python gui.py

Opens a CustomTkinter desktop window with a sidebar navigation and the following views:

View Purpose
Dashboard Overview: status, quick stats, recent activity
Mods Browse and manage downloaded mods by group
Tools Link/unlink, rename, sync missing, check server, clean orphans
Logs Real-time log output from pipeline operations
Settings Edit config.json (server URL, paths, credentials)

The Tools view has five tabs: Check Names, Update Mods, Link Mods, Sync Missing / Report Missing, and Clean Orphans (find and delete stale mod folders from old presets).

On first launch (no config.json), a setup wizard walks you through creating one.

Requires: customtkinter (pip install customtkinter).


Migrating Existing Mods

If the Arma 3 Server already has mods installed and you want to bring them under this toolchain without re-downloading:

Step 1 — Generate comparison.json:

python run.py --skip-fetch --skip-link

Step 2 — Check for name mismatches before linking:

python check_names.py

Step 3 — Fix any issues:

python check_names.py --fix          # rename mismatched folders
python check_names.py --fix-ids      # fix wrong steam IDs in meta.cpp

Step 4 — Create links:

python run.py --skip-parse --skip-compare --skip-fetch

Instead of downloading, you can create junctions from downloads/{group}/@ModName pointing to wherever the mods already live on disk:

mklink /J downloads\shared\@ace "C:\existing\path\@ace"

Then run the link step normally.


Folder Structure

arma-modlist-tools/
|
|- arma_modlist_tools/        # Python package (library code)
|   |- __init__.py            # Public exports
|   |- parser.py              # HTML preset parser
|   |- compare.py             # Preset comparison
|   |- fetcher.py             # Caddy server downloader
|   |- linker.py              # Junction/symlink manager
|   |- reporter.py            # Missing-mod report builder
|   |- cleaner.py             # Orphan folder detection
|   |- migrator.py            # Mod group migration (move folders to match comparison.json)
|   |- config.py              # config.json loader
|   |- compat.py              # OS detection + encoding fix
|
|- gui/                       # GUI package (CustomTkinter desktop app)
|   |- __init__.py            # Theme setup + run_app() entry point
|   |- app.py                 # Main window, view management, pipeline runner
|   |- wizard.py              # First-run config setup wizard
|   |- _constants.py          # Window size, color, path constants
|   |- _io.py                 # stdout/stderr → thread-safe queue for live logs
|   |- views/
|       |- dashboard.py       # Overview view
|       |- mods.py            # Mod browser view
|       |- tools.py           # Tool actions view
|       |- logs.py            # Real-time log view
|       |- settings.py        # Config editor view
|
|- modlist_html/              # INPUT: put your .html preset exports here
|   |- MyPreset_A.html
|   |- MyPreset_B.html
|
|- modlist_json/              # OUTPUT: generated JSON files (gitignored)
|   |- MyPreset_A.json
|   |- MyPreset_B.json
|   |- comparison.json
|   |- missing_report.json
|
|- downloads/                 # OUTPUT: downloaded mod files (gitignored)
|   |- shared/
|   |   |- @ace/
|   |   |- @cba_a3/
|   |- MyPreset_A/
|       |- @rhsusaf/
|
|- config.json                # YOUR config (gitignored — contains credentials)
|- config.template.json       # Template to copy from
|- requirements.txt
|- selection.json             # GUI selection state (persisted between sessions)
|
|- gui.py                     # GUI entry point
|- run.py                     # Orchestrator (parse + compare + fetch + link)
|- parse_modlist.py           # Step 1 standalone
|- compare_modlists.py        # Step 2 standalone
|- fetch_mods.py              # Step 3 standalone
|- link_mods.py               # Link management (status/link/unlink)
|- report_missing.py          # Missing mod report
|- sync_missing.py            # Sync newly available missing mods
|- update_mods.py             # Re-download updated mod files
|- check_names.py             # Diagnose and fix folder name / steam_id issues
|- clean_orphans.py           # Find and delete orphaned mod folders
|- check_deps.py              # Dependency checker
|- test_suite.py              # Test suite (158 tests)

Moving to a New Device

  1. Clone the repo:

    git clone https://git.revoluxiant.io.vn/revernomad17/arma-modlist-tools.git
    cd arma-modlist-tools
    
  2. Install dependencies:

    pip install -r requirements.txt
    
  3. Create your config:

    cp config.template.json config.json
    # Edit config.json with correct arma_dir and server credentials
    
  4. Add your preset exports: Copy your .html files from the Arma 3 Launcher into modlist_html/.

  5. Verify and run:

    python check_deps.py
    python run.py
    

The downloads/ folder can be several GB. On a second device you can either let run.py re-download everything, or copy the downloads/ folder manually and run python run.py --skip-fetch to skip downloading and just create links.


Running Tests

The test suite covers all modules with 158 tests. Network tests auto-skip when the server is unreachable.

python test_suite.py
------------------------------------------------------------
  compat              11 tests
  config               5 tests
  parser               9 tests
  compare              8 tests
  fetcher             24 tests   (pure functions + mock)
  reporter             8 tests
  linker              12 tests   (uses temp dirs)
  __init__             2 tests
  check_names         16 tests
  integration          2 tests
  gui._io             11 tests   (QueueWriter, no GUI required)
  cleaner              8 tests
  e2e — clean_orphans  6 tests   (subprocess CLI)
  coverage gaps       23 tests   (mocked platform branches)
  gui.views.mods       8 tests   (_find_folder matching)
  migrator             7 tests   (group migration logic)
  live server          9 tests   (skipped if server unreachable)
------------------------------------------------------------
  Results: 158 passed, 0 failed, 0 skipped  (158 total)
Description
No description provided
Readme 540 KiB
Languages
Python 86.6%
HTML 13.4%