The registry
A registry is a named, typed collection of records that every addon in the profile shares. A record is one item of data, such as a car, an engine sound or one season of a career. You add records, and change other addons' records, in code through the registry's handle: a C++ object, such as cars::vehicles, declared in a system header.
An engine sound pack adds its sound under its own key and points the 2017 Ferrari at it. add provides a record. layer changes someone else's.
Each registry is an inline Registry<T> in a system header. T is the record struct, so the compiler checks every field, and records point at each other with Ref<T>.
The Without code tab is the same addon with no DLL: the same records as data files in registry/.
#include <swofty/registry.hpp>
#include <swofty/systems/sounds.hpp>
#include <swofty/systems/cars.hpp>
using namespace swofty;
SWOFTY_REGISTER
{
// provide under your own key: the id is "mod.esp/v10_screamer"
auto v10 = sounds::engine_sounds.add("v10_screamer", {
.bundle = file("sounds/v10_screamer.bdl"),
.settings_from = sounds::engine_sounds["game/ferrari_2025"],
});
// change another addon's record: an edit, run on its effective value in load order
cars::vehicles.layer(cars::vehicles["swofty.classic-seasons-2017/ferrari"], [=](cars::Vehicle &v) {
v.engine_sound = v10;
});
}
SWOFTY_ADDON()swofty/loader.log:
[registry] register: mod.esp (esp.dll) 1 engine_sound, 1 layer on vehicle (early stage, 0.4 ms)
[registry] vehicle swofty.classic-seasons-2017/ferrari: edit by mod.esp changed /engine_sound -> mod.esp/v10_screamer
[registry] applied at boot: engine_sound 1 (1 bundle path redirected to mod.esp), vehicle 1 (3 database records)The handle
namespace swofty {
enum class Applies { boot, content, session, read }; // when the game reads the registry's data
enum class Scope { global, profile, career }; // how long a layer lasts and what it applies to
enum class Status { ok, no_record, no_registry, invalid, late }; // why get returned nothing
struct RegistryOptions {
Applies applies = Applies::read; // x-applies
bool snapshot = false; // frozen into careers, season by season (x-snapshot)
std::size_t snapshot_budget = 65536; // bytes after compression, per season of one career (x-snapshot-budget)
};
struct Text { // a lang key or a literal; a plain string converts to Text::key
static Text key(std::string_view lang_key);
static Text literal(std::string_view text);
};
struct Colour { Colour(std::string_view hex); Colour(uint32_t argb); }; // "#22A45D" or 0xFF22A45D
struct Curve { static Curve flat(float from, float to, float value); }; // a game curve: one value over a range
struct VtfSection { static VtfSection read(Path vtf); }; // one VTF section, decoded
template <class T> class Ref { // a typed reference to a record, by id
public:
std::string_view id() const;
Ref optional() const; // missing: the element that holds it is dropped
Ref or_else(Ref other) const; // missing: the fallback
bool resolved() const;
};
template <class T> using Edit = std::function<void(T &)>;
template <class T> class Select { // a selector: every match, now and later
public:
Select except(std::vector<Ref<T>> ids) const; // every match but these ids
};
template <class T> Select<T> where(std::function<bool(const T &)> match); // runs on the base value
template <class T> Select<T> under(std::string_view id_prefix); // ids under a prefix
template <class T> class Tag { // a shared, named set of records ("c/f1_cars")
public:
std::string_view id() const;
bool contains(Ref<T> record) const; // effective membership; a variant is in its base's tags
std::vector<Ref<T>> members() const;
};
template <class T> Select<T> tagged(Tag<T> tag); // every record in the tag
template <class T, class U> Select<T> tagged(Ref<U> T::*field, Tag<U> tag); // every record whose field is in the tag
template <class T> class LayerHandle { // what layer returns
public:
LayerHandle &in(events::Selector where); // apply only there: a setup mode, a career, a box
};
template <class V> V option(std::string_view id); // one of your addon's options; an edit that reads it re-runs when it changes
Path file(std::string_view path); // a file in your addon's folder
Path stock(std::string_view game_path); // the game's original file, whatever an import replaced
template <class T> class Registry {
public:
Registry(std::string_view name, RegistryOptions opts = {});
std::string_view name() const; // "vehicle", for logs, origins and data files
// provide (your own keys)
Ref<T> add(std::string_view key, T value); // base record "<your id>/<key>"
Ref<T> variant(std::string_view key, Ref<T> base, Edit<T> edit); // extends base; base's layers flow in
void provide(std::function<std::optional<T>(std::string_view id)> fn,
std::function<void(std::function<void(std::string_view)>)> list = {}); // dynamic
void invalidate(Ref<T> id = {});
void check(std::function<void(const T &, Errors &)> fn); // the owner's own rules
void protect(Ref<T> own_record, std::vector<std::string_view> fields); // fields other addons' selectors skip
// change (anyone's records)
LayerHandle<T> layer(Ref<T> target, Edit<T> edit, Scope scope = Scope::automatic);
LayerHandle<T> layer(Select<T> which, Edit<T> edit, Scope scope = Scope::automatic); // every match, now and later
void clear_layer(Ref<T> target, Scope scope = Scope::automatic); // drop this addon's layer
// tags
Tag<T> tag(std::string_view id) const; // "c/f1_cars"
void tag_add(Tag<T> tag, Ref<T> member); // put a record in a tag
// read
Ref<T> operator[](std::string_view id) const; // reference by id (checked at resolve)
std::optional<T> get(Ref<T> id) const; // the effective value
Status status(Ref<T> id) const; // why get returned nothing
std::vector<Ref<T>> all(std::string_view prefix = {}) const;
std::string origin(Ref<T> id, std::string_view field) const; // which addon set a field ("engine_sound")
Token watch(std::function<void(Ref<T>)> fn);
};
// a second major of a registry: convert both ways, and upgrade old snapshots
template <class A, class B> void adapter(Registry<A> &from, Registry<B> &to, std::function<B(A)> up, std::function<A(B)> down);
template <class A, class B> void migrate(Registry<A> &from, Registry<B> &to, std::function<B(A)> fn);
}add,variant: Keys, bases and variants.provide,invalidate: Providers.get,status,all,origin,watch: Reading.layer,LayerHandle::in,clear_layer,check,protect,where,under,tagged,option: Layers.tag,tag_add,Tag: Tags, below.Scope,snapshot,snapshot_budget: Scopes and snapshots.RegistryOptions::applies,Applies,stock: When changes apply.Ref: Dependencies and guards.adapter,migrate: Versioning.
SWOFTY_REGISTER runs once per game start, at the early stage, addon by addon in load order. SWOFTY_ON_LOAD runs at the stage your manifest asks for. Registries the game reads only at boot (Applies::boot) take records and layers from SWOFTY_REGISTER. Every other registry also takes them from SWOFTY_ON_LOAD and later, while the game runs, and the change takes effect the next time the game reads that data.
The core's registries
The core owns the registries that describe the game. Their handles live in one header per system under swofty/systems/, next to the system's events (swofty::<system>::events), hook payloads and operations (swofty::cars::set_attribute(...)):
swofty/registry.hpp Registry<T>, Ref<T>, Edit<T>, Select<T>, Tag<T>, LayerHandle<T>, Scope, RegistryOptions,
Applies, Status, SWOFTY_RECORD, Keyed<T, &T::key...>, SWOFTY_REGISTER,
swofty::option<T>(), file(), stock(), where(), under(), tagged(), adapter(), migrate()
swofty/systems/cars.hpp swofty::cars vehicles, packages, handling
swofty/systems/liveries.hpp swofty::liveries liveries
swofty/systems/teams.hpp swofty::teams constructors, identities, logos, pit_times
swofty/systems/drivers.hpp swofty::drivers people, identities, heads, looks, stats
swofty/systems/wearables.hpp swofty::wearables helmets, suits, gloves, season_sets
swofty/systems/sounds.hpp swofty::sounds engine_sounds
swofty/systems/career.hpp swofty::career modes, contents, seasons, calendars, ratings, characters, wizards,
hub_tabs, rnd_trees, markets
swofty/systems/content.hpp swofty::content packages (Grand Prix and Time Trial seasons), era_boxes
swofty/systems/power.hpp swofty::power drs_rules (the units, suppliers and component rules come later)
swofty/systems/ui.hpp swofty::ui strings, addon_options
swofty/systems/sessions.hpp swofty::sessions Rules, set_control (the registries come later)
swofty/systems/tracks.hpp swofty::tracks tracks, venues, data, per_package (experimental); set_track_fuel,
copy_track_fuel, restore_track_fuel
swofty/systems/ai.hpp swofty::ai the ai.controller.update payload
swofty/systems/spectator.hpp swofty::spectator set_active, follow, set_camera, currentThirty-seven registries are in the headers, each with a schema. The four tracks registries (track, track.venue, track.data, track.per_package) came in 1.6.5 and are experimental. The core applies most of them. For career.mode and vehicle.handling it stores the records and applies nothing, and it reads career.calendar only for the calendar a box's package names; at boot it logs [registry] <name>: N record(s); this core applies nothing from it for any registry that has records and no applier. The contract, R&D department, facility, economy, sponsor, car audio, damage model and car parts registries, and the track layer, slots and AI line, tyres, power (apart from rules.drs), session, weather, AI, UI-screen and graphics registries of the Systems catalogue, come in later headers; their pages describe the design.
The system headers sit under swofty/systems/, apart from the headers older addons use, such as swofty/career.hpp, and their names never clash. A handle's name() is the registry name the logs, origins and data files use: cars::vehicles is vehicle, career::seasons is career.season. Every system has a page in the Systems catalogue.
Records are structs
A record is a plain struct. SWOFTY_RECORD lists its fields. That list is the record's shape in data files, in Send Logs and in origin paths.
// swofty/systems/career.hpp (abridged)
namespace swofty::career {
struct SeasonTeam {
int slot = 0; // 1-11, the career team that carries this season's team
Ref<teams::Identity> team; // empty: the slot is empty this season
std::array<Ref<drivers::Identity>, 2> seats; // seat 0, seat 1
};
SWOFTY_RECORD(SeasonTeam, slot, team, seats)
enum class ChangeKind { swap, stand_in };
struct SeasonChange {
int round = 0, slot = 0, seat = 0;
Ref<drivers::Identity> out, in;
ChangeKind kind = ChangeKind::swap;
std::optional<int> until_round; // stand_in only
};
SWOFTY_RECORD(SeasonChange, round, slot, seat, out, in, kind, until_round)
struct Season {
static constexpr Version version{1, 0}; // the schema this header was built against
std::string data_group;
int year = 0;
Keyed<SeasonTeam, &SeasonTeam::slot> teams; // matched by slot, not by position
Keyed<SeasonChange, &SeasonChange::round, &SeasonChange::slot, &SeasonChange::seat> changes;
json::Value extra; // fields from a newer minor, kept for the round trip
};
SWOFTY_RECORD(Season, data_group, year, teams, changes)
inline Registry<Season> seasons{"career.season", { .applies = Applies::read, .snapshot = true, .snapshot_budget = 65536 }};
}Ref<T>points at a record of another registry by id. Validation checks that it exists..optional()and.or_else(other)say what happens when it doesn't (see Dependencies and guards).Keyed<T, &T::field...>is a list whose elements are matched by key. Edits, origins and conflicts name an element by its key (teams/slot=1), and a data file's layer changes one element without restating the list. A key can be several fields, as inchanges.at(key)returns the element and appends it when it's missing,find(key)returns a pointer or null, anderase(key)removes it.extraholds fields this header doesn't know. Code built against 1.0 reads a 1.1 record and writes it back without losing anything (see the tolerant reader rule).stat_idis the number the game's database uses for a car, team, driver, track or mode. A record'sstat_idisStatId::automaticunless you set it, and the core allocates one (see Stat ids).RegistryOptionsset when the game reads the registry's data and whether careers freeze it:appliesdefaults toApplies::read: the game, or another addon, reads the data through the registry each time it needs it. A registry the game reads at boot, when content becomes active or when a session loads says so (see When changes apply).snapshotdefaults tofalse. Withtrue, each career freezes the registry's records season by season (see Career snapshots).snapshot_budgetdefaults to 65536: the most bytes, after compression, that one season of one career may freeze from this registry.
Your own registry
An addon declares a registry for its own data the same way the core does: a struct and an inline Registry<T>. Put them in a header you ship. Other addons include it to add, layer and read.
Tails' "what if" addon owns a registry of career setup presets. Its name starts with the addon's id.
The header is the registry's public shape. Other addons include it to add presets of their own or layer Tails' presets.
#pragma once
#include <swofty/registry.hpp>
namespace whatif {
enum class Rnd { real_life, hybrid };
enum class TeamMoves { follow_driver, choose_team };
struct Preset {
static constexpr swofty::Version version{1, 0};
std::string name; // [a-z0-9_]+
int start_year = 2016; // 2016-2026
Rnd rnd = Rnd::real_life;
TeamMoves team_moves = TeamMoves::follow_driver;
};
SWOFTY_RECORD(Preset, name, start_year, rnd, team_moves)
inline swofty::Registry<Preset> presets{"tails.whatif.presets"};
}check adds the registry's own rules on top of the struct: ranges, patterns, anything the types can't say. It runs on the result of every edit:
// native/src/whatif.cpp
SWOFTY_REGISTER
{
whatif::presets.check([](const whatif::Preset &p, swofty::Errors &err) {
if (p.start_year < 2016 || p.start_year > 2026)
err.add("start_year", "must be 2016-2026");
if (!swofty::is_key(p.name))
err.add("name", "lowercase letters, digits and _ only");
});
whatif::presets.add("real_2016", { .name = "real_2016", .start_year = 2016 });
}An addon that doesn't have your header can still read your registry, untyped:
swofty::Registry<swofty::json::Value> presets{"tails.whatif.presets"};
if (auto p = presets.get(presets["tails.whatif-history/real_2016"])) {
int year = (*p)["start_year"].as_int(); // 2016
std::string moves = (*p)["team_moves"].as_string(); // "follow_driver"
}Ids
Every record has an id: the id of the addon that provides it, a /, and the key it was added under.
career.season <- registry
swofty.progressive/2016 <- record id: <addon id>/<key>
swofty.progressive/2017 { data_group: "f1_career_progressive", year: 2017, teams: [...] }
career.driver_ratings
swofty.progressive/2017/game/max_verstappen { driver: "game/max_verstappen", experience: 74, ... }
engine_sound
game/ferrari_2025 <- the core's record of the stock game
track.venue
venue/sochi <- a shared id the core owns; a track addon provides itadd("key")makes<your addon id>/<key>. No addon can provide a record under another addon's id.- The core's records describe the stock game and use
game/:game/ferrari_2025,game/f1_2026. - Use
/to build keys out of parts. A prefix then lists a group:swofty.progressive/lists every Progressive season (see Listing).
Shared ids for real places, people and teams
The core owns three kinds of shared ids. Each names something real that several addons can bring:
venue/sochi,venue/hockenheimring: a real circuit. A track record provides it.person/rio_haryanto,person/alain_prost: a real driver. A driver record provides it.constructor/force_india,constructor/williams: a real team. A team record provides it.
Other records point at the shared id instead of at one addon's record. A 2016 calendar asks for venue/sochi, and a 2016 line-up asks for person/rio_haryanto. The reference resolves to whichever enabled addon provides that id. When two addons provide it, the last one in load order wins (see Shared ids). The Tracks, Drivers and Teams pages show the records that provide them.
Stat ids
Database-backed records (cars, teams, drivers, tracks, setup modes, looks) need a stat_id. The core allocates the ones left at StatId::automatic, in the game:
- One store per install. The core keeps every id it has allocated in
$DATA/registry/ids.json(the launcher's data folder), keyed by record id. A record gets the same id at every game start. - An id is reused only when no career uses it. When an addon is removed, its records' ids stay reserved while any career still refers to them.
- Each career carries its own id map. The career's save entry holds the stat id of every record the career refers to. A save that Steam Cloud brings to another PC loads with the ids it was saved with, and a track or a driver can't change places.
Driver ids run 208 to 16382, and the core allocates 208 to 253, then 256 upward. It never allocates 254 (default_driver), 255, or an id whose low byte is 0xff, which the 2024 and 2025 UDP formats read as a network player. 16382 is the highest id saves and net messages carry in 14 bits; 16383 means no driver.
An explicit stat_id you ship stays your record's id from then on. Other addons and saved careers refer to it.
Rows an older addon's database operations copy with add-row keep their ids the same way, in $DATA/registry/row-ids.json, keyed by addon, table and row label (Kept ids); a clash in one profile moves a row's id in that profile only. A copy never takes an id ids.json holds.
[registry] stat ids: driver.identity swofty.f1-1990/alain_prost -> 241 (pool 208-16382, 16117 free after this)
[registry] stat ids: track mod.sochi/sochi -> 18 (kept from ids.json)A row already in the database the game loads keeps its id. That is a stock row, or a row an older addon's database operations add with an explicit stat_id. This covers the records the core writes rows for at boot: cars, vehicle packages, packages' entrant lists and scenarios, teams, drivers, heads, looks, helmets, suits, gloves and team kits:
- The record moves. When such a row holds your record's
stat_id, or the idids.jsonkept for it, under another label, your record takes the next free id of its pool. The core logs it once per game start. - The new id is kept. The record keeps it at every start while the row is there, and gets its own
stat_idback when the row is gone. - A career saved before the move still holds the old id in its map. At
career.load, the guard names the row that holds it (see Dependencies and guards). - Stock rows and the older addon's rows are never changed.
[registry] ids: swofty.classic-seasons-2017/ferrari pinned vehicle stat id 509 is held by bdx_s5_car_mclaren_3 from the database; uses 600Tags
A tag is a shared, named set of records in one registry. The core defines the shared tags, such as c/f1_cars on vehicle (and on vehicle.handling, for the same cars' handling) and c/street_circuits on track. Any addon can add its records to one. Selectors, event scopes and a box's track list can target a tag, and they reach records that addons add later.
A converted 2021 car joins the core's set of F1 cars. An AI addon that changes every car in the set then changes this one too.
tag looks a tag up by id. tag_add puts one of your records in it. A variant is in every tag its base is in, so Classic Seasons' 2017 Ferrari, a variant of game/ferrari_2025, is in c/f1_cars without saying so.
In a data file, registry/tags.json lists the full ids you add, under the registry and the tag.
#include <swofty/registry.hpp>
#include <swofty/systems/cars.hpp>
using namespace swofty;
SWOFTY_REGISTER
{
auto rb21 = cars::vehicles.add("redbull_2021", { .label = "rb16b", .donor = cars::vehicles["game/redbull_2025"] });
cars::vehicles.tag_add(cars::vehicles.tag("c/f1_cars"), rb21);
}
SWOFTY_ADDON()// another addon: every F1 car's AI config, whoever added the car, now and later
auto f1_cars = cars::vehicles.tag("c/f1_cars");
ai::vehicle_configs.layer(tagged(&ai::VehicleConfig::vehicle, f1_cars), [](ai::VehicleConfig &c) {
c.racecraft.overtake_aggression = 0.85f;
});- Only the core defines
c/tags. Addons add records to them. - Members are optional. A member whose addon is off is skipped. A tag never makes another addon required.
- Membership is read when the data is read. A selector on a boot registry reads its tag at boot. A box reads the tags in its track list when it becomes active. An event scope reads its tag when the event fires.
- In a data file's selector,
"tag": "c/f1_cars"picks the tag's members (see Layers).
Rules
- Registry names are dotted and lowercase. The core's are named after their system (
vehicle,track.data,tyre.class,career.season,session.mode,lang.string,era.box,hub.tab,addon.option), and each system page lists them. An addon's own registry starts with its id. - Values are at most 1 MB as JSON.
- A record has one base from one provider, or it's a variant of another record. Every change on top of it is a layer.
- Every write is validated (the struct, every
Ref, the owner'scheck) before it becomes visible.
Without code
An addon with no DLL ships records as data files in registry/*.json. The core finds them by folder name, with no manifest entry. A file holds records by registry name, then key:
// registry/sounds.json
{ "engine_sound": { "v10_screamer": { "bundle": "sounds/v10_screamer.bdl", "settings_from": "game/ferrari_2025" } } }Keys are your own: v10_screamer becomes <your id>/v10_screamer. A Ref is written as the record's full id. Tag members go in registry/tags.json. Fields you protect go in your own record as "$protect": ["performance_tier"].
The SDK ships a JSON Schema for each core registry and version, for checking data files: packages/sdk/std/registries/<name>/<major>.<minor>.json. It's generated from the headers, and its keywords match the code:
x-registryisRegistry<T>::name(), andx-versionisT::version.x-appliesisRegistryOptions::applies:boot,content,sessionorread.x-snapshotandx-snapshot-budgetareRegistryOptions::snapshotandsnapshot_budget.x-keymarks aKeyed<T, &T::field...>list, andx-refmarks aRef<T>.type,enumandrequiredcome from the struct's field types.minimum,maximumandpatterncome from the owner'scheck.
The core declares every registry the SDK headers define (37) itself at the start of the register pass, so a data-only addon needs no DLL to add records to any of them, and its registry/index.json entry is all the core needs to find it. A data-only addon can't declare a registry of its own. It can add records to any registry, add them to any c/ tag and layer any record.