Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/HarbourMasters/Starship/llms.txt

Use this file to discover all available pages before exploring further.

The event system is the primary hook mechanism for Starship mods. It lets you register typed events, attach prioritised listener callbacks, and optionally cancel default engine behaviour — all from plain C or C++ without patching original game code.

Core Types

Every piece of the event system is built on a handful of primitive types defined in port/hooks/impl/EventSystem.h.
TypeUnderlying TypePurpose
EventIDuint32_tIdentifies a registered event type
ListenerIDuint32_tIdentifies a registered listener
EventPriorityenumControls callback invocation order
IEventstructBase event — all event structs embed this
EventCallbackfunction pointerSignature every listener must match
EventListenerstructPairs a priority with a callback

EventPriority

Priority values
typedef enum {
    EVENT_PRIORITY_LOW,
    EVENT_PRIORITY_NORMAL,
    EVENT_PRIORITY_HIGH,
} EventPriority;
Listeners registered with EVENT_PRIORITY_HIGH run first, followed by EVENT_PRIORITY_NORMAL, then EVENT_PRIORITY_LOW. Use a higher priority when your listener must run before others — for example, if it may cancel the event.

IEvent

Base event struct
typedef struct {
    bool cancelled;
} IEvent;
Every event struct embeds IEvent as its first member (named event). Setting event.cancelled = true inside a listener signals cancellation; subsequent behaviour depends on which call-site macro the engine used.

EventCallback

Callback signature
typedef void (*EventCallback)(IEvent*);
All listener functions must match this signature. Inside the callback, cast the IEvent* to the concrete event type to access its extra fields.

EventListener

Listener struct
typedef struct {
    EventPriority priority;
    EventCallback function;
} EventListener;

API Reference

class EventSystem {
public:
    static EventSystem* Instance;

    // Allocate a new event slot and return its ID
    EventID RegisterEvent();

    // Attach a callback to an event; priority defaults to EVENT_PRIORITY_NORMAL
    ListenerID RegisterListener(EventID id,
                                EventCallback callback,
                                EventPriority priority = EVENT_PRIORITY_NORMAL);

    // Remove a previously registered listener
    void UnregisterListener(EventID ev, ListenerID id);

    // Fire an event, invoking all registered listeners in priority order
    void CallEvent(EventID id, IEvent* event);

private:
    std::unordered_map<EventID, std::vector<EventListener>> mEventListeners;
    EventID mInternalEventID = 0;
};
C++ code can call either the class methods via EventSystem::Instance or the C helper functions — both compile to the same underlying logic. Mods written in C must use the EventSystem_* functions or the convenience macros below.

Macros

The macros wrap the C API into a clean, type-safe interface. Prefer them over calling EventSystem_* functions directly.

Declaring and Defining Events

DEFINE_EVENT(eventName, /* optional extra fields */)
Expands to a typedef struct { IEvent event; /* fields */ } eventName; declaration and a companion uint32_t eventName##ID variable. Use this once, in the header that owns the event.
Example
DEFINE_EVENT(MyExplosionEvent,
    float x;
    float y;
    float z;
    int damage;
);
// Produces: MyExplosionEvent struct + extern uint32_t MyExplosionEventID;
DECLARE_EVENT(eventName)
Expands to extern uint32_t eventName##ID; so other translation units can reference the event without including the defining header. Put this in any shared header.
Example
// mymod.h
DECLARE_EVENT(MyExplosionEvent);

Registering Events and Listeners

