Skip to content

ARCHITEKTUR

Marc edited this page Aug 12, 2026 · 3 revisions

Anvil Organizer — Architektur-Dokumentation

Letzte Aktualisierung: 2026-03-21 Zweck: Diese Datei beschreibt wie Anvil aufgebaut ist. Sie ist die verbindliche Referenz für alle Entwickler (CC, Claude, Agents). Bevor Code geändert wird, muss diese Datei gelesen werden.


1. Was ist Anvil?

Anvil Organizer ist ein nativer Linux Mod Manager, inspiriert von MO2 (Mod Organizer 2). Er verwaltet Mods für mehrere Spiele (Cyberpunk 2077, Fallout 4, Witcher 3, RDR2, BG3).

Grundprinzip wie MO2: Mods werden NIEMALS direkt ins Game-Verzeichnis kopiert. Sie liegen in einem eigenen Verzeichnis (.mods/) und werden beim Spielstart per Symlink ins Game-Verzeichnis verlinkt. Nach dem Spielende werden die Symlinks entfernt. Das Game-Verzeichnis bleibt sauber.


2. Unterschied MO2 vs Anvil

Aspekt MO2 (Windows) Anvil (Linux)
Deploy-Mechanismus USVFS (Virtual File System, DLL-Injection) Physische Symlinks + JSON-Manifest
Dateien im Game-Dir Nie (nur virtuell) Symlinks (zeigen auf .mods/)
Frameworks (CET, F4SE) User installiert manuell Anvil kopiert direkt ins Game-Dir
Nach Game-Ende VFS wird automatisch beendet Symlinks bleiben bis Purge

WICHTIG: Anvil kann kein USVFS nutzen (Linux). Stattdessen werden physische Symlinks erstellt. Das Ergebnis ist äquivalent — das Game sieht die Mod-Dateien im richtigen Verzeichnis.


3. Verzeichnisstruktur

Anvil-Instanz (pro Spiel)

~/.anvil-organizer/instances/<Spielname>/
├── .anvil.ini                    # Instanz-Konfiguration
├── .mods/                        # ALLE Mods liegen hier
│   ├── ModName1/                 # Jede Mod = ein Ordner
│   │   ├── archive/pc/mod/       # Mod-Dateien in korrekter Struktur
│   │   │   └── mod_datei.archive
│   │   └── meta.ini              # Mod-Metadaten
│   ├── ModName2/
│   │   ├── r6/scripts/           # Script-Mods
│   │   │   └── script.reds
│   │   └── meta.ini
│   ├── naked_separator/          # Separator (Trenner)
│   │   └── meta.ini
│   └── ...
├── .profiles/                    # Profile
│   ├── modlist.txt               # GLOBALE Mod-Reihenfolge (für alle Profile)
│   ├── Default/
│   │   └── active_mods.json      # Welche Mods aktiv sind (pro Profil)
│   ├── Vanilla/
│   │   └── active_mods.json
│   └── ...
├── .downloads/                   # Heruntergeladene Archive
├── .overwrite/                   # (Placeholder, noch nicht funktional)
├── .deploy_manifest.json         # Tracking aller erstellten Symlinks
├── .modindex.json                # Zwischengespeicherte Dateilisten je Mod
└── categories.json               # Kategorien

Game-Verzeichnis (z.B. Cyberpunk 2077)

~/.local/share/Steam/steamapps/common/Cyberpunk 2077/
├── archive/pc/mod/               # Hier landen Symlinks für .archive-Dateien
│   └── mod_datei.archive → ../../.mods/ModName/archive/pc/mod/mod_datei.archive
├── r6/scripts/                   # Hier landen Symlinks für Scripts
│   └── script.reds → ../../.mods/ModName/r6/scripts/script.reds
├── bin/x64/plugins/              # Frameworks (ECHTE KOPIEN, nicht Symlinks)
│   └── cyber_engine_tweaks.asi   # CET — direkt kopiert
├── red4ext/                      # RED4ext — direkt kopiert
└── ...                           # Original-Spieldateien

4. Mod-Ordner Struktur (KRITISCH)

Jede Mod in .mods/ MUSS die gleiche Ordnerstruktur haben wie sie im Game-Verzeichnis landen soll.

Beispiel für eine Cyberpunk-Mod:

.mods/Blur Begone/
├── archive/pc/mod/           # ← DIESE Struktur ist PFLICHT
│   └── blur_begone.archive
└── meta.ini

Der Deployer erstellt einen Symlink:

Game-Dir/archive/pc/mod/blur_begone.archive → .mods/Blur Begone/archive/pc/mod/blur_begone.archive

FALSCH wäre:

.mods/Blur Begone/
├── blur_begone.archive       # ← FALSCH! Datei liegt direkt im Root
└── meta.ini

Das würde einen Symlink direkt im Game-Root erstellen — das zerstört das Game-Verzeichnis.

Die _flatten_single_subfolder()-Funktion darf NIEMALS die Mod-Ordnerstruktur verändern wenn sie Game-relevante Unterordner enthält (archive/, bin/, r6/, red4ext/, mods/, engine/).


5. Deploy-Mechanismus

⚠️ SCHUTZREGEL: Der Deploy-Mechanismus (Symlinks, Kopien, Manifest, Purge) darf NICHT ohne ausdrückliche Zustimmung von Marc geändert werden. Jede Änderung am Deployer muss vorher besprochen und genehmigt werden.

Ablauf beim Game-Start

  1. silent_purge() — Entfernt alle alten Symlinks (liest Manifest)
  2. silent_deploy() — Erstellt neue Symlinks für alle aktiven Mods
  3. Spiel wird gestartet

Deploy-Logik (mod_deployer.py)

Für jede aktive Mod (reversed, höchste Priorität zuletzt):
  Für jede Datei in .mods/ModName/:
    Berechne relativen Pfad (z.B. archive/pc/mod/datei.archive)
    Ziel = Game-Dir + relativer Pfad
    Wenn Ziel eine echte Datei ist → ÜBERSPRINGE (Safety Rule, AUSSER Frameworks)
    Wenn Ziel ein Symlink ist → Entferne (niedrigere Priorität)
    Prüfe ob Pfad in GameCopyDeployPaths liegt → JA: Kopiere (shim_copy) / NEIN: Symlink
    Erstelle Symlink oder Kopie: Ziel → Quelle in .mods/

Hinweis: Es gibt keinen Whitelist-Filter für Ordnernamen. Alle Dateien aus aktivierten Mods werden deployed (wie in v0.1.0). Metadata-Dateien (meta.ini, codes.txt) werden übersprungen.

GameCopyDeployPaths (neu seit 20.03.2026)

