Skip to content

Events

NarrativeCraft events let your addon react to story, dialog, cutscene, interaction, recording, and playback changes without polling.

Every event is an immutable Java record that implements Event.

Register a listener

Register listeners through your AddonContext:

java
context.registerEvent(DialogStartEvent.class, event -> {
    String speaker = event.speakerName();
    String text = event.text();
    ServerPlayer player = event.session().getPlayer();

    LOGGER.info("{} heard {} say: {}", player.getName().getString(), speaker, text);
});

The listener type determines which event records it receives.

Unregister a listener

Keep the listener instance when it must be removed later:

java
EventListener<DialogStartEvent> dialogListener = event -> {
    LOGGER.info("Dialog started: {}", event.text());
};

context.registerEvent(DialogStartEvent.class, dialogListener);

// Later:
context.unregisterEvent(DialogStartEvent.class, dialogListener);

Passing a new lambda to unregisterEvent() does not remove the original listener because it is a different object.

Listen to a group of events

The event bus dispatches an event to listeners registered for its class or one of its parent event types. Register Event.class to observe every NarrativeCraft event:

java
context.registerEvent(Event.class, event -> {
    LOGGER.debug("NarrativeCraft event: {}", event.getClass().getSimpleName());
});

Prefer a specific event class when the listener only needs one lifecycle moment.

Story and session events

EventFieldsFired when
PlayerSessionStartEventsessionNarrativeCraft creates a player session
PlayerSessionEndEventsessionA player session is removed
StoryStartEventsessionStory playback starts
StoryEndEventsessionStory playback stops
ChapterSceneStartEventsession, chapter, sceneThe initial chapter scene starts
ChapterSceneChangeEventsession, chapter, scenePlayback changes to another chapter scene
SceneEndEventsession, sceneThe current scene ends

Example:

java
context.registerEvent(ChapterSceneChangeEvent.class, event -> {
    LOGGER.info(
        "{} entered scene {}",
        event.session().getPlayer().getName().getString(),
        event.scene().getName()
    );
});

Characters

EventFieldsFired when
CharacterSpawnEventcharacter, sceneA story character entity is registered in a scene
CharacterDespawnEventcharacter, sceneA story character entity is removed from a scene

The event exposes the narrative character, not the spawned Minecraft entity. Use the player's IStoryHandler to resolve it:

java
context.registerEvent(CharacterSpawnEvent.class, event -> {
    // Resolve through the relevant running story when you also have its session.
    LOGGER.info("Spawned character {}", event.character().getName());
});

Dialog

EventFieldsFired when
DialogStartEventsession, speakerName, textA dialog line is shown
DialogEndEventsessionThe current dialog closes
DialogChoiceEventsession, choices, selectedIndexThe player selects an Ink choice

Use selectedIndex() to read the selected value safely:

java
context.registerEvent(DialogChoiceEvent.class, event -> {
    int index = event.selectedIndex();
    if (index >= 0 && index < event.choices().size()) {
        LOGGER.info("Selected: {}", event.choices().get(index));
    }
});

The choice strings are the localized text shown to the player.

Cutscenes

EventFieldsFired when
CutsceneStartEventsession, cutsceneA cutscene starts
CutsceneEndEventsession, cutsceneA cutscene ends
java
context.registerEvent(CutsceneStartEvent.class, event -> {
    LOGGER.info("Playing cutscene {}", event.cutscene().getName());
});

Interactions

EventFieldsFired when
InteractionTriggerEventsession, interactionAn interaction starts
InteractionZoneEnterEventplayer, zoneA player enters an active interaction zone
InteractionZoneLeaveEventplayer, zoneA player leaves an active interaction zone

Zone events provide the zone name, UUID, and stitch:

java
context.registerEvent(InteractionZoneEnterEvent.class, event -> {
    event.player().sendSystemMessage(
        Component.literal("Entered " + event.zone().getName())
    );

    LOGGER.debug("Zone stitch: {}", event.zone().getStitchName());
});

Ink actions

EventFieldsFired when
InkTagProcessedEventsession, keyword, rawTagA known Ink tag has been parsed and validated
InkActionStopEventsession, actionKeywordA running Ink action stops

rawTag() contains the original tag text. keyword() contains the registered action keyword.

Recording

EventFieldsFired when
RecordingStartEventplayer, recordingRecording starts
RecordingStopEventplayer, recordingRecording stops
RecordingSaveEventplayer, recording, recordingNameA recording is saved as an animation

Custom recording data can be added while a recording is active:

java
context.registerEvent(RecordingStartEvent.class, event -> {
    LOGGER.info("Recording started at tick {}", event.recording().getTick());
});

See Recording actions for adding custom actions.

Playback

EventFieldsFired when
PlaybackStartEventplaybackRecorded animation playback starts
PlaybackPauseEventplaybackPlayback pauses
PlaybackResumeEventplaybackPlayback resumes
PlaybackEndEventplaybackPlayback ends

The IPlaybackSession record field provides the level, playback entities, targeted players, and rewind block-state log:

java
context.registerEvent(PlaybackStartEvent.class, event -> {
    ServerLevel level = event.playback().getLevel();
    LOGGER.info("Playback started in {}", level.dimension().identifier());
});

Listener contract

EventListener<E> is a functional interface with one method:

java
void handle(E event);

IEventBus.register() and IEventBus.unregister() define the underlying typed event contract. Addons should use the equivalent AddonContext methods so addon compatibility state is respected.