Techniques for menu edits
These methods apply to any menu change. The formats are on UI scenes (.idf), the addon workflow on Editing menus with an addon, and the editor on Swofty UI. Native changes go through the mixin system. To trace native code while the game runs, see Debugging live.
Find what draws an element
A menu has three layers:
| Layer | Holds | Read it with |
|---|---|---|
. screen (UIDF) | Widgets, their data bindings and events, and where each attaches | tools/uitree., tools/pkg. |
Scene (.) | The rectangles widgets attach to, and the art they draw | Swofty UI, tools/idf. |
| View model | The data the bindings read, published by native code | The game's log, the Game API |
Trace an element through all three before changing any of them.
1. Find the screen
The game's log names the screens and flows that open. swofty/loader.log already carries the game's UI, VM and FLOW messages at info level; add channels in swofty/session.json when you need more (The game's own log). Open the menu, then read which screen opened.
List the screens an archive holds; their type is UIDF:
python3 tools/erp.py 2025_asset_groups/ui_package/flow_frontend.erp '*' | grep UIDFWith Swofty UI running, read a screen's node tree, fragment 0, from the archive. erp is the archive's path inside ui_package:
base=http://127.0.0.1:7341
curl -s "$base/api/resource?erp=flow_frontend.erp&name=eaid://ui_package/screens/fe_home.ui&frag=0" -o fe_home.ui2. Read the widget and its attach
python3 tools/uitree.py -f fe_home.ui # node tree: class, properties, bindings, events
python3 tools/pkg.py json fe_home.ui # everything, including each node's layout attachIn the tree, name [CLASS] is a node and its FourCC class. A property shown as value@path is bound to view-model data, and ~ on <event> -> <node>.<pin> is an event. In the JSON, a node's layout attach is one of:
{ componentId, isComponentId: true, debugName: "<path>.idf" }, on the screen's root: the scene the screen draws in.{ nodeId, debugName: "<leaf>" }: the scene element the node attaches to, by its leaf name. A list of several walks through references, outermost first.
3. Follow it into the scene
Open the root attach's scene in Swofty UI (Ctrl+K and its path) and filter the hierarchy by the leaf name. When the element is an eXRef, its art is in another scene: the scene its xref id names, which the inspector shows as the scene the game loads. fileRef does not decide it (References). Repeat until you reach the element with geoms.
python3 tools/idf.py find rnd_overview.idf tile_4 # one element: transform, size, params, xref, geoms
python3 tools/idf.py refs rnd_overview.idf # each reference: fileRef, xref id, OK or MISMATCH4. Find the data
A binding names a view-model path, such as @tiles or @#frontend_flow.fe_home.tutorials_enabled, and native code publishes it. How the screen gets its count tells you where to change it:
- A spawner node (class
F1TS) withdataContext=@rowsbuilds one child per entry of the bound list. More entries show more children, if the scene has room for them. - A fixed set of named nodes, such as
tile_1totile_4, each bound to its own path, is a fixed count in the.ui. A fifth needs a fifth node, a fifth element for it to attach to, and data at a fifth path.
Data layer and UI layer
Loading is usually data-driven. The game reads database rows by key and loops on _instancecount_ keys, so a new row loads with no code. Screens often aren't: a native builder can create a fixed number of widgets from constants, and a scene authors a fixed number of slots. A new row can therefore be loaded, real in the game's model, and never shown.
Prove which layer stops a change before editing either:
- Log what the loader reads: a read-only hook after the table cursor opens, logging the row count for each category.
- Log what the screen's builder receives: the results it keeps for each category.
- Compare. If the loader reads the new row and the builder never sees it, the limit is between them, in native code. If the builder sees it and nothing draws, the limit is in the
.uior the scene.
A category, such as a team, a department or a mode, usually reaches the screen through several paths: its name, icon, colour, counts, selection and Back navigation each read the category index through their own table or guard. Find every reader of the field before calling a change done. Search the disassembly for every instruction that reads the field's offset, such as every byte [reg+<field>], and record what each does with a new value.
Counts fixed in native code
These shapes mean a count is fixed in code. The values are examples.
| Shape | Looks like | Means |
|---|---|---|
| Unrolled construction | The same constructor called N times, each with literal arguments such as a name string and an enum | N widgets; there is no loop to extend |
| Exact-size buffer | mov edx, 0x4a0 where 0x4a0 = 0x20 + 4 × 0x120 | Room for 4 slots; growing the allocation constructs nothing |
| Inlined construction loop | add rax, 0x120 ; cmp rax, 0x4a0 ; jne | Exactly 4 slots are constructed |
| Vector sized exactly | end - begin compared with or set to N × 8 | N pointers, then truncated |
| Range guard | cmp r14d, 3 ; ja <skip> | Values above 3 are skipped |
| Clamp | xor ecx, ecx ; cmp al, 4 ; cmovae eax, ecx | An index of 4 or more becomes 0 |
| Early return | cmp byte [rcx+<field>], 3 ; ja <ret> at a function's start | The function does nothing for values above 3 |
| Sentinel branch | cmp byte [rcx+<field>], 4 ; jne <normal> | Value 4 gets special handling, such as a disabled placeholder |
| Jump table | cmp r14d, 3 ; ja <default> before an indirect jmp through a table | 4 cases; the default often produces an error_* name and white |
| Lookup table | lea rcx, [table] ; mov rdx, [rcx+rax*8] | Index N reads whatever follows the table |
For a lookup table, check what follows it in the executable. When another table follows, index N reads that table's first entry: a colour read as a char * crashes on first use. When a string follows, index N reads that string: no crash, wrong text. A table needs a real entry N before any guard in front of it is relaxed.
On screen, an error_* label or a missing icon frame, such as a lang key lng_…_error_… or an atlas frame error_…, shows that an index went past a bounded lookup.
Patch native builders safely
These rules come on top of the engine plugin rules.
- Gate on exactly one match. Find each site by signature in
.xtls;scansets the address only for exactly one hit. Then verify the bytes of every other site you touch at its offset from that gate. On any mismatch, install nothing and log which check failed. - All or nothing, guard last. Create every hook first, then grow buffers, then relax guards and clamps as the last step. When one step fails, undo the earlier ones. A relaxed guard is what sends new values into old code.
- Never relax a bound before the data behind it exists. Before
cmp …, 3becomescmp …, 4, entry 4 must exist in every table the guarded code indexes, every buffer must have room for it, and every object it touches must be constructed. - Check branch targets. No branch in the function may land inside the bytes a hook overwrites. Check the containing function's code, and search
.xtlsfor relative branches into the window. - Construct extra slots where the real constructor runs. Grow the allocation, then build the extra slot right after the real constructor returns, the same way it builds the others: the same vtables, empty links. Read the code around that point:
- A block after a move-assign that destroys the moved-from temporary can be dead. The move clears the source, so the block's null check always skips it, and code placed there never runs.
- A function called next to construction can be the destructor. Read its callers: when every caller is a teardown path, it destroys.
- Memory nobody constructed holds the allocator's leftovers. The first move-assign into it reads a garbage pointer and crashes while unlinking it.
- Destroy extra slots on every teardown path. A rebuild (move-assign), an explicit teardown and the destructor each destroy only the stock slots. Hook a point every path passes, such as the slot destructor's entry, and destroy your slot there before the stock ones. Otherwise its handles stay linked into live lists from freed memory.
- Gate use on a marker. Write a footer into the grown allocation, such as a magic value, the object's address xor a key, the capacity and a generation, and keep a registry of the objects you extended. Before code uses an extra slot, check both. On a miss, skip the slot and let the stock path run.
- Identify your objects by content. Games often build an object on the stack and then move it. Match your rows by name or hash, never by address.
- Log before you change behaviour. Ship read-only hooks first, logging counts and state: rows read per category, results kept per category, the size of a slot vector. Log once per build or rate-limit, prefix lines with your addon id, and log each refusal with its reason. Keep the diagnostics alongside the behaviour change for its first runs.
- Expect saved state. The game saves some runtime state and restores it as it was. A state an earlier build of your addon wrote comes back on load, so repair it after load, not only at creation.
Edit scenes
- Set
xrefwithfileRef. The game loads a reference by itsxrefid. Use Retarget in Swofty UI ortools/idf.py retarget, and check withtools/idf.py refsorswofty-ui --check-refs(References). - Keep ids consistent. Each
elementIdis the djb2 of its dotted path,nodeidlookupholds every leaf name's djb2, and every leaf name is unique. Clone, rename and delete with Swofty UI ortools/idf.py clone-slot(Identity). - Animations and stackers replace stored positions. Move an animated element's unanimated children instead. A stacker lays its children out at runtime. Some elements are stored collapsed and grown by an animation or live data (What a scene stores).
- New shapes need new meshes. A shape, such as a wedge's angle, is baked into its mesh, and rotating copies of a mesh made for four parts to make five overlaps them. A new image is a new texture (Textures and models).
- Replace the scene everywhere. Ship it to every archive that holds it; the game reads the copy in whichever archive loads first.
- Change the chain the screen uses. Different screens can draw the same component through different scenes. Follow the chain from the root attach of the screen you're changing.
- Wire the new element. A new element stays empty until a
.uinode attaches to its leaf name. When the screen refreshes its widgets through a list of named targets, such as an event sent to each tile, add the new node to that list.
Test loop
- Test in a duplicate of your profile; each profile keeps its own saves. Copy its saves before a run that changes native state.
swofty build, and read its warnings.swofty run --apply. It installs the profile, follows the logs, and rolls the game files back when the game exits.- Read
swofty/loader.log, orswofty logsafterwards: each hook installed, each refusal, and your diagnostics. - After the game exits, run
swofty verify. It checks that the game files match the backed-up originals. When they don't, runswofty restore.
When chasing a crash, change one thing per run. See the command line for each command.
For AI agents
Work through files and HTTP, not the browser:
- Swofty UI's HTTP API. Start it with
--no-browser(bin/swofty-ui --no-browser, orpython3 tools/swofty-ui/serve.py --no-browser).GET /api/openreturns adocid.POST /api/applywith{ doc, ops }replays edits and returns the scene model with aresultsentry per edit, or HTTP 400 with the failing edit'sopIndex.POST /api/exportwrites and checks the scene: readchecks.ok,checks.elementIdsBadandchecks.xrefMismatches; HTTP 422 means a reference mismatch, and nothing was written. Keys address elements by child index in the tree as it is when each edit runs (HTTP API). - Replays. An export's
.edits.jsonholds itsops. After a game update, send them to/api/exporton the new stock scene instead of editing the old file. - Validators. Check every scene you write with
python3 tools/idf.py roundtrip <file>,python3 tools/idf.py refs <file>(exit status 1 on a mismatch) and the export checks, and runswofty-ui --check-refson a built game. - Screens. Read
.uiscreens withGET /api/resourceandtools/uitree.py -fortools/pkg.py json, as in Find what draws an element.
Guardrails:
- Never write into the game folder. Ship changes as an addon and let the build install them through the journal. Never commit game files; the tools read them from
work/, which git ignores, orF1_25_DIR. - Ask before starting the game or touching saves.
- Claim only what you have evidence for: an address from disassembly, a count from a log line, a layout from a rendered scene. Mark the rest as unverified. Record engine findings in the RE knowledge base through
re/tools/kb.py, with the evidence levels inre/PROTOCOL.md. - Follow the patching rules above: exactly-one gates, byte checks, guards relaxed last, diagnostics before behaviour.
- Change one thing per test run, and read the log before the next.