YapYap YapYap Wiki NeoForge 1.21.1

Dialog-tree library for MinecraftTalk to anything.

YapYap API turns any entity into an RPG-style NPC with a programmable, branching conversation - authored in a datapack or in Java, rendered in a custom in-game screen.

NeoForge 21.1.190 Minecraft 1.21.1 Java 21 MIT
YapYap logo

What you get#

Branching trees

Nodes, click-through text pages, and responses gated by conditions. Same shape in JSON and Java.

Datapack native

data/<ns>/yapyap/dialogs/*.json, hot-reloaded with /reload, decoded through a Mojang Codec.

Extensible

condition and action types are a string-dispatched registry, so you can add your own from a mod.

Custom UI

Non-pausing screen with a portrait, a styled typewriter, and response rows. Works with zero textures.

Server-authoritative

The client only sends "I picked N". Every transition re-checks conditions, so modded clients cannot skip gates.

Persistent progress

Per-player flags and visited-node history saved with the player, copied on death.

60-second taste#

A datapack dialog at data/mypack/yapyap/dialogs/guard.json:

{
  "npc_name": "Town Guard",
  "start": "start",
  "nodes": {
    "start": {
      "text": ["Move along, traveller.", "Unless you have business with the captain?"],
      "responses": [
        { "text": "I need to see the captain.", "target": "pass",
          "conditions": [ { "type": "yapyap:advancement", "advancement": "mypack:reached_town" } ] },
        { "text": "Just passing through.", "target": "end" }
      ]
    },
    "pass": {
      "text": "...fine. Through the gate.",
      "on_enter": [ { "type": "yapyap:set_flag", "flag": "guard_pass", "set": 1 } ],
      "responses": [ { "text": "Thanks.", "target": "end" } ]
    }
  }
}

Bind it to a mob and start talking:

yapyap assign @e[type=villager,limit=1,sort=nearest] mypack:guard "Town Guard"
# now right-click the villager, or:
yapyap talk mypack:guard @s

OverviewGetting Started

Install as a dependency#

YapYap is a library mod. Drop the jar in mods/ alongside your mod, or consume it from Gradle.

gradle wrapper            # first checkout only (or use your IDE's Gradle)
./gradlew build           # -> build/libs/yapyap-0.1.0.jar
./gradlew runClient       # dev client with the example dialogs loaded
./gradlew publishToMavenLocal
// build.gradle
dependencies {
    implementation "dev.yapyap:yapyap:0.1.0"   // from mavenLocal / your maven
}
# your neoforge.mods.toml
[[dependencies.yourmod]]
modId = "yapyap"
type = "required"
versionRange = "[0.1.0,)"
ordering = "AFTER"
side = "BOTH"

Pick an authoring path#

You want to…Use
Ship dialogs a pack-maker can edit without recompilingDatapack JSON
Generate dialogs from your mod's data / register defaultsDialogBuilder + YapYapAPI
Gate conversations, react to choices, wire questsEvents
Add your own condition / action keywordsCustom Types
Try it now

In a dev client run /yapyap demo - you get a villager spawn egg. Place it, right-click the villager, and you're in a live conversation with a Trades option and a fetch-quest.

OverviewCore Concepts

The model#

  • Dialog - a whole conversation tree. Identified by the ResourceLocation it is registered under (datapack path or YapYapAPI.registerDialog).
  • Node - one “screen”: a speaker, one or more click-through pages of text, and a list of responses.
  • Response - a selectable answer. May carry conditions (whether it shows), actions (run when chosen), and a target node.

Server-authoritative flow#

All navigation happens on the server. The client renders a fully-resolved snapshot and can only send “I chose index N”. On every transition the server:

  1. runs the node's on_enter actions;
  2. evaluates every response's conditions and builds the visible list;
  3. resolves placeholders, names and portraits, then pushes the view.

When you pick a response the server re-checks its conditions (anti-cheat), runs its actions, then navigates. An open_dialog / goto / close action overrides the response's target.

Datapack vs code#

Datapack dialogs are reloaded wholesale on every /reload. Code dialogs (YapYapAPI.registerDialog) survive reloads. If both define the same id, the datapack wins.

Datapack AuthoringDialog Files

Dialog trees live at:

data/<namespace>/yapyap/dialogs/<path>.json

The dialog id is <namespace>:<path> - e.g. data/mypack/yapyap/dialogs/town/elder.json → mypack:town/elder. Files load on world load and every /reload; one bad file is logged and skipped, the rest still load, and active conversations are closed.

Dialog#

{
  "npc_name": "Elder Maren",
  "portrait": "mypack:textures/gui/portrait/maren.png",
  "portrait_entity": "minecraft:villager",
  "start": "start",
  "closable": true,
  "auto_close_range": 8.0,
  "nodes": {
    "start": { /* Node */ }
  }
}
FieldTypeDefaultNotes
npc_nametext component-Default speaker name.
portraitid-128×128 texture (see Textures).
portrait_entityid-Entity type rendered as a live model when there's no texture and no bound live entity.
startstring"start"Id of the first node.
nodesobjectrequiredAt least one entry; must contain start.
closablebooleantrueCan the player press Esc to leave?
auto_close_rangenumber8.0Blocks from the NPC before auto-close. ≤0 uses the server config value.
Text fields

