Wiki under construction. Many concepts here are from alpha 0.2 and may still change. The wiki will be finalised for version 1.0 — for now it's here to help closed testers and, later, early-access players find their way around. For the most up-to-date and accurate info, join the Discord — that's where changes are announced first and where you can ask directly.

Join the Discord for the latest info

Editing a Pack in SQL

The complete table-by-table schema of a mod pack, the tools to open one — sqlite3, DB Browser, a Python script — and the rules the loader plays by.

On this page

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.

Mod Editor in-game · checked · safe DB Browser spreadsheet-like editing sqlite3 · Python bulk edits · generation my_pack.db one SQLite file The game reads at career start

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.

KeyValueDefault
nameThe pack’s display name in the pickerthe file name
authorYour name — shown with the pack—
schema_versionFormat version this pack is written in (current: 2)1
min_game_versionOldest game build that understands it, e.g. 0.5.4-6none
scenarionew_series (you join as a new team) or imposed (you take over an authored grid)new_series
start_yearSeason one’s calendar yearthe game’s own
teams_mode · drivers_mode · sponsors_mode · suppliers_mode · circuits_modeextend (add to the generated world) or replace (yours only)extend
active_seriesCSV of championship abbreviations to run, e.g. PFS,PF2all active
rulesCSV of rule-set ids, e.g. points_classic_top6,quali_107series 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_version is 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_version is 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

ColumnMeaning
idStable key. What drivers, sponsors and suppliers point at. Any unique string.
nameDisplay name. A row with no name is skipped.
countryCountry name (Germany) or ISO code (de) — both work
budget_tiersmall · medium · large — the team’s behaviour, not just its cash
budget_musdStarting budget in millions. Sets the tier too, unless budget_tier says otherwise
reputation0-100
color1 · color2 · color3Hex, e.g. #1A5FB4. All three empty = keep the generated livery
car_indexCar model 0-11. Empty = keep the generated one (0 is a real car, not “unset”)
livery · suitVariant indices; 255 = generated
logoAn images row name, or a path relative to the mods folder, or a plain number for a built-in logo
car_imageAn images row name for a custom car sprite
infra_engineering · _rnd · _chassis · _handling · _aero · _engine · _marketing · _scoutingDepartment levels 0-10. 0 = keep generated
infra_academySame, capped at 3
career_wins · career_titles · best_championship · founding_yearPalmarès, for an authored history
historyPast 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

ColumnMeaning
idStable key
first_name · last_nameA row with neither is skipped
codeThree-letter code. Uppercased on load; a clash is renamed silently
countryISO code — this one is the flag key
age1-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 · aggressivityThe six abilities, 1-20
pot_intelligence … pot_aggressivityTheir ceilings, 0-20. 0 = let the generator decide
curve_intelligence … curve_aggressivityGrowth-curve ids; 255 = default. An unknown id falls back to the default rather than freezing the stat
salaryPer season
traitskind:tier pairs, comma-separated, e.g. 3:1,7:2
face_sheet · face_index · helmet_indexPortrait; 255 = generated
helmet_hue_0 · _1 · _2Helmet hue shifts. All three empty = generated
team · team_idThe seat. team_id wins when both are set
roleprimary · secondary · reserve
contract_years0-5
career_starts · career_wins · career_podiums · career_titles · best_finishPalmarès
fame0-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

ColumnMeaning
idStable key
nameSkipped if empty
sectorIndustry — drives the procedural logo
colorHex
budget · prominence1-10
originCountry name or ISO code
logoAn images row name or a path
team · team_idPre-sign it to a team. Accepts a list separated by ; or ,

A team’s first sponsor becomes its Principal, the rest Secondary.


suppliers

ColumnMeaning
idStable key
nameSkipped if empty
categoryengine · tires · fuel
color · origin · logoAs sponsors
budget · size1-10
money_per_gpPer-race money. Positive = they pay you, negative = you pay them
contractpartnership (exclusive) or client. Wins over the sign of money_per_gp
duration0-5 seasons
team · team_idPre-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:

CategoryNamed columns (0-10)
enginepower · reliability · endurance · potential
tiresperformance · endurance · reliability · heat
fuelefficiency

circuits

Hand-drawn tracks carried inside the pack. The Circuit Editor writes these, but the geometry is plain text, so a script can too.

ColumnMeaning
idStable key — what a calendar round points at
nameSkipped if empty
pointsThe centerline. x,y pairs joined by ;. Fewer than three points and the row is skipped
country · cityBlank gets a stable random place
target_kmLap length; under 1 falls back to 5 km
characterTrack character — this is what the simulation actually reads
presetVisual preset id
start_finish · pit_entry · pit_exitPositions along the lap; negative or absent = deduced
year · downforce · tire_wear · overtakingStored and shown, but not simulated — in play those three come from character

calendar

One row per round, in sort order, up to 24.

ColumnMeaning
kindcircuit = use a pack circuit; anything else = a generated track
circuit · circuit_idWhich one. circuit_id is tried first
character · target_kmFor 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. colour1 is not color1; 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 id first, 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.
  • 0 is not “unset” everywhere. It means keep generated for potentials, infrastructure and fame — but car_index 0 is 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