REGISTER_EVENT(eventType)
Calls EventSystem_RegisterEvent() and stores the result into eventType##ID. Call this once during your mod’s initialisation.
Example
REGISTER_EVENT(MyExplosionEvent);
// Equivalent to: MyExplosionEventID = EventSystem_RegisterEvent();
REGISTER_LISTENER(eventType, callback, priority)
Calls EventSystem_RegisterListener(eventType##ID, callback, priority). Returns a ListenerID you can hold onto if you ever need to unregister.
Example
REGISTER_LISTENER(GamePostUpdateEvent, OnGameUpdate, EVENT_PRIORITY_NORMAL);

Firing Events

CALL_EVENT(eventType, /* field initialisers */)
Constructs the event on the stack and calls every listener. Execution continues unconditionally afterwards. Use CHECK_IF_NOT_CANCELLED later if you need to inspect the result.
Example
CALL_EVENT(MyExplosionEvent, 100.0f, 200.0f, -300.0f, 50);
CALL_CANCELLABLE_EVENT(eventType, ...) {
    // runs only if no listener cancelled the event
}
Fires the event and wraps the next statement (or block) in an if (!cancelled) guard.
Example
CALL_CANCELLABLE_EVENT(DrawBoostGaugeHUDEvent) {
    // default gauge drawing — skipped if a listener cancelled
    HUD_DrawDefaultBoostGauge();
}
CALL_CANCELLABLE_RETURN_EVENT(eventType, ...)
Fires the event and immediately return;s from the enclosing function if any listener set cancelled = true. Convenient for guarding entire update or draw functions.
Example
void Actor_Update(Actor* actor) {
    CALL_CANCELLABLE_RETURN_EVENT(ObjectUpdateEvent,
        OBJECT_TYPE_ACTOR, actor);
    // default update logic — skipped if cancelled
}
CALL_EVENT(eventType, ...);
// ... other code ...
CHECK_IF_NOT_CANCELLED(eventType) {
    // runs only if not cancelled
}
Reads the cancelled flag from the event struct that CALL_EVENT left on the stack. Useful when you need to do work between the call and the cancellation check.

Cancellable vs Non-Cancellable Events

Not all events support cancellation. The distinction lies in how the engine fires the event:
  • Non-cancellable — the engine uses CALL_EVENT. Listeners may still set cancelled = true, but the engine ignores it.
  • Cancellable — the engine uses CALL_CANCELLABLE_EVENT or CALL_CANCELLABLE_RETURN_EVENT. Setting cancelled = true suppresses default engine behaviour for that frame.
Setting event.cancelled = true on a non-cancellable event has no effect on engine execution. It only affects other listeners that explicitly inspect the flag.
When writing a listener that replaces default draw logic, always set event->cancelled = true and register at EVENT_PRIORITY_HIGH so you run before other listeners that might read cancellation state.

Priority Ordering

EVENT_PRIORITY_HIGH  →  runs first
EVENT_PRIORITY_NORMAL
EVENT_PRIORITY_LOW   →  runs last
Within the same priority level, listeners run in the order they were registered. If two listeners at the same priority both try to cancel an event, the first one to run wins — the second will still execute but the flag is already set.

Complete Working Example

The following example hooks into the post-game-update tick — the most common integration point for per-frame mod logic.
mymod.c
#define INIT_EVENT_IDS
#include "port/hooks/Events.h"
#include "port/hooks/list/EngineEvent.h"

// Listener callback — cast IEvent* to the concrete type
void OnGameUpdate(IEvent* evt) {
    GamePostUpdateEvent* event = (GamePostUpdateEvent*)evt;
    // your mod logic here — runs every game tick
}

// Call once at mod startup
void MyMod_Register() {
    REGISTER_LISTENER(GamePostUpdateEvent, OnGameUpdate, EVENT_PRIORITY_NORMAL);
}
Include port/hooks/Events.h with INIT_EVENT_IDS defined exactly once per mod — typically in your main .c file. All other files should include individual event headers without defining INIT_EVENT_IDS.

Cancelling a Draw Call

Replace the default boost gauge
void MyBoostGauge_Draw(IEvent* evt) {
    // Suppress the engine's default gauge
    evt->cancelled = true;

    // Draw custom gauge here
    My_DrawCustomBoostBar();
}

void MyMod_Register() {
    // HIGH priority so we cancel before other listeners inspect the flag
    REGISTER_LISTENER(DrawBoostGaugeHUDEvent, MyBoostGauge_Draw, EVENT_PRIORITY_HIGH);
}

Unregistering a Listener

Teardown example
ListenerID gMyListenerID;

void MyMod_Register() {
    gMyListenerID = REGISTER_LISTENER(
        GamePostUpdateEvent, OnGameUpdate, EVENT_PRIORITY_NORMAL
    );
}

void MyMod_Unregister() {
    EventSystem_UnregisterListener(GamePostUpdateEventID, gMyListenerID);
}

Build docs developers (and LLMs) love