Anything the vanilla text parser accepts: a bare string, a {"text": …} object, or an array of components. In a text field on a node, a top-level array = one page per entry.

Datapack AuthoringNodes & Responses

Node#

{
  "speaker": "Elder Maren",
  "text": [
    "You came back.",
    "I wasn't sure you would."
  ],
  "portrait": "mypack:textures/gui/portrait/maren_angry.png",
  "type_speed": 60.0,
  "on_enter": [ /* Action, ... */ ],
  "responses": [ /* Response, ... */ ],
  "auto_advance": "next_node"
}
FieldTypeNotes
speakertextOverrides the dialog / NPC name for this node.
texttext or arrayOne entry = one click-through page.
portraitidOverrides the portrait for this node.
type_speednumberTypewriter chars/second for this node (0 = instant).
on_enterAction[]Run the moment the node becomes active.
responsesResponse[]If empty, the node is terminal unless auto_advance is set.
auto_advancestringWith no responses, the “Continue” button jumps here.

Response#

{
  "text": "Ask about the ruins",
  "conditions": [ /* Condition, ... */ ],
  "hide_when_locked": true,
  "once": false,
  "actions": [ /* Action, ... */ ],
  "target": "ruins"
}
FieldTypeDefaultNotes
texttextrequiredThe button label.
conditionsCondition[][]All must pass for the option to be usable.
hide_when_lockedbooleantruefalse = show it greyed-out instead of hiding it.
oncebooleanfalsetrue = gone forever after being chosen once.
actionsAction[][]Run in order when chosen.
targetstring-Next node id. Omit / "end" / "close" ends the conversation.
Branching

Point several responses at different nodes and gate them with conditions (flags, items, advancements…). There is no separate “router” node type - the conditions are the branching.

Datapack AuthoringConditions

Every condition is { "type": "yapyap:<id>", … }. They gate whether a response is shown or usable.

TypeFieldsPasses when
all_ofconditions: [ … ]every sub-condition passes
any_ofconditions: [ … ]at least one passes
notcondition: { … }the sub-condition fails
flagflag, one of equals / (at_least and/or at_most)the player flag matches
visitednode, dialog? (defaults to current)the player has entered that node before
advancementadvancement (id)the player has completed it
scoreobjective, at_least?, at_most?the player's scoreboard score is in range (missing = 0)
permission_levellevel (int)source.hasPermission(level)
has_itemitem (id), count? (default 1)the player's inventory holds ≥ count
chancechance (0.0-1.0)a random roll succeeds (re-rolled each time the node is shown)
sneakingsneaking (bool)the player's crouch state matches
dimensiondimension (id)the player is in that dimension
"conditions": [
  { "type": "yapyap:flag", "flag": "smith_quest", "equals": 1 },
  { "type": "yapyap:has_item", "item": "minecraft:iron_ingot", "count": 5 },
  { "type": "yapyap:not", "condition": { "type": "yapyap:visited", "node": "lore" } }
]

Datapack AuthoringActions

