Hooks
A hook point is a named place in the game where the core runs every subscribed addon in one ordered chain. A game hook point covers one game function: the core owns the mixin on that function, and addons subscribe to the point instead of patching the function themselves. Two addons that need the same game function then work side by side.
For example, Progressive Career and Career Editor both need Game_CareerActivities_AddActivity. Each subscribes to career.calendar.add_activity, and the core runs both handlers, in order, on every call.
A hook in full
Progressive Career keeps the contract event off a Follow-your-driver player's calendar. It subscribes a replace handler to the core's career.calendar.add_activity point.
The payload type is in the career system's header, swofty/systems/career.hpp, in swofty::career::events.
#include <swofty/hooks.hpp>
#include <swofty/systems/career.hpp>
using swofty::career::events::AddActivity;
SWOFTY_ON_LOAD
{
// replace: return true to stand in for the game's code; false lets the next handler (or the game) run
swofty::hooks::replace<AddActivity>([](AddActivity &a) -> bool {
if (a.type != AddActivity::contract_event || !a.player || progressive::follow_mode() != 1)
return false;
if (!progressive::has_contract(a.driver)) {
swofty::log("[progressive] follow: contract event on day %u: no contract yet; the first Team HQ signs", a.day);
return false; // a new career: let the game add it, Team HQ removes it later
}
return progressive::sign_instead(a); // signed: the event is never added
});
}
SWOFTY_ADDON()The payload is a plain struct with a size and a version. The core fills it from the game function's arguments:
// swofty/systems/career.hpp (excerpt)
namespace swofty::career::events {
struct AddActivity {
static constexpr const char *point = "career.calendar.add_activity";
static constexpr uint32_t version = 1;
static constexpr uint8_t contract_event = 0x0f;
uint32_t size, payload_version;
void *calendar; // the driver calendar the activity goes on
const void *activity; // the activity definition
uint32_t driver; // the calendar's driver, stat id << 16
uint32_t day; // before handlers may move it
uint8_t type; // the activity type
bool player; // the calendar belongs to the human player
};
} // namespace swofty::career::eventsPayload fields are C types. A record id or a text is a const char * with a member function for its C++ form (vehicle_id and vehicle(), section_str and section()), and a callback is a C function pointer with a user pointer and a member function that calls it (see ABI).
Phases
Every call to a hook point runs three phases:
game calls Game_CareerActivities_AddActivity(calendar, activity, day)
core fills AddActivity
before handlers, highest priority first, then load order; each may change the payload
replace handlers, same order, until one returns true; if none does, the game's own code runs
after handlers, all of them; they see the final payload and the result
core returns to the game// before: change the input
swofty::hooks::before<AddActivity>([](AddActivity &a) {
if (a.type == AddActivity::contract_event && a.player)
a.day = std::max(a.day, progressive::first_race_day()); // never before round 1
});
// after: observe what happened
swofty::hooks::after<AddActivity>([](const AddActivity &a, const swofty::hooks::Outcome &o) {
if (a.type == AddActivity::contract_event)
swofty::log("[career-editor] contract event %s on day %u", o.replaced ? "skipped" : "added", a.day);
});
// a priority, for when order matters (default 0; higher runs first)
swofty::hooks::before<AddActivity>(swofty::hooks::Priority{10}, [](AddActivity &a) { ... });A hook point's reference says which thread it runs on: the flow thread for career points, the session thread for session points. Handlers must return quickly and must not call the same hook point.
Game hook points
The core adds a game hook point when a second addon needs the same game function, with the payload and a verified binding. The first planned points, with the game function each covers and the addons that need it:
career.calendar.add_activity Game_CareerActivities_AddActivity Progressive (contract events), Career Editor
career.contract.decide Career_Decide_ContractNegotiation Progressive (follow), Career Editor (contracts)
career.contract.sign Career_Sign_DriverContract Progressive, Career Editor
career.season.process the season change (vtable slot +0x188) Progressive (seasons), Career Editor
session.fuel.load the race fuel lookup Progressive (seasons before 2026)
team.logo.resolve the logo key lookups Progressive, Classic Seasonscareer.season.process covers a function at L0 in the KB. It needs reverse engineering to L2 before it ships.
The core installs a game hook point's mixin only once an addon subscribes to it. With no subscriber, the game's code is untouched.
Hook points the game calls every frame
Some game functions run every frame or hundreds of times a second: the logo key for each row of the timing tower, a text lookup, a view-model path, a driver's rating. The catalogue marks these points hot:
team.logo.resolve driver.ratings.read lang.lookup ui.vm.resolve track.data.getOn a hot point the core builds a table at a content or session moment: when a box is picked, when a career loads, when a session loads. It runs the handler chain once per entry (each team, each driver and stat, each key) and stores the result. The game's calls then read the table. Your handler is written the same way as on any other point. It runs once per entry when the table is built, and it must not depend on anything that changes during the session.
[hooks] team.logo.resolve (hot): table built at session.load: 11 teams, 2 handlers (swofty.progressive, swofty.classic-seasons-2017), 0.08 ms
[hooks] driver.ratings.read (hot): table built at career.loaded: 22 drivers x 5 stats, 1 handler (swofty.progressive), 0.31 msAddon hook points
An addon can define its own point and fire it, so other addons can extend it:
// api/progressive_api/hooks.hpp: let other addons adjust a season right before Progressive applies it
namespace progressive_api {
struct SeasonApply {
static constexpr const char *point = "swofty.progressive.season.apply";
static constexpr uint32_t version = 1;
uint32_t size;
uint32_t payload_version;
int year;
swofty::career::Season *season; // before handlers may change it
};
}
// progressive.dll
SWOFTY_ON_LOAD
{
swofty::hooks::define<progressive_api::SeasonApply>();
}
void apply_season(swofty::career::Season season)
{
progressive_api::SeasonApply p{ sizeof p, progressive_api::SeasonApply::version, season.year, &season };
if (!swofty::hooks::fire(p)) // false: a replace handler applied it instead
return;
swofty::career::apply_lineup(season);
}// another addon: a driver who skipped a season comes back on the reserve seat
swofty::hooks::before<progressive_api::SeasonApply>([](progressive_api::SeasonApply &p) {
if (p.year == 2017)
swofty::log("[whatif] season %d has %zu teams", p.year, p.season->teams.size());
});The owning addon ships the payload header (progressive_api/hooks.hpp) for others to include. hooks::define is the declaration: nothing goes in the manifest.
When a handler faults
The core calls every handler inside a guard that catches access violations and C++ exceptions:
- An
afterhandler that faults is isolated. The core logs the addon, the point and the phase, detaches that handler for the rest of the session, and lists it inconflicts.json. The game carries on. - A
beforeorreplacehandler that faults crashes the game. The crash report's first line names the addon, the point and the phase.
[hooks] career.calendar.add_activity: after handler from someone.calendar-log faulted
(access violation at calendar_log.dll+0x2210); handler detached for this sessionLiving with mixins
A hook point is one entry in the game function's mixin chain, at the core's priority. Raw mixins keep working around it:
[mixin] Game_CareerActivities_AddActivity: swofty_core.dll(hooks, 100) > old_addon.dll(0)- A
SWOFTY_WRAPon the same function chains below the hook point, as any two wraps chain. - A raw overwrite on the same function, from any addon, conflicts with the hook point's mixin. The core's mixin is the one dropped. The hook point reports
unavailable, its subscribers don't run, and the addon with the overwrite keeps working. No addon is stopped from overwriting a function that has a hook point.
[hooks] career.calendar.add_activity unavailable: old_addon.dll overwrites Game_CareerActivities_AddActivity;
subscribers swofty.progressive, swofty.career-editor not run (the launcher lists this as a conflict)- A hook point never drops an addon. The core installs hook-point mixins at the window stage, after the mixin conflict pass, whatever stage the subscriber registered at. The early-stage overlap rule (a mixin that overlaps one installed at the early stage drops the later addon whole) never applies to a hook point's mixin.
- A hook point installed while the game runs, when its first subscriber arrives late, checks for overlap against every mixin installed at that moment, mid-hooks included. On an overlap it refuses itself and reports
unavailable.
Subscribers can check before relying on a point:
if (!swofty::hooks::live<AddActivity>())
swofty::log("[progressive] the add-activity hook point isn't live; falling back to the contract scene skip");