Skip to main content
 Warning: Beta Version
Version: Beta ⚠️

Interaction Context

Interaction context is a powerful data-passing system that allows entries to share information throughout a sequence. This enables dynamic, stateful interactions where user input, computed values, and state changes can flow between different entries seamlessly.

Overview

Think of interaction context as a shared data store that persists throughout an interaction sequence. When a player engages with your content, the context travels with them from entry to entry, accumulating and modifying data along the way.

Key Benefits

  • Dynamic Interactions: Create responsive content that adapts based on player choices and input
  • Data Flow: Values set in one entry remain available to all subsequent entries
  • Flexible Communication: Pass complex data between different types of entries seamlessly

Context Types

There are two main types of context in Typewriter:

Entry Context Keys

Entry context keys are specific to individual entries and are typically used for capturing user input or storing entry-specific data. These are set and managed by individual entries like dialogue inputs, event captures, or action computations.

InteractionContextExamples.kt
enum class ExampleEntryContextKeys(override val klass: KClass<*>) : EntryContextKey {
// The two `String::class` have to be the same.
// The @KeyType is for the panel to know
@KeyType(String::class)
// The type here is for casting during runtime
TEXT(String::class),

@KeyType(Int::class)
NUMBER(Int::class),

// More complex types are also allowed.
@KeyType(Position::class)
POSITION(Position::class)
}

Global Context Keys

Global context keys are keys that any entry can access and modify. This is useful for common pieces of information that may differ between entries yet should be regarded as the same for consuming entries. For example, the location of interest, which might be an NPC's eye position or the interacting block position.

InteractionContextExamples.kt
@GlobalKey(Int::class)
object LuckyNumberKey : GlobalContextKey<Int>(Int::class)

How Context Works

Context flows through interaction sequences in a directional manner, with each entry having access to data from its predecessors while maintaining isolation between parallel branches. Understanding this flow is crucial for building complex, stateful interactions.

Data Flow Principles

  1. Unidirectional Flow: Context data flows forward through the sequence - entries can access data from previous entries but not from future ones
  2. Branch Isolation: When an interaction splits into parallel paths, each branch maintains its own isolated context scope
  3. Accumulative Nature: Each entry can read all previous context data and add new data for subsequent entries

Basic Linear Flow

In a simple linear sequence, context flows straightforward from one entry to the next:

Branching Flow Example

When interactions branch, context isolation becomes important.

Context Access Rules

EntryCan AccessCannot AccessReason
E(initial context only)Future entries (S, A1, O, A2, A3)Root entry - no predecessors
SEFuture entries (A1, O, A2, A3)Can only access predecessors
A1E, SO, A2, A3Different branch from O→A3
A2E, S, A1O, A3Same branch as A1, isolated from O branch
OE, SA1, A2, A3Different branch from A1→A2
A3E, S, OA1, A2Same branch as O, isolated from A1 branch

Working with Context in Different Entry Types

Action Entries

Action entries frequently read from context to determine their behavior and can also modify context for future entries.

InteractionContextExamples.kt
@Entry("example_action_with_context", "An action that reads/writes from/to interaction context", Colors.RED, "material-symbols:touch-app-rounded")
// This tells Typewriter that this entry exposes some context
@ContextKeys(ExampleEntryContextKeys::class)
class ExampleActionWithContextEntry(
override val id: String = "",
override val name: String = "",
override val criteria: List<Criteria> = emptyList(),
override val modifiers: List<Modifier> = emptyList(),
override val triggers: List<Ref<TriggerableEntry>> = emptyList(),
) : ActionEntry {
override fun ActionTrigger.execute() {
// Writing values to the context
context[ref(), ExampleEntryContextKeys.TEXT] = "Hey there"
context[ref(), ExampleEntryContextKeys.NUMBER] = 42
context[ref(), ExampleEntryContextKeys.POSITION] = Position.ORIGIN
context[LuckyNumberKey] = 69

// Reading values from the context
val text: String? = context[ref(), ExampleEntryContextKeys.TEXT]
val number: Int? = context[ref(), ExampleEntryContextKeys.NUMBER]
val position: Position? = context[ref(), ExampleEntryContextKeys.POSITION]
val luckyNumber = context[LuckyNumberKey]

player.sendMessage("$text, the number is $number at $position and the lucky number is $luckyNumber".asMini())
}
}

Dialogue Entries

Dialogue entries can both capture user input into context and use context values to determine what to display.

InteractionContextExamples.kt
@Entry("example_dialogue_with_context_keys", "A dialogue that captures string input", Colors.BLUE, "material-symbols:keyboard-rounded")
// This tells Typewriter that this entry exposes some context
@ContextKeys(ExampleEntryContextKeys::class)
class ExampleStringInputDialogueEntry(
override val id: String = "",
override val name: String = "",
override val criteria: List<Criteria> = emptyList(),
override val modifiers: List<Modifier> = emptyList(),
override val triggers: List<Ref<TriggerableEntry>> = emptyList(),
override val speaker: Ref<SpeakerEntry> = emptyRef(),
) : DialogueEntry {
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*> {
return ExampleDialogueMessenger(player, context, this)
}
}

class ExampleDialogueMessenger(
player: Player,
context: InteractionContext,
entry: ExampleStringInputDialogueEntry,
) : DialogueMessenger<ExampleStringInputDialogueEntry>(player, context, entry) {

override fun init() {
super.init()

// We can read and write the context in the init
context[entry, ExampleEntryContextKeys.TEXT] = "Hey there"
val text: String? = context[entry, ExampleEntryContextKeys.TEXT]
}

override fun tick(context: TickContext) {
// We can also read and write the context in the tick method.
// If we modify it, it will live modify the player.interactionContext values which other entries can use.
this.context[entry, ExampleEntryContextKeys.NUMBER] = 42
this.context[entry, ExampleEntryContextKeys.POSITION] = player.position
this.context[LuckyNumberKey] = 69

// And also read from the context
val number: Int? = this.context[entry, ExampleEntryContextKeys.NUMBER]
val position: Position? = this.context[entry, ExampleEntryContextKeys.POSITION]
val luckyNumber = this.context[LuckyNumberKey]

state = MessengerState.FINISHED
super.tick(context)
}
}

Event Entries

Event entries often serve as the starting point for context creation, capturing initial data from player actions.

InteractionContextExamples.kt
@Entry(
"example_event_with_context_keys",
"An example event entry with context keys.",
Colors.YELLOW,
"material-symbols:bigtop-updates"
)
// This tells Typewriter that this entry exposes some context
@ContextKeys(ExampleEntryContextKeys::class)
class ExampleEventEntryWithContextKeys(
override val id: String = "",
override val name: String = "",
override val triggers: List<Ref<TriggerableEntry>> = emptyList(),
) : EventEntry

@EntryListener(ExampleEventEntryWithContextKeys::class)
fun onEventAddContext(event: SomeBukkitEvent, query: Query<ExampleEventEntryWithContextKeys>) {
val entries = query.find()
entries.triggerAllFor(event.player) {
// Make sure these values are drawn from the event.
// You MUST supply all the context keys.
ExampleEntryContextKeys.TEXT withValue "Hello World"
ExampleEntryContextKeys.NUMBER withValue 42
// You can also use += to assign the value to the key
ExampleEntryContextKeys.POSITION += Position.ORIGIN

// Or we can assign any global key to it.
LuckyNumberKey += 69
}
}