A mod pack is a plain SQLite database. The Mod Editor is the comfortable way to write one, but nothing stops you opening the file yourself — and for some jobs that is by far the faster route:
- importing a real-world season from a spreadsheet or a CSV;
- renaming forty sponsors, or shifting every driver’s salary by 10 %;
- generating a pack from a script, so you can regenerate it whenever your source data changes;
- reading someone else’s pack to see how they did something.
This page is the reference for that. It documents what the game actually reads — column by column — and the rules it plays by. Nothing here is a private detail: the loader is deliberately forgiving, and knowing how forgiving is what lets you hand-write a pack with confidence.
You never need this page to make a mod. The Mod Editor writes every table described here, checks your work, and upgrades older packs. Reach for SQL when you want bulk edits or scripting.
Opening a pack
Any SQLite tool works — the file has no custom header, no compression and no encryption.
Graphical — DB Browser for SQLite is free, runs on Windows, macOS and Linux, and edits tables like a spreadsheet. It is the gentlest way in if you have never written SQL: open the file, go to Browse Data, pick a table, type. Remember to hit Write Changes before you close it.
Command line — sqlite3 ships with macOS and most Linux distributions, and is a single small download on Windows:
sqlite3 my_pack.db
sqlite> .tables
sqlite> .schema teams
sqlite> SELECT name, country FROM teams ORDER BY sort;
sqlite> .quit
From a script — Python has SQLite built in, which makes a pack a natural build output:
import sqlite3
db = sqlite3.connect("my_pack.db")
db.execute("UPDATE drivers SET salary = CAST(salary * 1.1 AS INTEGER)")
db.commit()
db.close()
Always work on a copy. A pack is one file — backing it up is one drag.
The rules the loader plays by
These six rules are what make hand-editing safe. They are worth reading before the schema itself.
1 — Every table is optional. A pack with only a drivers table is valid. A missing table means “that section stays procedural”, never an error.
2 — Every column is optional. The loader asks SQLite which columns your table actually has and serves the rest as NULL. So CREATE TABLE teams(name TEXT) works. It also means a pack written for an older version keeps loading, and a typo in a column name is not an error — the column is simply never read, which is the one failure mode to watch for.
3 — Types are forgiving. Every value is read as text, whatever you stored. 12, "12" and even 12.0 all arrive as the number 12 where a number is expected. Store what is convenient.
4 — Rows are ordered by sort. Add an integer sort column and rows are read in that order; without one they come in insertion order. Order matters for teams (the grid is built in it) and for calendar rounds.
5 — Empty means “keep what the game generated”. This is the single most useful rule. An empty colour keeps the generated livery, an empty car_index keeps the generated model, a 0 potential lets the generator set the ceiling. You override what you care about and leave the rest alone.
6 — Bad rows are skipped, not fatal. A team row with no name, a circuit with fewer than three points, a malformed history entry — each is dropped on its own. The rest of the pack still loads.
meta — the pack’s own settings
Two columns, key and value, one row per setting. Keys are case-insensitive.
| Key | Value | Default |
|---|---|---|
name | The pack’s display name in the picker | the file name |
author | Your name — shown with the pack | — |
schema_version | Format version this pack is written in (current: 2) | 1 |
min_game_version | Oldest game build that understands it, e.g. 0.5.4-6 | none |
scenario | new_series (you join as a new team) or imposed (you take over an authored grid) | new_series |
start_year | Season one’s calendar year | the game’s own |
teams_mode · drivers_mode · sponsors_mode · suppliers_mode · circuits_mode | extend (add to the generated world) or replace (yours only) | extend |
active_series | CSV of championship abbreviations to run, e.g. PFS,PF2 | all active |
rules | CSV of rule-set ids, e.g. points_classic_top6,quali_107 | series defaults |
replace also accepts only; imposed also accepts takeover or existing. An unknown value falls back to the default rather than failing.
Two keys are a handshake with the game, and they are the reason a pack never fails silently:
- if
schema_versionis higher than the build understands, the pack’s row in the picker says so — “made for a newer pack format” — and the parts it cannot read are ignored; - if
min_game_versionis newer than the build, the row says “needs game X or newer”.
Set both. They cost two rows and they turn a mystifying half-working pack into a clear message.
CREATE TABLE meta(key TEXT, value TEXT);
INSERT INTO meta VALUES
('name', 'Season 1994'),
('author', 'Your Name'),
('schema_version', '2'),
('min_game_version', '0.5.4-6'),
('scenario', 'imposed'),
('start_year', '1994'),
('teams_mode', 'replace'),
('drivers_mode', 'replace');
teams
| Column | Meaning |
|---|---|
id | Stable key. What drivers, sponsors and suppliers point at. Any unique string. |
name | Display name. A row with no name is skipped. |
country | Country name (Germany) or ISO code (de) — both work |
budget_tier | small · medium · large — the team’s behaviour, not just its cash |
budget_musd | Starting budget in millions. Sets the tier too, unless budget_tier says otherwise |
reputation | 0-100 |
color1 · color2 · color3 | Hex, e.g. #1A5FB4. All three empty = keep the generated livery |
car_index | Car model 0-11. Empty = keep the generated one (0 is a real car, not “unset”) |
livery · suit | Variant indices; 255 = generated |
logo | An images row name, or a path relative to the mods folder, or a plain number for a built-in logo |
car_image | An images row name for a custom car sprite |
infra_engineering · _rnd · _chassis · _handling · _aero · _engine · _marketing · _scouting | Department levels 0-10. 0 = keep generated |
infra_academy | Same, capped at 3 |
career_wins · career_titles · best_championship · founding_year | Palmarès, for an authored history |
history | Past seasons as season:position:wins, joined by ; or newlines |
Infrastructure and palmarès only really matter in an imposed scenario, where the grid is meant to be a going concern from race one.
drivers
| Column | Meaning |
|---|---|
id | Stable key |
first_name · last_name | A row with neither is skipped |
code | Three-letter code. Uppercased on load; a clash is renamed silently |
country | ISO code — this one is the flag key |
age | 1-55. Under 16 is allowed for a prospect: he ages, stays hidden from Scouting until 14, and cannot be seated before the series’ own age rules allow |
intelligence · pace · consistency · adaptability · stamina · aggressivity | The six abilities, 1-20 |
pot_intelligence … pot_aggressivity | Their ceilings, 0-20. 0 = let the generator decide |
curve_intelligence … curve_aggressivity | Growth-curve ids; 255 = default. An unknown id falls back to the default rather than freezing the stat |
salary | Per season |
traits | kind:tier pairs, comma-separated, e.g. 3:1,7:2 |
face_sheet · face_index · helmet_index | Portrait; 255 = generated |
helmet_hue_0 · _1 · _2 | Helmet hue shifts. All three empty = generated |
team · team_id | The seat. team_id wins when both are set |
role | primary · secondary · reserve |
contract_years | 0-5 |
career_starts · career_wins · career_podiums · career_titles · best_finish | Palmarès |
fame | 0-100; 0 keeps the computed value |
Two drivers per team, one per seat. Two primary drivers on one team leaves car 2 empty — and a car with no driver does not start.
sponsors
| Column | Meaning |
|---|---|
id | Stable key |
name | Skipped if empty |
sector | Industry — drives the procedural logo |
color | Hex |
budget · prominence | 1-10 |
origin | Country name or ISO code |
logo | An images row name or a path |
team · team_id | Pre-sign it to a team. Accepts a list separated by ; or , |
A team’s first sponsor becomes its Principal, the rest Secondary.
suppliers
| Column | Meaning |
|---|---|
id | Stable key |
name | Skipped if empty |
category | engine · tires · fuel |
color · origin · logo | As sponsors |
budget · size | 1-10 |
money_per_gp | Per-race money. Positive = they pay you, negative = you pay them |
contract | partnership (exclusive) or client. Wins over the sign of money_per_gp |
duration | 0-5 seasons |
team · team_id | Pre-sign; a list, like sponsors |
The technical stats have two spellings, and the named ones are the reason this page exists. Four generic slots (stat_a … stat_d) meant something different per category, which no third-party tool could guess. Write the named columns instead — they win whenever they are set and non-zero:
| Category | Named columns (0-10) |
|---|---|
engine | power · reliability · endurance · potential |
tires | performance · endurance · reliability · heat |
fuel | efficiency |
circuits
Hand-drawn tracks carried inside the pack. The Circuit Editor writes these, but the geometry is plain text, so a script can too.
| Column | Meaning |
|---|---|
id | Stable key — what a calendar round points at |
name | Skipped if empty |
points | The centerline. x,y pairs joined by ;. Fewer than three points and the row is skipped |
country · city | Blank gets a stable random place |
target_km | Lap length; under 1 falls back to 5 km |
character | Track character — this is what the simulation actually reads |
preset | Visual preset id |
start_finish · pit_entry · pit_exit | Positions along the lap; negative or absent = deduced |
year · downforce · tire_wear · overtaking | Stored and shown, but not simulated — in play those three come from character |
calendar
One row per round, in sort order, up to 24.
| Column | Meaning |
|---|---|
kind | circuit = use a pack circuit; anything else = a generated track |
circuit · circuit_id | Which one. circuit_id is tried first |
character · target_km | For a generated round |
A pack carrying a calendar replaces the season. A pack carrying circuits but no calendar drops its tracks into the world pool and lets them win rounds on their own.
images
name (the key other tables point at) and data, the PNG bytes as a BLOB. Logos are authored at 195×195; a custom car sprite is a side view facing right on transparency, 256 px tall.
Inserting a picture from the command line:
sqlite3 my_pack.db "INSERT INTO images(name, data) VALUES('lotus.png', readfile('lotus.png'))"
…or from Python:
db.execute("INSERT INTO images(name, data) VALUES(?, ?)",
("lotus.png", open("lotus.png", "rb").read()))
Then point a row at it: UPDATE teams SET logo = 'lotus.png' WHERE name = 'Lotus'.
Two names that differ only by punctuation (my logo.png and my-logo.png) unpack to the same file name; the first one keeps it and the second gets a numbered suffix, so nothing is silently overwritten.
A minimal pack, start to finish
CREATE TABLE meta(key TEXT, value TEXT);
INSERT INTO meta VALUES
('name','Two Teams'), ('schema_version','2'),
('min_game_version','0.5.4-6'), ('teams_mode','extend');
CREATE TABLE teams(sort INTEGER, id TEXT, name TEXT, country TEXT,
budget_tier TEXT, color1 TEXT, color2 TEXT);
INSERT INTO teams VALUES
(1,'t_alba','Alba Corse','Italy','large','#B3122C','#F2E8D5'),
(2,'t_nord','Nordwind GP','Germany','medium','#14304F','#C8D3DC');
CREATE TABLE drivers(sort INTEGER, id TEXT, first_name TEXT, last_name TEXT,
code TEXT, country TEXT, age INTEGER,
pace INTEGER, consistency INTEGER,
team_id TEXT, role TEXT);
INSERT INTO drivers VALUES
(1,'d_ros','Elio','Rossi','ROS','it',27,17,15,'t_alba','primary'),
(2,'d_ber','Karl','Berger','BER','de',31,15,17,'t_alba','secondary');
That is a complete, valid pack. Everything not mentioned stays procedural.
Checking your work
Ask the game what it sees. Launch it once from a terminal with --list-mods: it prints the folders it scans in order, then every pack, circuit and language it found. This is the first thing to run when a pack does not appear.
mod folders (read in this order):
/home/you/.local/share/TheUndercut/mods
/path/to/game/mods (does not exist)
packs:
my_pack
Read the pack’s row in the picker. The game states there what it half-understood — a newer format, a game version it needs, or two teams sharing a name, which makes bindings ambiguous.
Open it once in the Mod Editor. Its Audit tab catches what SQL cannot: an empty second seat, a driver bound to a team that does not exist, a duplicate code. Opening a hand-written pack also stamps stable ids on rows that have none, which is the single best thing you can do to a pack written by hand.
Traps worth knowing
- A misspelt column is silent.
colour1is notcolor1; it is simply never read. If an edit seems to do nothing, check the spelling against this page first —PRAGMA table_info(teams)lists what your table really has. - Bindings follow
idfirst, name second. Give every row an id and renaming a team keeps its drivers. Without ids, renaming a team drops its drivers into the free-agent pool. 0is not “unset” everywhere. It means keep generated for potentials, infrastructure and fame — butcar_index0is a real car, so leave that column empty to keep the generated model.- A pack applies at career creation only. Editing it never changes a career already under way. Start a new one.
- The file must stay where it is. Logos and car sprites are read from it for the life of the career.
- Write your changes. A graphical browser keeps edits in a pending transaction — close it without Write Changes and nothing was saved.
See also
- Mod Packs — what each section means in play
- Mod Editor — the tool that writes all of this for you
- Circuit Modding — drawing the tracks a pack carries
- Publishing on the Steam Workshop — sharing the finished pack