Every action is { "type": "yapyap:<id>", … }. Actions run on on_enter and on chosen responses.

TypeFieldsEffect
run_commandcommand, as? (player/npc/server, default server), silent? (default true)runs a command; placeholders substituted first. Gated by allowCommandActions.
run_functionfunction (id)runs an .mcfunction as the player (perm 2, output suppressed). Gated by allowCommandActions.
set_flagflag, set or addwrites / increments a player flag (0 = unset).
give_itemitem (id), count? (1)gives the item, dropping overflow.
take_itemitem (id), count? (1)removes up to count from the inventory.
grant_advancementadvancement (id), criterion?awards a criterion (or all of them).
play_soundsound (id), volume? (1.0), pitch? (1.0)plays a sound at the player.
messagemessage (text)system chat message to the player.
actionbarmessage (text)action-bar message to the player.
open_dialogdialog (id), node?ends this conversation and starts another (keeps the NPC).
open_trades-ends the conversation and opens the NPC's merchant screen (villager/merchant; trades auto-generate).
close-ends the conversation.
gotonodejumps to another node in the current tree (overrides target).
"on_enter": [
  { "type": "yapyap:take_item", "item": "minecraft:iron_ingot", "count": 5 },
  { "type": "yapyap:give_item", "item": "minecraft:iron_chestplate", "count": 1 },
  { "type": "yapyap:set_flag", "flag": "smith_quest", "set": 2 },
  { "type": "yapyap:play_sound", "sound": "minecraft:block.anvil.use", "volume": 0.7, "pitch": 1.1 },
  { "type": "yapyap:run_function", "function": "mypack:quests/smith_done" }
]
Security

run_command and run_function can be disabled server-wide with the allowCommandActions config key - do that when loading untrusted datapacks.

Datapack AuthoringPlaceholders

Substituted inside literal text runs at display time (styling and siblings are preserved).

TokenResolves to
%player%the talking player's account name
%player_display%the player's display name (team colours, nicknames)
%npc%the resolved speaker name
%npc_profession%capitalised villager profession, or traveler / villager
%dialog%the current dialog id
%node%the current node id
%<key>%anything added with DialogContext.setVar("key", "value") from a custom action
"text": ["Oh - hello there, %player%.", "I'm the local %npc_profession%. What can I do for you?"]

Java AuthoringYapYapAPI

The single public entry point (dev.yapyap.api.YapYapAPI). All of it is safe to call from common code - the UI is driven automatically over the network.

AreaMethods
DialogsregisterDialog(id, Dialog) · registerDialog(DialogBuilder) · getDialog(id) · dialogIds()
Custom typesregisterCondition(id, MapCodec) · registerAction(id, MapCodec)
ConversationsopenDialog(player, id) · openDialog(player, id, npc) · openDialogAtNode(player, id, node) · closeDialog(player) · isTalking(player)
NPC bindingsbindNpc(entity, id) · bindNpc(entity, NpcDialog) · unbindNpc(entity) · npcBinding(entity)
Player stategetFlag · setFlag · addFlag · hasVisited · resetProgress
// open one yourself (server side)
YapYapAPI.openDialog(serverPlayer, MyMod.id("smith"));
YapYapAPI.openDialog(serverPlayer, MyMod.id("smith"), someVillager); // NPC context: %npc%, live portrait, freeze, range

// bind so right-click works
YapYapAPI.bindNpc(entity, MyMod.id("smith"));

// flags
YapYapAPI.setFlag(serverPlayer, "smith_quest", 2);
int stage = YapYapAPI.getFlag(serverPlayer, "smith_quest");

Java AuthoringDialogBuilder

Fluent trees in code. Register the result once (e.g. during FMLCommonSetupEvent).

import static dev.yapyap.api.action.DialogActions.*;
import static dev.yapyap.api.condition.DialogConditions.*;

ResourceLocation ingot = ResourceLocation.parse("minecraft:iron_ingot");

YapYapAPI.registerDialog(DialogBuilder.create(MyMod.id("smith"))
    .npcName("Bram the Smith")
    .portraitEntity(ResourceLocation.parse("minecraft:villager"))
    .node("start", n -> n
        .text("The forge runs hot today, %player%.")
        .response(r -> r.text("Any work for me?")
            .condition(flag("smith_quest", 0))
            .target("offer"))
        .response(r -> r.text("Here are your ingots.")
            .condition(flag("smith_quest", 1), hasItem(ingot, 5))
            .action(take(ingot, 5), grant(MyMod.id("smithing")))
            .target("thanks"))
        .response(r -> r.text("Passing through.").closeDialog()))
    .node("offer", n -> n
        .onEnter(setFlag("smith_quest", 1))
        .text("Five iron ingots. Don't dawdle.")
        .response(r -> r.text("You'll have them.").closeDialog()))
    .node("thanks", n -> n
        .text("Forged fresh. Wear it well.")));

Builder surface

OnMethods
DialogBuildercreate(id) · npcName · portrait · portraitEntity · start · notClosable() · autoCloseRange · node(id, spec)
node specspeaker · text (repeat for pages) · portrait · typeSpeed · onEnter(actions…) · autoAdvance · response(spec)
response spectext · condition(…) · showWhenLocked() · once() · action(…) · target(node) · closeDialog()

Terse factories live in DialogConditions (flag, flagAtLeast, visited, advancement, hasItem, chance, allOf, anyOf, not, …) and DialogActions (command, function, setFlag, give, take, grant, sound, message, openDialog, openTrades, goTo, close, …).

Java AuthoringEvents

Fired on NeoForge.EVENT_BUS around every conversation (dev.yapyap.api.event.YapYapEvents). The base DialogEvent exposes getPlayer() and getDialogId().

EventCancelableExtraFires
OpenyesgetNpc()before a conversation starts
NodeEnternogetNodeId()each time a node becomes active (after its on_enter)
ChoiceyesgetNodeId(), getResponseIndex()when a valid response is picked, before its actions
ClosenogetNodeId(), getReason()when a conversation ends
@SubscribeEvent
static void onOpen(YapYapEvents.Open event) {
    if (questMod.isMidCutscene(event.getPlayer())) {
        event.setCanceled(true);          // block the conversation
    }
}

@SubscribeEvent
static void onChoice(YapYapEvents.Choice event) {
    analytics.record(event.getDialogId(), event.getNodeId(), event.getResponseIndex());
}

getReason() is a CloseReason: FINISHED, PLAYER_CLOSED, OUT_OF_RANGE, NPC_GONE, REPLACED, RELOADED, DISCONNECT, SHUTDOWN, API, INTERRUPTED, ERROR.

Java AuthoringCustom Condition & Action Types

condition and action types are a string-dispatched codec registry. Register yours before dialogs are parsed (e.g. FMLCommonSetupEvent); datapacks can then use them by id.

public record WeatherCondition(String state) implements DialogCondition {

    public static final MapCodec<WeatherCondition> CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
        Codec.STRING.fieldOf("state").forGetter(WeatherCondition::state)
    ).apply(i, WeatherCondition::new));

    @Override public boolean test(DialogContext ctx) {
        boolean raining = ctx.player().serverLevel().isRaining();
        return raining == "rain".equals(state);
    }

    @Override public MapCodec<? extends DialogCondition> codec() { return CODEC; }
}

// during setup:
YapYapAPI.registerCondition(MyMod.id("weather"), WeatherCondition.CODEC);
{ "type": "mymod:weather", "state": "rain" }

Actions are identical: implement DialogAction (void run(DialogContext) + codec()) and call YapYapAPI.registerAction(id, codec). Steer the conversation with ctx.control().close() / goTo(node) / open(dialog, node).

RuntimeNPCs & Behaviour

Binding a dialog to an entity#

A binding is a persistent data attachment. Set it from a command or the API; right-click then opens it.

yapyap assign @e[type=villager,limit=1,sort=nearest] mypack:elder "Elder Maren"
yapyap unassign @e[type=villager,limit=1,sort=nearest]
YapYapAPI.bindNpc(entity, MyMod.id("elder"));
YapYapAPI.npcBinding(entity).ifPresent(b -> ...);

