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

DialogueEntry

The DialogueEntry is used to define a type of dialogue. When a DialogueEntry is triggered it's associated DialogueMessenger will be used to display the dialogue to the player. Multiple DialogueMessenger's can be associated with a single DialogueEntry and the DialogueMessenger that is used is determined by the DialogueMessenger's MessengerFilter.

 info

There can always be at most one DialogueEntry active for a player. This is automatically handled by Typewriter.

Usage

ExampleDialogueEntry.kt
@Entry("example_dialogue", "An example dialogue entry.", Colors.BLUE, "material-symbols:chat-rounded")
class ExampleDialogueEntry(
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(),
@MultiLine
@Placeholder
@Colored
@Help("The text to display to the player.")
val text: String = "",
) : DialogueEntry {
// May return null to skip the dialogue
override fun messenger(player: Player, context: InteractionContext): DialogueMessenger<*>? {
// You can use if statements to return a different messenger depending on different conditions
return ExampleDialogueDialogueMessenger(player, context, this)
}
}

To define the messenger that will be used to display the dialogue to the player, you must create a class that implements the DialogueMessenger interface.

ExampleDialogueEntry.kt
class ExampleDialogueDialogueMessenger(player: Player, context: InteractionContext, entry: ExampleDialogueEntry) :
DialogueMessenger<ExampleDialogueEntry>(player, context, entry) {

// Called every game tick (20 times per second).
// The cycle is a parameter that is incremented every tick, starting at 0.
override fun tick(context: TickContext) {
super.tick(context)
if (state != MessengerState.RUNNING) return

player.sendMessage("${entry.speakerDisplayName}: ${entry.text}".parsePlaceholders(player).asMini())

// When we want the dialogue to end, we can set the state to FINISHED.
state = MessengerState.FINISHED
}
}

Lifecycle

The state of the messenger determines what happens to the messenger.

  • MessengerState.FINISHED - The dialogue is finished and the next dialogue in the chain will be triggered.
  • MessengerState.CANCELLED - The dialogue is cancelled and dialogue chain is stopped, even if there are more dialogues in the chain.
  • MessengerState.RUNNING - The dialogue is still running and will continue to run until the state is changed.

The state object can be changed inside the tick method or from outside. It can even be changed from the plugin itself. For example when the user runs a command the dialogue will be cancelled.

Confirmation Key

Some dialogue messengers may wait for the player to press a key before continuing. This key is configured globally in the plugin's config.yml under confirmationKey. Available values are JUMP, SWAP_HANDS, and SNEAK. Use the <confirmation_key> tag in your messages to display the correct keybind to the player.

Here is a minimal example of a dialogue that waits for the configured confirmation key before moving on.

ExampleDialogueEntry.kt
class ExampleConfirmationDialogueMessenger(
player: Player,
context: InteractionContext,
entry: ExampleConfirmationDialogueEntry,
) : DialogueMessenger<ExampleConfirmationDialogueEntry>(player, context, entry) {

private var confirmationKeyHandler: ConfirmationKeyHandler? = null

override fun init() {
super.init()
player.sendMessage(
"${entry.speakerDisplayName}: ${entry.text} <gray><confirmation_key>".parsePlaceholders(
player
).asMini()
)
confirmationKeyHandler = confirmationKey.handler(player) {
state = MessengerState.FINISHED
}
}

override fun dispose() {
super.dispose()
confirmationKeyHandler?.dispose()
confirmationKeyHandler = null
}
}

There are some additional lifecycle methods that can be overridden.

  • init - Called when the messenger is initialized. Will be called before the first tick call.
  • dispose - Called when the messenger is disposed. By default this will unregister any listeners that were registered by the messenger.
  • end - Normally this does not need to be overwritten. Only if you do not want to resend the chat history for some reason.

Working with Interaction Context

Dialogue entries have unique access to the interaction context, allowing them to both capture user input and make conditional decisions about what to display.

Capturing User Input

Dialogue entries are commonly used to capture player input and store it in the interaction context

The DialogueMessenger has access to the interaction context through the context parameter, allowing you to both read from and modify context within your dialogue implementations.

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)
}
InteractionContextExamples.kt
@GlobalKey(Int::class)
object LuckyNumberKey : GlobalContextKey<Int>(Int::class)
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)
}
}

For more detailed information about interaction context, see the Interaction Context guide.