core.events
core.events is a broadcast that is delivered and forgotten: the owner says something happened, every listener in every realm hears it in the same tick, and nobody keeps it.
It is the third thing the transport does, beside pushing a value (core.shared) and answering a question (RPC). A value you want peers to have is a shared key; a question you want peers to ask is an RPC method; a thing that happened is an event. It has the shape world.afterEvents already has.
Import
import { core, event } from '@bedrock-core/server';
import type { EventsTree, PeerEventsTree } from '@bedrock-core/server';
Declare
An events shape names each event and, through event<T>(), the type of its payload. There is no default and no initial value: what crosses is the name and the payload.
export const eventsDef = {
purchase: event<{ playerId: string; gold: number }>(),
levelUp: event<{ playerId: string; level: number }>(),
reset: event(), // no payload
};
/** Export the type: a peer gets the typed tree from `core.events.of<EconomyEvents>()`. */
export type EconomyEvents = typeof eventsDef;
const { events } = core.register({
manifest: { creator: 'drav0011', pack: 'economy', packName: 'Economy', version: '1.0.0' },
events: eventsDef,
});
register() returns the typed tree under events, beside config and shared.
Emit and listen to your own
events.purchase.emit({ playerId: player.id, gold: 5 }); // one message, every realm, this tick
events.purchase.subscribe(({ playerId, gold }) => { // the owner hears its own first
ledger.record(playerId, gold);
});
The owner's listeners run synchronously, before the message leaves — that is how one addon decouples its own modules without a second mechanism.
Listen to a peer's
import type { EconomyEvents } from '@drav0011/economy-types';
const economy = core.events.of<EconomyEvents>('drav0011_economy');
const release = economy.purchase.subscribe(({ playerId, gold }, from) => {
hud.flash(playerId, `-${gold}`);
});
release();
A peer's tree has subscribe and no emit, in the type and at runtime. The second listener argument is the sending namespace.
of() never answers undefined. A subscription is a filter on namespace and name, so a listener attached before the owning addon has registered — or before it is installed — simply hears the first event it announces. Being late is unrecoverable here, so being early is free.
API
core.events.of
core.events.of<Def extends EventsDef>(namespace: string): PeerEventsTree<Def>
Another addon's events as a typed tree. Always a tree; nothing is read from the mirror to build it.
core.events.on
core.events.on(namespace: string, name: string, listener: (payload: unknown, from: string) => void): Unsubscribe
Listen without a declaration — the escape hatch for a name computed at runtime, and what the typed tree is built on.
core.events.own
core.events.own: EventsTree<EventsDef> | undefined
This addon's tree, untyped, once declared.
core.events.define
core.events.define<Def extends EventsDef>(def: Def): EventsTree<Def>
What register({ events }) calls. Once per addon.
Notes
- Nothing is replayed. Subscribe after it fired and you missed it. An event is not a value: a peer that wants the latest wants a shared key or an RPC answer.
last()turns an event into one when a screen needs it. - Owner only. The namespace a listener matches is the envelope's sender, read from the transport rather than the payload, so a message cannot claim to come from an addon that did not send it.
- Isolation. A listener that throws is caught and logged against the namespace and name; the others still run.
- One
emitis one message. A per-tick counter belongs incore.shared, where the cost is one publish rather than one message per change.
Limits
- No delivery guarantee. A realm that is not listening when an event is announced has missed it.
- No ordering across senders. Per sender, messages arrive in order; between senders there is no clock worth trusting.
- No reply. That is RPC; an event has no answer.