Rebuild of the xEdit project in Rust. Additionally features CLI control of all xEdit features and native support for AI agents to control it.
The build plan is in docs/PLAN.md. Agents start with CLAUDE.md; completed phase evidence is linked from the plan.
Overall: 7 of 10 phases merged.
[##############......] 70% phases 0-6 of 0-9 merged into main
| Phase | Scope | Progress | State |
|---|---|---|---|
| 0 | Foundations | [##########] |
Merged |
| 1 | Read path for the first games | [##########] |
Merged |
| 2 | All games and saves | [##########] |
Merged |
| 3 | Write path and daemon | [##########] |
Merged (PR #12) |
| 4 | Analysis and tool modes | [##########] 10 of 10 steps |
Merged (PR #13) |
| 5 | Archives, assets and LOD | [##########] |
Merged (PR #14) |
| 6 | Scripting | [##########] 11 of 11 steps |
Merged (PR #20) |
| 7 | GUI | [########..] 8 of 9 steps |
In progress, draft PR #101 |
| 8 | Performance | [##........] 2 of 10 steps |
In progress (steps 2a and 2b) |
| 9 | Release and cutover | [..........] |
Not started |
The overall bar counts merged phases only: 2 cells per phase out of 20. A phase on a draft pull request counts once it merges. Per-phase bars have 10 cells and show completed steps as a share of the phase's steps, as listed in docs/PLAN.md.
Phase 8 (Performance) step 2 (issue #113, step 2b) takes the non-cloning element access through xedit-core: the element trait gains a name_matches that compares a name without building a String, a container gains with_elements that reads its list under the read lock and element_at that reads one element by its index, element_by_name and init_main_record use them, read_built skips the upgrade of the weak reference when the record is already built, the writer and the navigation tree build read the children one at a time instead of copying the whole list, and the regex VM decodes the character of the subject without validating the rest of it. The output of every command is the same as before.
Phase 8 (Performance) starts with the fast paths of xedit-io (issue #112, step 2a): the text encodings decode and encode a string of ASCII bytes without a call to MultiByteToWideChar, the LZ4 frame and block routes go through the linked C library instead of the pure Rust one, and the workspaces of the compressors are kept per thread instead of being built per call. The bytes of every output are the same as before.
Phase 8 (Performance) step 2 (issue #113, step 2b) takes the non-cloning element access through xedit-core: the element trait gains a name_matches that compares a name without building a String, a container gains with_elements that reads its list under the read lock and element_at that reads one element by its index, element_by_name and init_main_record use them, read_built skips the upgrade of the weak reference when the record is already built, the writer and the navigation tree build read the children one at a time instead of copying the whole list, and the regex VM decodes the character of the subject without validating the rest of it. The output of every command is the same as before.
Phase 8 (Performance) step 2a (issue #112) adds the fast paths of xedit-io: the text encodings decode and encode a string of ASCII bytes without a call to MultiByteToWideChar, the LZ4 frame and block routes go through the linked C library instead of the pure Rust one, and the workspaces of the compressors are kept per thread instead of being built per call. The bytes of every output are the same as before.
Keep this section current. Update it in the same pull request whenever a step lands, a phase starts or a phase merges: redraw the bars, change the step counts and the state column, and update the overall percentage.
- Port xEdit to Rust with 1:1 functionality*
- Morrowind, Enderal and Oblivion Remastered load but are not verified against the oracle yet: the 4.1.5q release cannot dump or save Morrowind plugins (issue #9), and Enderal (issue #7) and Oblivion Remastered (issue #80) are not installed on the development machine. The rest of what is verified, deferred and out of the oracle's reach is under Status.
- Match the upstream release build exactly
- The official binaries of the baseline tag (
xedit-4.1.5q) are the oracle; a differential harness compares the port's output with theirs on the real game files, and each phase closes only when its parity gate holds. Upstream quirks are reproduced, not corrected; an upstream bug that loses or corrupts data is the one exception: the port keeps the data and records the divergence (see Status).
- The official binaries of the baseline tag (
- Keep existing scripts, plugins and workflows working. Compatibility with xEdit behaviour, its Pascal scripts and the files it writes wins over every other concern.
- Be easier to contribute to than the Delphi original: a modern toolchain (
cargo build,cargo test, clippy, one command for the parity check), no proprietary compiler, typed interfaces instead of 103 COM-style interfaces in one 50,000-line unit, and a module per upstream unit so the Pascal source stays a readable map of the Rust. - Expose every operation through the
xeditCLI as a typed command with--jsonoutput, stable error codes and a printable schema (xedit schema), so that an AI agent can discover and drive the whole tool without the GUI.- Make mutation safe for agents: an explicit edit flag,
--dry-run, structured outcomes, atomic saves with a backup, and a long-lived session (xedit batch,xedit serve,xedit mcp) that runs the same commands.
- Make mutation safe for agents: an explicit edit flag,
- Stay mergeable with upstream. One Rust module per Pascal unit with matching names, and
upstream-map.tomlrecords the last upstream commit merged into each, so upstream changes map onto the port line by line. - Get faster than upstream once parity holds
- No new file formats, record definitions or game support beyond what the baseline tag ships. New upstream releases are taken in through the sync procedure, not ahead of it.
- No redesign of the data model, the scripting language or the GUI. JvInterpreter Pascal stays the script language; the GUI reproduces the upstream forms on top of the same commands.
- No separate agent API. Agents use the same command layer as the CLI and the GUI; there is no second surface to keep in step.
- No dependence on a Delphi compiler. The oracle is the released binary, never a rebuild.
- No non-Windows platform before the Windows port is complete. Linux and macOS follow, behind thin platform modules.
- No game files in the repository. The parity corpus comes from local installs through environment variables.
The progress bar above counts merged phases of docs/PLAN.md; it says nothing about how much of xEdit is verified. That is below, from the full-corpus reruns of every parity mode that closed the parity backlog (issue #41, pull request #77, port 610b70f, oracle xEdit 4.1.5q, the vanilla plugins of the local installs of 10 games). Every number names the cargo xtask parity mode that produced it; docs/PLAN.md has the history of each mode and the checking-parity skill how to run and read it.
- Read and dump every game mode of the baseline from Morrowind to Starfield and every save format:
xedit dump(xDump's output),xedit saves dump,xedit checkandxedit dump --check(Check for Errors), with the GUI's archive discovery, resource lookup and string tables. - Edit and save: elements by path (
xedit elements get|set|add|remove|assign|templates|move|check), records (xedit records copy|delete|set-deleted|set-hidden|cleanup-injected|compare|compare-selected), masters (xedit masters add|sort|clean), FormIDs (xedit formids change|renumber), module flags and new modules (xedit files flags|new), the string tables of localized plugins (xedit localization ...) andxedit save, with the AfterLoad and AfterSet callbacks of the definitions. A mutating command needs--editor runs as--dry-run. - Analysis: conflict status and the view (
xedit conflicts,xedit compare,--compare-to), the reference index and its cache files (xedit refs), the filter of the navigation tree with its presets and the scripted filter (xedit filter), the quick auto clean and its steps with the Cleaning options (xedit clean), mod groups and their presets (xedit modgroups), the merged patch (xedit patch merged), and the tool modes and legacy switches ofxeInit.pas(xedit tool modes|run, and the command line a mod manager passes to xEdit). - Assets: BSA and BA2 archives (
xedit archive, thebsarchbinary, BSArchPro's XBox target), NIF, KF, materials and the other asset formats (xedit assets), Sniff's 50 batch operations (xedit sniff, thesniffbinary) and LODGen (xedit lodgen). - Scripting: the JvInterpreter Pascal of the
Edit Scriptsfolder (xedit script list|check|runandscript.resume), with the script API ofxEditAPI.pasand the JVCL units bound, and a headless VCL object model that runs forms and dialogs from recorded answers. - GUI shell and view (phase 7 steps 1 and 3):
cargo run -p xedit-gui(binaryxedit-gui) opens the main form ofxeMainFormwith the per-game window title and icon, the hamburgerpmuMainmenu and the form's popup menus, the navigation pane with its splitter, the tabbed view with the Messages page and the View tab's record grid (one column per record version or per selected record, definition-ordered rows with their folds, the conflict colours and in-place edits throughelements.set, with thepmuView/pmuViewHeaderitems and the view filter), the status bar with the load gauge, and the game select -> module select -> load startup flow, all through the same command registry as the CLI. The dialogs of steps 5-7 are in place (see below). - GUI record panes (phase 7 step 4): the Referenced By list with its filter strip and
pmuRefBy, the Messages log with its Clear/Save selected/Autoscroll menu, the Information memo verbatim from the DFM, the localization view of the selected record, and the Weapon/Armor/Ammo spreadsheets with the DFM's columns and per-game element mapping (shown for Oblivion and Skyrim only), plus the element detail editor ofxeElementDetailForm's rules (value types, flags checkboxes, FormID conversion) with every commit throughelements set. The rows come fromrefs get,localization getandrecords list/get; the dialogs of steps 5-7 are in place (see below). - GUI dialogs and tools (phase 7 steps 5, 6 and 7): the selection dialogs (the game, module, file, filter options, mod group and FormID pickers), the viewers and editors (the log analyzer, the script editor with the Pascal highlighter, the element viewer with its page tabs, the rich text viewer with its table of contents, the localization editor, the developer message and the tip), and the modal tools (the options form with its General, View, Cleaning, UI Settings and UI Theme pages, the LOD generator, the legend, the edit warning, the worldspace cell details and the wait form's progress and cancel). Long operations run on a worker thread through the job runner, so the window keeps repainting and Cancel works.
- One command layer: the same typed commands drive the CLI,
xedit batch,xedit serve(JSON-RPC over stdio or a named pipe) andxedit mcp(MCP tools), with--jsonoutput andxedit schema; a test checks that the three list the same commands. The skills under.claude/skillsdescribe how to use them.
cargo xtask parity mode |
Corpus | Result |
|---|---|---|
script --phase parse |
150 corpus .pas |
150 of 150 equal (149 equal, 1 known-difference) |
script --phase probe |
935 probes | 934 of 935 equal (890 equal, 37 known-difference, 7 equal-error; 1 oracle dialog) |
script --phase run |
156 run cases | 146 of 156 at b157821 (138 equal, 5 equal-log-loss, 3 known-difference); 5 different accepted with their issues (#64, #84); 4 oracle timeouts under load, rerun pending |
dump |
249 plugins of 11 games | 246 of 249 equal; the 3 Morrowind masters are oracle-unsupported |
saves |
49 save files | 49 of 49 equal |
check |
249 plugins, 5,852,286 records, 260,691 error lines | 246 of 249 equal; 3 oracle-unsupported |
check-dump |
249 plugins | 245 of 249 equal, 3 oracle-unsupported; Starfield.esm hit the 23.7 GiB port cap in the batch (rerun alone pending) |
clean |
238 plugins | 235 equal, 2 oracle-unsupported; NW.esm is oracle-log-lost (the release's log drops the counts; saved bytes equal) |
clean-scenarios |
8 scenarios | 8 of 8 equal |
tool-modes |
74 mode runs | 66 equal, 7 oracle-unsupported; setesm on BlueprintShips-SFBGS050.esm hit the 20-minute oracle limit (rerun alone pending) |
modgroups |
16 scenarios | 16 of 16 equal |
conflicts |
11 games, 23.4M records | 10 of 11 equal (Morrowind oracle-unsupported); the record probe 2,203 lines, 0 different |
refs |
10 games | 10 of 10 equal: 7,013,961 records, 61,219,249 entries, 79 of 79 cache files |
roundtrip |
249 plugins | 239 equal + 10 refused (the release refuses them too) |
oracle-save |
249 plugins | 239 equal + 7 equal-error; 3 oracle-unsupported |
oracle-edit |
76 edit sequences | 68 equal + 8 known-difference (upstream bugs not reproduced) |
filter |
4 scenarios, 51 configurations | all equal (sse-filters 37 of 37) |
merged |
12 scenarios | 12 of 12 equal |
resources |
8 scenarios | 8 of 8 equal |
discovery |
6 runs (2 games' own plugin lists, 4 module-list cases) | 6 of 6 equal |
lodgen |
20 scenarios | 20 of 20 equal |
bsarch |
1,472 pack/unpack/list checks | 1,472 equal |
bsarchpro |
6 GUI packing cases | 6 of 6 equal: five archives byte for byte with BSArchPro's own autopacking, and the XBox refusal DDS is not in XBox format |
nif |
the NIF and material corpus | 542,505 equal + 250,543 equal-error (the release's own crashes) |
sniff |
every archive of 10 games, 69 operations | 2,028,002 equal, 3,360 equal-error, 10,665,231 unchanged, 6,157 oracle-failed (the release's Sniff crashes or hangs), 0 output-different; 578 log differences, 537 of them the release's uninitialised XBOX flag |
coverage/ledger.toml lists every upstream GUI binding, switch, game mode, tool mode and script registration with its command; cargo xtask check refuses an open entry that names no owner. The open work:
- 2 xDump switch entries (issue #79):
-bts/-btdchange only Skyrim LE saves (theUnused/Remainingarrays are commented out in the FO4/FO3/FNV definitions), and a full TES5 save dump exceeds the harness timeouts, so no oracle case can witness them; the port takes both from the saves dump CLI and the parity saves mode. Every other switch is covered (-dois excluded: it prints raw pointers againstwbBaseOffset, which only a scanned plugin has). - Game modes not verified (issues #7, #9, #80): Enderal and Oblivion Remastered are not installed on the development machine, and the 4.1.5q oracle cannot dump a Morrowind plugin (below).
The four JvInterpreterFm form runners (issue #81), the last open script registrations, landed 2026-10-18: the jvfm run case compares JvInterpreterMakeForm/RunForm/RunFormModal/RunUnit over a fixture unit and its DFM with the oracle (cargo xtask parity script --phase run --script jvfm equal).
- Morrowind (issue #9): the 4.1.5q
xDump.exedumps no TES3 plugin and its GUI cannot save one, sodump,check-dump,roundtripandoracle-savereport the TES3 masters asoracle-unsupported; the port still loads them, andrefs,conflicts-recordand the other modes that do not need xDump run on them. Update MOPP code(Sniff): the released 64-bitSniff.exerefuses before it looks forNifMopp.dll, and the port reproduces the refusal (sniff --case mopp-updateequal); no released binary can run the operation, so its file body has no oracle.- BSArchPro's GUI packing route: the XBox 360 target and the
xtexconv.execonversion only exist in the GUI, which packs from its own autopacking mode;cargo xtask parity bsarchprodrives that route. - Enderal and Oblivion Remastered: not installed (issues #7 and #80), so their game modes are ledger entries only.
The port reproduces upstream's quirks (UPSTREAM-QUIRK in the code). An upstream bug that loses or corrupts data is not reproduced (the user's decision of 2026-10-16): the port keeps the data, the code says UPSTREAM-BUG, and the parity case is shaped so both sides end in the same state, or the case is counted known-difference with its reason, never as equal.
- A record named first loses its subrecords upstream (issue #64): the release drops them when a script's first access is by name.
Add Prefix Plus Name To EDID.pas(850 records),Oblivion - Export Dialogues.pas(898 lines),probes/tes4-topic-full.pas,Put worldspace references in the right cells.pas(the release's save empties 20ACTIrecords the script names first; the other 869,668 records and 50,483 groups are byte-identical) andwalk-move.pasare acceptedknown-difference, and the port's behaviour is the one that keeps the data. - String tables of an unloaded localized plugin (issue #75):
TwbLocalizationHandler.AddValuewrites empty tables over a plugin whose tables were not loaded, which loses every existing string. The port loads the tables and adds the value (UPSTREAM-BUG; the oracle-edit case isknown-difference). - Starfield perk JSON after a record copy (issue #73): a set through the virtual perk tree after a copy loses the first edit upstream. The port keeps it (
UPSTREAM-BUG,known-difference). Worldspace displacement.pasvisits some references twice (residue of issue #84): the walk now builds the navigation tree's nodes lazily and frozen as the release does (the issue's two cases and thewalk-*fixtures are equal), but the release'sRemoveNodesearches the tree for the element's ancestors during the move, which initialises nodes earlier than the walk would, and the GUI pump re-enters it; the port does not emulate those, so the case's saved file still differs (acceptedknown-differenceon the run case).- The scale the cleaning's scale step leaves behind (parity #35): the release keeps a scale where the port removes it, data the release loses; the clean case is
known-differencefor that file. Find texturesprints an uninitialisedXBOXflag on BA2 DDS archives: the release's local record inProcessFileis never assigned in the BA2 DDS branch, and its report printsXBOXfrom whatever the stack holds (every line of a DDS-type BA2 archive on the corpus). The port prints nothing. No saved bytes differ; the log ofsniff --case find-texturesand-filtercounts as a log difference on those archives, never a failing output.
Later phases (release and cutover) have not started; the GUI is at phase 7 step 9 of 9, the gate (the shell, the view and the dialogs above; the ledger is closed and the menus are walked through against the 4.1.5q build, see docs/history/phase-7.md). The parity harness rerun at the phase tip covered the dump, check and clean steps; its saves step and the rest of the chain continue separately. The performance phase is at step 2 of 10 (the cheap, safe and broad fast paths).
xEdit-rust is a port of xEdit by ElminsterAU and the xEdit contributors. xEdit is licensed under the Mozilla Public License 2.0, and ported files are Modifications that must stay under it, so this whole repository is licensed under MPL-2.0. See NOTICE for attribution and third-party terms.
Rules for contributions: