Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MCM Editor

Edit a SkyUI MCM's on-screen text — page titles, tabs, headers, option labels, info text — on Linux, with a GUI. One interface, every MCM style:

Style Where the text lives This editor
Legacy, script-driven compiled .pex string table patches the pex directly
MCM Helper config.json edits the JSON
Translation-driven ($tokens) Interface/Translations/*_english.txt resolves tokens and edits the translation file

Most Skyrim MCMs mix these — e.g. a config.json full of $RespawnSoulslike tokens whose real words are in a translation .txt. The editor links them automatically: you see and edit the actual English text, and each change is written back to whichever file truly holds it. No Papyrus compiler, Creation Kit, or Windows box.

Implements the mcm-pex-editor stub from the Linux-Native-Tools project.

Why patch the pex directly

A .pex keeps its whole set of strings in a single plaintext string table, and every reference to a string elsewhere in the file is a u16 index into that table — never a byte offset. So changing a label's text is just swapping one table entry; nothing else in the file has to move. That is the single fact this tool rests on, and it's what makes editing safe without recompiling.

Renaming a page, fixing casing, rewording an info string, correcting a typo — all of these are string-table edits. (Adding new options or changing option logic still needs the Papyrus compiler; that's out of scope here.)

What it does

  1. Fully parses the .pex format (header, string table, debug info, user flags, and the entire object/state/function/instruction tree) — not to rewrite the code, but to (a) prove the parse is correct and (b) learn, from the bytecode, how each string is used.
  2. Classifies each string by the SkyUI call it flows into: SetTitleText → page title, AddToggleOption/AddSliderOption/… → option label, SetInfoText → info text, AddHeaderOption → header. Strings used only as variable/function names are marked identifiers and hidden by default (renaming them would desync bindings).
  3. Edits the string table in a GUI, with a live diff of pending changes.
  4. Writes back a byte-exact .pex, keeping a .bak, and refuses to write anything it can't re-parse.

Page (tab) detection

Page names aren't handed to a display call — a legacy MCM's OnPageReset dispatches on them with string equality (If page == "General"). The tool finds page names by spotting string literals used in a cmp_eq (opcode 15), filtering out asset paths and blanks, and tags them Page name.

Translation tokens

~80% of Skyrim MCMs use $translation_key tokens instead of literal text (both in pex and JSON). The editor locates the mod's Interface/Translations/ *_english.txt, resolves each token to its English text, and routes edits there — the pex/JSON keeps the token, the translation file gets the new words. A translation .txt can also be opened and edited on its own.

Install

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt   # PySide6

Use

GUI — opens a .pex, a config.json, or a translation .txt:

./mcm-editor path/to/SomeMod_MCM.pex     # or just ./mcm-editor and File > Open
./mcm-editor path/to/MCM/Config/SomeMod/config.json

Inspect / script from the terminal (no Qt needed):

tools/mcmdump.py SomeMod_MCM.pex               # list displayed text by role
tools/mcmdump.py config.json --all --json      # every field, machine-readable
tools/mcmdump.py SomeMod_MCM.pex --set 356="Enable Sheet Lightning"   # patch + .bak

The KEY for --set is the key column shown in a listing (a pex string index, or a JSON path like /pages/0/content/3/text).

Authoring a new MCM (AI-friendly)

You can create a menu from scratch from a compact spec — small enough for an assistant to write, forgiving enough that you only name options by their label. The builder fills in the exact MCM Helper machinery so the result works: correct valueOptions.sourceType per control, unique id:section binding keys, slider formatString, enum option lists, and groupControl/groupCondition wiring for options that reveal a dependent block.

tools/mcmbuild.py --example > spec.json          # a starting template
tools/mcmbuild.py spec.json -o "MyMod/MCM/Config/MyMod/config.json"
cat spec.json | tools/mcmbuild.py - --dry-run --json   # validate, machine output

Spec (everything but modName and one option is optional):

{
  "modName": "MyMod", "displayName": "My Mod", "tokens": false,
  "pages": [{"name": "General", "content": [
    {"header": "Core"},
    {"toggle": "Enable mod", "help": "Turn it on.", "default": true},
    {"slider": "Damage", "min": 0.5, "max": 3, "step": 0.1, "default": 1},
    {"enum": "Difficulty", "options": ["Easy","Normal","Hard"], "default": 1},
    {"keymap": "Hotkey"}, {"input": "Name"}, {"color": "Tint"},
    {"text": "Version", "value": "1.0"}, {"empty": true},
    {"group": "Advanced", "content": [
      {"toggle": "Verbose logging"},
      {"slider": "Cache size", "min": 0, "max": 500, "step": 10}
    ]}
  ]}]
}

tokens: true externalises every label into a translation file with generated $keys (SkyUI's localisation path); the default inlines literal text.

New pex menus aren't buildable this way — that needs the Papyrus compiler. MCM Helper JSON is the declarative, code-free way to ship a config menu.

Validation — will the buttons actually work?

mcmbuild validates as it builds; you can also check any menu:

tools/mcmdump.py config.json --validate          # or --validate --json

It reports, errors first, the things that make a menu look right but misbehave: a control with no id (its value won't persist), two controls sharing an id, a slider with min >= max, an empty enum, a groupCondition pointing at a missing groupControl (a block that never shows), and unresolved $tokens. Severity is calibrated against real published menus — patterns that working mods ship are warnings, not errors. In the GUI: View → Validate menu (Ctrl+Shift+V).

Live controls in the preview

The preview isn't just a picture: toggles flip, sliders drag, enums cycle, so you can confirm the layout and controls behave before loading the game. File → New from spec… (Ctrl+N) builds a menu and drops you straight into the Arrange tab to refine it.

Arranging options (MCM Helper JSON)

The Arrange tab shows the menu as a page → option tree in document order. Reorder options within a page, drag them to another page, reorder pages, and add / remove / duplicate — by drag-and-drop or with the toolbar (Alt+↑/Alt+↓ to move the selection). Edits apply live with full undo/redo (Ctrl+Z/Ctrl+Y); renaming a $token label in the tree still writes to the translation file.

This works for MCM Helper (JSON) menus, where the layout is data. It does not work for legacy pex: there the option order and page layout live in the script's compiled OnPageReset code, so rearranging would mean rewriting Papyrus bytecode (recompile territory, out of scope). The Arrange tab says so when you open a pex; use the Fields tab to edit its text.

Preview (see it before the game does)

View → Preview in browser (Ctrl+P) renders the menu the way SkyUI draws it — pages down the left, options in two columns, help text along the bottom; hover a row for its help, click a page to switch tabs. The preview reflects your unsaved edits, so you can proof a rename or reword first. It's faithful for MCM Helper menus (full page/item structure) and approximate for legacy pex (pages are detected but options aren't segmented by page). mcmedit/preview.py renders a self-contained HTML fragment reusable outside the GUI.

Applying changes in-game (important)

A .pex is never hot-swapped: a running session keeps whatever script it loaded when the save was made. To see edits:

  • Cold-load the game (not a mid-session reload of a save made with the old pex).
  • SkyUI caches an MCM's title and page list per save (set once in OnConfigInit). After a cold load, run in the console:
    setstage SKI_ConfigManagerInstance 1
    
    to re-register the title and tabs.
  • Row labels and info text regenerate every time a page is opened, so those update on a normal reload.

Guarantees / safety

  • ASCII only — Papyrus strings must be ASCII; non-ASCII edits are rejected (non-ASCII shows up as •-style garbage in-game).
  • Byte-exact round-trip — the parser is validated by parse → emit == identical over the entire local .pex corpus (see tests/roundtrip.py); 51,564 / 51,564 files pass. Every write is re-parsed before it hits disk.
  • Shared strings are flagged — if a table entry is referenced by several call sites (or is also used as an identifier), the GUI/CLI marks it risky, so you don't rename one thing and change five.

Layout

mcmedit/pex.py         byte-exact .pex reader/writer + string-reference map (core)
mcmedit/mcm.py         SkyUI semantic layer: classify pex strings by role
mcmedit/translation.py SkyUI translation .txt reader/writer + mod-tree locator
mcmedit/document.py    one interface over pex / JSON / translation, w/ auto-linking
mcmedit/builder.py     spec -> valid MCM Helper menu (authoring)
mcmedit/validate.py    "will the controls work?" checks
mcmedit/preview.py     SkyUI-styled interactive HTML preview
mcmedit/gui.py         PySide6 editor
tools/mcmdump.py       headless inspect / patch / --validate CLI (all formats)
tools/mcmbuild.py      build a new menu from a spec (AI-friendly)
examples/spec.example.json  starting-point spec
tests/roundtrip.py     ground-truth .pex round-trip over the whole local corpus
tests/test_documents.py  edit-routing across pex / JSON+translation / .txt
tests/test_arrange.py    structural (reorder/move/add/remove) editing for JSON
tests/test_build.py      spec -> build -> validate

The format was modelled on Orvid/Champollion's reader (big-endian = Skyrim, little-endian = Fallout 4+) and then verified against ground truth — the real files — rather than trusted from docs, per project doctrine.

About

Linux-native GUI to edit SkyUI MCM text by patching compiled .pex string tables directly

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages