Skip to content

lextpf/salma

Repository files navigation

salma

Wizardless FOMOD installer, processor, and selection inference engine

🎀 Features | 💃 Quick Start | 📘 Documentation | 🤝 Contributing

ReactJS Crow Mo2 Tailwind Vite CMake License
Quality Gate Status Maintainability Rating Reliability Rating
build tests
Sponsor

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.


Preview

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 >>
 *
 * ============================================================================================== */

Features

Interface

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
Loading

FOMOD Processing

---
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
Loading
  • 📄 XML Parser - Parses fomod/ModuleConfig.xml for 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.)

Inference Engine

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.ini when 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 SelectAny caps (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

MO2 Integration

  • 🐍 Python Plugin - mo2-salma.py loads 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.bat and purge.bat scripts for plugin lifecycle management

Archive Support

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)

Additional Capabilities

  • 🌐 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

Scope

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.

Limits

The deliberate guardrails are:

  • 📦 256 MiB per-archive-entry cap (kMaxEntrySize in ArchiveService.cpp) - guards against zip / 7z decompression bombs during in-memory reads.
  • 📤 512 MiB upload cap (kMaxUploadBytes in InstallationController::handle_upload) - oversized multipart uploads return HTTP 413 before any temp-file write.
  • 🛡️ Path-traversal rejection in ArchiveService::extract_with_libarchive and extract_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 a cmd.exe` child process.
  • Whitelist regex on run_tests args - ^[a-zA-Z0-9 _\-\.]*$; quotes and path separators are excluded, and the literal .. substring is rejected even within the whitelist.

Technology Stack

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)

Quick Start

Prerequisites

  • 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 the x64-windows-static-md triplet; the manifest in vcpkg.json pins 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)

Building

# 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.exe

Output:

  • 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 only

build.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).

Frontend development

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=0

Important

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.

Deploying to MO2

# Copy DLL + Python plugin to your MO2 instance
.\deploy.bat

deploy.bat resolves the target as <mods_path>/../../MO2/plugins/, with SALMA_DEPLOY_PATH overriding the computed location. purge.bat removes them again.

Configuration & Environment

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.

Testing

There are two test layers; treat them as separate workflows.

C++ unit tests

.\test.bat

Builds 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.

Round-trip integration tests (Python)

python test_all.py                       # walks every mod under SALMA_MODS_PATH
python test_one.py <archive> <mod>   # single-mod variant for debugging

test_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.

Architecture

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
Loading

The three artifacts

  • 📦 mo2-salma.dll - the headless library. Built from the mo2-core CMake target. Has no HTTP, no UI, no Crow dependency. Mod Organizer 2 loads it through mo2-salma.py (a Python plugin) using ctypes and calls C-linkage exports like install(), inferFomodSelections(), and installWithConfig(). Use this when you want MO2-adjacent FOMOD selection inference or replay without the wizard.

  • 🌐 mo2-server.exe - the HTTP shell. Built from the mo2-server CMake target. Links the same mo2-core library 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 from web/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 from web/src/. Pure HTML/CSS/JS - knows nothing about C++. Talks to mo2-server.exe over fetch('/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.

Two ways salma is used

Path A - inside Mod Organizer 2 (no server, no browser).

  1. deploy.bat copies mo2-salma.dll and mo2-salma.py into <MO2 instance>/plugins/.
  2. MO2 starts and Python loads mo2-salma.py.
  3. The plugin loads the DLL via ctypes.CDLL("mo2-salma.dll").
  4. When the user processes a FOMOD archive, the plugin calls install() (or inferFomodSelections() for batch scans) directly into mo2-core.
  5. 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.

  1. The user runs mo2-server.exe (port 5000).
  2. The browser opens http://localhost:5000 and the server's static handler returns web/dist/index.html.
  3. The React SPA loads, then calls endpoints like POST /api/installation/upload for file uploads or GET /api/mo2/fomods/scan/status for background-job progress.
  4. The Crow controllers in mo2-server translate those requests into calls into mo2-core (the same calls the DLL exports).
  5. 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-to-purpose drill-down

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

Project Structure

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

Documentation

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 build

The site is output to site/ and can be served locally with mkdocs serve.

Contributing

Contributions are welcome! Please read the Contributing Guidelines before submitting pull requests.

Development Workflow

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests and ensure the build passes
  5. Commit with descriptive messages
  6. Push to your fork and open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

About

Wizardless FOMOD-aware mod installation and FOMOD inference for Mo2, backed by libarchive, bit7z, pugixml and Crow HTTP hosting React web.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors