Skip to content

Working with events#

Petrichor mods can exchange custom signals through Petrichor.Events, a shared pub/sub bus inside the single Luau VM. One mod publishes, and another subscribes without importing each other's files.

API details: Petrichor.Events. Mod helpers live on Script entry point.

Event names#

Publisher events are namespaced as <mod_id>:<event>:

  • mod("hello-cow") with self:fire("ready") fires hello-cow:ready.
  • A listener uses self:on("ready", "hello-cow", fn) or Events.on("hello-cow:ready", fn).

Use the id from mod.json as mod_id so load order and folder names do not matter.

Publisher#

local HelloCow = mod("hello-cow")

function HelloCow:init()
    self:fire("ready", { version = 1 })
end

return HelloCow
declare function mod(name: string): any

const HelloCow = mod("hello-cow")

HelloCow.init = function () {
    this.fire("ready", { version: 1 })
}

export = HelloCow

Subscriber#

Register listeners from init so they are in place before other mods finish loading. Mod load order under mods/ is filesystem-dependent, so if you must react to another mod's startup event, subscribe early and handle a missed fire in tick or use a handshake event.

local Log = require("Petrichor.Log")

local CowWatcher = mod("cow-watcher")

function CowWatcher:init()
    self:once("ready", "hello-cow", function(data)
        Log.info("cow-watcher", "hello-cow ready, version " .. tostring(data.version))
    end)
end

return CowWatcher
import * as Log from "Petrichor.Log"

declare function mod(name: string): any

const CowWatcher = mod("cow-watcher")

CowWatcher.init = function () {
    this.once("ready", "hello-cow", (data: { version?: number }) => {
        Log.info("cow-watcher", `hello-cow ready, version ${data.version}`)
    })
}

export = CowWatcher

CReact panel actions#

When a mod panel calls emit("save", { value = 42 }), the host forwards it to Luau as Petrichor.Events on <modId>:save with the optional data table. Register listeners the same way as any other event:

local Events = require("Petrichor.Events")
local UI = require("Courage.UI")

Events.on("my-mod:save", function(data)
    -- apply data.value, then refresh the panel
    UI.setState("my-mod", { saved = true })
end)
import * as Events from "Petrichor.Events"
import * as UI from "Courage.UI"

Events.on("my-mod:save", (data: { value?: number }) => {
    UI.setState("my-mod", { saved: true })
})

Panel emits are delivered on the main Luau thread when the UI dispatches them. See Courage.UI and User Interfaces with CReact.

Direct module use#

When you need a custom channel or already know the full event string:

local Events = require("Petrichor.Events")

Events.on("hello-cow:moo", function(data)
    -- ...
end)

Events.fire("hello-cow:moo", { loud = true })
import * as Events from "Petrichor.Events"

Events.on("hello-cow:moo", (data) => {
    // ...
})

Events.fire("hello-cow:moo", { loud: true })

One-shot and cleanup#

once removes the listener after the first delivery. Use it for handshakes or single-use notifications.

off needs the same function reference you passed to on or once. Store the callback in self if you unsubscribe from shutdown:

function MyMod:init()
    self._onCowReady = function(data) end  -- store ref for off()
    self:on("ready", "hello-cow", self._onCowReady)
end

function MyMod:shutdown()
    self:off("ready", "hello-cow", self._onCowReady)
end
MyMod.init = function () {
    this._onCowReady = (data: unknown) => {}
    this.on("ready", "hello-cow", this._onCowReady)
}

MyMod.shutdown = function () {
    this.off("ready", "hello-cow", this._onCowReady)
}

Events vs hooks#

Events Hooks (before / after / replace)
Purpose Mod-to-mod messages Intercept game functions
Registration on / once on a string before(sym, fn) on a symbol
Callback args data only self, ctx with hook context
Timing When a mod calls fire When the game calls the hooked function

Use hooks to change game behavior. Use events to coordinate between your mods.

Limitations#

  • All delivery runs on the main Luau thread during fire. Keep listeners short.
  • No delivery guarantee across restarts, so pair with Petrichor.Storage if you need durable state.
  • Host lifecycle callbacks (pre_frame, post_frame, scene_load) exist on the C++ PetrichorEvents API but are not yet exposed to Luau through Petrichor.Events. Use tick and hooks for frame- and scene-level logic today.