While talking#

  • The NPC is pinned in place and turned to face the player every tick. No persistent NoAi flag is set, so a server crash mid-conversation leaves nothing to clean up - the mob simply resumes on the next tick once the session ends.
  • Any incoming damage to the player or the NPC ends the conversation immediately (CloseReason.INTERRUPTED).
  • Walking further than auto_close_range from the NPC, or the NPC dying / unloading, also ends it.

Right-click-to-talk#

Controlled by [npc] config: interactToTalk, requireEmptyHand, sneakBypassesDialog. See Configuration.

Auto dialogs#

Two opt-in behaviours give un-bound entities a bundled conversation:

ToggleCommandConfig keyEffect
Villagers/yapyap villager_on <bool>autoVillagerDialogevery un-bound adult villager greets you (yapyap:auto_villager) - uses %player% / %npc_profession%, has a Trades option
Funny mode/yapyap funny <bool>funnyModeiron golems (yapyap:auto_iron_golem) and wardens (yapyap:auto_warden) get something to say

Both default off. The commands flip keys in config/yapyap-common.toml under [auto_dialogs], so you can also just edit the file.

RuntimeThe Conversation Screen

A non-pausing screen drawn over the world. It renders fully procedurally with zero textures; art is optional and swapped in per-asset (see Textures).