Manche Programme (z.B. CET's Lua VM) können Linux-Symlinks nicht durch Wine's S:-Drive folgen. Für diese Pfade werden Dateien als echte Kopien statt Symlinks deployed.

  • Typ im Manifest: shim_copy (wird beim Purge entfernt)
  • Konfiguration pro Game-Plugin: GameCopyDeployPaths = ["bin/x64/plugins/cyber_engine_tweaks"]
  • Gleiche Purge-Logik wie der F4SE Proton Shim

Priorität

  • modlist.txt: Erste Mod = höchste Priorität (oben in GUI)
  • Deployer reversed die Liste intern → "last wins"
  • Bei Dateikonflikten gewinnt die Mod mit der höchsten Priorität

Purge-Logik

  • Liest .deploy_manifest.json
  • Entfernt alle Symlinks die auf .mods/ zeigen
  • Entfernt shim_copy-Dateien (CET-Lua-Mods, Proton-Shims)
  • Entfernt leere Verzeichnisse die beim Deploy erstellt wurden
  • Framework-Kopien (type: "copy") werden NICHT entfernt

6. Frameworks (Direct-Install)

⚠️ SCHUTZREGEL: Der Deploy-Mechanismus für Frameworks (Kopien, Erkennung, Installation) darf NICHT ohne ausdrückliche Zustimmung von Marc geändert werden.

Frameworks (CET, RED4ext, ArchiveXL, etc.) werden ANDERS behandelt als normale Mods:

  • Sie werden direkt ins Game-Verzeichnis KOPIERT (nicht per Symlink)
  • Sie werden beim Purge NICHT entfernt
  • Sie gehören NICHT in .mods/
  • Sie gehören NICHT in die modlist.txt
  • Sie werden im Downloads-Tab NICHT als "installiert" markiert wenn ein .mods/-Ordner existiert
  • Sie werden im Frameworks-Bereich angezeigt (unten in der Mod-Liste)
  • Die Erkennung ob ein Archiv ein Framework ist geschieht über is_framework_mod() mit Datei-Patterns
  • Frameworks dürfen echte Dateien im Game-Verzeichnis überschreiben (Safety-Check wird bypassed)
  • Der root/-Prefix (MO2 RootBuilder-Pattern) wird automatisch gestrippt
  • Frameworks werden IMMER deployed, unabhängig von active_mods.json (über GameDirectInstallMods)
  • Bei Neuinstallation eines Frameworks kommt ein Update-Dialog zur Bestätigung

WICHTIG: Die Framework-Erkennung muss VOR dem Flatten des Archivs stattfinden, weil Flatten die Patterns zerstören kann.

WICHTIG: Framework-Reinstall (_fw_reinstall) nutzt den konfigurierten Downloads-Pfad und durchsucht Unterordner (nicht nur .downloads/).


6b. Charakter-Presets

Ein Preset (z.B. ACU bei Cyberpunk) ist eine Einstellungsdatei, die ein Framework einliest — keine Mod im ueblichen Sinn. Anvil baut daraus trotzdem eine gewoehnliche Mod in .mods/, mit dem Zielpfad schon im Ordner:

.mods/ACU-Preset - Grace (female)/
  bin/x64/plugins/cyber_engine_tweaks/mods/
    AppearanceChangeUnlocker/character-presets/female/Grace.preset

Gruende: Der gewoehnliche Deploy bringt sie ohne Sonderweg an die richtige Stelle, und ein Update des Frameworks wirft sie nicht weg — was passierte, schriebe man sie in dessen Ordner hinein.

  • Presets stehen in der modlist.txt und in active_mods.json wie jede andere Mod. Ohne Eintrag in der modlist.txt werden sie nie ausgerollt (mod_deployer.deploy() liest die Liste).
  • Nur die Anzeige ist anders: sie werden aus der Mod-Liste herausgefiltert und in einem eigenen Bereich darunter gezeigt (Gegenstueck zum Frameworks-Bereich). Zustaendig ist MainWindow._split_presets().
  • Die Erkennung entscheidet der Inhalt, nicht der Name und nicht der Trenner: character_presets.is_preset_mod() verlangt, dass alle Dateien der Mod die Preset-Endung tragen UND unter dem Zielpfad der Preset-Art liegen. Eine Mod mit zusaetzlichem .archive greift ins Spiel ein und bleibt eine gewoehnliche Mod.
  • Welche Preset-Arten ein Spiel kennt, sagt plugin.get_preset_kinds(). Leer heisst: der Bereich existiert dort gar nicht.
  • Die Erkennung liest die zwischengespeicherte Dateiliste aus .modindex.json (siehe 10.6). Ist der Index leer oder veraltet, erscheint das Preset wieder in der Mod-Liste — ein bewusster, unschaedlicher Rueckfall.
  • Von Anvil selbst angelegte Dateien (meta.ini, codes.txt, fomod_choices.json) und Punktdateien zaehlen bei der Erkennung nicht mit. Die Liste muss zu mod_deployer._SKIP_FILES passen; ein Test haelt beide Mengen gleich.
  • Beim Umsortieren haengt ein Preset an seinem vorherigen sichtbaren Vorgaenger (merge_hidden_order()), nicht am Trenner. Zieht man ausgerechnet die Mod ueber ihm woanders hin, wandert es mit. Fuers Ausrollen folgenlos — Presets kollidieren mit nichts.
  • Im Bereich gibt es nur Ein/Aus, Ordner oeffnen und Loeschen. Umbenennen, Eigenschaften, Kategorien und das Verschieben in der Reihenfolge sind fuer Presets nicht erreichbar. Bewusste Einschraenkung, kein Versehen.

7. modlist.txt

Format (v2)

# Managed by Anvil Organizer v2
+wichtige Reihenfolge_separator
+Preem Fixes - Cloth
+Blur Begone
+Body_separator
+VTK Vanilla HD - Head
  • Erste Zeile = höchste Priorität (oben in GUI, gewinnt Konflikte)
  • Trenner stehen VOR ihren Mods (gleiche Reihenfolge wie GUI)
  • + Prefix = Mod ist in der Liste (Aktiv-Status kommt aus active_mods.json)
  • Frameworks stehen NICHT in der modlist.txt

active_mods.json (pro Profil)

["Blur Begone", "Preem Fixes - Cloth", "VTK Vanilla HD - Head"]
  • Set von Mod-Ordnernamen die aktiv sind
  • Muss bei JEDEM Rename und Delete in ALLEN Profilen aktualisiert werden

8. Separatoren

  • Ordner in .mods/ mit Suffix _separator (z.B. naked_separator/)
  • Werden vom Deployer ÜBERSPRUNGEN (keine Symlinks)
  • Können nicht aktiviert/deaktiviert werden
  • Dienen nur zur visuellen Gruppierung in der GUI
  • Kompatibel mit MO2-Format

9. Was NIEMALS passieren darf

  1. NIEMALS Mod-Dateien direkt ins Game-Verzeichnis kopieren (nur Symlinks, außer Frameworks und GameCopyDeployPaths)
  2. NIEMALS die Ordnerstruktur in .mods/ verändern (Flatten nur für den obersten Wrapper-Ordner des Archivs, NICHT für Game-Verzeichnisse wie archive/, bin/, r6/)
  3. NIEMALS Frameworks in .mods/ oder modlist.txt ablegen
  4. NIEMALS modlist.txt ändern ohne auch active_mods.json zu aktualisieren (bei Rename/Delete)
  5. NIEMALS Legacy-API (per-Profile modlist.txt) verwenden — immer globale API
  6. NIEMALS den Deploy-Mechanismus (Symlinks, Kopien, Manifest, Purge, Frameworks) ändern ohne ausdrückliche Zustimmung von Marc. Das schließt ein: mod_deployer.py, GameCopyDeployPaths, GameDirectInstallMods, install_framework(), die Purge-Logik, und alles was bestimmt wie Dateien ins Game-Verzeichnis gelangen.
  7. NIEMALS die Mod-Ordnerstruktur durch Flatten zerstören
  8. NIEMALS einen Eintrag, der nur aus der ANZEIGE genommen wird (gesperrte Frameworks, Presets), beim Umsortieren seine Position in der modlist.txt verlieren lassen — siehe merge_hidden_order()
  9. NIEMALS Code ändern ohne vorher MO2-Referenz zu lesen (/home/mob/Projekte/Fremd-Mod Manager/mo2-referenz/src/)

10. Bekannte Unterschiede zu MO2 (bewusst anders)

  1. Physisches Deploy statt VFS — Linux hat kein USVFS. Symlinks sind die Alternative.
  2. Getrennte Dateien — modlist.txt (Reihenfolge) + active_mods.json (Aktiv-Status). Ermöglicht globale Reihenfolge mit profil-spezifischem Aktiv-Status.
  3. modlist.txt nicht invertiert — Einfacher zu lesen. Deployer reversed intern.
  4. Framework Direct-Install — MO2 verwaltet keine Frameworks. Anvil kopiert sie ins Game-Dir. 4b. Eigene Bereiche statt einer Liste — MO2 zeigt jede Mod in einer einzigen Liste und filtert hoechstens. Anvil zieht ganze Mod-Klassen (Frameworks, Charakter-Presets) in eigene Bereiche unter der Liste.
  5. Persistent Deploy — Symlinks bleiben nach Game-Ende bestehen (kein VFS das beendet wird).
  6. Zwischengespeicherte Dateilisten — MO2 liest den Mod-Baum bei jedem Refresh neu (DirectoryRefresher::refresh()). Anvil merkt sich die Dateiliste je Mod in .modindex.json, sonst dauert der Start bei mehreren hundert Mods zu lange. Ob eine Mod neu eingelesen werden muss, entscheidet ein Abdruck ueber alle Ordner der Mod (Pfad + Aenderungszeit, blake2b). Die Zeit des Wurzelordners allein genuegt nicht — eine Datei, die in einem Unterordner umbenannt wird, aendert sie nicht. Nicht erkannt wird ein an Ort und Stelle ueberschriebener Dateiinhalt; die Dateiliste stimmt dann trotzdem, nur die Groessenangabe nicht. Der Deployer prueft zusaetzlich vor jeder Mod (ModIndex.refresh()) und meldet ueber DeployResult.missing_sources, wenn eine Mod nicht vollstaendig ins Spiel gelangt ist. Das blockiert den Spielstart bewusst nicht.

11. MO2-Referenz

Der MO2-Quellcode liegt in /home/mob/Projekte/Fremd-Mod Manager/mo2-referenz/src/. Wichtige Dateien:

MO2-Datei Anvil-Äquivalent Beschreibung
modlist.cpp mod_list_io.py modlist.txt lesen/schreiben
modinforegular.cpp mod_entry.py Mod-Metadaten, Rename
modlistviewactions.cpp mainwindow.py Kontextmenü-Aktionen
usvfsconnector.cpp mod_deployer.py Deploy-Mechanismus
installationmanager.cpp mod_installer.py Mod-Installation
profile.cpp Profil-Logik in mainwindow.py Profil-Verwaltung

REGEL: Bevor Code an Mod-Verwaltung, Deploy, oder modlist.txt geändert wird, MUSS die entsprechende MO2-Datei gelesen werden.

Clone this wiki locally