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.

Actor events let you observe and intercept the full lifecycle of every in-game object — actors, bosses, scenery, sprites, items, and effects. They are defined in port/hooks/list/ActorEvent.h, with the item-drop event in port/hooks/list/ItemEvent.h.

Object Types

All lifecycle events carry an ObjectEventType tag that identifies which engine subsystem owns the object. Always check this field before casting void* object to a concrete pointer.
ObjectEventType enum
typedef enum {
    OBJECT_TYPE_ACTOR,       // Standard game actor (Actor*)
    OBJECT_TYPE_ACTOR_EVENT, // Scripted actor event object (ActorEvent*)
    OBJECT_TYPE_BOSS,        // Boss object (Boss*)
    OBJECT_TYPE_SCENERY,     // Static scenery piece (Scenery*)
    OBJECT_TYPE_SCENERY360,  // All-range mode scenery (Scenery360*)
    OBJECT_TYPE_SPRITE,      // 2-D sprite object (Sprite*)
    OBJECT_TYPE_ITEM,        // Collectible item (Item*)
    OBJECT_TYPE_EFFECT,      // Visual effect (Effect*)
} ObjectEventType;

Casting void* object

Type-safe cast pattern
void OnObjectUpdate(IEvent* evt) {
    ObjectUpdateEvent* event = (ObjectUpdateEvent*)evt;

    switch (event->type) {
        case OBJECT_TYPE_ACTOR: {
            Actor* actor = (Actor*)event->object;
            // use actor fields
            break;
        }
        case OBJECT_TYPE_ITEM: {
            Item* item = (Item*)event->object;
            // use item fields
            break;
        }
        case OBJECT_TYPE_BOSS: {
            Boss* boss = (Boss*)event->object;
            // use boss fields
            break;
        }
        default:
            break;
    }
}
Never cast void* object without checking type first. The object pools are distinct and an incorrect cast will read garbage data or crash the game.

Lifecycle Events

All five lifecycle events share the same two fields.
type
ObjectEventType
required
Identifies the subsystem that owns this object. Use a switch statement to select the correct cast target.
object
void*
required
Opaque pointer to the object. Cast to Actor*, Boss*, Item*, etc. based on the type field.
EventWhen it firesCancellable
ObjectInitEventWhen an object is first initialisedYes
ObjectUpdateEventEvery frame an object is activeYes
ObjectDrawPreSetupEventBefore draw state is configuredYes
ObjectDrawPostSetupEventAfter draw state is configuredYes
ObjectDestroyEventWhen an object is destroyedNo
Fires once when the engine first sets up an object. Cancelling this event suppresses the rest of the engine’s init code for that object, allowing you to replace initialisation entirely.
Log every actor spawn
void OnObjectInit(IEvent* evt) {
    ObjectInitEvent* event = (ObjectInitEvent*)evt;

    if (event->type == OBJECT_TYPE_ACTOR) {
        Actor* actor = (Actor*)event->object;
        printf("[MyMod] Actor spawned: id=%d\n", actor->obj.id);
    }
}
Fires every frame for each active object in the scene. This is the most frequently called actor event — keep per-frame listener code tight to avoid frame-rate impact.Cancelling the event prevents the engine’s own update logic from running for that object that frame.
Fires before the engine configures the render state (matrix stack, display list, material) for this object. Cancel here to skip the engine’s setup and provide your own before drawing.
Fires after render state is configured but the draw call may still be running. Cancelling here suppresses the rest of the default draw path, letting you substitute custom rendering. This is the right place to swap a mesh or change draw-call parameters.
Replace gold ring mesh (from PortEnhancements.c)
void OnItemGoldRingDraw(IEvent* evt) {
    ObjectDrawPostSetupEvent* event = (ObjectDrawPostSetupEvent*)evt;

    if (event->type != OBJECT_TYPE_ITEM) {
        return;
    }

    Item* item = (Item*)event->object;
    if (item->obj.id != OBJ_ITEM_GOLD_RING || CVarGetInteger("gRestoreBetaCoin", 0) != 1) {
        return;
    }

    // Suppress default ring draw and substitute beta coin mesh
    event->event.cancelled = true;
    RCP_SetupDL(&gMasterDisp, SETUPDL_29_OPTIONAL);
    Graphics_SetScaleMtx(item->width * 2.0f);
    gSPDisplayList(gMasterDisp++, D_101D870);
}
Fires when an object is removed from the scene. This event is not cancellable — destruction has already been committed by the time listeners run. Use it for cleanup, logging, or triggering secondary effects tied to an object’s death.
Spawn particles on actor death
void OnObjectDestroy(IEvent* evt) {
    ObjectDestroyEvent* event = (ObjectDestroyEvent*)evt;

    if (event->type == OBJECT_TYPE_ACTOR) {
        Actor* actor = (Actor*)event->object;
        MyMod_SpawnDeathParticles(actor->obj.pos.x, actor->obj.pos.y, actor->obj.pos.z);
    }
}

