Modding recipes ​
These rules come from built and runtime-tested variants of the database (the 24 to 30 car work and the first track activations). Each one cost a crash or a silent no-op to learn.
Editing a value ​
- Find the key in the catalogue or
tools/gamedb/catalog/keys.tsv.gz. Keep the type the stock key has. - Write the value in the stock form:
True/Falsefor bools,HH,MMfor times, comma-joined lists and vectors with no spaces. - Express it as a database operation:
{ "op": "set", "select": "tracks.Track[_metadata_=bahrain]", "field": "numberOfCorners", "value": "16" }selectnames a row by path (tracks.Track_3), by label (tracks.Track[_metadata_=bahrain]) or by hash placeholder ({446668bc25ba571c}[_metadata_=f1_26]). Any segment can carry a label:track_data.game_setup.TrackDataSet[_metadata_=f1_2026].TrackData[_metadata_=bahrain]. Use labels; row numbers can change between game builds. See Selecting rows.value_fromcopies a value from another key:"value_from": "{446668bc25ba571c}[_metadata_=myteam_26].worldEri".seton a key that does not exist stops the build withno such key."create": truewith"type": <tag>adds it.- Strings in an operation can hold
{{option}}placeholders and awhencondition, filled from the addon's settings at build time.
The launcher applies database changes in this order: track activation (one per enabled track), then each addon's operations in load order. A later write to the same key wins. It then writes the file sorted by key_hi.
Adding a row ​
A new row must have every key a stock row of the same table has, including keys of its sub-objects and nested tables. The game reads them and crashes on a missing one.
| Table | Keys per row |
|---|---|
entrants. | 246: 28 top-level and 218 under ai_vehicle_config (202 read by Sess_Parse_AiHandlingConfig) |
entrants. | 30: 26 top-level, _story_info_instancecount_ and story_info_0.{_metadata_,description,hashtag} |
entrants. | 10 |
tracks. | 268 schema keys over the row and its sub-tables (Daylight, CareerHubFirstVisit, CareerHubLastVisit, TrackCrowds, TrackHighlights, TrackStats, TrackTextures, TrackVideo, Tyres, Weather) |
Then:
- Copy every key of a stock row, with its type, to the new row number (the current count). The key schema and
keys.tsv.gzlist the keys;tools/track_keys.py keyslists a track's. - Give the row a new
_metadata_label, unique in the table. - Give it a new
stat_id(teams, vehicles) orstatId(tracks) above the stock maximum. Stock maxima: team 499, vehicle 508. - Raise the table's
_instancecount_by one.
Both halves are required:
- A row at or above the count is invisible: no enumeration reads it and no label lookup finds it. An entrant whose
teamnames such a row crashes the entrant parser. - A row inside the count is read at startup. Teams and vehicles inside the count join the availability passes and the vehicle AI loop.
New labels have no rows in side tables keyed by team or vehicle label (kits, showroom, stings, academies, career ratings, DLC unlocks, special liveries, equal-performance entrants). Cars and teams still load without them; no reader of those tables has been audited.
add-row copies every key of a source row, gives the copy a new stat_id in a table indexed by it, and raises the count:
{ "op": "add-row", "table": "entrants.teams", "from": "entrants.teams.team[_metadata_=williams_26]",
"set": { "_metadata_": "team12_26" } }It needs the table's real path (entrants.teams, not a {hash}). The build names the source row's keys from the launcher's key catalogue and warns when it can't vouch for a copy; see Key names.
Grid size ​
How a 2026 Grand Prix builds its grid, all of it data:
- The GP game mode row names game setup mode
f1_26, which names entrant setup modef1_26. - Three
EntrantSetupModeOverriderows replacef1_26withf1_custom_team_2026, row 10 ofentrants.entrant_setup_modes.entrant_setup_mode_N. - Row 10's list is
entrant_list_66(f1_custom_team_2026): 2 custom-team seats plusentrant_list_references=f1_2026. The parser reads the referenced list first, so the pool is list 34's entrants plus 2. - The selector takes
min(max_entrants, pool)entrants (reuse_entrantsFalse), then drops teams while there are more thanmax_teams(-1 means no limit). - With no player in the custom team,
remove_team_without_playersremoves its 2 seats.
Stock values: list 34 (f1_2026) has 22 entrants; row 10 has max_entrants 24; rows 8 (f1_26) and 9 (f1_generic_2026) have 22. That gives 22 cars, or 24 with a player-made team.
For N cars:
| Key | Value |
|---|---|
entrants. | N, with entrant_22 … rows of 10 keys each |
max_entrants on f1_26 and f1_generic_2026 | N |
max_entrants on f1_custom_team_2026 (row 10) | N + 2 |
entrants., entrants. | stock + new teams, with complete rows (above) |
World variant {446668bc25ba571c}[_metadata_=f1_26] worldEri, worldAssetGroup | the myteam_26 values, from 24 cars |
- Row 10 must be N + 2. The custom seats are drawn first and removed afterwards; N alone fields N − 2.
- The world switch is needed from 23 cars: the
_f1_26_myteamworld has 24 grid slots and 12 garages. More than 24 cars needs extra grid slots, garages and pit boxes in each track's world archive, which is outside the database. The Custom Grid Size addon adds them withpatch-pkgoperations. - More than 29 AI cars overruns the game's 30-entry AI vehicle name table (
Sess_Build_AiSystem). Nothing extends it. The Custom Grid Size addon stops at 30 cars, 29 of them AI when the player drives for a real team. - Still sized for 22:
f1_2026_equal_performance(list 35) and the per-team weighted-random grid table. Use realistic performance and a random grid. - Garages follow
garage_assignmentPreviousSeasonTeamStandingsfromf1_teams_2025. Teams without a standing are appended.
Adding a track ​
The launcher does this when a track is enabled (packages/core/src/activation/). Its table list is packages/core/src/activation/track_tables.json. For a new track id it:
- Clones the slot track's
tracks.Track_Nrow with its full schema and sub-tables, sets a newstatId(max + 1), and raisestracks._Track_instancecount_(43 in the shipping file). - Adds a track resource row
swofty_<code>totracks.track_resource.TrackResource_Nand names it in the track row'strack_resource. - Clones
TrackData_Nrows in every dataset where the base track has one (19 of 20), raising each dataset's_TrackData_instancecount_. - Adds a row, cloned from the slot track (or the base track), to every per-track table:
| Table | Rows | Missing row |
|---|---|---|
canned_events. | 44 | crash |
pit_lane. | 45 | checked |
pit_lane. | 43 | checked |
practice_programmes_{25,26}.{fuel_management,ers_management,quali_pace,race_pace_evaluation}. | 39–40 | checked |
| practice tyre wear (2 tables) | 39 | reader not found |
specialist track_data_N (2 tables) | 43 | career and F1 World |
replay. (with CameraShot_N) | 38 | optional |
tyre wear scales track_tyre_wear_scale_N (3 tables) | 31–44 | not traced |
track trivia TrackTriviaTrack_N | 32 | not traced |
- Adds the code to every
content_sets.content_set_N.available_tracks_N.trackslist that holds the base track, and to bothtrack_data.sorted_track_order.sorted_track_order_N.TrackRef_Norders. - Applies the track's overrides to its
TrackDatarows and track overrides to itsTrackrow.
Tables keyed by track that are calendars, challenge rounds, F1 World activities, map markers and trophies are not cloned.
Renaming a track ​
The menus read a track's names from its TrackNameData_0 row in each dataset, not from tracks.Track_N. An edit to tracks.Track_N.title alone leaves every menu unchanged. The row holds title, country_name, circuit, city_name, city_name_short, gp_long_name, gp_long_name_multiline, gp_long_name_multiline_previous, gp_short_name and acronym_name. The values are language keys (lng_track_aus_title).
Melbourne's rows, under track_data.game_setup in the shipping database:
| Dataset | Row | Fields set |
|---|---|---|
f1 | TrackDataSet_0. | all ten |
f2 | TrackDataSet_1. | all ten |
classic | TrackDataSet_2. | gp_long_name, gp_short_name; the rest are empty strings |
f1_2026 | TrackDataSet_16. | all ten |
Select the dataset and the TrackData row by their labels and name the sub-row in field, once per dataset:
{ "op": "set", "select": "track_data.game_setup.TrackDataSet[_metadata_=f1_2026].TrackData[_metadata_=melbourne]",
"field": "TrackNameData_0.title", "value": "lng_my_track_title" }Checking a build ​
Build without starting the game, then check the result against the pristine game:
swofty build --profile <name>
python3 tools/map_check.py <overlay folder> --game <pristine game folder>map_check.py checks that every key of each source row exists on the new rows, using the names F1_25.exe reads. Two more checks:
# every key the game's track readers use on one track
python3 tools/track_keys.py keys F1_25.exe gamedatabases.2025.bin bahrain
# the missing-key sentinel for a built file
python3 tools/gdb_hashsim.py <overlay>/game_package/gamedatabases/gamedatabases.2025.bintrack_keys.py and the reader-key part of map_check.py need numpy, pefile and capstone, and an exe build listed in track_keys.BUILDS.
For a hand-made database, also check:
- the file round-trips (
python3 tools/gamedb.py roundtrip <file>); - every new row has every key of its source row, with the same types;
- every label an entrant, track or dataset references resolves inside its table's count;
- the counts read back as intended (
python3 tools/gamedb.py get <file> <path>).