Make an engine sound pack
You'll build mod.esp, an addon that registers a V10 engine sound, puts it on Classic Seasons' 2017 Ferrari, and adds "F1 2017 (V10)": the 2017 season as its own Grand Prix and Time Trial box, with the Mercedes and the Red Bull on the V10 too. The player picks the Ferrari in the F1 2017 box and hears the V10 on track.
The finished addon is templates/std/engine-sound in the launcher's repository.
The folder
mod.esp/
swofty.json
registry.json
sounds/
v10_screamer.bdl your bundle; standin.sh copies a stock one for testing
standin.sh
native/
build.sh
esp.dll
src/
esp.cpp
test/
register_test.cpp1. Add the bundle
Put your V10 sound bundle at sounds/v10_screamer.bdl: one you made, or one from a sound mod you have permission to use. Sound mods keep their bundles at audio/2025/bundles/<maker>_<year>.bdl, which is how the importer recognises them.
To test the addon before you have one, sounds/standin.sh copies the stock 2025 Mercedes bundle from your game folder. The Ferrari then sounds like the Mercedes.
#!/usr/bin/env bash
# The repository ships no sound bundle. This puts a stand-in at sounds/v10_screamer.bdl: a copy of a stock
# bundle from your own game folder (the 2025 Mercedes by default), or the bundle you name.
# sounds/standin.sh the stock mercedes_2025.bdl
# sounds/standin.sh ~/sounds/my_v10.bdl your own bundle
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")"
GAME=${GAME_DIR:-$HOME/.steam/steam/steamapps/common/F1 25}
SRC=${1:-$GAME/audio/2025/bundles/mercedes_2025.bdl}
cp "$SRC" v10_screamer.bdl
echo "sounds/v10_screamer.bdl <- $SRC"2. Register the sound, the layer and the box
settings_from copies the stock Ferrari sound's mixing settings, while tuning_from copies its engine, era, cabin and exhaust tuning; the V10 audio comes from your bundle (Sounds).
The layer changes the 2017 Ferrari's own sound, wherever that record is used: the F1 2017 box, Progressive Career's 2017 season, and the V10 box below. Later layers on the same field win by load order.
"F1 2017 (V10)" is a variant of the 2017 package. Every team, seat and field it doesn't change comes from the 2017 package. Its Mercedes and Red Bull drive variants of their cars with the V10, so the real 2017 box keeps their sounds. Its era box sits next to the real one.
// SPDX-License-Identifier: MIT
#include <map>
#include <string>
#include <swofty/registry.hpp>
#include <swofty/systems/cars.hpp>
#include <swofty/systems/content.hpp>
#include <swofty/systems/sounds.hpp>
using namespace swofty;
SWOFTY_REGISTER
{
// mod.esp/v10_screamer: the bundle is this addon's file; mixing and tuning come from the stock Ferrari
auto v10 = sounds::engine_sounds.add("v10_screamer", {
.bundle = file("sounds/v10_screamer.bdl"),
.settings_from = sounds::engine_sounds["game/ferrari_2025"],
.tuning_from = sounds::engine_sounds["game/ferrari_2025"],
});
// Classic Seasons' 2017 Ferrari gets the V10, everywhere that car races
cars::vehicles.layer(cars::vehicles["swofty.classic-seasons-2017/ferrari"], [=](cars::Vehicle &v) {
v.engine_sound = v10;
});
// "F1 2017 (V10)": the 2017 season as its own box, with the Mercedes and the Red Bull on the V10 too.
// Their V10 cars are variants, so the real 2017 box keeps its sounds.
std::map<std::string, Ref<cars::Vehicle>> v10_cars;
for (std::string team : {"mercedes", "redbull"})
v10_cars[team] = cars::vehicles.variant(team + "_v10", cars::vehicles["swofty.classic-seasons-2017/" + team],
[=](cars::Vehicle &v) {
v.label = "esp_cs17_" + team;
v.engine_sound = v10;
});
auto season = content::packages.variant("f1_2017_v10", content::packages["swofty.classic-seasons-2017/f1_2017"],
[=](content::Package &p) {
p.title = Text::literal("F1 2017 (V10)");
p.entrant_list = "esp_f1_2017_v10";
p.stat_id = StatId::automatic; // its own stat id, not the 2017 package's
for (const auto &[team, car] : v10_cars)
p.teams.at(team).vehicle = car;
});
content::era_boxes.add("f1_2017_v10", {.package = season, .title = Text::literal("F1 2017 (V10)"), .order = 2017});
}
SWOFTY_ADDON()SWOFTY_REGISTER runs before the game loads its database. A vehicle's engine_sound supplies the audio records the core applies at boot (Cars), and a package and its era box apply when the box opens (Content).
To put the V10 on the stock 2025 Ferrari instead, in every year that inherits its sound, layer cars::vehicles["game/ferrari_2025"]. A year that sets its own sound keeps it, including Classic Seasons' cars with period sounds. See How changes reach seasons and careers.
3. Name the addon and declare its target
swofty.json identifies the addon and its DLL, and copies sounds/ into the addon's folder in the game with the DLL. Its requires names Classic Seasons 2017, so the launcher stops Play when it's off. registry.json requires the records the addon builds on, so Play also stops when an update to Classic Seasons removes them (Dependencies).
{
"schema_version": 1,
"id": "mod.esp",
"name": "V10 Engine Sounds",
"description": "A V10 engine sound on Classic Seasons' 2017 Ferrari, and an F1 2017 (V10) box with the V10 in the Mercedes and the Red Bull too.",
"version": "1.0.0",
"author": "you",
"license": "MIT",
"kind": ["native"],
"category": "cars",
"compat": { "game": "f1_25", "launcher": ">=1.6.0" },
"requires": [ { "id": "swofty.classic-seasons-2017" } ],
"operations": { "native": [ { "dll": "native/esp.dll", "files": ["sounds/"] } ] }
}{
"requires": [
{ "record": "vehicle:swofty.classic-seasons-2017/ferrari" },
{ "record": "content.package:swofty.classic-seasons-2017/f1_2017" }
]
}4. Test it without the game
swofty dev test runs test/register_test.cpp against the fake core. It checks the sound, the layer on the Ferrari, the two car variants, the package variant and its box. Linked with the core's registry engine, it also checks the effective records: the real 2017 box keeps its Mercedes, and the V10 box drives the V10 variant.
// SPDX-License-Identifier: MIT
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <swofty/fake_core.hpp>
#include "../native/src/esp.cpp"
namespace json = swofty::json;
static int failures = 0;
static void check(bool ok, const char *what)
{
std::printf("%s %s\n", ok ? "ok " : "FAIL", what);
failures += !ok;
}
// What Classic Seasons 2017 registers, as far as this addon uses it.
static void classic_seasons_2017()
{
using namespace swofty;
for (const char *team : {"mercedes", "ferrari", "redbull"})
sounds::engine_sounds.add(team, {.bundle = file(std::string("sounds/cs17_") + team + ".bdl")});
for (const char *team : {"mercedes", "ferrari", "redbull"})
cars::vehicles.add(team, {.label = std::string("cs17_") + team,
.model = file(std::string("cars/cs17_") + team + ".erp"),
.engine_sound = sounds::engine_sounds[std::string("swofty.classic-seasons-2017/") + team]});
content::Package season{.title = "lng_cs_f1_2017", .entrant_list = "cs_f1_2017", .season = 2017};
for (const char *team : {"mercedes", "ferrari", "redbull"})
season.teams.push_back({.key = team, .vehicle = cars::vehicles[std::string("swofty.classic-seasons-2017/") + team]});
content::packages.add("f1_2017", season);
}
static const swofty::fake::Record *recorded(const swofty::fake::Core &core, std::string_view op, std::string_view id)
{
for (const auto &r : core.records())
if (r.op == op && r.id == id)
return &r;
return nullptr;
}
int main()
{
swofty::fake::Core core{"mod.esp"};
if (core.rules()) {
swofty::fake::Core::default_engine()->begin_addon("swofty.classic-seasons-2017");
classic_seasons_2017();
}
core.run_register();
auto sounds = core.ids("engine_sound");
check(std::find(sounds.begin(), sounds.end(), "mod.esp/v10_screamer") != sounds.end(), "adds mod.esp/v10_screamer");
auto sound = core.base("engine_sound", "mod.esp/v10_screamer");
check(sound && (*sound)["bundle"].as_string() == "sounds/v10_screamer.bdl", "its bundle is the addon's file");
check(sound && (*sound)["settings_from"].as_string() == "game/ferrari_2025", "mixing from the stock Ferrari");
auto ferrari = core.layers("vehicle", "swofty.classic-seasons-2017/ferrari");
check(ferrari.size() == 1 && ferrari[0]->scope == SWOFTY_SCOPE_AUTOMATIC, "one layer on the 2017 Ferrari");
if (ferrari.size() == 1) {
auto v = core.apply(*ferrari[0], json::parse(R"({ "label": "cs17_ferrari", "model": "cars/cs17_ferrari.erp",
"engine_sound": "swofty.classic-seasons-2017/ferrari" })"));
check(v && (*v)["engine_sound"].as_string() == "mod.esp/v10_screamer", "the layer puts the V10 on it");
check(v && (*v)["label"].as_string() == "cs17_ferrari", "and leaves the rest of the car alone");
}
for (const char *team : {"mercedes", "redbull"}) {
std::string id = std::string("mod.esp/") + team + "_v10";
const auto *r = recorded(core, "variant", id);
check(r && r->base == std::string("swofty.classic-seasons-2017/") + team, ("a V10 variant of the " + std::string(team)).c_str());
}
const auto *box = recorded(core, "variant", "mod.esp/f1_2017_v10");
check(box && box->base == "swofty.classic-seasons-2017/f1_2017", "F1 2017 (V10) is a variant of the 2017 package");
auto era = core.base("era.box", "mod.esp/f1_2017_v10");
check(era && (*era)["package"].as_string() == "mod.esp/f1_2017_v10", "with its own era box");
check(era && (*era)["order"].as_int() == 2017, "sorted with 2017");
if (!core.rules()) {
std::printf("skip effective values and origins: no registry engine linked\n");
return failures ? 1 : 0;
}
auto car = core.effective("vehicle", "swofty.classic-seasons-2017/ferrari");
check(car && (*car)["engine_sound"].as_string() == "mod.esp/v10_screamer", "the effective 2017 Ferrari has the V10");
check(core.origin("vehicle", "swofty.classic-seasons-2017/ferrari", "/engine_sound") == "mod.esp", "from mod.esp");
auto mercedes = core.effective("vehicle", "swofty.classic-seasons-2017/mercedes");
check(mercedes && (*mercedes)["engine_sound"].as_string() == "swofty.classic-seasons-2017/mercedes",
"the real 2017 Mercedes keeps its sound");
auto v10 = core.effective("vehicle", "mod.esp/mercedes_v10");
check(v10 && (*v10)["engine_sound"].as_string() == "mod.esp/v10_screamer", "its V10 variant has the V10");
check(v10 && (*v10)["model"].as_string() == "cars/cs17_mercedes.erp", "and the base car's model");
check(v10 && (*v10)["label"].as_string() == "esp_cs17_mercedes", "under its own label");
auto pkg = core.effective(swofty::content::packages, swofty::content::packages["mod.esp/f1_2017_v10"]);
check(pkg && pkg->title.is_literal() && pkg->title.str() == "F1 2017 (V10)", "the box is called F1 2017 (V10)");
check(pkg && pkg->season == 2017 && pkg->teams.size() == 3, "with the 2017 season's teams");
check(pkg && pkg->teams.find(std::string("mercedes")) &&
pkg->teams.find(std::string("mercedes"))->vehicle.id() == "mod.esp/mercedes_v10",
"the Mercedes drives the V10 variant");
check(pkg && pkg->teams.find(std::string("ferrari")) &&
pkg->teams.find(std::string("ferrari"))->vehicle.id() == "swofty.classic-seasons-2017/ferrari",
"the Ferrari is the 2017 Ferrari, with its layer");
auto real = core.effective(swofty::content::packages, swofty::content::packages["swofty.classic-seasons-2017/f1_2017"]);
check(real && real->teams.find(std::string("mercedes")) &&
real->teams.find(std::string("mercedes"))->vehicle.id() == "swofty.classic-seasons-2017/mercedes",
"the real 2017 box keeps its Mercedes");
auto era_box = core.effective("era.box", "mod.esp/f1_2017_v10");
check(era_box && (*era_box)["package"].as_string() == "mod.esp/f1_2017_v10", "the era box opens the variant");
return failures ? 1 : 0;
}$ swofty dev test
ok adds mod.esp/v10_screamer
ok its bundle is the addon's file
ok one layer on the 2017 Ferrari
ok the layer puts the V10 on it
ok F1 2017 (V10) is a variant of the 2017 package
...
1 test, 0 failedswofty dev pack runs the same registration against the recorder and writes registry.lock.json:
{
"generated_by": "swofty dev pack 1.6.0",
"provides": {
"content.package": [ { "id": "mod.esp/f1_2017_v10", "label": "F1 2017 (V10)" } ],
"engine_sound": [ "mod.esp/v10_screamer" ],
"era.box": [ { "id": "mod.esp/f1_2017_v10", "label": "F1 2017 (V10)" } ],
"vehicle": [ { "id": "mod.esp/mercedes_v10", "label": "esp_cs17_mercedes" },
{ "id": "mod.esp/redbull_v10", "label": "esp_cs17_redbull" } ]
},
"needs": [ { "record": "content.package:swofty.classic-seasons-2017/f1_2017", "policy": "required" }, "..." ]
}Try it
Build native/esp.dll from inside mod.esp with native/build.sh, which builds the DLL with zig. From the directory containing mod.esp, install and enable the addon, then run the game:
mod.esp/native/build.sh
(cd mod.esp && swofty dev pack)
swofty addons install ./mod.esp
swofty addons enable swofty.classic-seasons-2017
swofty addons enable mod.esp
swofty run --applyPick the Ferrari in the F1 2017 box in Time Trial, then open the F1 2017 (V10) box next to it. Restart the game after changing the pack.
swofty/loader.log:
addon esp.dll: calling SwoftyAddonRegister (API v9) at the early stage
[registry] register: mod.esp 1 layer(s)When something's missing
If Classic Seasons 2017 is off, the requires entry in swofty.json stops Play and the launcher names the missing dependency. Turn Classic Seasons back on before launching.
If an update removes or renames the target Ferrari record, the registry.json requirement also stops Play and identifies the missing record. Update the layer and its record requirement together to a target the provider defines.
Turning off mod.esp removes its sound layer at the next game start, revealing the sound beneath it; a missing vehicle sound reference falls back to the donor's sound (Missing references).
Publish
Follow Packaging and publishing to publish the finished mod.esp addon to the Addon Browser.