Authoring Dialogues & Placed NPCs

Branching conversations, and the placements that decide who stands where and what press-F opens

Two independent things, both shared library content. A conversation is a graph of screens and the lines a player can pick. A placement says which character stands where, in which worlds, whether they appear at all, and what pressing F on them opens. A conversation names no character, and a placement carries no dialogue text - which is what lets one conversation serve several characters and one character stand in two worlds.

Where the files go

Content pack assetServer/ZiggfreedCommon/Dialogues/<Namespace>/<Name>.jsonDialogues/

The file IS the conversation; its id is the filename, lower-cased. A reusable base is an ordinary file marked Abstract. Precedence bottom to top: the shared store, then the owner layer at mods/ziggfreedcommon/dialogues.json - a bare map from id to body, no envelope, read by the same codec and folded leaf by leaf.

defaults<pack<owner
Content pack assetServer/ZiggfreedCommon/NpcPlacements/<Id>.jsonNpcPlacements/

Who stands where. The owner switch is mods/ziggfreedcommon/npc-placements.json, and /mmonpc manages placements live.

defaults<pack<owner

The old per-mod dialogue stores are gone

The Server/MMOSkillTree/DialogueTemplates/ store is retired: a reusable base is now an ordinary conversation file marked "Abstract": true and named as a Parent. The Server/MMOSkillTree/Dialogues/ store is retired the same way: a conversation left there is not loaded, and the server names its id in one boot warning pointing at the shared folder above (the body is identical, so moving the file across is the whole change). The Server/MMOSkillTree/QuestGivers/ store is a warn-only stub, and the owner files spawn-hub.json, quest-givers.json and spawn-hub-placements.json are renamed .legacy at boot with one warning naming the replacement. Everything they did is an NPC placement now - see Placements and Placed NPCs & the Adventurer's Guide. The old per-mod mods/mmoskilltree/dialogues.json is renamed .legacy at boot with a single warning; nothing in it is carried across automatically.

The conversation file

Server/ZiggfreedCommon/Dialogues/MyStudio/My_Hermit.json
{
  "Name": "My_Hermit",
  "Memories": { "knows_my_name": {} },
  "Start": {
    "First": [ { "Node": "first_meeting", "Once": true } ],
    "Quests": { "hermit_favour": { "Ready": true, "Offerable": "offer", "Active": "brief" } },
    "Fallback": "menu"
  },
  "Nodes": {
    "menu": {
      "TextKey": "dialogue.my_hermit.menu.text",
      "Options": [
        { "LabelKey": "dialogue.my_hermit.menu.opt.leave", "Close": true }
      ],
      "IncludeOptions": ["footer"]
    }
  },
  "Fragments": {
    "footer": [ { "LabelKey": "dialogue.my_hermit.opt.hub", "Open": "Mmo_Hub" } ]
  }
}
FieldTypeDefaultDescription
Namestringthe filenameA human-readable echo of the filename-derived id. Nothing reads it as authority.
Parentstring (dialogue id)noneInherit another conversation. Screens merge per key: a child screen overrides or adds by key while parent-only screens carry down, and each screen's own fields inherit individually.
AbstractboolfalseMarks a file that exists only to be inherited, so it never runs as a conversation of its own.
Memoriesmap of name to declaration{}Named state, declared once and used by bare name - see Once and Memories.
StartgroupnoneWhich screen the conversation opens on, in sections - see The Start ladder.
Nodesmap of screen id to screenoptionalThe graph itself.
Fragmentsmap of fragment id to option[]{}Reusable option lists a screen splices in with IncludeOptions. They resolve within the same body only, so a base's fragments are not visible to a child.
* required

The Start ladder

Start is written as SECTIONS the engine walks in a fixed order, so nothing has to be hand-sorted into one flat list:

First (in the order written) > a quest that is READY to hand in > a quest this character can OFFER > a quest that is ACTIVE > Then (in the order written) > Fallback. Within one quest band, the order the rows are written settles which quest wins. Every section is independently optional: a one-screen conversation writes only Fallback, and a giver that says nothing of its own writes only Quests.

FieldTypeDefaultDescription
Firstbeat[][]Beats that outrank anything about quests: a first-visit greeting, a beat for one world, a line gated on another mod's reading.
Questsmap of quest id to row{}One row per quest, saying what this conversation does while that quest is Ready, Offerable or Active. The engine reads the quest's own state, so no condition is written.
Thenbeat[][]Beats tried once no quest row applies: a steady-state greeting, a seasonal line, flavour that varies.
Fallbackstring (screen id)noneThe screen of last resort, with no conditions, so a conversation is never left with nothing to show.
* required

A beat is { "Node": "<screen>", "When": [ ...conditions ], "Once": ... }, or { "Pick": [ { "Node": "a" }, { "Node": "b", "Weight": 3 } ] } to draw between screens. A quest row value is written three ways: true opens this character's quest list with that quest called out, a bare word is a SCREEN of this conversation, and an object is any destination. false reads as no beat at all, so turning one off is a one-word edit.

Under Parent, a child that writes Start replaces the whole of it

It is one ladder, so half of a child's and half of a parent's would be a ladder nobody wrote - and replacing is the only way a child can take a parent's opening beat out of the running.

Screen fields

FieldTypeDefaultDescription
TextKeystringoptionalLocalization key for the character's line. Rich markup works here (<color is="#hex">, <b>, \n).
TextstringoptionalA plain fallback line for a screen whose key is not written yet. It reaches every player in the one language it is typed in - author TextKey for anything you ship.
Conditionscondition[]optionalWhen this screen may be entered at all. A screen that gates itself keeps a conversation to one set of screens instead of one per state.
Optionsoption[]optionalThe lines the player can pick, in the order they are shown.
IncludeOptionsstring[] (fragment ids)optionalNames of shared option groups from this file's Fragments, appended after this screen's own options. An unknown id logs a warning and the screen keeps its own options.
* required

Option fields

FieldTypeDefaultDescription
LabelKeystringoptionalLocalization key for the button label. Buttons carry no rich markup - use Presentation or Style for colour instead.
LabelstringoptionalA plain fallback label. Never ship one.
Conditionscondition[]optionalAND-combined; the option hides while any fails. Re-evaluated on every render AND again on click, so the server always has the last word.
Actionsaction[]optionalOrdered; they run in array order on click.
Oncetrue or {World} or {Where}optionalOffer the option until its actions have run once - see Once and Memories.
OnceIdstringoptionalAn explicit identity for the Once guard, for a duplicate-label or label-less option. Otherwise identity comes from LabelKey, never from position, so reordering a screen's options can neither resurrect nor spend one.
Presentation{Color, Icon}optionalPer-option colour and icon. Color is a hex string; Icon is { "Item": "<item id>" } or { "Glyph": "<token>" }.
StylestringoptionalOne of accept / turnin / continue / neutral / farewell. Overrides the look the option's own decisive action would have given it.
Doarray of single-key objectsoptionalThe escape hatch when order matters or a step repeats. Each atom expands in ARRAY order, and a Do array shadows any bare shorthand on the same option.
* required

With no Style or Presentation, each option takes the look of its decisive action: accepting a quest reads as accept, handing one in reads as turn-in, opening a screen reads as neutral, and the farewell row is muted. An option with no Goto or Close simply re-renders the same screen, which is what an informational line wants.

Option shorthand

Instead of a verbose Actions array, an option can carry shorthand keys. They fold into the exact canonical actions, so a validator and the engine only ever see canonical ones. Bare shorthand keys on one option expand in a fixed order regardless of how they are typed: Accept, Complete, TurnIn, Remember, Forget, Reward, Run, Open, Goto, Close. Shorthand appends after any explicit Actions, so the two can be mixed.

FieldTypeDefaultDescription
Acceptstring (quest id)optionalAccepts the quest through the normal gate.
TurnInstring (quest id)optionalHands the quest in at the character the conversation is with.
Completestring (quest id)optionalForce-completes a quest even with unmet steps - the "skip the tutorial" path.
Rewardobject (+ sibling RewardOnce)optionalGrants one reward. The guard is spelled RewardOnce, not Once, because an option's own Once is the engine's seen-ness knob; it still defaults to true, so a plain gift is granted once.
Runstring (command)optionalRuns a console command. Arbitrary commands: review untrusted packs.
Remember / Forgetstring (memory name)optionalWrites or clears a declared memory - see Once and Memories.
OpendestinationoptionalOpens a screen or another conversation - see Destinations.
Gotostring (screen id)optionalMoves to another screen of this conversation.
ClosetrueoptionalEnds the conversation.
* required
Ordered shorthand via Do
{ "LabelKey": "...", "Do": [ { "TurnIn": "wilds_call_of_the_dunes" }, { "Remember": "helped_refugees" }, { "Goto": "desert_lore" } ] }

Style is not shorthand

Style is a separate top-level option key, never shorthand for an action.

Actions

Written out, an action is { "Type": "...", ... } in an option's Actions array. The library provides the generic ones and this mod registers its own into the same engine, so one conversation can mix two mods' actions.

FieldTypeDefaultDescription
MarkTalked{Target?, Qualifier?}optionalCredit this conversation - see MarkTalked. It has no shorthand, on purpose.
AcceptQuest / TurnInQuest{Quest}optionalAccept through the normal gate, or hand in the active turn-in step at the character being talked to.
CompleteQuest{Quest}optionalForce-completes even with unmet steps. Idempotent for a finished one-shot quest.
Reward{Once, Reward}optionalOne grant of a reward object, in the shared reward vocabulary.
RunCommand{Command}optionalRuns a console command with the player substituted. The generic "run anything" escape hatch, with no offline retry behind it.
Remember / Forget{Memory}optionalWrites or clears a declared memory.
OpenPage{Target}optionalOpens a destination. The shorthand for it is Open.
Goto{Node}optionalMoves to another screen.
Close{}optionalEnds the conversation.
* required

MarkTalked: crediting a conversation

MarkTalked is the ONE way a conversation counts as having happened - for a talk step on a quest, for an achievement criterion, for the per-character statistics breakdown.

json
{ "Type": "MarkTalked" }
{ "Type": "MarkTalked", "Target": "blacksmith", "Qualifier": "hired" }

Nothing credits implicitly. Not pressing F, not a screen opening, not a conversation re-rendering. Put the beat on the option that IS the moment - the greeting, the hail, the "I have returned" - and only that option credits. What the engine gives you for free is the TARGET, not the trigger: omit Target and it credits the character being talked to, alias set included, so you never type an id to credit the character whose conversation you are already writing. Pair it with the option-level Once to credit only the first hail.

The Talk action and its shorthand are retired

Talk no longer reads, in either the action or the shorthand form; a file still carrying one fails to load. Write MarkTalked instead. Outside a conversation, the ZigTalkCredit NPC action does the same job from a role's own interaction, so a character with no conversation at all can still credit: { "Type": "ZigTalkCredit", "Npc": "<id>" }.

Conditions

A condition object goes in a When list on a Start beat, or a Conditions list on a screen or an option. They are AND-combined, and the combinators AllOf / AnyOf / Not nest freely.

FieldTypeDefaultDescription
QuestState{Quest, State} or {Quest, States[]}optionalReads the quest's effective state. "States": ["ACTIVE","NOT_STARTED"] is the OR shorthand for "greet while active or fresh", instead of two duplicate beats.
Factor{Factor, Param, Min, Max}optionalA number another mod owns, in the shared factor vocabulary. Fail-closed: an id nothing registered hides the content. See Factor spellings.
World{Where}optionalThe player's current world, scored against a nested Where selector (Match / GameplayConfig / ExcludeMatch). An omitted Where matches nothing.
Remembered / NotRemembered{Memory}optionalReads a declared memory by bare name.
Gate{Requirements}optionalThe shared Requires block, evaluated exactly as it is on a quest.
HasOfferableQuests(none)optionalTrue when the character being talked to has at least one quest to give that the player could take now.
HasReadyToTurnIn(none)optionalTrue when any active quest's remaining step resolves at this character.
ReadyToTurnIn{Quest}optionalTrue when THIS quest is active and its remaining step resolves at this character.
HasActiveBounties(none)optionalTrue when the player can reach a board with at least one active contract.
AllOf / AnyOf / Not{All} / {Any} / {Of}optionalEach holds a child condition list.
* required

Destinations: what Open opens

A destination is a value, not a string somebody parses: one word for a screen that takes no settings, or { "Type": "...", ... } plus that type's own fields. A name nothing registered fails the file's read at startup rather than becoming a button that does nothing.

DestinationOpens
{ "Type": "Dialogue", "Dialogue": "<id>" }Another conversation, automatically with the same character
QuestsThis character's quest list
{ "Type": "Shop", "Shop": "<id>" } / { "Type": "Board", "Board": "<id>" }A storefront or a board (the library's own screens)
Mmo_HubThe feature launcher
Mmo_Skills, Mmo_Quest_Log, Mmo_Achievements, Mmo_Abilities, Mmo_Mastery, Mmo_Boosts, Mmo_OnboardingThat feature's screen
{ "Type": "Mmo_Shop", "Shop": "<id>" }A storefront in this mod's own shop screen
{ "Type": "Mmo_Board", "Board": "<id>" }A board in this mod's own board screen
{ "Type": "Mmo_Skill_Tree", "Skill": "MINING" }One skill's tree
Mmo_Theme_PickerThe menu-theme picker (the screen guards admin itself)

Every Mmo_ destination carries this mod's prefix because the vocabulary is shared across the whole server: reading Mmo_Board tells an author which mod has to be installed for the file to read at all. The two unprefixed ones a file may also write, Dialogue and Quests, belong to the library that owns conversations and quest lists.

Once and Memories

Two knobs cover everything a conversation needs to remember, and neither one asks you to invent, spell, or scope a key by hand.

Once: a beat that plays once

json
"Once": true                                                  // once per character
"Once": { "Where": { "Match": ["*Forgotten_Temple*"] } }      // once per world family
"Once": { "Where": { "GameplayConfig": ["ForgottenTemple"] } } // once per instance kind

On a Start beat the beat stops matching once the player COMPLETES it (chooses any option on the screen it opened, the implicit farewell row included), so leaving with Escape shows it again. On an option, the option is offered until its actions have run once. Where is the same world selector an NPC placement carries: Match for a world name or name pattern, GameplayConfig for an instance world, ExcludeMatch to carve worlds back out. Leave it out and the state is kept once per character.

A world pattern keys state by its literal core

A dynamically created instance world is named with a fresh random suffix each time and is destroyed when it empties, so "already greeted here" keyed by the world's own name would come back on every fresh instance. A pattern like "*Forgotten_Temple*" keys the state under the literal core, so a rebuilt instance lands on the same key. Narrow the core to scope tighter. For an instance world reach for GameplayConfig instead: it is authored, carries no generated suffix, and survives the rebuild. In a world the selector does not match, the Once neither reads nor writes.

A bare World string is retired

The old single-pattern "World": "<pattern>" leaf on Once and on a memory refuses to load and tells you to write Where instead. Rewrite it as { "Where": { "Match": ["<pattern>"] } }, or GameplayConfig for an instance world.

Memories: named state, declared once

json
"Memories": {
  "helped_refugees": { "Where": { "Match": ["*Emerald_Wilds*"] }, "ResetWithQuest": "guide_trust" },
  "knows_my_name": {}
}
FieldTypeDefaultDescription
Whereworld selector groupper characterRemember it per world instead of per character, using the same selector an NPC placement carries: Match for a world name or pattern, GameplayConfig for an instance world, ExcludeMatch to carve worlds back out.
ResetWithQueststring (quest id)neverFile it inside that quest's own state, so resetting the quest forgets it too.
SharedboolfalseMake it visible to every conversation that declares it, instead of private to this one.
SessionboolfalseKeep it only while the player is connected, for state that belongs to something that ends: a round, a visit, a run. Leave it out and it survives a restart.
* required

Use sites are the Remember / Forget actions and the Remembered / NotRemembered conditions, always by bare name. The map merges per key under Parent, so a child re-declares or adds ONE memory and inherits the rest. A name used but never declared still works as a plain per-conversation memory, but it warns and is a validation error - the declaration is the only place its scope and lifetime could have been written. Two conversations sharing a name must declare it identically, or they are quietly talking about different state.

Authored flags are retired

SetFlag, Flag and NotFlag are gone. A first-visit greeting (a guard plus a set) is one Once knob on the beat; anything longer-lived is a declared memory. A flag that sat on ONE option is not the same knob as a beat Once (a beat's Once is spent by every completion path, the farewell row included), so migrate that shape to a memory: declare it, Remember on the option that carried the set, and NotRemembered on the beat.

Self-heal convention

Never mirror quest state in a memory

Quest-driven option and screen visibility must derive from QuestState, or from the Start ladder's own quest rows - never a memory that mirrors quest state. A memory outlives a quest reset and desyncs, soft-locking the conversation at a stale branch. When a memory genuinely must die with a quest, declare ResetWithQuest and the reset clears it for you.

Localization keys

Conventions: dialogue.<dialogueId>.<screen>.text for a line and dialogue.<dialogueId>.<screen>.opt.<index> for an option label (index is the option's position in the decoded screen, after fragments are spliced in). Author them in your pack .lang; never author a raw Text or Label. A child inheriting by Parent gets its OWN namespace from its own filename, which is why a base should omit TextKey and LabelKey rather than hardcode them.

Worked example: the jar's hub conversation

The Adventurer's Guide conversation shows the ladder doing its job: a temple-only first meeting, one row per onboarding quest, a steady-state line once tools are done, and a menu of last resort.

Server/ZiggfreedCommon/Dialogues/MMOSkillTree/Mmo_Hub_Intro.json (abridged)
{
  "Name": "Mmo_Hub_Intro",
  "Memories": { "temple_greeted": { "Where": { "GameplayConfig": ["ForgottenTemple"] } } },
  "Start": {
    "First": [
      { "Node": "temple_greet",
        "When": [ { "Type": "World", "Where": { "GameplayConfig": ["ForgottenTemple"] } },
                  { "Type": "NotRemembered", "Memory": "temple_greeted" } ] }
    ],
    "Quests": {
      "getting_started":    { "Offerable": "greet", "Active": "greet" },
      "craft_starter_tools":{ "Ready": "tools_ready", "Offerable": "briefing", "Active": "briefing" },
      "make_camp":          { "Ready": true }
    },
    "Then": [
      { "Node": "tools_done",
        "When": [ { "Type": "QuestState", "Quest": "craft_starter_tools", "State": "COMPLETED" } ] }
    ],
    "Fallback": "menu"
  },
  "Nodes": {
    "tools_ready": {
      "TextKey": "dialogue.mmo_hub_intro.tools_ready.text",
      "Options": [
        { "LabelKey": "dialogue.mmo_hub_intro.tools_ready.opt.turnin",
          "Do": [ { "TurnIn": "craft_starter_tools" }, { "Goto": "tools_to_gather" } ] },
        { "LabelKey": "dialogue.mmo_hub_intro.opt.quests", "Open": "Quests" }
      ],
      "IncludeOptions": ["open_menu"]
    }
  }
}

The side quests are offered through the quest list rather than as lines of their own: a "Ready": true row is the whole authoring for "this one is handed in here".

An in-game NPC dialogue panel, showing the character's line and 2-3 colored/styled option buttons (accept in green, a neutral option in steel).
An in-game NPC dialogue panel, showing the character's line and 2-3 colored/styled option buttons (accept in green, a neutral option in steel).

NPC placements

Where a character stands, which worlds they stand in, whether they appear at all, and what pressing F on them does are ONE file: Server/ZiggfreedCommon/NpcPlacements/<Id>.json. The Adventurer's Guide is an ordinary placement authored exactly this way; nothing in the engine is privileged to it.

FieldTypeDefaultDescription
EnabledbooltrueFalse despawns a standing character on the next sweep.
Identity{Role, NpcId, Aliases}optionalWho stands there - see Identity.
Where{Match, GameplayConfig, ExcludeMatch}Match ["default"]Which worlds - see Where.
Anchor{WorldSpawn, Coords, Structure, Zone, Custom}optionalWhere in the world - see Anchors.
Requires{Factors[]}alwaysWhether the character appears at all, as a bare list of factor conditions under Factors. This is a gate on the PLACEMENT, not on the player: with the named feature off, the character never appears, which is kinder than one who stands there and refuses. Spell the list Factors - anything else is an unknown key, and the gate then reads empty and the character stands there unconditionally.
Limits{SpawnChance, MaxPerWorld, OncePerWorld}optionalHow many, and how likely.
Lifecycle{KeepAlive, Respawn, Fortify, FortifyHealth}all falseUpkeep. Every knob is opt-in, because each one costs something real: a pinned chunk, a re-place, a health pool. Fortify matters for a service character, since a role's invulnerable flag does not cover a direct health write.
Interact{Dialogue} or {Open}the quest listWhat press-F opens - see Interact.
Parentstring (placement id)noneEvery leaf of every group is wired for inheritance, so a child that overrides one leaf still inherits every untouched sibling.
* required

Where: which worlds a file applies to

Every world-targeting field in the library is this same nested group, so you learn the vocabulary once.

FieldTypeDefaultDescription
Match-optionalWorld-name patterns: Foo (exact), Foo* (prefix), *Foo (suffix), *Foo* (contains), * (every world)
GameplayConfig-optionalExact match against the world's own gameplay-config key
ExcludeMatch-optionalName patterns that REJECT a world even when a positive axis matched
* required
  • A bare word under Match is an EXACT world name. "Match": ["default"] applies to the world called default and nothing else.
  • ExcludeMatch is a filter, never a complement. A Where carrying only ExcludeMatch matches nothing, because it never said which worlds it applies to.
  • Only the contains form reaches a live instance world, whose name carries a fresh random suffix each time. GameplayConfig is the sturdier route: it is authored inside the instance's own definition, survives the instance being torn down and rebuilt, and ranks above every name pattern.
  • A Where describing none of the worlds the server currently has is reported by /mmoconfig validate as MATCHES_NO_LOADED_WORLD, and one with no positive axis at all as EXCLUDE_ONLY - so a renamed main world shows up as a finding rather than as content that quietly never appears.

Anchors: where in the world

Five INDEPENDENT groups, never a placement-mode word, so "at world spawn AND beside every matching camp" needs no new schema value. Authoring several produces the UNION of their resolved positions, each an independent character.

FieldTypeDefaultDescription
WorldSpawn{Offset, Yaw}optionalThe world's own spawn point, shifted by an offset in blocks and faced by a yaw in degrees.
Coords{X, Y, Z, Yaw}optionalFixed coordinates (all three required). Only useful in a world whose layout you control.
Structure{MarkerIds, Roles, KeyContains, Offset, Yaw}optionalA worldgen spawn marker, matched by an any-of allow-list; authoring none anchors to nothing. Run /mmonpc list --arg1=structures in a world to list the marker sightings recorded there (and --arg1=markers for the marker entities live right now).
Zone{Zone, Offset, Yaw}optionalWhere a named zone was discovered.
Custom{Provider, Params}optionalWherever a registered provider says, with free-form params passed to it verbatim.
* required

Limits.MaxPerWorld counts across the union rather than per group, and Limits.OncePerWorld collapses it to the first resolved position in the fixed order WorldSpawn, Coords, Structure, Zone, Custom.

Identity: who content is talking about

Identity.Role names an NPC role, which owns everything about the character: the look, the nameplate, the press-F prompt, what they wear and hold. Identity.NpcId is what CONTENT calls that character - a quest's giver, a hand-in target, a talk step - and it is optional: unauthored, the character IS its role, so identity costs nothing to have. Two placements naming one role are one character, and a quest is offered, credited and handed in at either of them with nothing authored twice.

Author an NpcId when a standing must be a character of its own that nothing else answers to, and list Identity.Aliases for further ids it ALSO answers to. The split earns its keep: a quest bound to Mmo_Hub works at every placement listing that alias, while a step aimed at Mmo_Hub_Temple stays creditable only where that is the PRIMARY id. Aliases go one way.

Spell an id the Hytale way, and match it in your lang file

Write Guide_Wilds, not guide_wilds. Lookups are case-insensitive wherever a character is looked up, but the display key a name is read from - npcs.<NpcId>.name - is not, so a pack shipping a name key spells it exactly as the id does. That key lives in its own npcs.lang file (the filename supplies the npcs. prefix), separate from the conversation's own dialogue.* keys in mmoskilltree.lang - see Which .lang file does my key go in?

Interact: what press-F opens

Two spellings of ONE value, and you author one or the other. Dialogue is the terse form almost every talking character wants; Open names any destination.

json
"Interact": { "Dialogue": "Mmo_Hub_Intro" }
"Interact": { "Open": "Mmo_Mastery" }
"Interact": { "Open": { "Type": "Mmo_Board", "Board": "daily" } }

A placement authoring neither opens that character's quest list, which is the sensible default for a quest giver. Authoring both is reported as INTERACT_BOTH_FORMS, and the explicit Open is what runs. The conversation is automatically WITH the character standing here: the header name, any @self target, talk credit and quest conditions all read this placement's own identity, so nothing about a destination ever restates an npc id.

Under Parent, a destination is a whole leaf

A child that authors one REPLACES the one it inherited rather than merging into it. The two leaves inherit independently, so a child answering an inherited Open with the terse Dialogue spelling ends up carrying both, which the validator reports. Write "Open": { "Type": "Dialogue", "Dialogue": "..." } there instead.

Worked example: the same character in a second world

The jar's temple guide exercises Parent, a world swap, and both halves of identity in four authored lines:

Server/ZiggfreedCommon/NpcPlacements/Mmo_Hub_Forgotten_Temple.json
{
  "Name": "Mmo_Hub_Forgotten_Temple",
  "Parent": "Mmo_Hub",
  "Where": { "GameplayConfig": ["ForgottenTemple"] },
  "Identity": { "NpcId": "Mmo_Hub_Temple", "Aliases": ["Mmo_Hub"] }
}

The role, the anchor, the lifecycle knobs and the conversation are all inherited from Mmo_Hub, so re-tuning the guide in that one file re-tunes this one with it. Where swaps the world by its gameplay-config key rather than by name, because the temple is an instance whose NAME changes every time it is entered. The primary id Mmo_Hub_Temple exists nowhere else, so a "meet me in the temple" step can only be credited underground, and the quest itself needs no world condition. The alias makes every quest bound to the guide at spawn work here too.

For comparison, the parent file - the guide beside the ordinary world's spawn point - and the Mastery Trainer's placement, which the free MMO Skill Mastery Pack ships (character, role, conversation and placement all travel in the pack). The trainer anchors to a worldgen structure marker rather than to world spawn, starts a conversation, and only appears while the mastery feature is on:

Server/ZiggfreedCommon/NpcPlacements/Mmo_Hub.json (jar) and Mmo_Mastery_Trainer_Temple.json (mastery pack, abridged)
{ "Name": "Mmo_Hub",
  "Identity": { "Role": "Mmo_Hub" },
  "Where": { "Match": ["default"] },
  "Anchor": { "WorldSpawn": { "Offset": { "X": 2.5 }, "Yaw": 180.0 } },
  "Lifecycle": { "KeepAlive": true, "Respawn": true, "Fortify": true },
  "Interact": { "Dialogue": "Mmo_Hub_Intro" } }

{ "Name": "Mmo_Mastery_Trainer_Temple",
  "Identity": { "Role": "Mmo_Mastery_Trainer" },
  "Where": { "GameplayConfig": ["ForgottenTemple"] },
  "Anchor": { "Structure": {
      "MarkerIds": ["Temple_Kweebec_Merchant_Static"],
      "Offset": { "X": 3 }, "Yaw": 180.0 } },
  "Requires": { "Factors": [
      { "Factor": "mmoskilltree:feature", "Param": "mastery", "Min": 1.0 } ] },
  "Lifecycle": { "KeepAlive": true, "Respawn": true, "Fortify": true },
  "Interact": { "Dialogue": "Mmo_Mastery_Trainer" } }

Roles: what the character looks like

A placement says WHERE somebody stands and WHAT pressing F does. Everything about who they are - the model, the nameplate, the press-F prompt, the armour, the held items - lives in an ordinary Hytale NPC role file, which your pack ships at Server/NPC/Roles/Passive/<RoleId>.json (the filename is the role id). Name it from the placement with Identity.Role.

Almost always that role is a three-line native variant of a template the jar already ships, so you inherit the whole stationary-greeter behaviour and write only what makes the character yours:

Server/NPC/Roles/Passive/Mypack_Sands_Guide.json
{ "Type": "Variant",
  "Reference": "Template_Mmo_QuestGiver",
  "Modify": { "Appearance": "Human_Male_01",
              "NameTranslationKey": "npcs.Sands_Guide.name" } }

Reference is Template_Mmo_QuestGiver for a quest giver or Template_Mmo_Hub for a guide-style hub character. Modify may name ONLY the five parameters those templates declare - Appearance (a model id), NameTranslationKey, Weapons (the hotbar, first entry in hand), OffHand, DefaultOffHandSlot. Naming anything else makes the engine refuse the whole role, and the only symptom in game is a character who is never anywhere. The jar ships the roles Mmo_Hub, Mmo_Hub_Temple and Mmo_QuestGiver beside the two Template_* roles, every one a thin variant. When a variant is not enough (a different press-F prompt needs a full role body, because the engine reads a role's armour list and its hint literally), the shape to copy is Mmo_Mastery_Trainer.json, which ships in the free MMO Skill Mastery Pack - download the pack to read the file.

A different skin or size is a MODEL, not a role: ship one at Server/Models/<ModelId>.json carrying Parent plus whatever differs, and keep the minimum and maximum scale EQUAL, or every copy of your character is a different height.

Identity overlays

Most characters need no file here at all: an id defaults to the role id, lower-cased. An overlay at Server/ZiggfreedCommon/NpcIdentities/<id>.json covers the three cases the convention cannot express - a second id content was written against before the role existed, two roles that are the same person in the fiction, and a rename that should keep old content working.

json
{ "Role": "Kweebec_Elder", "NpcId": "kweebec_elder", "Aliases": ["village_elder"] }

Role names one role and is exact; Group names a native NPC group and covers every role in it at once. Author one or the other, never both in one file. The identity ladder, first answer wins: the placement's own id, then a role overlay, then a group overlay, then the role id lower-cased.

Switching a placement off, and checking it

Server owners never edit a placement file to turn one off. The owner layer is mods/ziggfreedcommon/npc-placements.json:

json
{ "mypack_sands_guide": { "enabled": false } }

"*" stops every placement at once. /mmonpc enable|disable writes that file and sweeps immediately, and a disable DESPAWNS what is already standing rather than merely declining to spawn it next time. /mmonpc list shows every placement targeting the current world with its state (standing, pending, or denied with the reason), /mmonpc reconcile forces a sweep now, /mmonpc list --arg1=structures lists the structure-marker sightings recorded in this world, /mmonpc list --arg1=markers scans for the marker entities actually live right now (the two disagreeing localizes a sighting problem), and /mmonpc list --arg1=channels lists what other mods have registered.

/mmoconfig validate runs the placement audit alongside every other content domain. It reports a placement with no identity or role (NO_IDENTITY, NO_ROLE), no usable anchor (NO_ANCHOR, INCOMPLETE_COORDS, STRUCTURE_NO_MATCHER, ZONE_NO_NAME, CUSTOM_NO_PROVIDER), both interact forms at once (INTERACT_BOTH_FORMS), a conversation nobody ships (UNKNOWN_DIALOGUE), a character with no display name (NO_DISPLAY_NAME), a spawn chance of zero (SPAWN_CHANCE_ZERO), and a factor or anchor provider nothing registered (UNREGISTERED_FACTOR, UNREGISTERED_ANCHOR_PROVIDER). Every one of those registries fails closed, so a typo surfaces at load rather than as a silently missing character.

Pack folder layout

MyGiverPack/
├── manifest.json
└── Server/
    ├── ZiggfreedCommon/
    │   ├── Dialogues/
    │   │   └── MyStudio/My_Scout.json
    │   └── NpcPlacements/
    │       └── Mypack_Sands_Guide.json
    ├── NPC/Roles/Passive/
    │   └── Mypack_Sands_Guide.json
    └── Languages/en-US/
        ├── mmoskilltree.lang
        └── npcs.lang

Zip that folder with the manifest at the zip root and drop the .zip in your mods directory - see Building the zip.

See Content Types for the full asset-type catalog and feature gating, Quests for the steps a conversation's accept and turn-in actions drive, and Placed NPCs & the Adventurer's Guide for the server owner's view of the same files.