UI patterns ​
These patterns are read from the stock .ui files and .idf scenes. The data behind every statement is in tools/uicatalog/catalog.json and on UI node reference. Each section marks what was verified in game and what is inferred from stock files alone.
Tab containers ​
A container screen (UISC) holds a tab controller (UITC) and hosts tab templates from fragment 1 of the same .ui.
- The
UITCbinds the scene's content element with one layout entry, such asscreen_container, aneXRef.contentPoint(Scene Root.screen.screen_cc) andusedElement(screen_cc) name where a tab template's scene is placed, andtabContainernames the stacker that holds the tab headers. - The tab templates are
UIGProots in theTemplateslist, each with its own scene in the components slot. Which templates become tabs comes from the view model: stock bindings name them astab_controller.tab_<n>( <template> ), such as#f1_world_flow.f1_world_compendium_container.tab_controller.tab_1( f1_world_compendium_chapter_tab ). - The controller raises lifecycle commands on a tab template root. Stock
UIGPnodes declareonTabSelected(43),onShowCompleted(30),onTabActivated(30),onTabDeselectBegin(10),onTabDeselectedandonTabDeactivated, and forward them to their children. Stockdynamic_era_selectdeclares onlyonShowCompleted(→tiles.trigger, which spawns the row) andonTabDeselectBegin(→tiles.detach, which despawns it). - Fragment 1 of
f1_world_activity_container.uiholds the tab templatesf1_world_activityandf1_world_activity_offline(the play screen),dynamic_era_select(the era picker),public_gp_selectandtime_trial_select, and the tile templates they spawn (activity_*_tile,era_select_tile). The container's view model creates seven tabs: the two activity tabs,dynamic_era_selectthree times (Grand Prix, Time Trial, split-screen),public_gp_selectandsession_create_dlc_select_tabfromf1_world_container.ui. No tab usestime_trial_select(engine).
Verified in game: when the game rejects one edited tab template, the whole container fails without an error at warn level. The play screen's category boxes vanish even though every other template in the file is byte-identical to stock. With the era UI edit off, the play screen always shows. With the same edited build (two rows, earlier navigation, no masks), the play screen showed in one session and not in a later one.
Traced in game: in that build the dynamic_era_select root kept the NBVN behaviour without its visualNav property, so its init returned 3 in all three era tabs. The tab controller dropped those three content instances without a log and built all seven headers (engine).
Inferred: the controller then maps the seven tabs to the four content instances it has out of step, which empties the play screen depending on which tab is selected first.
Multi-row lists ​
A row is a UIGP holding a tile spawner (F1TS), magnets (UIGP nodes bound to magnet_full_left, magnet_full_centre, magnet_full_right) and a scrollbar (UISB).
- The
F1TSbinds the row's stacker,item_spawnerorspawner_list(eStacker, 207 of 267 spawners; 19 more through aneXRef).dataContextbinds the list the spawner builds from andselectionthe selected item. - On
spawneda spawner signals itselftrigger_childrenandfocus_and_reset, and the scrollbarattach. - The row's scene is a component reached through an
eXRefin the screen's scene. Invehicle_type.idfthat isplay_categories_row, which referencesvehicle_type_spawner.idf(scroll_bar,item_spawner, three magnets). promo_storeinunified_store.uihas three spawned rows (featured_item,row_1,row_2). They spawn on the root commandsattach_spawneranddetach_spawner, which the store's native view model raises.
Verified in game: two half-scale rows at the top level of vehicle_type.idf (the stock play_categories_row and a clone), each F1TS bound to its own view-model list, draw and scroll.
Navigation between rows ​
A spawner fires navigate_up_failed, navigate_down_failed, navigate_left_failed and navigate_right_failed when focus tries to leave it. Two stock ways route that to another row:
Direct. The failed event signals the other row's spawner
focus. Verified in game in the era picker:navigate_down_failedon the Built In spawner focuses the Addons spawner andnavigate_up_failedon Addons focuses Built In.Through the row (
promo_store). Each rowUIGPdeclares the commands and events, and the spawner reports to its row:- spawner
navigate_*_failedsignals its row'snavUp,navDown,navLeft,navRightpin; a signal to aUIGPpin named like an event raises that event; - the row declares
focus→ spawnerfocus,block_input→ spawnerinputDisallow,unblock_input→ spawnerinputAllow; - the row's
focusedevent signals the other row'sunfocus; row_1'snavDownsignals theSVCDrow1_items_visiblequeryand theUIAMtrigger_nav_on_up_downnavigate; thatSVCDreads the view-model boolrow2_items_visibleand onyesraises the screen commandrow_2_focused, which native code handles;navLeftandnavRightraise screen commands (row_navigate_left,row_navigate_right);- the template root forwards
onTabDeselectBeginto the rows'block_inputandonTabActivatedto theirunblock_input.
row_1declares nonavUpevent. Every stockUIGPnavUp,navDown,navLeftandnavRightevent has at least one signal (106, 109, 22 and 102 of them).- spawner
Inferred: the promo_store chain depends on native handlers: row_2_focused and the attach_spawner commands come from the store's view model.
Focus and input blocking ​
InputOnActPrtis aboolset onF1TS(37),UITG(32) andSCRL(4) only. Verified in game: setting it on the eraF1TSin place ofconsumeStrategyworks.consumeStrategyis aui32on 9 node types, mostlyDDMCmessage containers (26).inputAllowandinputDisalloware pins onF1TS,UIGP,TILE,UITC,UIKLand others.inputAllowedis the matching property (a literal or boundbool).focus,unfocus,focus_and_resetandfocusFirstAvailableare the spawner focus pins. Verified in game: the magnet rename fromfocusedtounfocusworks. A template root that dropsvisualNavmust also dropNBVNfrom its behaviours; withNBVNkept, the template fails to build (behaviour.nbvn_visualnavbelow).
Masks and clipping ​
Three element types clip:
| Type | Stock elements | How stock uses it |
|---|---|---|
eMaskTexture | 899 | A texture alpha mask (eMaskTexture., eMaskTexture., width, height). Its children are eNull (1,053), eXRef (81), eStacker (29) and eScroller (17) |
eWindow | 491 | A clipping rectangle with rounded corners (cornerCurves, cornerCurvesRadius, maskedWindow). It holds 1,217 eXRef children |
eMask | 3 | Only in driver_of_the_day. and tut_overlay_mask_group.; its children are eNull |
- In stock the mask sits inside the list's own component scene, between the transition nulls and the stacker:
play_button_list.idfistransition_panel_list>mask_list(eMaskTexture,play_side_panel_button_mask.tif) >transition_list>spawner_list(eStacker). The screen scene places that component with oneeXRef. Other examples:con_info_button_list.idf(mask_list>transition_list>spawner_list), the laptop message lists (content_mask>scroller>item_stacker) and the career rewards lists (mask_list>scroller). - The UI side binds the stacker inside the masked component, through the
eXRefwhen needed (eXRef>eStacker). Nodes bind aneMaskTextureitself only to animate or hide it:UIGP40,UIXA9,UIL22. - Every resolved stock layout path with more than one entry passes only through
eXRefelements before its last entry.
Verified in game: wrapping each row eXRef of vehicle_type.idf in an eMaskTexture cloned from mask_list, with layout paths [row_mask_x, row], empties the play screen. That build also added a new fade texture resource for the masks.
Runtime bindings ​
A binding names a view-model key relative to the template's view model, such as tiles, or a global path starting with #. Native code publishes the keys. A key that native code adds after the template is created, such as the era picker's addon_tiles, selected_addon_tile and addon_tiles_visible (published at site_FeTile_EraTilesBuilt), exists at bind time only if the native step ran first. Stock dynamic_era_select binds only tiles and selected_tile.
The launcher flags every view-model key an edited template binds that no stock node of the same template binds (vm-key-new).
Checking UI edits ​
packages/core/src/ui/check.rs checks edited .ui and .idf resources in three layers:
| Layer | Source | Effect |
|---|---|---|
| Hard, engine | re/db/ui/loader_rules.: the checks the engine's UI loader applies, read from F1_25. and documented in docs/reference/ui/engine. | The build stops. The finding cites the rule id, its evidence level, the error code, what a failure takes down and the function address |
| Hard, format | The hashes and ids the engine resolves by, and bindings the edit breaks | The build stops |
| Warning | The stock catalogue, and a diff against the stock template | Listed in the build's warnings |
In a profile build, each finding starts with the name of the addon that last wrote the resource. A stale-hash finding in an addon's resource is a warning there: the engine resolves the node, event or signal by its stored hash, so it keeps the identity it had before the rename. The launcher's own menus still stop on it.
Engine rules ​
Each rule in re/db/ui/loader_rules.json that the checker implements runs when the file lists its id; its id, level, fail.code, fail.scope and fn go into every finding it raises.
| Rule id | Checks |
|---|---|
node. | Every node is [props with name and type, children, commands, events, layout, components(, behaviours, slot7)] |
node. | The type FourCC is one of the 136 registered node classes |
layout. | The first layout path is not an empty list |
layout. | Each layout step has isComponentId with a u64 componentId, or a u32 nodeId |
components. | The first component has name and a u64 componentId |
behaviours. | Every behaviour has a type |
commands., events. | Every command has command and signals, every event event and signals (an empty list is allowed) |
signals. | Every signal has to_pin and to_object, every mapping a value |
listbase. | List classes bind data with a non-empty dbentry |
svcd. | An SVCD sets value |
f1ts. | An F1TS inputType is normal or dpadOnly |
behaviour. | A node that lists the NBVN behaviour sets a visualNav object. Without it NBVN init returns 3 and a template root takes the whole instance down. Verified in game: the era picker edit that dropped visualNav from the dynamic_era_select root emptied the F1 World play screen |
template. | No template name repeats in the file or matches a template another stock . registers |
screen. | No screen name repeats in the file or matches a screen another stock . registers |
Rules with a property requirement carry it as data, and the checker applies every such rule the file lists:
{
"id": "behaviour.nbvn_visualnav",
"applies_to": { "behaviours": ["NBVN"] },
"props": [{ "key": "visualNav", "required": true, "shape": "object" }]
}applies_to names node classes (classes), behaviour types in slot 6 (behaviours), or both. Each entry in props has a key, required, a shape (any, object, value for { value }, binding for { value, dbentry } with a non-empty dbentry), and optionally values with case_insensitive.
The rules node.fourcc_matches_class, template.child_abort, template.link and screen.child_abort describe what a failure takes down; they have no check of their own. Findings for pins and empty signal lists quote the engine's silently ignored cases: a pin the target class does not handle, and an event with no signals, do nothing at run time.
Format checks ​
| Code | Checks |
|---|---|
stale-hash | Node id and nameId, layout nodeId, event and command hashes, and signal to_object and to_pin hashes match the djb2 of their names |
integrity | Signals to nodes missing from their own root |
xref-mismatch | An eXRef's xref is md5-64 of its fileRef |
scene-id | A scene's name is the componentId of its resource path |
nodeidlookup | A scene's nodeidlookup is the sorted djb2 of every element's leaf name |
component-missing | A screen or template root's scene is in this archive or in a stock archive |
layout-node-missing | A layout step whose nodeId was in the stock scenes of the archive is still in an edited scene's nodeidlookup |
layout-unresolved | A layout path that resolved against the stock scenes still resolves against the edited ones |
check::archives runs these for every archive that carries an edited resource, each with its own scenes, so a screen carried by both flow_f1_world.erp and screens/f1_world_activity_container.erp is checked in both.
Warnings ​
| Code | Flags |
|---|---|
node-type, element-type | A node or element type no stock file uses |
root-type, parent-type, element-parent | A type as a screen or template root, or under a parent type, where stock never puts it |
prop, prop-shape, param, param-type | A property or param stock never sets on that type, or a value shape stock never uses for it |
command, event | A command or event name stock never declares on that type |
event-no-signals, command-no-signals | An event or command with no signals where every stock one of that name on that type has signals |
event-signals, command-signals | Signals on an event or command that never has any in stock |
pin-in, pin-out, link, pin-attributes | A signal to a pin stock never signals on the target type, from a pin the source type never sends from, between types stock never links, or with attribute shapes stock never sends |
layout-shape, layout-element, layout-through | A layout attach shape, a bound element-type chain, or a path through a non-eXRef element that stock never uses |
layout-node-unknown | A new layout step whose nodeId is in no scene of the archive |
component-elsewhere | A new root whose scene is in another archive |
element-id | An elementId that is not the djb2 of the element's dotted path |
copies-differ | Two archives carry different copies of the same resource; the game uses the copy from the archive that loads first |
vm-key-new | A view-model key that no stock node of the same template binds |
prop-dropped | A property the stock node sets and the engine reads (a rule's props, the class init, or the element base init) that the edit removes |
Where it runs ​
- Every addon build: after an addon's ERP operations apply, every archive with a changed
.uior.idfgoes throughcheck::archivesagainst its stock copy. Hard findings stop the build. - Every addon screen in
operations.screens: hard findings stop the build; warnings go in the build's warnings. - The core menu build: the hard checks run on every ADDON SETTINGS screen it generates. Menu edits written with
write_erpcallcheck::archive_files(&[(name, stock archive, built archive), …])and stop on hard findings. - By hand:
cargo run --release -p swofty-core --example ui_check -- <stock.erp>=<built.erp> [<stock.erp>=<built.erp> …] [--against <stock.erp>=<other.erp>]Each pair is a stock archive and the built archive that replaces it. --against prints only findings the other build does not have. The exit status is 1 when there are hard findings.
Rebuilding the catalogue ​
F1_25_DIR=<game folder> python3 tools/ui_catalog.py nodes --baseline <baseline.json> [--stock <pristine copy>]
python3 tools/ui_catalog.py nodes-doc
F1_25_DIR=<pristine game folder> cargo test --release -p swofty-core --test real_ui_catalogThe last command checks that every stock .ui and .idf passes with no findings.