Repository navigation
Dialogues
What this page covers: the dialogue engine in br.com.finalcraft.evernifecore.dialogue (:common) - a tree of
nodes and gated options, the session one player holds, what starts one (the start-dialogue effect, an NPC, a region,
the plugin's own command), the default screen evernifecore:dialogue and how to replace it, the three events, the
refusals at load, and DialogueTrees for tests.
A Dialogue is a definition: nodes, each with the speaker's line and the options the player answers with. Each
option carries a Gate (may the player pick it?), effects (what picking it does) and where the talk goes next.
The core keeps nothing on disk: the tree lives in the plugin's own file, Dialogues.of(plugin).register(...) keeps it
in memory, and a DialogueSession - one per player, in memory too - walks it, showing each node on whatever
screen answers ScreenKey.DIALOGUE.
The core has no verb that starts a dialogue. A plugin starts one from its command, an NPC trigger, a region trigger or
the start-dialogue effect; the core only reads (/ecore dialogue <player>).
| Class | What it is |
|---|---|
DialogueId |
owner:name, both lowercased, [a-z0-9_-]{1,64} like a region id; DialogueId.parse("finalcolonies:tomas")
|
Dialogue |
the tree: getSpeaker(ctx), getEntryNode() (default start), getNodes() (written order), getTimeout() (default 2 min, zero = forever), getFailWhen(), getTriggers(); texts through getText(node, ctx) / getText(node, option, ctx)
|
DialogueNode |
getText, getOptions() (empty = terminal: showing it ends the talk), getRemember(), getEffectsOnEnter()
|
DialogueOption |
getText, getGate() (Gate.open() when none), getEffects(), getGoto(), isEnd(), isHidden()
|
DialogueTrigger |
npc(NpcRef) or regionEnter(RegionId)
|
Built in code:
Dialogue tomas = Dialogue.builder(plugin, "tomas").speaker("Tomás").timeout(Duration.ofMinutes(2))
.node(DialogueNode.builder("start").text("Did you bring the logs?").remember("finalcolonies:tomas-answer")
.option(DialogueOption.builder("deliver").text("Here.").gate(deliverGate).effects(thanks).goTo("thanks"))
.option(DialogueOption.builder("later").text("Not yet.").goTo("later")))
.node(DialogueNode.builder("thanks").text("Come back any time.")) // terminal
.node(DialogueNode.builder("later").text("Bring them soon.").option(DialogueOption.builder("bye").text("Bye.").end()))
.build(); // checks, or throws
Dialogues.of(plugin).register(tomas);or read from the plugin's YAML with Dialogue.parse(plugin, "tomas", config.getRoot(), zone) - zone is what a
window without its own zone is read in - or from a YAML text with Dialogue.parseYaml(plugin, "tomas", yaml), in the
server's zone (Window.getServerZone()); text that is not YAML is a DialogueDefinitionException naming the dialogue:
speaker: "Tomás"
entry: start
timeout: 2m
fail-when:
- { type: near-npc, npc: "citizens:12", radius: 6 } # walking away ends the talk
triggers:
- npc: "citizens:12"
nodes:
start:
text:
pt-BR: "Olá! A colônia precisa de madeira. Trouxe as 32 toras?"
en-US: "Hello! The colony needs wood. Did you bring the 32 logs?"
remember: "finalcolonies:tomas-answer"
on-enter: [] # effects run each time the node is shown
options:
deliver:
text: { pt-BR: "Sim, aqui estão.", en-US: "Yes, here they are." }
gate:
cost:
- { type: item, item: "minecraft:oak_log", amount: 32 }
effects:
- { type: add, variable: "finalcolonies:warehouse-points", amount: 10 }
- message: "&aObrigado! A colônia agradece."
goto: thanks
later:
text: { pt-BR: "Ainda não.", en-US: "Not yet." }
goto: later
thanks:
text: "Volte sempre."
options: [] # terminal
later:
text: "Volte quando tiver as toras."
options:
- { id: bye, text: "Até logo.", end: true } # a list works too, each option with its idKeys: dialogue speaker, entry, timeout, fail-when, triggers, nodes; node text, remember, on-enter,
options; option id (list form), text, gate ({require, cost}, see Logic), effects, goto, end,
hidden. A condition, cost or effect with two required parameters is written {type: ..., ...}.
A text is a string, a list of lines or a map of locale to text. The first entry of a map is the author's language.
Registering puts every text in the plugin's LocaleOverlay under the definition dialogue:<name>
(paths speaker, nodes.<node>.text, nodes.<node>.options.<option>.text), so /ecore i18n export writes them for a
translator and the overlay file of a locale wins over the inline entry. pt-BR and PT_BR are one locale. The text
the player reads is the overlay's for the context's locale, its {variables} filled by ExecutionContext.resolve.
Builder.build() (and so Dialogue.parse) throws a DialogueDefinitionException that names the dialogue, the node,
the option and the way out:
| Refusal | Example message |
|---|---|
a goto to a missing node |
The dialogue finalcolonies:tomas has option 'deliver' on node 'start' with goto 'delivery' but no node is called 'delivery'. Nodes defined: start, thanks, later. Add the node or point the option at one of these. |
an entry that is not a node |
... starts on the node 'hello' but no node is called 'hello'. Nodes defined: ... |
| a loop with no way out | The dialogue finalcolonies:tomas has a loop with no way out: nodes [haggle, insist] only lead back into themselves. Give one of them an option with 'end: true' or a goto to a node that ends (thanks, later). |
remember naming no custom variable, or one that is not a String
|
... remembers the answer of node 'start' in 'finalcolonies:tomas-answer', but no plugin defined that custom variable. Define it before the dialogue loads: Variables.custom(plugin, storage).define("<name>", String.class, "") ... |
| a key the reader does not know, a definition that does not parse | The dialogue finalcolonies:tomas node 'start' option 'bye' has the key 'ends', which a dialogue does not read. Known here: id, text, gate, effects, goto, end, hidden. |
"A loop with no way out" is measured backwards from the nodes that end (no options, or an option with end): a node
that cannot reach one of them is refused. A loop that can still reach an end is fine. A fail-when condition this
platform cannot run is refused by the type registry while parsing.
DialogueSession session = Dialogues.start(player, tomas, Caller.of(plugin, "command:talk"),
context -> context.npc(NpcRef.parse("citizens:12"))); // null when a DialogueStartedEvent cancelled it-
One per player, server-wide. Starting another ends the first with
REPLACED.Dialogues.sessionOf(player). -
choose(optionId)(what a click on the default screen calls): a cancellableDialogueChoiceEvent, then the option's gate paid (Gate.pay), the option id written into the node'sremembervariable, the effects run, then the talk moves - to thegoto, to the end (end), or stays (neither). A cancelled or denied pick charges nothing and leaves the node as it was; theChoiceResultsaysCHOSEN,DENIED(with every failure),CANCELLED,UNKNOWN_OPTIONorCLOSED. - Reaching a terminal node shows its line and ends the session with
END; the screen stays until the player closes it. -
end(cause)is idempotent: the first cause wins and posts oneDialogueEndedEvent. -
newExecutionContext()builds a context for the player under the session'sCaller, carrying everyDialogueContextattribute (the NPC underDialogueContext.NPC) and the session underDialogueContext.SESSION.
DialogueEndCause |
When |
|---|---|
END |
an option with end, or a terminal node reached |
CLOSED |
the session's screen went away (the player closed it, or something did) |
TIMEOUT |
no answer within getTimeout(), armed on ECScheduler at the start and re-armed by every pick |
FAIL_WHEN |
a fail-when condition failed when one of its getNeeds() sources fired for this player; a condition with no source is asked at each pick instead (no sampling timer) |
PLAYER_LEFT |
the player quit |
REPLACED |
another dialogue started for the player |
OWNER_DISABLED |
the owning plugin shut down (Dialogues.closeOwnedBy, called by the core before its screens close) or registered a dialogue of the same name again |
API |
a plugin ended it: session.end(...), the end-dialogue effect |
A reboot ends everything: sessions are engine state, never saved. The core listens to quits and screen closes only while a session is open.
| Entry | Who | How |
|---|---|---|
effect start-dialogue
|
any gate, quest, menu, flag |
{type: start-dialogue, dialogue: owner:name, node: <optional>}; the context's caller becomes the session's origin, its npc attribute the session's NPC. end-dialogue ends the player's session (and does nothing without one). The dialogue is looked up when the effect runs, so its owner may load later |
| NPC | a trigger npc: <provider:id>
|
a player using that NPC with the right click (InteractNpcActionEvent) starts it - a left click (a punch) opens nothing - origin npc:<ref>, the NPC in the context. The core hears NPC uses only while a registered dialogue has an npc: trigger - that is what keeps the interact-npc source needed |
| region | a trigger region-enter: <owner:name>
|
a RegionCrossedEvent entering that region starts it, origin region-enter:<id>; holds MOVE while such a trigger exists |
| command | the plugin |
@Arg("<dialogue>") Dialogue dialogue (ArgParserDialogue: owner:name, or a bare name exactly one plugin has) then Dialogues.start(...)
|
A trigger never interrupts a dialogue the player is already in.
DialogueScreen answers ScreenKey.DIALOGUE (evernifecore:dialogue) by default: the speaker as the title and on a
label with the node's line as lore, then one Choice per option (element id option:<id>). Each draw checks the
option's gate with the session's context:
- passes: the choice is active, the costs of the gate as its last lore line;
- fails: the choice is drawn disabled with every failure as its reason, one per line - the chest draws the name grey and each line in red at the end of the lore, a renderer that draws by element shows it as a tooltip;
- fails and
hidden: true(Choice.hiddenUntilPassed()): not drawn; the gate's sources redraw it, so it appears when the gate passes.
That one check per redraw decides all three, so a redraw posts one GateCheckEvent per option. A name with no colour
of its own reads green when the option can be picked and grey when not.
The admin restyles it in guis/DialogueLayout.yml (SPEAKER, OPTION - its slots are where options go, in order -,
BACKGROUND). A plugin replaces it for its own dialogues, reading the session from the request:
Screens.override(plugin, ScreenKey.DIALOGUE, ScreenScope.ownedBy(plugin), ctx -> {
DialogueSession session = ctx.getRequest(DialogueSession.class);
return myPixelmonDialogScreen(session); // a click calls session.choose(optionId)
});Each node is a new screen: the session replaces the previous one in place (ScreenNavigation.replace), and only the
closing of its current screen ends it with CLOSED. An engine of its own can reuse the two pieces the default screen
uses: Choice.pickedBy(click -> ...) hands the pick over instead of paying the gate in the choice, and
Screens.build(player, key, request) resolves the four override layers without opening.
All extend DialogueEvent (getSession(), getPlayer(), getDialogue(), getCaller()), are posted only when
something listens, and pass ECEventConformance - see Events.
| Event | Fired when | Carries |
|---|---|---|
DialogueStartedEvent |
a session is about to open - cancellable: cancelled, no session | - |
DialogueChoiceEvent |
an option was picked, before its gate is paid - cancellable: nothing charged, the node stays |
getNode(), getOption()
|
DialogueEndedEvent |
a session ended, once | getCause() |
Permission evernifecore.command.dialogue. Prints the dialogue a player is in, the node, since when, who started it
(the caller) and the NPC. It never starts or ends one.
DialogueTrees in evernifecore-common-tests works over ScreenWorld (the screen) and LogicWorld (the owner,
its custom variables and the clock):
Dialogue talk = DialogueTrees.linear(owner, "greeting", "Hello!", "Bye!"); // n0 --next--> n1 (terminal)
Dialogue.Builder trap = DialogueTrees.withLoopWithoutExit(owner, "haggler"); // build() refuses it
Dialogue tomas = DialogueTrees.fromYaml(owner, "tomas", yamlText);
List<DialogueTrees.DrawnChoice> drawn = DialogueTrees.openDialogueScreen(screens, alex, talk); // registers, starts, reads
drawn.get(0).isEnabled(); drawn.get(0).getLore(); drawn.get(0).getReason();
List<DialogueTrees.DrawnChoice> now = DialogueTrees.drawn(screens, alex); // after a refresh/advanceTicksA node change replaces the screen one tick later (screens.advanceTicks(1)); the timeout runs on the shared
TestClock (logic.advance(...), logic.runDue()). Close the owner's dialogues (Dialogues.closeOwnedBy) and any
screen left open before the worlds close.
EverNifeCore · Home · made by Petrus Pradella
Getting Started
Commands & Text
Player Data & Storage
- PlayerData & PDSections
- Accounts
- Storage Backends
- Inline Backends for Plugins
- Block Data (SVWorldDataManager)
- Legacy Data Migration
- Cooldowns
Config & Minecraft Systems
- Configuration
- Logging & Debug Modules
- Events
- Regions
- Groups
- Notifications
- NPCs
- Logic
- Quests
- Provenance
- Scheduler & Threading
- Items & NBT
- Screens
- Dialogues
- Menus
- Integrations
- Economy
- Boosters
- Deliveries
- Audit Log
- Dynamic Values
- Player Ignore
- Version Compatibility
Architecture & Reference