Quests and dialogue
Quest chains in JSON, shown by the game itself: its journal, NPC menu, map markers, notifications and voice.
Two fixes await a client retest: an NPC placed in a second hosted world of one session, and done quests notifying again after a world switch. Party scope hasn’t been played with two players.
You write a quest once. The game shows it: journal, tracked-quest line, notifications, step counters, map markers, its NPC menu and voice. The server runs it, per character on each world. Kiln draws no window of its own.
A first quest
quests/errand.json, with "clients": "mod" and "side": "both" in the manifest. Kiln reads it from
every loaded mod, on the server and on every client.
{ "format": 1,
"npcs": {
"ilse": { "template": "NPC_Workshop_Blacksmith01", "dialogue": "ilse",
"names": { "en": "Caravan Master" } } },
"quests": {
"lost_crates": {
"name": { "en": "The Lost Crates", "de": "Die verlorenen Kisten" },
"giver": "ilse",
"requires": [ { "level": 8 } ],
"steps": [
{ "id": "bandits",
"title": { "en": "Drive off the scavengers" }, // the journal's list, the tracked quest
"text": { "en": "They took the crates on the old road." }, // the journal's paragraph
"objective": { "kill": { "tag": "crates", "count": 6 } }, // the journal counts 0/6
"spawn": { "template": "Enemy_Scavenger_Melee01", "count": 6, "tag": "crates",
"near": "ilse", "distance": [60, 120] },
"marker": { "name": { "en": "The Old Road" } } }, // the marker comes by itself; this names it
{ "id": "return", "text": { "en": "Bring three logs back" },
"objective": { "deliver": { "item": "Material_T1_Wood_Logs", "count": 3, "to": "ilse" } } }
],
"rewards": [ { "xp": 1500 }, { "item": "Weapon_T1_1H_SwordEpic_01" } ] } },
"dialogues": {
"ilse": {
"start": "hello",
"nodes": {
"hello": { "options": [
{ "text": { "en": "I'll find them." },
"if": { "quest": "lost_crates", "is": "available" },
"do": [ { "start": "lost_crates" } ],
"reply": { "en": "Scavengers took our crates on the old road." }, "hint": "Attention" },
{ "text": { "en": "Here are your logs." },
"if": { "step": "lost_crates.return", "is": "active" },
"do": [ { "advance": "lost_crates.return" } ],
"reply": { "en": "The road is safe again. Take this." }, "hint": "MainQuest" } ] } } } } }
kiln lint checks the schema, the game names, and every step, node, NPC and quest the file names. An NPC with no dialogue
offers the quests it gives and takes the deliveries it’s owed, each as a topic.
Quest and dialogue blocks (dev.20+)
Available in 0.2.0-dev.20 and newer. Put quests and dialogues beside NPC, item and status blocks in
blocks/*.json, or define QuestBlock and DialogueBlock in C#. kiln new quest MyQuest starts a project.
{ "format": 1,
"quests": { "errand": { "name": "Wood for Ilse", "giver": "ilse",
"steps": [ { "id": "wood", "text": "Bring three logs to Ilse",
"objective": { "deliver": { "item": "Material_T1_Wood_Logs", "count": 3, "to": "ilse" } } } ],
"rewards": { "xp": 100 } } },
"dialogues": { "ilse-talk": { "start": "hello", "nodes": { "hello": { "options": [
{ "text": "Do you need wood?", "if": { "quest": "errand", "is": "available" },
"do": [ { "start": "errand" } ], "reply": "Three logs, please." } ] } } } },
"npcs": { "ilse": { "name": "Ilse", "body": "villager", "talk": "ilse-talk" } } }
Block names resolve the NPC, quest, item, status and loot-table references in this mod. NpcBlock.Talk points to a
dialogue block. C# uses StepBlock, Goal.Kill(...), Req.Level(8), Do.Start("errand") and RewardBlock.
Blocks.Define returns a QuestHandle; .FullId goes to Quests.Start and Quests.StatusOf.
A reward is one bundle (xp, items, status, knowledge, lootTable); step rewards run before onDone. Overflow items
land at the player’s feet. A world quest pays every player present using one loot-table roll.
The block form uses the game’s UI: side: both, clients: mod. JSON blocks enter journal data on the first start. C#
definitions enter the generated native sidecar for the next start; ship quests/generated/<mod>.json for first-start UI.
Restart server and clients after changes. For a mod vanilla clients join, keep the existing chat/non-native quests/*.json
form. Blocks have no top-level knowledge, option set, toast or waypoint actions; unknown fields fail.
Keep quest/block names, step ids, dialogue node names and option order stable after release. Unloading removes runtime quest/dialogue registrations; reloading creates a fresh owning lifetime. The native journal is still boot data, so reload does not replace a restart or prove the new form’s appearance. Journal, dialogue, rewards and party behaviour need the client checks.
What players need
| Your mod has | Manifest | Who can join |
|---|---|---|
| Quests and dialogue: the default | "clients": "mod", "side": "both" |
players with the mod; servers refuse the others |
Every quest "journal": "chat", every dialogue "native": false |
"clients": "none" |
anyone, with chat lines instead of the game’s UI |
The game builds its journal and NPC lines from your mod’s files at boot, so no server can send them. kiln lint reports the
wrong clients as KL210. Changes to these files need a restart on both sides.
Scope
"player"(default): each character on its own. Two characters of one player have separate quests."party": own state, but players withinpartyRadiusmetres (default 60) share kills, crafts, builds and signals, and each gets the rewards."world": one state. Anyone’s progress counts, rewards go to everyone in game, and it can’t be given up.
Quest state belongs to the world. A character on another world starts that world’s quests from nothing, and finds its progress here when it returns. Rewards stay with it, and talents follow the character.
Objectives
A step has "objective" or "objectives"; it’s done when all are.
| Objective | Keys | Counts |
|---|---|---|
kill |
template, family, tag, count, near + radius |
a death credited to the player |
collect |
item, count |
what the inventory holds now; takes nothing |
deliver |
item, count, to |
takes the items at hand-over |
reach |
site, at, offset or marker, radius |
being there |
talk |
npc, line, node |
talking to that NPC |
craft |
item or recipe, count |
a successful craft |
build |
prop, count |
placing it |
knowledge |
a knowledge name | the game’s knowledge holds it |
custom |
a signal name, count |
Quests.Signal, a signal action, or C# |
The journal shows kill, collect, craft, build and custom as one counter. A step the player already fulfils stays open
about 3 s, so it gets seen.
Places
A step with a place gets a map marker and the game’s waypoint pin by itself. Any of these is a place: site, spawn, a reach
place, a kill’s near, a marker’s at, an NPC of your mod to talk or deliver to.
"marker": { "name": … } names it. "marker": { "waypoint": false } leaves the player’s own pin alone. "marker": false shows
nothing.
Don’t guess positions. Kiln finds real ground outside bases:
"site": { "near": "ilse", "distance": [30, 45] }, // "near": "player", an NPC of npcs, or [x, y, z]
"objective": { "reach": { "site": true, "radius": 8 } }
A place has open sky, isn’t too steep, is on no one’s building, and is at least margin metres (default 20) from every base. If
distance has nothing good, the search widens. It’s kept per world, so everyone goes to one place. A fixed at moves out of a
base and onto the ground too; "exact": true takes it as it is.
Creatures
"spawn": { "template": "Enemy_Vukah_Melee", "count": 2, "tag": "hunt", "near": "ilse", "distance": [50, 90] }
Never in a base: the game pacifies enemies there and may remove them. Kiln checks every second. One gone without dying spawns
again further out, up to retries (default 5). They aren’t saved: after a restart or rejoin, as many as the kill still needs
come back. When the step ends, the rest go.
Givers
"place": { "near": "player", "distance": [25, 45], "once": true, "unlock": true }
The game keeps one entity per named NPC and destroys one this world hasn’t unlocked. place handles that. To make new NPCs, see
Friendly NPCs.
Conditions
requires and an option’s if are checked on the server: {"level": 8}, {"quest": "id", "is": "done"},
{"step": "quest.step", "is": "active"}, {"item": "name", "count": 3}, {"knowledge": "…"}, {"predicate": "name"}, and
all, any, not.
A quest is locked, available, active, done, failed or started. A step is pending, active or done.
Actions
In rewards, onStart, onFail, a step’s onEnter and onDone, and a node’s or option’s do:
| Action | Does |
|---|---|
{"xp": 1500} |
experience |
{"item": "name", "count": 2}, {"take": "name", "count": 2} |
give (overflow lands at their feet), take |
{"start": "q"}, complete, fail, reset, {"advance": "q.step"} |
other quests |
{"spawn": "template", "offset": [5,0,0], "tag": "x"}, {"despawn": "x"} |
a one-off, on the ground outside bases; for what a step needs, use its spawn |
{"teleport": [x,y,z]}, {"waypoint": [x,y,z]}, {"waypoint": false} |
the player; their map pin |
{"chat": {…}} |
a chat line. toast is the same: the game has no free-text notification |
{"talk": "dialogue", "node": "n"} |
opens a chat conversation |
{"knowledge": "…"}, {"revoke": "…"} |
the game’s knowledge; a revoke clears that id only |
{"signal": "name"}, {"call": "name"} |
custom objectives; your C# |
In an option’s do, the first failure stops the rest and the player gets the reason in chat. Elsewhere, failures are logged and
the rest runs.
Dialogue
A dialogue goes into the game’s NPC menu. A node has text and options. An option has text, if, do, and what the NPC
answers: reply, else the text of the node it goes to with goto. The start node’s text is a greeting (on a game NPC, only
with "greet": true). greetings win over it while their if holds.
The menu is one flat list of topics:
- A
gototo a node with options adds those options to the list, and they stay. onceremoves a topic after it’s heard.closeends the talk with the NPC’s goodbye.hint(Hint,Attention,MainQuest) is the marker over its head.- A hand-in is on offer only while the player holds the items.
ifcan be any condition.quest,stepandknowledgeact at once; anything else is about a second behind.- The journal has no give-up button. A topic can be one:
"if": {"quest": "q", "is": "active"}, "do": [{"reset": "q"}].
Voice: "voice": "name" plays your cooked audio, "voice": "<guid>" any line of the game. Add new options at
the end of a node: positions feed the ids.
The menu opens only when a player talks to the NPC. It can’t open from the server or show a refusal: use a marker and a hint.
For a mod vanilla clients join
Set "journal": "chat" on every quest, "native": false on every dialogue, and "clients": "none". Players get chat lines, with
/quests, /quest <id>, /quest abandon <id>, /talk (within 8 m), /talk <n> and /talk end. This has run on test servers only.
NPCs
npcs finds one by template, Quests.BindNpc from code, a spawn action with bindNpc, or place. A dialogue in the game’s
menu needs template.
From code
Quests.Define(new QuestDefinition
{
Id = "from_code",
Name = UiText.Of(("en", "From Code")),
Steps = [ new QuestStep { Id = "ring", Text = "Ring the bell",
Objectives = [QuestObjective.Custom("mymod.bell", 3)] } ],
Rewards = [QuestAction.Experience(500), QuestAction.Code(ctx => Pay(ctx))],
});
[Subscribe]
private void OnBell(BellRungEvent e) => Quests.Signal("mymod.bell", QuestHolder.Of(e.Player));
It gets the game’s UI too, with one difference. The journal is built at boot, before C# runs, so Kiln writes what your code defines
to kiln/data/kiln-quests/native/<mod>.json and builds it at the next start. Until then the quest uses chat lines. To have it at
the first start, ship that file as quests/generated/<mod>.json.
Saving and testing
Progress saves with the world, per character, by quest and step id. Renaming an id loses players’ place. Adding or removing other steps is fine. Removing a mod keeps the records for when it returns.
/questadmin reload rereads the engine’s side of the files. What the game shows needs a restart. /questadmin list says
NOT BUILT for a quest the journal doesn’t have. /questadmin state <player|world> [quest], start, step, advance, complete,
fail, reset and signal drive a quest. Without a player, QuestInput feeds a QuestHolder.Synthetic("name").