Item Drop Event

ItemDropEvent is defined separately in port/hooks/list/ItemEvent.h and fires when an item enters the world (drops from a defeated enemy or a scripted spawn).
item
Item*
required
Pointer to the newly dropped Item struct. Inspect item->obj.id to identify the item type.
EventWhen it firesCancellable
ItemDropEventWhen an item is dropped into the worldYes
Log every item drop
#include "port/hooks/list/ItemEvent.h"

void OnItemDrop(IEvent* evt) {
    ItemDropEvent* event = (ItemDropEvent*)evt;
    printf("[MyMod] Item dropped: id=%d at (%.1f, %.1f, %.1f)\n",
           event->item->obj.id,
           event->item->obj.pos.x,
           event->item->obj.pos.y,
           event->item->obj.pos.z);
}

void MyMod_Register() {
    REGISTER_LISTENER(ItemDropEvent, OnItemDrop, EVENT_PRIORITY_NORMAL);
}

Complete Example — Hooking Actor Spawning

The following example demonstrates a real-world pattern: filtering by object type and acting only on a specific actor ID.
spawn_hook.c
#include "port/hooks/list/ActorEvent.h"

// Track how many times a specific enemy has spawned this level
static int sEnemySpawnCount = 0;

void OnObjectInit(IEvent* evt) {
    ObjectInitEvent* event = (ObjectInitEvent*)evt;

    // Only interested in standard actors
    if (event->type != OBJECT_TYPE_ACTOR) {
        return;
    }

    Actor* actor = (Actor*)event->object;

    // Example: count OBJ_ENEMY_GRANGA spawns
    if (actor->obj.id == OBJ_ENEMY_GRANGA) {
        sEnemySpawnCount++;
        printf("[MyMod] Granga #%d spawned!\n", sEnemySpawnCount);
    }
}

void OnObjectDestroy(IEvent* evt) {
    ObjectDestroyEvent* event = (ObjectDestroyEvent*)evt;

    if (event->type != OBJECT_TYPE_ACTOR) {
        return;
    }

    Actor* actor = (Actor*)event->object;
    if (actor->obj.id == OBJ_ENEMY_GRANGA) {
        printf("[MyMod] Granga destroyed. Total spawned: %d\n", sEnemySpawnCount);
    }
}

void MyMod_Register() {
    REGISTER_LISTENER(ObjectInitEvent, OnObjectInit, EVENT_PRIORITY_NORMAL);
    REGISTER_LISTENER(ObjectDestroyEvent, OnObjectDestroy, EVENT_PRIORITY_NORMAL);
}

Tracking Item Updates

item_update_hook.c
#include "port/hooks/list/ActorEvent.h"

void OnObjectUpdate(IEvent* evt) {
    ObjectUpdateEvent* event = (ObjectUpdateEvent*)evt;

    if (event->type != OBJECT_TYPE_ITEM) {
        return;
    }

    Item* item = (Item*)event->object;

    // Custom behaviour for a specific item type
    if (item->obj.id == OBJ_ITEM_SUPPLY_RING) {
        // Rotate faster than the engine default
        item->obj.rot.y += 5;
        // Let the engine continue its own update (do not cancel)
    }
}

void MyMod_Register() {
    REGISTER_LISTENER(ObjectUpdateEvent, OnObjectUpdate, EVENT_PRIORITY_NORMAL);
}
Register all actor event listeners at EVENT_PRIORITY_NORMAL unless you specifically need to run before or after another listener. Use EVENT_PRIORITY_HIGH only when you intend to cancel the event and replace the engine’s default behaviour.

Build docs developers (and LLMs) love