Behaviour

  • Typewriter - text reveals character-by-character with styles preserved. Click or press Space/Enter to skip to the full page, again to turn the page. Speed is the client typewriterCps (or a node's type_speed; 0 = instant).
  • Responses appear only once the last page is fully shown. A locked (hide_when_locked: false) row is greyed with a padlock and ignores clicks.
  • Portrait fallback chain: authored texture → live model of the bound NPC (rotates toward the cursor, inventory-preview style) → a stand-in model of the dialog's portrait_entity → a drawn silhouette.
  • Esc leaves if the dialog is closable.
Just show the mob

You rarely need a portrait PNG. A bound NPC already renders live; for command-started dialogs add "portrait_entity": "minecraft:villager".

RuntimeProgress & Flags

Per-player state is a data attachment - saved with the player, copied on death.

  • flags - named integers set by set_flag / add_flag. 0 means unset. Read them in conditions (flag) or from the API.
  • visited - "<dialogId>/<nodeId>" for every node the player has entered (drives the visited condition).
  • consumed - which once responses the player has already taken, keyed as <dialogId>/<nodeId>#<index>.
yapyap flag @s get smith_quest
yapyap flag @a set intro_seen 1
yapyap reset @s          # clears visited + once history, keeps flags
Reordering

once responses are keyed by index. Re-ordering responses in a shipped tree can shift which one counts as “already taken”.

ReferenceCommands

/yapyap … - all subcommands require permission level 2.

CommandDoes
talk <dialog> [<players>] [<npc>]starts a conversation (optionally with an NPC entity for context)
assign <npc> <dialog> [<name>]binds a dialog to an entity
unassign <npc>removes the binding
stop <players>force-closes conversations
reset <players>clears visited / once history (keeps flags)
flag <players> get|set|add|clear <flag> [<value>]inspects / edits player flags
listlists every loaded dialog id (datapack + code)
demo [<players>]gives a villager spawn egg; the placed villager gets a random profession + the yapyap:demo_villager dialog (Trades + a wheat quest)
villager_on <true|false>toggles autoVillagerDialog (persisted to the config file)
funny <true|false>toggles funnyMode (persisted to the config file)

ReferenceConfiguration

config/yapyap-common.toml#

Section / keyDefaultMeaning
[npc] interactToTalktrueright-click a bound entity to start its dialog
[npc] requireEmptyHandfalseonly start when the interacting hand is empty
[npc] sneakBypassesDialogtruesneaking while interacting skips the dialog
[conversation] autoCloseRange8.0fallback blocks-from-NPC before auto-close
[security] allowCommandActionstrueallow run_command / run_function
[auto_dialogs] autoVillagerDialogfalsegeneric greeting for un-bound adult villagers
[auto_dialogs] funnyModefalsedialogs for iron golems and wardens

config/yapyap-client.toml#

KeyDefaultMeaning
typewriterCps45typewriter chars/second (0 = instant)
dimBackgroundfalsedarken the world behind the panel
playTypingSoundtruesoft click while text reveals

The toggle commands (villager_on, funny) write the common file immediately; NeoForge's file watcher also picks up manual edits.

ReferenceTextures

All optional - the screen has drawn fallbacks for everything. Files live under assets/yapyap/textures/gui/ and are re-checked on every resource reload.

FileSizeNotes
dialog_panel.png64 × 64nine-slice, 8px border; drawn wide & short at the bottom
portrait_frame.png32 × 32nine-slice, 6px border; keep the centre ~20px transparent
icons.png32 × 16two 16px cells: [0] continue chevron, [1] lock
portrait/<name>.png128 × 128referenced by "portrait" on a dialog / node / binding
response_button.png-reserved for a future textured button skin

ReferenceExamples

Bundled in the jar and loaded in runClient:

IdShows
yapyap:village_smithdatapack file - pages, condition branching, once, on_enter actions, run_function, flag-gated quest
yapyap:demo_villagerdatapack file - portrait_entity, open_trades, %npc_profession%, a wheat fetch-quest (from /yapyap demo)
yapyap:auto_villagercode dialog - the greeting used by villager_on
yapyap:auto_iron_golem · yapyap:auto_wardencode dialogs - funny mode

The smith quest, end to end

{
  "npc_name": { "text": "Bram the Smith", "color": "gold" },
  "start": "greet",
  "auto_close_range": 6.0,
  "nodes": {
    "greet": {
      "text": ["Well met, %player%.", "Need something shaped, or here to gawk?"],
      "responses": [
        { "text": "Any work for me?", "target": "offer",
          "conditions": [ { "type": "yapyap:flag", "flag": "smith_quest", "equals": 0 } ] },
        { "text": "Here are your ingots.", "target": "reward",
          "conditions": [
            { "type": "yapyap:flag", "flag": "smith_quest", "equals": 1 },
            { "type": "yapyap:has_item", "item": "minecraft:iron_ingot", "count": 5 }
          ] },
        { "text": "Just passing through.", "target": "end" }
      ]
    },
    "offer": {
      "text": "Bring me five iron ingots.",
      "on_enter": [ { "type": "yapyap:set_flag", "flag": "smith_quest", "set": 1 } ],
      "responses": [ { "text": "You'll have them.", "target": "end" } ]
    },
    "reward": {
      "text": "Ha! Good steel sense. Hold still...",
      "on_enter": [
        { "type": "yapyap:take_item", "item": "minecraft:iron_ingot", "count": 5 },
        { "type": "yapyap:give_item", "item": "minecraft:iron_chestplate", "count": 1 },
        { "type": "yapyap:set_flag", "flag": "smith_quest", "set": 2 }
      ],
      "responses": [ { "text": "[Nod]", "once": true, "target": "end" } ]
    }
  }
}

ReferenceChangelog

Unreleased#

  • NPCs are frozen in place and turned to face the player during a conversation (no persistent NoAi); any incoming damage to the player or NPC ends it (INTERRUPTED).
  • /yapyap villager_on <bool> and /yapyap funny <bool> - toggle persistent [auto_dialogs] config keys. Bundled code dialogs yapyap:auto_villager / auto_iron_golem / auto_warden.
  • Close hint no longer overlaps the bottom response row.
  • portrait_entity on a dialog / NPC binding / DialogBuilder - renders a live 3D entity model in the portrait.
  • yapyap:open_trades action; %npc_profession% placeholder.
  • /yapyap demo - villager spawn egg with a random profession + the demo dialog.

0.1.0 - initial#

  • Dialog / DialogNode / DialogResponse model with Codec serialization.
  • Datapack loading, hot-reloaded on /reload; fluent DialogBuilder.
  • String-dispatched condition / action registry - 12 conditions, 12 actions.
  • Server-authoritative state machine: per-transition re-checks, action-driven steering, once responses, auto_advance chains.
  • Custom conversation screen with a style-preserving typewriter.
  • Per-player persistence, NPC bindings, per-tick range auto-close.
  • Lifecycle events, /yapyap command, common + client config.