Templates (KDL)
Template patches let you tweak the data MENACE ships with: unit stats, skill parameters, weapon damage, perk trees, anything stored in a DataTemplate. Edits live in templates/*.kdl files in your project. The compiler reads them at build time and emits patches the loader applies at runtime.
Templates are MENACE's authored game data. There are dozens of subtypes (EntityTemplate, UnitLeaderTemplate, WeaponTemplate, SkillTemplate, PerkTreeTemplate, and so on), each with its own field schema. Patches apply at runtime. They don't modify your resources.assets file. Removing a mod restores vanilla behaviour next launch.
Studio workflow
- Open the Template Browser pane.
- Type the template id or type in the search box. Pick the template you want to edit. The browser shows its fields and current values in the detail panel.
- Open the Scaffold dropdown in the detail panel and pick Create Patch or Create Clone. Studio generates the KDL snippet and either appends it to the template file you're currently editing, opens a picker so you can choose an existing
templates/*.kdlfile, or creates a new one. Add patch to file… and Add clone to file… in the same dropdown always go through the picker. - Studio opens the resulting KDL file in the code editor. Edit it directly to author the patch or clone.
- Compile.
File layout
templates/<any-name>.kdl
templates/subdir/<any-name>.kdlFiles can be organised any way you like. The compiler walks the entire templates/ tree recursively and parses every .kdl file. Convention-wise, naming files by the template id they patch (unit_leader-darby.kdl, weapon-rifle.kdl) keeps things scannable.
Each template id can be patched in at most one file across the project. Cloning the same id twice, or patching it in two files, is a compile-time error.
Patches
A patch block targets a specific template by type and id. Inside the block you list operations.
patch "EntityTemplate" "player_squad.darby" {
set "HudYOffsetScale" 2.0
set "AIRole" {
set "AvoidOpponents" #true
}
}The two arguments after patch are the template's subtype name and its serialised m_ID.
Clones
A clone block creates a new template by deep-copying an existing one, then registers the copy under a fresh id. Operations inside the block apply to the clone, not to the source.
clone "UnitLeaderTemplate" from="squad_leader.darby" id="squad_leader.darby_alt" {
set "InitialAttributes" index=4 0
set "InitialPerk" "perk.assassin"
}Clones run before patches at load time, so the clone's id is targetable by ref="..." in subsequent patches and by other game systems that look up templates by id.
Create
Reach for clone first
You almost never want create. A create starts from a blank template with every field at its type default, so you have to set everything the game expects and it is easy to miss one, leaving a broken or inert template. clone deep-copies a real template instead, so the new id inherits all of the source's Inspector-baked defaults and you set only what differs. Use create only when no existing template of that type is a sensible base.
A create block makes a fresh template of a given type and registers it under a new id, with no source to copy from. It takes no from=, and everything is set inside the block.
create "PerkTemplate" id="my_mod.brand_new_perk" {
// nothing is inherited: set every field the game needs
}Like clones, creates run before patches, so the new id is targetable by ref="..." in subsequent patches and by other game systems that look up templates by id.
Operations
Inside a patch or clone block, operations modify fields on the targeted template. A patch is a list of operations rather than a desired final state: set "Damage" 50 only changes Damage, and append "Perks" ref="..." only adds an entry. This keeps each modder's intent intact when patches from different mods stack (see Composition).
For the apply-order model (clones before patches, source order within a template) and how operations compose conceptually, see Concepts.
| Operation | Purpose |
|---|---|
set "<field>" <value> | Set a scalar, ref, enum, or asset field. |
set "<field>" { ... } | Descend into the object/struct at the field to edit its sub-fields in place. |
set "<field>" index=N <value> | Set element N of a collection field. |
set "<field>" index=N { ... } | Descend into element N to edit its sub-fields. The element's type is inferred. |
set "<field>" cell="r,c" <value> | Set one cell of a multi-dimensional array field. |
set "<field>" type="<X>" { ... } | Construct a fresh X for a polymorphic field (names the subtype). |
set "<field>" index=N type="<X>" { ... } | Construct a fresh X and replace polymorphic element N. |
append "<field>" <value> | Append to a collection field. |
append "<field>" type="<X>" { ... } | Construct a new X and append it. |
insert "<field>" index=N <value> | Insert into a collection field at position N. |
insert "<field>" index=N type="<X>" { ... } | Construct a new X and insert it at N. |
remove "<field>" index=N | Remove element N from a List<T> field. |
remove "<field>" <value> | Remove a matching entry from a set-style (HashSet) field. |
clear "<field>" | Empty a collection, or reset an inline object to its defaults. Composes with append ("replace the whole list") or with a descent block ("reset, then set a few fields"). |
Indexes are zero-based. append doesn't take an index= property, so use insert for positional writes. clear takes neither index nor value.
type= names a concrete subtype and only applies where there's a choice to make, a polymorphic field or list element. On a monomorphic field type= is an error: edit it in place with set "<field>" { ... }, reset it with clear, or replace a monomorphic element with remove + insert. References (GameObject, ScriptableObject, another template) are nulled with #null, not clear.
Composition across installed mods
The loader merges patches from all installed mods before applying. Mods load in lexical folder-name order, with each mod's declared dependencies loaded before it (see Load order).
setdedups by field path. Two installed mods setting the same field on the same template: later-loaded mod wins, earlier is dropped, and the loader logs anOverride template patch ...warning naming both mods.append,insert,remove,clearaccumulate. No deduplication, and all ops apply in load order. Three mods each appending a perk to the same tree leave three perks added.
Collection-style edits compose without per-mod compatibility patches. Genuine scalar conflicts become explicit warnings rather than silent overrides.
Templates registered by other loaders
A patch or clone can depend on a template id that a MelonLoader mod outside Jiangyu registers, for example a template another modding framework creates at runtime: as the template it patches, as the from= source of a clone, or as a ref="..." value, inside a constructed handler included.
Your operations on one template form a block. A block applies only when that template and every template its values refer to exist, and it then applies in order, all at once. Until then it is held and nothing in it is written, so a template is never left half-patched. A held block is looked up again on every later pass in the scene, every five seconds after those passes end for as long as anything is held, on each scene load, and once more when a new game starts or a save loads, after the hooks other mods had already installed on that entry point. Held blocks do not delay the rest of the mod, and the Template self-check line reports their operations as waiting. If a block is held when a scene's passes end, or is first held by a later pass, the loader logs Template patch '<Type>:<id>': waiting on <Type>:<id>, ... once per scene, naming what is missing, and keeps the block held. A block whose reference never appears never applies. A clone whose from= source is such an id is held the same way.
Another mod's block on the same template applies on its own. Operations you write under an ancestor type name (a WeaponTemplate addressed as BaseItemTemplate:<id>) belong to the same block. An append from a held block lands after the appends of blocks that were ready before it, and a set from a held block never overwrites the value a later-loaded mod's set put on the same field.
Within a single project, (templateType, templateId) collisions remain a hard error at compile time (see File layout).
Field paths
A field name targets a member of the template:
| Form | Meaning |
|---|---|
HudYOffsetScale | A top-level scalar field. |
InitialAttributes | A whole-collection field (with index=N for elements). |
To reach fields inside a nested object or a collection element, use a descent block (set "AIRole" { ... }) rather than a dotted path.
Descent: editing inside an object or element
When you need to edit fields inside a nested object or a collection element, wrap the inner edits in a descent block. The outer set carries no value of its own and just navigates, while inner directives operate in place on the target's own fields, and every other field is left untouched.
set "<field>" { ... }descends into the object/struct at<field>.set "<field>" index=N { ... }descends into element N of a collection.
patch "EntityTemplate" "player_squad.darby" {
// Edit two fields of the inline AIRole object; the rest stay as they are.
set "AIRole" {
set "AvoidOpponents" #true
set "SafetyScale" 2.0
}
}The descent block is purely an authoring shape: it edits the existing value in place. To build a fresh one instead, see Construction (clear, remove/insert, or type=).
Editing a polymorphic element
Editing into an element needs no type annotation, even when the collection's declared element type is an abstract base with multiple concrete subclasses (for example, SkillTemplate.EventHandlers is List<SkillEventHandlerTemplate> but the actual elements are Attack, AddSkill, ChangeProperty, etc.). The descent edits the element already at that slot, so the concrete subtype is inferred:
patch "PerkTemplate" "perk.unique_darby_high_value_targets" {
set "EventHandlers" index=0 {
set "ShowHUDText" #true
}
}The runtime reads the live element's concrete type and edits it, leaving every other field untouched. The compiler validates each inner field against the element base's concrete subtypes, so a typo on a field that exists on no subtype is still caught. To replace a polymorphic element with a freshly-constructed one instead of editing it, add type="<X>" (see Construction).
Descent blocks may nest: each inner set "<field>" index=N { ... } works the same way, descending one level further.
Value-type (struct) elements edit in place the same way, as long as the struct is blittable (its fields are value types all the way down):
patch "ShipUpgradeTemplate" "oci.faction_zayn_beecher_garage_improved_dropship_engines" {
set "Effects" index=0 {
set "DelayMods" index=0 {
set "MinDelay" 0
}
}
}The runtime reads the element (a struct copies on read), applies the inner ops, and writes the mutated copy back through the collection. A struct that carries a reference or string member is the exception: it can't round-trip reliably into the game's value-type array, so set "<field>" index=N { ... } into it is rejected. Replace the whole element with remove + insert, or edit it from C#.
Construction
Editing keeps what's there. Construction builds a fresh value. Which tool you reach for depends on whether there's a type choice to make.
type=: pick a polymorphic subtype
type="X" names the concrete subtype to build, and only applies where there's a choice, a polymorphic field or list element (an abstract base with multiple concrete subclasses), or a tagged-string field. The most common case is adding an event handler:
patch "PerkTemplate" "perk.unique_darby_high_value_targets" {
append "EventHandlers" type="AddSkill" {
set "Event" enum="AddEvent" "OnAttack"
set "SkillToAdd" ref="SkillTemplate" "effect.bleeding"
set "ShowHUDText" #true
}
}The handler name in type= (AddSkill), the fields you set (Event, SkillToAdd, ShowHUDText), and the values an enum= field accepts (OnAttack) all come from the event handler reference. It lists every built-in handler with its settable fields and their enum values, plus the conditions, value providers and filters that fill a handler's interface-typed fields, and marks the few slots with no built-in subtype, which need the C# path instead.
append "F" type="X" { ... }/insert "F" index=N type="X" { ... }: build a fresh X and add it.set "F" index=N type="X" { ... }: build a fresh X and replace polymorphic element N.set "F" type="X" { ... }: build a fresh X for a polymorphic scalar field (an Odin-routed interface such asAddSkill.Condition).
X is the subtype's short name, and it only has to be unique among the destination's own subtypes. The compiler resolves it within that family and writes the full name into the compiled output, so a class elsewhere in the game with the same short name never shadows it (ItemSlotFilter exists under both SkillFilters and ItemFilters, and set "SkillFilter" type="ItemSlotFilter" picks the skill filter). The compiler learns which classes implement an interface from the game's metadata, which jiangyu assets index records in the project's cache. Run it again after a game update, or a class the update added is not known to the compiler and a type= naming it on an interface field is rejected. When two subtypes of the same family share a short name, the compile error lists both full names and you write the one you mean. A ref="X" short name resolves the same way, by the field it is written to, and a bind="X" source name by which of its twins is a template type.
The inner set directives provide the new instance's fields, and everything else takes its type default (0, null, empty list, …). Nothing carries over, so set every field the new instance needs to function, including its trigger. A passive perk's AddSkill, for example, needs set "Event" enum="AddEvent" "OnMissionStart" or it never grants the skill. Inner directives accept the full set / append / insert / remove / clear vocabulary against the new instance.
type= is an error on a monomorphic destination: there's no subtype to pick. The compiler also errors when the named subtype isn't a subclass of the destination's element type, or when an inner field doesn't exist on it.
Monomorphic: clear and remove/insert
A monomorphic field has only one possible type, so there's nothing for type= to name. To build a fresh one:
- Object/struct field:
clear "F"resets it to defaults, then a descent block sets the fields you want:kdlclear "AIRole" set "AIRole" { set "AvoidOpponents" #true } // fresh RoleData, only AvoidOpponents changed - Collection element:
remove "F" index=Ntheninsert "F" index=N { ... }(the element analogue ofclear+set). - Append a monomorphic element:
append "F" { ... }infers the only element type, notype=needed.
clear is the mirror image of type=: it resets a monomorphic inline object, and is an error on a polymorphic one: there's no single default to pick, so reconstruct it with type="<Subtype>" instead. A reference field (a GameObject, ScriptableObject, or another template) is pointed with ref=/asset= and nulled with #null, so clear doesn't apply to it.
from=: inherit an existing element's fields
append and insert accept from="<name>" to seed the new element from a named entry already in the destination collection. The name is the element's own name, or failing that the name of its type, so a mod's own constructed elements are found by their type as well. Inner directives apply on top, so only the fields you want to change need to be listed:
patch "SoundBank" "weapons_soundbank" {
append "sounds" from="aimed_shot" {
set "id" "custom_rifle_fire"
set "name" "custom_rifle_fire"
clear "variations"
append "variations" {
set "clip" asset="weapons/custom_rifle/fire_01"
}
}
}This copies every field of the aimed_shot sound (volume, pitch, retrigger mode, distance falloff, etc.) and then overrides id, name, and variations on the copy. Without from=, every scalar field defaults to zero and the modder has to remember to set sensible playback parameters. The from= name matches the source element's name property, and the destination type doesn't need to be named explicitly when only one type is possible.
Value kinds
A value at the end of a set, append, or insert line is one of:
| Kind | Syntax | Example |
|---|---|---|
| Scalar number | bare number | set "HudYOffsetScale" 2.0 |
| Boolean | #true or #false | set "AvoidOpponents" #true |
| String | quoted string, or triple-quoted multi-line | set "InitialPerk" "perk.assassin" |
| Template reference | ref="<TemplateType>" "<templateId>" | append "PerkTrees" ref="PerkTreeTemplate" "perk_tree.tech" |
| Enum | enum="<EnumType>" "<value>" | set "Tier" enum="PerkTier" "Advanced" |
| Construction | type="<TypeName>" { ...nested set ops... } | build a polymorphic element/scalar or tagged string (see Construction) |
| Null | #null | set "CustomHead" #null (null a reference field) |
| Numeric placeholder | bind="<TemplateType>" "<templateId>" path="<field>" | append "m_Placeholders" bind="PerkTemplate" "perk.first_aid" path="Uses" |
type= names a polymorphic subtype (or a tagged-string discriminator). On a monomorphic destination it's an error.
A bare child block with no type= is a descent: it edits the existing object/struct in place (see Descent). It's also what append/insert use to build a monomorphic element, where the element type is inferred from the destination:
patch "UnitLeaderTemplate" "squad_leader.darby_clone" {
set "UnitTitle" {
set "m_DefaultTranslation" "Tactical Doll"
}
}Here UnitTitle is a monomorphic LocalizedLine, so the block edits it in place: m_DefaultTranslation changes and its other fields stay. (To wipe it first, clear "UnitTitle" then set the fields you want.)
#null clears a reference-typed scalar field (GameObject, ScriptableObject, MonoBehaviour, etc.) so the runtime falls back to its default-when-null behaviour. Useful on cloned templates where the source's field holds an overlay you don't want, for example dropping a soldier clone's CustomHead so the body mesh's own head shows through. Value-typed fields (numbers, enums, structs) reject #null at compile time.
Strings containing newlines emit as KDL v2 triple-quoted multi-line literals. The visual editor flips to a textarea when a value has any newline in it, and Studio's source view round-trips both forms without rewriting:
set "Description" """
A first line.
A second line.
A fourth, after a blank line.
"""The leading whitespace on the closing """ defines the common indent stripped from every line on re-parse. Single-line strings stay on the standard "text" form, so most patches still diff compactly.
The compiler infers numeric width (Byte, Int32, Single) from the destination field's type. For polymorphic destinations (an abstract base type with multiple concrete subclasses), specify the type explicitly via ref= or type= as appropriate.
Tagged-string polymorphic fields
Some MENACE fields store polymorphic typed values as strings of the form "DISCRIMINATOR|{json}", paired with a sibling typed field that the game rebuilds on load. ConversationTemplate's node tree and role-requirement list, and ActionConversationNode's sub-action, all use this convention.
Author them with the same type="X" op. The X is the discriminator (ACTION, SAY, HasOneTag, SetFlag, ...), which Jiangyu maps to the concrete CLR subtype via the polymorphic family's naming convention. The compiler builds the typed instance, JSON-serialises it, and prefixes the discriminator before storing the result.
clone "ConversationTemplate" from="JeanSy/click_bark" id="Voymastina/click_bark" {
set "Roles" index=0 {
set "m_SerializedRequirements" index=2 type="HasOneTag" {
set "Tags" "voymastina"
}
}
set "Nodes" {
clear "m_SerializedNodes"
append "m_SerializedNodes" type="ACTION" {
set "m_SerAction" type="SetFlag" {
set "FlagName" "click_bark_voymastina_test"
set "FlagValue" #true
}
}
append "m_SerializedNodes" type="SAY" {
set "Sound" {
set "bankId" "tactical_barks_voymastina_va"
set "itemId" "voymastina_click_bark_test"
}
set "RoleGuid" "Entity"
set "Text" "Roger."
}
append "m_SerializedNodes" type="EMPTY" {}
}
}set "Nodes" { ... } edits the cloned container in place, so clear "m_SerializedNodes" first defines a fresh node list rather than appending after the source's nodes. Drop the clear to extend the cloned conversation instead.
Recursion is automatic. The inner type="SetFlag" packs first to "SetFlag|{...}", assigns to the typed ActionConversationNode.m_SerAction, then the outer node packs to "ACTION|{"Guid":...,"m_SerAction":"SetFlag|{...}"}". The raw string form (set "F" "TYPE|{\"...\":\"...\"}") still works as an escape hatch when a discriminator isn't recognised yet or a modder pastes from a decompiled asset.
Ergonomic auto-fills
Four common omissions are filled automatically:
- Node Guids. Every
BaseConversationNodesubtype andConversationNodeContainercarries anint Guid. When omitted, the compiler emitsFNV-1a("{patchId}#node_{counter}"), stable across rebuilds and distinct from the source's Guids. Leave it to the auto-fill: the guid is also the game's runtime node id (conversation jumps, save-state), so a hand-picked value risks colliding with another node's. - Clone identity.
clone "SoundBank" id="X"impliesset "bankId" "X".clone "ConversationTemplate" id="X"impliesset "Path" "X". Skipped when the modder set the field explicitly. Stem.Sound.idfromname. AStem.Soundcomposite withset "name" "X"and noset "id"defaultsidto FNV-1a(X). Within-bank uniqueness only requires distinct names.VariationCopyCountsync.VariationConversationNode's parallelVariationCopyCountarray is padded with1s to match the number ofappend "Variations"ops. Branches without a matching copy-count entry play silently in MENACE's engine.
Symbolic role references
SAY.RoleGuid accepts a role name string instead of the int. The compiler resolves it against the source conversation's Roles[].RoleName via the asset index (rebuild with jiangyu assets index after a MENACE update so the latest Guids are recorded). For a clone, lookup follows the clone-to-source chain.
set "RoleGuid" "Entity" // resolves against source.Roles[].RoleName
set "RoleGuid" 1248015120 // literal int still worksDiscovering templates
Studio's Template Browser is the fastest way to find a template id and inspect its current values, but the same flow exists on the CLI:
jiangyu templates index
jiangyu templates list # all template subtypes
jiangyu templates list --type UnitLeaderTemplate # all ids of one subtype
jiangyu templates search darby # substring search
jiangyu templates inspect --type UnitLeaderTemplate \
--name squad_leader.darby --output texttemplates inspect --output text is the scan-friendly view. The default is pretty, and --output json is for scripting. Pass --with-mod <project-path> to inspect the effective template state after your project's clones and patches apply, before you launch MENACE.
Compile-time errors
Compile refuses the build, with a clear message, when:
- A KDL file fails to parse. The error names the file and line.
- The template id targeted by a
patchorclonedoesn't exist in the index. - A
cloneid collides with an existing template id or another clone in the project. - The same
(type, id)is patched in two different files. - A field path doesn't resolve on the target template's schema.
- A value kind doesn't match the destination field type (for example, writing a
Stringinto aSinglefield). - An
enum=value isn't a member of the named enum. - A
ref=target isn't a template of the declared type.
A field typed with an interface (ITacticalCondition, IValueProvider, a filter) takes a constructed subtype rather than a scalar, see Construction.
CLI alternative
jiangyu templates index
jiangyu templates query EntityTemplate.Properties.Accuracy
jiangyu compiletemplates query is a jq-like navigator that emits copy-pasteable KDL snippets for leaf fields.
Formatting
jiangyu templates format rewrites every *.kdl under templates/ (or a single file path) to the canonical form Studio emits when you save in the visual editor. It runs the same parse → validate → normalise → serialise pipeline as the editor:
- Strips redundant
type=/ref=attributes on monomorphic destinations. - Resolves symbolic Conversation role names into the numeric guids the game stores.
- Coerces shorthand value forms the validator accepts.
- Round-trips leading and inline
//comments alongside the code.
Use --check in CI: it prints which files would change and exits non-zero if any do, without writing.
Numeric description placeholders
Bind a localised string's $1, $2, and subsequent placeholders to numeric fields on a template. The game reads the current field when displaying the description, so patched and inherited balance values also update its text.
set "Description" {
set "m_DefaultTranslation" "Damage increased by $1%."
clear "m_Placeholders"
append "m_Placeholders" bind="SkillTemplate" "effect.example" path="EventHandlers[0].AmountMult" format="bonus-percent"
}In Studio's visual editor, choose Binding from the value type menu on a placeholder entry to supply a template, field path and number format. Choose Text for a literal placeholder.
bind= names the source template type, the positional string names its ID, and path= addresses the numeric field. Collection entries require explicit indices. Only entries in a localised string's m_Placeholders array accept bindings. Use append, insert, or set with index= as with ordinary placeholder strings. Adding a binding enables m_AllowPlaceholders. Literal strings and bindings can share an array. Clear inherited placeholders before authoring a new sequence.
| Format | Source | Displayed number |
|---|---|---|
number (default) | 5 | 5 |
percent | 0.75 | 75 |
bonus-percent | 1.08 | 8 |
reduction-percent | 0.875 | 12.5 |
magnitude | -80 | 80 |
Numbers use up to two decimal places. Put % in the description itself. There are no arithmetic expressions or method calls. The compiler checks field types and path syntax. Missing runtime fields, invalid indices and non-finite values produce a logged error and ? in the description.
Bindings keep their explicit source when a description is cloned. For a variant that reads different fields, replace its placeholder entries. Native skill-provided placeholder overrides still take precedence. GetTranslated() resolves bindings, while GetDefaultTranslation() continues to return the raw default text.