Wizardless FOMOD installer, processor, and selection inference engine
🎀 Features | 💃 Quick Start | 📘 Documentation | 🤝 Contributing
A FOMOD installer, processor, and inference engine built with C++23 and powered by Crow and React. It pairs a C DLL that handles archive extraction, FOMOD XML processing, install replay, and selection inference with a Crow HTTP server and React web interface for interactive FOMOD workflows. salma integrates with Mod Organizer 2 via a Python plugin, automatically inferring FOMOD selections by comparing archives against installed files - no wizard clicks required.
salma is intentionally scoped to FOMODs. It is useful for reproducing existing FOMOD choices and preparing future installs, and it can write the resulting file set to a mod directory, but it is not a general-purpose mod installer or a replacement for MO2's full installer pipeline. It does not attempt to process every mod format, scripted installer, or package layout that Mod Organizer 2 can handle.
Important
Early release - salma is under active development and has been tested against:
- Nolvus Ascension 6.0.20 with 350 FOMODs.
- Other mods should work without issues - if you run into a problem, please report it.
/* ============================================================================================== *
*
* :::::::: ::: ::: :::: :::: ::: ⢠⣤⣤⣀ ⠀⠀⠀⠀⠀⠀ ⣀⣤⣤⡄
* :+: :+: :+: :+: :+: +:+:+: :+:+:+ :+: :+: ⢸⣿⣿⣿⣿⣦⣄⣀⣠⣴⣿⣿⣿⣿⡇⠀⊹
* +:+ +:+ +:+ +:+ +:+ +:+:+ +:+ +:+ +:+ ⣸⣿⣿⣿⣿⣿⡽⣿⣯⣿⣿⣿⣿⣿⣇
* +#++:++#++ +#++:++#++: +#+ +#+ +:+ +#+ +#++:++#++: ⢻⣿⣿⣿⠿⣻⣵⡟⣮⣟⠿⣿⣿⣿⡟
* +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+ ⠀⠀⠀⠀⣼⣿⡿ ⠀⢿⣿⣷⡀
* #+# #+# #+# #+# #+# #+# #+# #+# #+# ⊹⠀⣠⣾⣿⣿⠃ ⠀⠈⢿⣿⣿⣦⡀
* ######## ### ### ########## ### ### ### ### ⠀⠈⠉⠹⡿⠁⠀⠀⠀⠀⠈⢻⡇⠉⠉
*
* << F O M O D E N G I N E >>
*
* ============================================================================================== */
salma ships with a C DLL for direct integration, a REST API for programmatic access, and a React web UI for interactive FOMOD processing and install replay.
---
config:
look: handDrawn
theme: mc
themeVariables:
fontSize: 18px
layout: elk
---
graph LR
classDef dll fill:#134e3a,stroke:#10b981,color:#e2e8f0
classDef api fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
classDef web fill:#2e1f5e,stroke:#8b5cf6,color:#e2e8f0
Web["⚛️ React Frontend<br/>Interactive UI"]:::web
API["🌐 REST API<br/>Programmatic access"]:::api
DLL["📦 C DLL<br/>Direct integration"]:::dll
Web --> API --> DLL
---
config:
look: handDrawn
theme: mc
themeVariables:
fontSize: 18px
layout: elk
---
graph LR
classDef parse fill:#7c2d12,stroke:#f97316,color:#fef3c7
classDef eval fill:#4c1d95,stroke:#e879f9,color:#e2e8f0
classDef ops fill:#064e3b,stroke:#34d399,color:#e2e8f0
classDef detect fill:#713f12,stroke:#facc15,color:#fef9c3
P["📄 XML Parser"]:::parse
D["🧩 Dependency Evaluator"]:::eval
F["📂 File Operations"]:::ops
S["🔎 Structure Detector"]:::detect
P --- D --- F --- S
- 📄 XML Parser - Parses
fomod/ModuleConfig.xmlfor installation steps and options - 🧩 Dependency Evaluator - Resolves flag-based and file-based FOMOD dependencies
- 📂 File Operations - Priority-sorted file copy, folder creation, and patching
- 🔎 Structure Detector - Identifies candidate content roots inside archives (meshes/, textures/, SKSE/, etc.)
salma's inference service compares an archive's FOMOD options against an already-installed mod to determine which selections were originally chosen. Naively walking every permutation of steps, groups, and flags is intractable on large installers, so the pipeline is a layered solver:
- ⚡ Tier-1 shortcut - reads cached fomod-plus JSON directly from
meta.iniwhen present and skips the rest of the pipeline. Hits on a large fraction of mods produced by an MO2 user who already ran the wizard once. - 🧮 Constraint propagator - a deterministic pre-pass that narrows each group's plugin domain using plugin-type rules (
Required/NotUsable/SelectAll), file-evidence elimination, and cardinality. Groups that resolve to a single combination skip the solver entirely. - 🧠 5-phase CSP solver - greedy -> iterative local search -> independent-component decomposition -> residual repair (m=0, e=0) -> mismatch-focused search -> global fallback with widening
SelectAnycaps (narrow -> medium -> full). Each phase short-circuits on an exact reproduction. - 🪞 Forward simulator - executes a candidate selection against the IR (without writing to disk) and scores the simulated file tree against the target with a five-key lex order:
(missing, extra, size_mismatch, hash_mismatch, -reproduced). - 📋 Returns a JSON configuration that reproduces the original install
- 🧪 Powers the round-trip test suite for validation
- 🐍 Python Plugin -
mo2-salma.pyloads the DLL via ctypes and exposes tools inside MO2 - 🔬 Scan FOMOD Choices - Batch-scans all installed mods for FOMOD selections
- 📁 Centralized Output - Reinstalled mods go to a dedicated "Salma FOMODs Output" folder
- ⚡ Deploy & Purge -
deploy.batandpurge.batscripts for plugin lifecycle management
These are archive container formats salma can read while processing FOMOD packages. Archive support does not mean salma implements every non-FOMOD installer path or mod-package convention supported by MO2.
| Format | Backend |
|---|---|
| 7z | bit7z (native 7-Zip SDK wrapper) |
| ZIP | libarchive |
| RAR | libarchive |
| TAR.* | libarchive (bzip2, lz4, lzma, zstd) |
- 🌐 REST API - Full programmatic access for custom FOMOD tooling and automation
- 🎨 Web UI - React SPA with dark/light theme, served directly by the Crow backend
- 📝 Logging - Unified thread-safe logger with subsystem tags; 10 MiB rotation, up to 3 archived files
- 🧪 Round-Trip Testing - Infer selections, replay a FOMOD install, and diff against the original mod
salma focuses on FOMOD packages: parsing fomod/ModuleConfig.xml, evaluating dependencies, inferring selected options from an existing install, and replaying those choices for future installs. It has a simple non-FOMOD copy fallback for archives with a recognizable content root, but that fallback is not a claim of full mod-manager compatibility. MO2 remains the broader mod manager and supports installer formats, scripted flows, and package edge cases outside salma's intended scope.
The deliberate guardrails are:
- 📦 256 MiB per-archive-entry cap (
kMaxEntrySizeinArchiveService.cpp) - guards against zip / 7z decompression bombs during in-memory reads. - 📤 512 MiB upload cap (
kMaxUploadBytesinInstallationController::handle_upload) - oversized multipart uploads return HTTP 413 before any temp-file write. - 🛡️ Path-traversal rejection in
ArchiveService::extract_with_libarchiveandextract_filtered- any entry whose canonical destination falls outside the extraction root is dropped before write. - 🚫 Shell-metachar sanitization in
Mo2Controller::deploy_plugin/purge_plugin- blocks& | > < ^ % ! ( ) " ; ' \`` in paths before they flow into acmd.exe` child process. - ✅ Whitelist regex on
run_testsargs -^[a-zA-Z0-9 _\-\.]*$; quotes and path separators are excluded, and the literal..substring is rejected even within the whitelist.
| Component | Technology |
|---|---|
| Language | C++23 |
| HTTP Framework | Crow (with CORS handler) |
| Frontend | React 18 + TypeScript + Vite |
| Styling | Tailwind CSS 4 |
| XML Parsing | pugixml |
| JSON | nlohmann-json |
| Archive | libarchive + bit7z |
| Formatting | clang-format (Google-based) |
| Build System | CMake 3.20+ |
| Package Manager | vcpkg |
| Documentation | Doxide + MkDocs |
| Plugin | Python 3 (MO2 ctypes bridge) |
| CI/CD | GitHub Actions |
| Platform | Windows 10/11 (64-bit) |
- Windows 10/11 (64-bit)
- Visual Studio 2022 (MSVC v143, C++23)
- CMake 3.20+
- vcpkg - set
VCPKG_ROOT; the default CMake preset reads it directly. The build uses thex64-windows-static-mdtriplet; the manifest invcpkg.jsonpins versions, so the first configure may take several minutes while vcpkg builds the cache. - Node.js (for building the React frontend)
- Python 3 (for MO2 plugin and documentation post-processing)
- clang-format (optional, for code formatting - CI enforces it)
- doxide + mkdocs (optional, for API docs generation)
# 1. Clone the repository
git clone https://github.com/lextpf/salma.git
cd salma
# 2. Build (format + configure + compile + docs)
.\build.bat
# 3. Run the server
.\build\bin\Release\mo2-server.exeOutput:
- DLL:
build/bin/Release/mo2-salma.dll - Server:
build/bin/Release/mo2-server.exe - Tests:
build/bin/Release/salma_tests.exe
For a faster iteration loop, configure once and then scope the build to a single target:
cmake --preset default # configure (reads VCPKG_ROOT)
cmake --build build --config Release --target mo2-core # DLL only
cmake --build build --config Release --target mo2-server # EXE only
cmake --build build --config Release --target salma_tests # unit-test binary onlybuild.bat runs clang-format, then configure, then a full Release build, and finally the optional doc pipeline (skipped silently if doxide and mkdocs are missing).
The React SPA in web/ builds into web/dist/ and is served by mo2-server.exe. For interactive development use Vite's dev server, which proxies /api/* requests through to the C++ backend:
cd web
npm install # one-time
npm run dev # Vite dev server on :3000, proxies /api -> :5000
npm run build # outputs to web/dist/, served by Crow at :5000
npm run lint # eslint, max-warnings=0Important
Start mo2-server.exe on :5000 before npm run dev. Vite's proxy assumes the backend is already up; if it isn't, every /api/* request the SPA makes will fail with a proxy error and the UI will look broken.
# Copy DLL + Python plugin to your MO2 instance
.\deploy.batdeploy.bat resolves the target as <mods_path>/../../MO2/plugins/, with SALMA_DEPLOY_PATH overriding the computed location. purge.bat removes them again.
salma reads a small set of environment variables at runtime. None are required for a default Quick Start build, but they unlock specific deployment / dev workflows.
| Variable | Purpose | Read by |
|---|---|---|
VCPKG_ROOT |
vcpkg toolchain path used by the default CMake preset. | CMakePresets.json |
SALMA_BIND_ADDR |
Override the default loopback bind. Non-loopback values log a security warning. | main.cpp |
SALMA_MODS_PATH |
MO2 mods directory. Used by the deploy fallback chain and the round-trip tests. | Mo2Helpers.cpp, test_all.py |
SALMA_DEPLOY_PATH |
Override the <MO2 instance>/MO2/plugins deploy target used by deploy.bat. |
Mo2Helpers.cpp, deploy.bat |
SALMA_DOWNLOADS_PATH |
Lookup root for resolve_mod_archive when installationFile is relative. |
FomodArchiveResolver.cpp |
The mods/deploy/downloads trio above is required for the round-trip integration tests; see Testing. A persistent setup helper exists for them:
.\scripts\setup-env.bat # sets the three SALMA_* vars via setx (persists across shells)The runtime-configurable settings (currently just the MO2 mods directory) are persisted to salma.json next to mo2-server.exe by ConfigService. The dashboard's PUT /api/config endpoint writes it via a write-then-rename so a partial write cannot corrupt the file. The FOMOD output directory is derived from the mods path as <mo2ModsPath>/Salma FOMODs Output/fomods/ and is not separately configurable.
There are two test layers; treat them as separate workflows.
.\test.batBuilds the salma_tests target and runs salma_tests.exe. Sources live under tests/. The build is the standard CMake target, so cmake --build build --config Release --target salma_tests and running the binary directly work too.
python test_all.py # walks every mod under SALMA_MODS_PATH
python test_one.py <archive> <mod> # single-mod variant for debuggingtest_all.py enumerates every mod folder under SALMA_MODS_PATH, infers FOMOD selections through the DLL, reinstalls the result into a temporary directory, and diffs the produced file tree against the original install. Output goes to test.log and logs/salma.log.
Round-trip tests require the three SALMA_* env vars listed in Configuration & Environment. Run scripts\setup-env.bat once to set them via setx (persists across shells); the test scripts no longer fall back to a hardcoded path and exit with a setup hint if any required variable is unset.
salma ships three deployment artifacts that share one core library. The library (mo2-core) holds the FOMOD parsing, inference, and install-replay logic. The artifacts are different shells around that library - one for MO2 (a DLL), one for HTTP (an EXE), and one for the browser (a React SPA served by the EXE). You can use any subset depending on how you want to drive salma.
---
config:
look: handDrawn
theme: mc
themeVariables:
fontSize: 18px
layout: elk
---
graph LR
classDef host fill:#1e1e2e,stroke:#94a3b8,color:#e2e8f0,stroke-dasharray:6 4
classDef artifact fill:#1e3a5f,stroke:#3b82f6,color:#e2e8f0
classDef core fill:#134e3a,stroke:#10b981,color:#e2e8f0
classDef user fill:#2e1f5e,stroke:#8b5cf6,color:#e2e8f0
User["👤 User"]:::user
subgraph MO2["🧩 Mod Organizer 2 process"]
Plugin["🐍 mo2-salma.py<br/>Python plugin"]:::host
DLL["📦 mo2-salma.dll<br/>C-API exports"]:::artifact
end
subgraph Browser["🌍 Web browser"]
SPA["⚛️ React SPA<br/>web/dist"]:::artifact
end
subgraph SrvHost["🖥️ mo2-server.exe"]
Server["🌐 Crow HTTP server<br/>REST endpoints"]:::artifact
Static["📄 Static file handler"]:::artifact
end
subgraph Core["🛠️ mo2-core"]
Archive["📦 Archive extraction"]:::core
Parser["🧩 FOMOD parser"]:::core
Infer["🧠 Inference engine"]:::core
FileOps["📂 File operations"]:::core
Log["📋 Logger"]:::core
end
User --> Plugin
User --> SPA
Plugin -- ctypes --> DLL
SPA -- "HTTP /api" --> Server
Static -- "serves" --> SPA
Server --- Static
DLL --> Core
Server --> Core
-
📦
mo2-salma.dll- the headless library. Built from themo2-coreCMake target. Has no HTTP, no UI, no Crow dependency. Mod Organizer 2 loads it throughmo2-salma.py(a Python plugin) usingctypesand calls C-linkage exports likeinstall(),inferFomodSelections(), andinstallWithConfig(). Use this when you want MO2-adjacent FOMOD selection inference or replay without the wizard. -
🌐
mo2-server.exe- the HTTP shell. Built from themo2-serverCMake target. Links the samemo2-corelibrary that the DLL exports, plus the Crow HTTP framework on top. Exposes the install / infer / scan / status / log endpoints under/api/*and serves the React SPA fromweb/dist/at/. Listens on:5000. Use this when you want a graphical interface or programmatic REST access. -
⚛️
web/dist/- the browser front-end. Built with Vite fromweb/src/. Pure HTML/CSS/JS - knows nothing about C++. Talks tomo2-server.exeoverfetch('/api/...'). Use this when you want to drive an installation interactively, browse FOMOD JSON files, or watch logs in real time.
Note
The DLL and the EXE are different binaries built from the same source tree. They share mo2-core so that improving FOMOD inference once benefits both the MO2 plugin and the web UI - there is no duplicated logic to keep in sync.
Path A - inside Mod Organizer 2 (no server, no browser).
deploy.batcopiesmo2-salma.dllandmo2-salma.pyinto<MO2 instance>/plugins/.- MO2 starts and Python loads
mo2-salma.py. - The plugin loads the DLL via
ctypes.CDLL("mo2-salma.dll"). - When the user processes a FOMOD archive, the plugin calls
install()(orinferFomodSelections()for batch scans) directly intomo2-core. - Results return as JSON or path strings; the plugin renders them in MO2's tools menu.
Path B - the standalone server with the web UI.
- The user runs
mo2-server.exe(port 5000). - The browser opens
http://localhost:5000and the server's static handler returnsweb/dist/index.html. - The React SPA loads, then calls endpoints like
POST /api/installation/uploadfor file uploads orGET /api/mo2/fomods/scan/statusfor background-job progress. - The Crow controllers in
mo2-servertranslate those requests into calls intomo2-core(the same calls the DLL exports). - Results come back as JSON; the SPA renders progress, logs, and FOMOD trees.
Both paths converge in mo2-core. The DLL exposes it as a flat C ABI; the server exposes it as REST endpoints.
| File | Purpose |
|---|---|
main.cpp |
Crow HTTP server entry point |
InstallationService |
FOMOD install/replay orchestrator |
ArchiveService |
Archive extraction (libarchive + bit7z) |
FomodService |
FOMOD XML parsing and install replay |
FomodInferenceService |
Infers FOMOD selections from installed files |
FomodDependencyEvaluator |
Evaluates FOMOD flag and file dependencies |
FileOperations |
Priority-sorted file copy and patching |
ModStructureDetector |
Detects candidate archive content roots |
CApi |
C-linkage DLL exports for ctypes |
Logger |
Thread-safe logging with callback support |
ConfigService |
Configuration management |
salma/
|-- .github/ # CI workflows
| +-- workflows/ # build.yml, sonar.yml, test.yml
|-- src/ # C++ source code
| |-- main.cpp # Crow HTTP server entry point
| |-- CApi.h/cpp # C-linkage DLL API (ctypes)
| |-- Export.h # MO2_API export macro
| |-- Types.h # Shared type definitions
| |-- Utils.h/cpp # Shared string/path helpers
| |-- BackgroundJob.h # Async job runner
| |-- Logger.h/cpp # Thread-safe logging
| |-- ArchiveService.h/cpp # Archive extraction
| |-- FileOperations.h/cpp # Queued file operations
| |-- ModStructureDetector.h/cpp # Archive content-root detection
| |-- FomodArchiveResolver.h/cpp # Resolves mod source archive paths
| |-- FomodService.h/cpp # FOMOD processing and install replay
| |-- FomodDependencyEvaluator.h/cpp # FOMOD dependency evaluation
| |-- FomodInferenceService.h/cpp # Selection inference engine (orchestrator)
| |-- FomodIR.h # FOMOD IR types (header-only)
| |-- FomodIRParser.h/cpp # XML to IR parser
| |-- FomodCSP*.h/cpp # CSP solver, options, precompute, types
| |-- FomodPropagator.h/cpp # Constraint propagator
| |-- FomodForwardSimulator.h/cpp # Forward-simulates installs against the IR
| |-- FomodInferenceAtoms.h/cpp # Atom-level inference helpers
| |-- FomodAtom.h # Atom type definitions
| |-- InstallationService.h/cpp # Main orchestrator
| |-- InstallationController.h/cpp # REST endpoint handlers
| |-- Mo2Controller.h/cpp # MO2 dashboard controller (shared state)
| |-- Mo2...Controller.cpp # Per-subsystem endpoints
| |-- Mo2Helpers.h/cpp # Shared helpers for MO2 controllers
| |-- ConfigService.h/cpp # Configuration management
| |-- MultipartHandler.h/cpp # Form data parsing
| +-- StaticFileHandler.h/cpp # SPA serving
|-- web/ # React frontend
| |-- src/ # TypeScript source
| |-- dist/ # Built SPA (served by Crow)
| |-- package.json # Dependencies
| +-- vite.config.ts # Dev proxy to :5000
|-- tests/ # GoogleTest C++ unit tests
|-- scripts/ # MO2 plugin & utilities
| |-- mo2-salma.py # MO2 Python plugin
| |-- setup-env.bat # Persist SALMA_* env vars (one-time setup)
| |-- common.py # Shared utilities
| |-- install.py # Installation helper
| |-- compare.py # Round-trip diff utility
| |-- scan.py # Mod scanning utility
| |-- _clean_docs.py # Doc post-processing
| +-- _promote_subgroups.py # FOMOD subgroup promotion tool
|-- triplets/ # Custom vcpkg triplet (x64-windows-static-md)
|-- overrides/ # MkDocs theme overrides
|-- logs/ # Runtime logs
| +-- salma.log # Application log
|-- .clang-format # Code formatting rules
|-- .gitignore # Git ignore rules
|-- CMakeLists.txt # Build configuration (mo2-core + mo2-server targets)
|-- CMakePresets.json # Build presets (vcpkg)
|-- vcpkg.json # Dependency manifest
|-- sonar-project.properties # SonarCloud configuration
|-- build.bat # Build pipeline
|-- deploy.bat # Deploy to MO2
|-- purge.bat # Remove plugin & clean output
|-- test.bat # Run C++ unit tests (salma_tests.exe)
|-- test_all.py # Round-trip test runner (Python)
|-- test_one.py # Round-trip test for a single mod
|-- test.log # Round-trip test output
|-- fomod_archives.txt # Test archive manifest
|-- doxide.yml # API doc config
|-- mkdocs.yml # Documentation site config
|-- LICENSE # License
|-- CONTRIBUTING.md # Contributor guide
+-- PREVIEW.png # README banner image
API documentation is generated via a three-stage pipeline:
# 1. Generate markdown from C++ headers
doxide build
# 2. Post-process (strip noise, fix formatting)
python scripts/_clean_docs.py
# 3. Build the documentation site
mkdocs buildThe site is output to site/ and can be served locally with mkdocs serve.
Contributions are welcome! Please read the Contributing Guidelines before submitting pull requests.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
Run tests andensure the build passes- Commit with descriptive messages
- Push to your fork and open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- MO - Mod Organizer for managing mod collections
- Crow - C++ HTTP framework
- React - Frontend UI library
- libarchive - Multi-format archive extraction
- bit7z - 7-Zip SDK wrapper
- pugixml - XML parsing
- nlohmann-json - JSON for Modern C++
- vcpkg - C++ package manager
- Doxide - API documentation generator
- MkDocs Material - Documentation theme
- Tailwind CSS - Utility-first CSS framework
- Claude - AI coding assistant by Anthropic
- Codex - AI coding assistant by OpenAI
