State Machine and Animation Clip JSON Reference¶
Type: Reference (information-oriented, for lookup). Applies to: Minecraft 1.21.1, NeoForge 21.1.203, Parchment 2024.11.17, Java 21. The fields on this page only hold within this version range. Line numbers are locator hints only and may drift as the source changes; treat class names, method names, and test names as authoritative. Authoritative sources: element fields come from the codecs (
StateMachineDefinition.CODEC,AnimationClip.CODEC, and their sub-codecs); wrappers and discovery rules come fromStateMachineDefinitionLoader/AnimationClipLoader/ReloadOrchestrator/FsmReloadHandler/FsmClipsReloadHandler. Every rule cites its source; this page does not carry over the old conclusions from before the rewrite. Boundary: this page covers only the element format, the file wrapper, and the reload orchestration of state machine definitions and animation clips. For index files and registration-phase content files seedata-driven/schema.md; for Java API signatures seedata-driven/README.md; for content-organization practice seedata-driven/guide-content.md.
0. Two Files, Three Kinds of Readers¶
The animation state machine (FSM, finite state machine) is a Mecanim-style layered state machine: multiple layers (state layers) run in parallel, and every tick composites the pose of each layer into the model. Both the FSM and the animation clips it plays (an animation clip is a segment of bone / morph / material-frame tracks sampled along a timeline) are defined in JSON.
The reload phase (the resource re-read triggered by /reload) has exactly one orchestrator: ReloadOrchestrator (data-driven module, package lib.kasuga.registration.data_driven.reload). It reads the pack stack (not the jar), so data packs can override these files. Types are not hard-coded into it: the typeName() of each registered ReloadHandler is a valid top-level key; the modelling side registers two handlers, corresponding to two top-level keys:
| Top-level key | handler (modelling) | Decoder it delegates to | Content |
|---|---|---|---|
state_machines |
FsmReloadHandler |
StateMachineDefinitionLoader.decodeFile |
State machine definitions |
animation_clips |
FsmClipsReloadHandler |
AnimationClipLoader.decodeFile |
Animation clip elements |
Both decoders (StateMachineDefinitionLoader / AnimationClipLoader) are pure file-layer components and stay in modelling; neither of them holds a reload listener or bucket-clearing logic of its own anymore. The two ReloadHandlers are one layer wrapped around them: they claim the top-level keys, declare the glob directories, write to buckets, and run the wrap-up validation, and FsmReloadHandlerRegistrar registers them into ReloadHandlerRegistry at context startup in fsm → clips order. The whole reload cycle (clear buckets / discover / register / re-check) is owned by ReloadOrchestrator; the orchestrator itself does not know either name, state_machines or animation_clips.
1. File Locations and Discovery¶
| Content | Location | Discovery |
|---|---|---|
| State machine | data/<namespace>/state_machines/<path>.json, or any path listed by on_reload |
Directory glob + index, two entry points |
| Animation clip | Any path listed by on_reload (by convention placed under animation_clips/) |
Index only |
state_machines/has a directory glob:FsmReloadHandler.globDirectories()declaresStateMachineDefinitionLoader.PATH = "state_machines"(FsmReloadHandler.java:69-71), andReloadOrchestrator.collectGlob(:348-367) usesResourceManager.listResourcesto list that namespace's.jsonfiles in resource-location order. A file is read even if no index lists it.animation_clips/has no directory glob (AnimationClipLoader.java:29;FsmClipsReloadHandlerkeepsglobDirectories()at its default empty set). A clip file that no index lists is never read. Test:AnimationClipReloadTest.animationClipsDirectoryIsNotGlobDiscovered.- A
state_machines/file that the index lists is skipped by the glob entry point so it is read only once (collectGlob,ReloadOrchestrator.java:348-367; testReloadOrchestratorTest.globAndIndexListingTheSameFileReadItOnce).
A file path and a definition
idneed not match: theidis decided by the element's ownidfield (see §5.1, §6.1). A bare path (no colon) is given the default namespaceminecraftbyId.parse(Id.java:49-71).
2. The state_machines File Wrapper¶
2.1 The Top-Level Key state_machines¶
Every state_machines file is a JSON object in which the value of the state_machines key must be an array:
{ "state_machines": [ <definition>, ... ] }
Rules (StateMachineDefinitionLoader.decodeFile, :67-100):
- the body must be a JSON object;
- it must have the top-level key
state_machines(constantFIELD_STATE_MACHINES = "state_machines",:42); - the value of that key must be an array.
Multiple top-level keys are legal: a file may carry other top-level keys as well (such as animation_clips); this decoder reads only its own key and ignores the rest, and no longer rejects the file for that reason. Among them, keys claimed by no reload type (genuinely unknown keys) get one unsupported top-level field report from the orchestrator, but this does not affect the contribution of state_machines (see §2.4).
The three violations (non-object body, missing key, non-array value) reject the whole file, return one error, and register no definitions. The per-case wording is in decodeFile (:67-100); the missing-key diagnostic quotes the expected shape {"state_machines": [ <definition>, ... ]} verbatim (EXPECTED_SHAPE, :45).
2.2 A Legal Example¶
Below is a complete file the current loader can parse (id = example:lamp, two states, one trigger transition + one guard transition):
{
"state_machines": [
{
"id": "example:lamp",
"state_vars": [
{ "name": "lit", "type": "bool", "default": false, "ephemeral": true }
],
"layers": [
{
"id": "main",
"mode": "base",
"initial_state": "off",
"states": [
{ "id": "off" },
{ "id": "on", "pose": { "morphs": { "glow": 1.0 } } }
],
"transitions": [
{ "id": "off_to_on", "from": "off", "to": "on", "trigger_on": "lit", "cross_fade_seconds": 0.2 },
{ "id": "on_to_off", "from": "on", "to": "off", "when": ["example:not_lit"], "cross_fade_seconds": 0.2 }
]
}
]
}
]
}
trigger_on: "lit" points to a boolean variable name declared in this machine's state_vars; when: ["example:not_lit"] points to a guard function registered on the code side. If either is not registered it only warns when the machine is built and does not affect file decoding (see §5.7).
2.3 The Old Wrapper-Less Shape Is Rejected (Breaking Change)¶
The old format put the definition directly at the top level (with no state_machines wrapper). This is the legacy shape for the current implementation, and the whole file is rejected:
{
"id": "example:lamp",
"state_vars": [ { "name": "lit", "type": "bool" } ],
"layers": [ { "id": "main", "initial_state": "off" } ]
}
Writing it this way yields missing top-level key 'state_machines' and 0 registrations. The pre-rewrite fsm.md gave exactly this format; the two are directly opposite.
Executable evidence:
StateMachineDefinitionLoaderTest.legacyShapeIsRejectedAndReportsExpectedShape(:77-83): asserts that the decode result is an empty definition list and that the error text containsstate_machines;ReloadOrchestratorTest.legacyShapeRegistersNothing(:244-247): after a full reload cycle the id isnull.
2.4 Extra Top-Level Keys No Longer Reject the File¶
{
"state_machines": [
{ "id": "example:lamp", "layers": [ { "id": "main", "initial_state": "off" } ] }
],
"extra": 1
}
extra is claimed by no reload type, so the orchestrator reports one unsupported top-level field 'extra' for it (with a reverse D9 hint attached); but state_machines is still decoded and still registered as usual. The decision no longer depends on "how many top-level keys there are" but checks each unknown key one by one (ReloadOrchestrator.dispatch; ContentStructure.unknownKeys). Tests StateMachineDefinitionLoaderTest.extraTopLevelKeyDoesNotAffectThisType (:96-102), ReloadOrchestratorTest.extraTopLevelKeyIsReportedButDoesNotRejectTheFile (:265-275).
Earlier versions checked
root.size() != 1insidedecodeFileand rejected the whole file for one extra key; that check has been removed.
3. The animation_clips File Wrapper¶
The shape is symmetric, and the claimed top-level key is animation_clips (constant AnimationClipLoader.FIELD_ANIMATION_CLIPS = "animation_clips", :45):
{ "animation_clips": [ <clip>, ... ] }
The exact same three file-level violations, with the same whole-file-rejection semantics (AnimationClipLoader.decodeFile, :69-102). Tests in AnimationClipLoaderTest: rejectsANonObjectBody / rejectsAMissingTopLevelKey / ignoresAnExtraTopLevelKey / rejectsANonArrayValue.
Multiple top-level keys are legal: one file may declare both state_machines and animation_clips, and each of the two handlers claims its own key and contributes its own entries, with no effect on the other (ReloadOrchestrator.dispatch, :429-470; test AnimationClipReloadTest.mixedTopLevelKeysAreConsumedByBothHandlers). Only a genuinely unknown key (claimed by no handler) reports unsupported top-level field, and it does not block the known keys.
4. File-Level Strictness vs Element-Level Tolerance¶
Both decoders share the same failure isolation, whose boundary is file and element:
| Level | Violation | Consequence |
|---|---|---|
| File level | Non-object body / missing top-level key / non-array value | The whole file is rejected, one error, 0 registrations |
| File level | A genuinely unknown top-level key (claimed by no handler) | One unsupported top-level field report; known keys still contribute as usual (partial-apply) |
| Element level | An array element is not an object | Only that element is skipped, reporting key[i] must be an object ...; element skipped |
| Element level | The element is an object but codec decoding fails (missing required field / wrong type / unknown enum token) | Only that element is skipped, reporting key[i] failed to decode ...; element skipped; siblings carry on as usual |
| Element level | The element object has a key the codec does not name | No error; ignored |
- The dividing line is implemented by two sections of logic in
decodeFile: the shape check runs first and returns for the whole file; only after it passes are elements decoded one by one throughCODEC.parse(...).resultOrPartial(...)(StateMachineDefinitionLoader.java:88-98;AnimationClipLoader.java:90-100). - Tests for element-level tolerance:
StateMachineDefinitionLoaderTest.badElementIsSkippedWhileSiblingsDecode(:85-94, an array with a number and an object missinglayerssandwiched in it, where both good elements still decode),AnimationClipLoaderTest.skipsAMalformedElementAndKeepsItsSiblings(:104-114) andskipsAnElementWithoutAnId(:117-126). - Unknown keys inside an element are ignored:
AnimationClipLoaderTest.ignoresKeysTheClipRecordDoesNotName(:132-144). This is the biggest difference between the element level and the file level: a genuinely unknown key at the file's top level does get reported (though the file's known keys still take effect as usual), whereas one extra key inside an element object is silently ignored with no output at all. State machine elements are the same (StateMachineDefinition.CODECis also aRecordCodecBuilder), but so far only the clip side has a dedicated test for this. - An empty array is legal:
{"animation_clips": []}passes cleanly (AnimationClipLoaderTest.emptyClipArrayIsValid,:53-59).
5. state_machines Element Fields¶
The element schema is decided by StateMachineDefinition.CODEC (StateMachineDefinition.java:21-25) and is independent of the wrapper: the wrapper exists only at the file layer, so inline / script registration, content hashing, and programmatic registration are all unaffected (StateMachineDefinitionLoader class Javadoc :14-33).
5.1 Definition Top Level¶
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
id |
string (namespace:path) |
Yes | — | Machine identifier; blocks / BEs use it to bind; parsed by Id.CODEC (Id.java:112) |
state_vars |
array | No | [] |
Typed variable declarations |
layers |
array | Yes | — | Parallel state layers |
Source: StateMachineDefinition.CODEC (StateMachineDefinition.java:21-25). A missing or illegal id → that element fails to decode and is skipped.
5.2 state_vars[]¶
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
name |
string | Yes | — | Reference name within this machine; trigger_on resolves by it (this machine's declarations are checked first, then the registry) |
type |
string | No | "float" |
Built-in type token, see below |
default |
any JSON | No | That type's zero value | Default value matching the codec of type |
reference |
string | No | — | namespace:path, referencing an already-registered variable; once given, type / default are ignored |
ephemeral |
bool | No | false |
true = cleared automatically at the end of the tick, used as a trigger |
external_writable |
bool | No | true |
false = a derived parameter, writable only inside the machine |
sync |
bool | No | false |
true = goes through the FSM sync channel (server-authoritative) |
Source: StateVarDefinition.CODEC (StateVarDefinition.java:51-59); for field semantics see that class's Javadoc (:12-27).
The built-in type token catalog (StateVarType, StateVarType.java:24-28): bool / int / float / string / vec3. Three things to note:
typeanddefaultare resolved only at build time:StateVarDefinition.CODECstores them as a string / raw JSON, and an unknowntypeor a mis-decodeddefaultdoes not reject the file. Resolution happens inDefinitionStateMachineFactory.resolveInline(:124-138): an unknowntypemakesStateVarType.byTokenthrow; a faileddefaultdecode only WARNs and falls back to that type's zero value (decodeOrDefault,:140-146).- For
reference-mode resolution seeresolveReference(:112-121): a referenced id that does not exist → WARN, and that variable does not take effect. - The pre-rewrite documentation also listed
resourceas a built-in type. The currentStateVarTypecatalog has noresource, and noStateVarType.register(...)call can be found anywhere in the repository; todayresourceis called built-in only in scripting's Javadoc (AnimatorBuilderApi.java:192). See Appendix B, "To Be Confirmed".
5.3 layers[]¶
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
id |
string | Yes | — | Layer identifier |
mode |
string | No | "base" |
base (bottom layer) / additive (accumulate) / override (overwrite) |
weight |
float | No | 1.0 |
Weight of this layer |
bone_mask |
string | No | all | Bone mask (affects only the specified bone group); resolution in LayerDefinition.resolvedMask |
states |
array | No | [] |
State nodes |
transitions |
array | No | [] |
Transition edges |
initial_state |
string | Yes | — | Initial state id |
Source: LayerDefinition.CODEC (LayerDefinition.java:24-32). mode is parsed by BlendMode.CODEC (BlendMode.java:18-23), and an unknown value reports a decode error outright, causing the whole definition element to be skipped.
Layer compositing semantics (BlendMode Javadoc, BlendMode.java:10-17): base is the bottom layer and its pose is the baseline; additive accumulates on top of the baseline; override overwrites the channels selected by the mask.
The
BlendModehere (layer compositing: base / additive / override) andMorphInstance.BlendMode(color multiply-add for a single morph) are two different axes (BlendMode.java:6-9).
5.4 states[]¶
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
id |
string | Yes | — | State identifier |
duration_ticks |
int | No | none | Number of ticks the state lasts; with a when_complete transition it ends automatically |
pose |
object | No | empty pose | The static pose this state applies to the model (see §5.6) |
on_enter / on_exit / on_update |
id[] | No | [] |
Action function ids (see §5.7) |
clip |
string or object | No | none | Animation clip reference (see §5.8) |
Source: StateDefinition.CODEC (StateDefinition.java:25-33).
5.5 transitions[]¶
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
id |
string | Yes | — | Transition identifier |
from / to |
string | Yes | — | From / to state ids |
trigger_on |
string | No | none | Trigger: points to a boolean variable name / id, fires when it is set |
when_complete |
bool | No | false |
Fires when the from state reaches duration_ticks |
cross_fade_seconds |
float | No | 0 |
Cross-fade seconds; 0 = switch immediately |
when |
id[] | No | [] |
Guard function ids; the transition is allowed only if all of them are true |
on_fire |
id[] | No | [] |
Action function ids executed when the transition fires |
Source: TransitionDefinition.CODEC (TransitionDefinition.java:25-34). trigger_on requires the variable to be of boolean type; a non-ephemeral variable only WARNs and is handled with tick semantics (DefinitionStateMachineFactory.resolveTrigger, :231-248). When from / to point to a state that does not exist, that transition is skipped at build time (buildLayer, :175-180).
5.6 pose¶
pose is the generic static pose a state applies to the model: morph weights, bone transforms, material frames, and IK switches.
The same Pose is also usable through ModelPosing, and the shared runtime does not require the target model to come from any particular file format.
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
morphs |
object (string→float) | No | {} |
morph name → weight |
bones |
array | No | [] |
List of bone transforms |
frames |
array | No | [] |
List of material frames |
ik_enabled |
object (string→bool) | No | {} |
Generic IK chain name to enabled state, see Pose IK channel |
Source: PoseDefinition.CODEC. The IK channel follows the same pose blending and write-ownership rules; after driving stops, or after switching to a pose that lacks that channel, the default enabled state is restored.
bones[] (PoseDefinition.BoneDefinition.CODEC, :31-35):
| Field | Type | Required | Default |
|---|---|---|---|
name |
string | Yes | — |
transform |
object | Yes | — |
mode |
string | No | "replace" (replace / add / multiply; an unknown value falls back to replace, ApplyMode.byName, ApplyMode.java:28-38) |
transform (TransformDefinition.CODEC, TransformDefinition.java:26-30): translate / rotate / scale are all arrays of exactly 3 floats (VEC3F_CODEC, :19-22), defaulting to [0,0,0] / [0,0,0] / [1,1,1] respectively; rotate is in degrees.
frames[] (PoseDefinition.FrameDefinition.CODEC, :42-45): material (string, required), frame (int, required).
5.7 Behavior Function References¶
The ids in when / on_enter / on_exit / on_update / on_fire are registered by the code side (Java or script) through FsmFunctionLibrary: guards with registerCondition, actions with registerAction (FsmFunctionLibrary.java:19-25). JSON stores only the ids.
Unregistered functions do not affect file decoding: they are aggregated into a single WARN when the machine is built, a missing condition degrades to false, and a missing action is a no-op (DefinitionStateMachineFactory.validateDefinition Javadoc, :321-326).
5.8 clip¶
states[].clip references an already-registered animation clip, written in two ways (StateDefinition.ClipDefinition.CODEC, StateDefinition.java:47-50):
- Object:
{ "id": "ns:clip", "loop": true }, whereloopdefaults tofalse(:40-45); - String shorthand:
"ns:clip", equivalent toloop: false.
Reference resolution and dangling handling are in §9. All four states of the real fixture fan_machine_data_driven.json use the object form to reference the same clip and set loop: true.
6. animation_clips Element Fields¶
The element schema is decided by AnimationClip.CODEC (AnimationClip.java:47-54). An element carries its own id (the very id states[].clip references); the loader does not extract an id separately (AnimationClipLoader class Javadoc :20-26).
6.1 Clip Top Level¶
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
id |
string (namespace:path) |
Yes | — | Clip identifier |
duration_seconds |
float | No | 1.0 |
Clip duration (seconds) |
bones |
array | No | [] |
Bone keyframe tracks |
morphs |
array | No | [] |
morph value tracks |
frames |
array | No | [] |
Material frame tracks |
functions |
array | No | [] |
Formula tracks (evaluated every frame) |
6.2 bones[]¶
| Field | Type | Required | Default |
|---|---|---|---|
bone |
string | Yes | — |
keyframes |
array | Yes | — |
keyframes[] (Keyframe.CODEC, AnimationClip.java:71-75): time (float, required), transform (TransformDefinition, required, fields as in §5.6), easing (string, optional, default "linear").
6.3 morphs[]¶
morph (string, required) + keyframes (required); a keyframe is { "time": float, "value": float, "easing": string? } (MorphKeyframe.CODEC, :93-97).
6.4 frames[]¶
material (string, required) + keyframes (required); a keyframe is { "time": float, "frame": int } (FrameKeyframe.CODEC, :111-114). Material frame tracks round to the nearest keyframe and do not interpolate (FrameTrack Javadoc, :100).
6.5 functions[]¶
| Field | Type | Required | Default | Meaning |
|---|---|---|---|---|
bone |
string | Yes | — | Target bone |
channel |
string | Yes | — | rotate / translate / scale; an unknown value falls back to rotate (CHANNEL_CODEC, :133-145) |
x / y / z |
string | No | "" |
Per-axis formula expression; an empty string = that axis keeps its unit value (rotate 0 / translate 0 / scale 1) |
Source: FunctionTrack.CODEC (AnimationClip.java:147-153); the expressions are evaluated frame by frame at runtime in the lib.kasuga.formula namespace. The real fixture (fan_fsm_data_driven.json) has exactly these three formula tracks:
{
"animation_clips": [
{
"id": "example:fan",
"duration_seconds": 12.0,
"functions": [
{ "bone": "group", "channel": "rotate", "y": "query.angle" }
]
}
]
}
When encoding, fields whose value equals the default are omitted. In the real fixture
fan_fsm_data_driven.json, the empty axes (x/z) of the three formula tracks are not written out, which is precisely the canonical encoded shape ofAnimationClip.CODEC;FanDataDrivenClipTest.clipFileIsInCanonicalEncodedShapepins that the file is verbatim equal to the codec's encoding result.
6.6 easing Names¶
easing maps a string to a built-in easing (EASING_CODEC, AnimationClip.java:39-45). An unknown name does not error; it falls back to linear. The available names are Easing.NAMED (Easing.java:232-246):
linear, ease_in_quad, ease_out_quad, ease_in_out_quad, ease_in_cubic, ease_out_cubic, ease_in_out_cubic, ease_in_sine, ease_out_sine, ease_in_out_sine, back, elastic, bounce.
6.7 Unknown Keys Inside an Element Are Ignored¶
Keys the AnimationClip record does not name are ignored by the codec, so an author can add comment-like fields without breaking loading (AnimationClipLoader class Javadoc :24-26; test AnimationClipLoaderTest.ignoresKeysTheClipRecordDoesNotName, :132-144).
7. Differences Between the Two Discovery Paths¶
state_machines |
animation_clips |
|
|---|---|---|
| Directory glob | Yes (data/<ns>/state_machines/*.json) |
No |
Listed by the index on_reload |
Supported | The only entry point |
| When not listed in the index | Still picked up by the glob | Never read |
| Both entry points listing the same file | The glob entry skips it; read once | Not applicable |
Source: FsmReloadHandler.globDirectories() (FsmReloadHandler.java:69-71) declares StateMachineDefinitionLoader.PATH (StateMachineDefinitionLoader.java:39); the glob listing is in ReloadOrchestrator.collectGlob (:348-367); AnimationClipLoader class Javadoc (:29); ReloadOrchestrator class Javadoc (:77-91). Tests AnimationClipReloadTest.animationClipsDirectoryIsNotGlobDiscovered, ReloadOrchestratorTest.globAndIndexListingTheSameFileReadItOnce.
The glob only handles discovery; routing always follows the file's top-level key. A file that lands in the
state_machines/directory but writesanimation_clipsat the top is still handled by the clip handler (ReloadOrchestrator.collectGlobJavadoc,:340-347).
8. The Orchestrator ReloadOrchestrator and the Two Handlers¶
8.1 The Stage Order of reload()¶
reload(ResourceManager) (:186-193) wraps the private runCycle (:198-248); the order within one cycle is fixed and type-agnostic (it walks the registry rather than hard-coding the two types):
- Build the cycle: create one
HandlerCycleper handler inReloadHandlerRegistry.all()(:199-201). - Clear the buckets: call
runClear(entry)→clearBucket()for each handler, exactly once per cycle (runClear,:250-261; call sites:213-215). An exception thrown byclearBucket()is caught in place byrunClearand recorded as aDomain.RELOAD_DATAdiagnostic, while the other handlers clear their buckets as usual (a per-handler guard, the same kind as in the wrap-up stage).FsmReloadHandlerclears only the RESOURCE half ofFsmDefinitions, andFsmClipsReloadHandlerclears only the RESOURCE half ofFsmAnimationClips(script wins, see §10). - Aggregate the glob directories: collect each handler's
globDirectories()(:222-225);state_machinescontributesstate_machines/, clips contributes nothing. - List the namespaces and sort them, with dual-path discovery per namespace (
collectNamespace,:266-288): firstreadReloadPaths(:307) reads the index manifest'son_reload, thencollectGlob(:348-367) scans the aggregated glob directories (skipping paths already listed), and finallycollectIndex(:369) reads the files thaton_reloadpoints to. - Register: call
register(entry)for each handler (:507-526); glob candidates come first and index candidates after (HandlerCycle.candidates(),:595-599) → deduplicate and write to the bucket. - Wrap-up re-check: call
runAfterReload(entry)for each handler (:530-538), only for entries registered this cycle, and after all the registers.FsmReloadHandler.afterReloadruns the dangling clip check here.
The index side reuses the registration domain's JsonTreeBuilder.parseIndexManifest / resolveIndexManifests, so D6's "the same path listed in both → neither domain consumes it" also holds on the reload side (readReloadPaths, :307).
8.2 Types Are Registry-Driven, Not Hard-Coded¶
The orchestrator does not know any reload type name: the set of top-level keys is derived from ReloadHandlerRegistry.all() by knownTypeNames() (:473-486), and dispatch (dispatch, :429-470) routes by each handler's typeName(). Adding one reload type = implementing one ReloadHandler and calling ReloadHandlerRegistry.register in a @Context bean, without touching the orchestrator. This is what §4.6 means by "the reload side is symmetric with the registration phase"; the two FSM types are registered by FsmReloadHandlerRegistrar (:23-31) in fsm → clips order, and that order decides the dispatch/registration order and in turn the cross-type last-wins.
To understand how one handler's responsibilities are split, see §8.5.
8.3 last-wins and "No Side Effects for the Loser"¶
Across the two entry points, the last same-id candidate in the load sequence wins (glob first, index second; within a file, the array's writing order). The decision uses the shared DuplicateIdResolver, and the loser never enters the handler's register at all, so no override notification is triggered; each conflict logs a WARN plus one bucket entry (resolveAndReport, :545-556). Tests: ReloadOrchestratorTest.indexEntryWinsOverTheGlobEntryAndTheLoserIsNotRegistered, duplicateIdWithinFileLastWins; on the clip side AnimationClipReloadTest.laterClipFileWinsForTheSameId.
8.4 Four Layers of Exception Guards + a Fallback¶
reload() sits on the reload listener path; a thrown exception would abort the whole pack reload with the buckets half-cleared, so every failure must be turned in place into a "log + bucket" diagnostic:
| Layer | Location | Attribution target |
|---|---|---|
| Per handler (bucket clearing) | runClear :250-261 |
The library mod (KasugaLib.MODID) |
| Per file (read / decode) | readAndDispatch :405-415 |
The namespace the file lives in |
| Per namespace (discovery mechanism) | collectNamespace :266-288 |
That namespace |
| Per entry (registration) | register :507-526 |
The namespace the entry lives in |
| Per handler (wrap-up re-check) | runAfterReload :530-538 |
The library mod (when no finer attribution exists) |
| Fallback (no attribution available) | reload() :186-193 |
The library mod (KasugaLib.MODID) |
Tests ReloadOrchestratorExceptionBoundaryTest: uppercaseIndexPathIsRejectedAndTheCycleContinues, throwingFileIsIsolatedAndTheNextFileStillLoads, wholeCycleFailureIsCaughtAndRecordedAgainstTheLibraryMod. When the whole cycle fails, the buckets stay empty: the empty state is the truthful outcome and the errors stay in the bucket (class Javadoc :106-114).
8.5 How the Two Handlers Split Responsibilities¶
FsmReloadHandler |
FsmClipsReloadHandler |
|
|---|---|---|
typeName() |
state_machines |
animation_clips |
globDirectories() |
{state_machines} |
default empty = index-only |
describeFile() |
"state machine file" |
"animation clip file" |
clearBucket() |
definitions.clearResource() |
clips.clearResource() |
decode() |
delegates to StateMachineDefinitionLoader.decodeFile |
delegates to AnimationClipLoader.decodeFile |
register() |
definitions.registerResource(id, def) |
AnimationClipLoader.register(clips, clip) |
afterReload() |
runs the dangling clip check (see §9) | not overridden (default empty) |
8.6 Diagnostics Go Through the Domain/Source-Key Dimension (Domain.RELOAD_DATA)¶
ReloadOrchestrator.reportError (:566-573) and both handlers' reportError / reportWarning all call Diagnostics.report(Diagnostics.Domain.RELOAD_DATA, namespace, ...), where namespace is the namespace of the file that failed. Reload-phase diagnostics go through the domain/source-key dimension (Domain.RELOAD_DATA, key = namespace), separate from the registration-phase mod dimension; runCycle clears that dimension at the start of each cycle and does not accumulate (Diagnostics.clear(Domain.RELOAD_DATA), :208). Read them with /kasuga_data errors [mod] (which lists both dimensions); details in sections 6 and 13 of data-driven/README.md.
The reload phase also gives a reverse hint for an unknown top-level key: if those fields are in fact registration content (such as
blocks/items/registry_groups), list the file underon_registerinstead (dispatch,:439-446). The registration-type examples in the hint are derived at runtime fromTypeHandlerRegistry.
9. clip References and Dangling References¶
- Build time:
states[].clipis resolved by id inFsmAnimationClips; if it does not exist → WARN, and the state degrades to a static pose (configureState,:184). - Reload wrap-up:
FsmReloadHandler.afterReload(:95-116) runsDefinitionStateMachineFactory.unknownClipReferences(:303-323) over each definition that did land this cycle, aggregating the dangling references (layer id + state id + missing clip id) into one WARN + bucket entry. - A dangling reference is only recorded; it does not block registration, nor does it change the build-time degradation behavior (
ReloadOrchestratorclass Javadoc:100-104;FsmReloadHandler.afterReload). - A candidate already shadowed by a script definition with the same id skips the re-check; it is no longer a live entry in the bucket (
FsmReloadHandler.java:99-104). - Tests:
AnimationClipReloadTest.missingClipFileIsReportedAfterTheLoad,wrongClipIdInTheClipFileIsReported; the same rule also feeds the build-time aggregate warning (unknownClipReferencesJavadoc:289-302).
10. script-wins (Bucketing)¶
Both buckets split their entries in half by source:
| Bucket | SCRIPT entry | RESOURCE entry | Bucket clearing |
|---|---|---|---|
FsmAnimationClips |
register (FsmAnimationClips.java:53-59) |
registerResource (:66-74) |
clearResource (:100-102) drops RESOURCE only; clear() = clearResource (:117-119); clearAll (:108-110) clears everything |
FsmDefinitions |
register (FsmDefinitions.java:59-67) |
registerResource (:71-81) |
clearResource (:84-92) drops RESOURCE only |
- Write-side invariant:
register(SCRIPT) overrides RESOURCE (with a WARN, "script wins");registerResource(file) does not override SCRIPT entries. - So
AnimationClipLoader.registeralways goes throughregisterResource(:113-121): a file clip can never shadow a same-id clip registered by a script / content test / game test. - Each reload cycle's
clips.clearResource()drops only file-origin entries, so script-registered clips survive across reloads. - Tests:
AnimationClipReloadTest.scriptClipWinsOverTheFileVersion,secondCycleReplaysClipFilesWithoutLeftovers; bucket unit tests inFsmAnimationClipsTest.
11. Counterexample Roundup¶
These snippets are listed so a reader can recognize "a shape copied from the old documentation" at a glance; two of them are no longer illegal under the new contract:
- A top-level definition without a wrapper (§2.3) →
missing top-level key 'state_machines', whole file rejected. - A genuinely unknown top-level key (§2.4) → one
unsupported top-level field '...'report plus a reverse D9 hint, but known keys still contribute as usual (no longer a whole-file rejection). - The top-level key's value written as an object (such as
{"state_machines": {}}) →must be an array, whole file rejected (StateMachineDefinitionLoaderTest.nonArrayValueRejectsWholeFile;ReloadOrchestratorTest.NON_ARRAY_JSON). - One file writing both kinds of content (§3) → now legal: each of the two handlers claims its own key and contributes its own entries, 0 diagnostics.
- An element missing
layers(§4) → only that element is skipped; siblings carry on as usual.
12. Binding to Blocks¶
A data-driven registered block binds a machine id through the fsm_block / fsm_be factories: fsm_block creates the block (with a default block item), and fsm_be creates an AnimationBlockEntity, reading state_machine / model from params (FsmBlockEntityFactories.java:27-30,48-74). The state_machine string at the top level of the block entry is forwarded into the embedded BE's params.state_machine (BlockEntityTypeHandler.extractEmbedded, see schema.md §2.5).
This part belongs to registration-phase content files; for the format see the blocks and embedded block_entity chapters of data-driven/schema.md; for a working end-to-end example see data-driven/guide-content.md.
13. Further Reading¶
| What you want | Where to go |
|---|---|
| Index files, registration-phase content files, error classification | data-driven/schema.md |
| Java classes / method signatures, load ordering | data-driven/README.md |
| Building your first content file from scratch | data-driven/guide-content.md |
| Data-driven overview | data-driven/intro.md |
Custom type factories / TypeHandler |
data-driven/guide-extension.md |
Appendix A: Rule-to-Source Map¶
| Rule | Source (class:line / method / test name) |
|---|---|
The state_machines top-level key, the three file-level violations |
StateMachineDefinitionLoader.decodeFile :67-100; StateMachineDefinitionLoaderTest |
| The legacy wrapper-less shape being rejected | StateMachineDefinitionLoaderTest.legacyShapeIsRejectedAndReportsExpectedShape; ReloadOrchestratorTest.legacyShapeRegistersNothing |
The animation_clips wrapper |
AnimationClipLoader.decodeFile :69-102; AnimationClipLoaderTest |
| File-level strictness / element-level tolerance | StateMachineDefinitionLoaderTest.badElementIsSkippedWhileSiblingsDecode; AnimationClipLoaderTest.skipsAMalformedElementAndKeepsItsSiblings / skipsAnElementWithoutAnId |
| Unknown keys inside an element being ignored | AnimationClipLoaderTest.ignoresKeysTheClipRecordDoesNotName (clip side) |
| State machine element fields | StateMachineDefinition.CODEC StateMachineDefinition.java:21-25; sub-codecs StateVarDefinition / LayerDefinition / StateDefinition / TransitionDefinition / PoseDefinition / TransformDefinition |
state_vars built-in types |
StateVarType.java:24-28; build-time resolution in DefinitionStateMachineFactory.resolveInline :124-146 |
| Clip element fields | AnimationClip.CODEC AnimationClip.java:47-54 plus sub-codecs; easing Easing.java:232-246 |
| Directory glob vs index-only | ReloadOrchestrator.collectGlob :348-367; AnimationClipLoader.java:29; FsmReloadHandler.globDirectories :69-71 |
| reload stage order / dual entry points | ReloadOrchestrator.reload :186-193, runCycle :198-248, collectNamespace :266-288 |
| last-wins | ReloadOrchestrator.resolveAndReport :545-556; DuplicateIdResolver |
| Four exception guards + fallback (+ bucket-clearing guard) | ReloadOrchestrator :250-261,405-415,266-288,507-526,530-538,186-193; ReloadOrchestratorExceptionBoundaryTest |
| Diagnostics going through the domain/source-key dimension | ReloadOrchestrator.reportError :566-573; FsmReloadHandler.reportWarning :127-130; Diagnostics |
| Types being registry-driven | ReloadOrchestrator.knownTypeNames :473-486, dispatch :429-470; ReloadHandlerRegistry; FsmReloadHandlerRegistrar :23-31 |
| script-wins bucketing | FsmAnimationClips.java:53-119; FsmDefinitions.java:59-92; AnimationClipLoader.register :113-121 |
Appendix B: To Be Confirmed¶
- The
resourcetype token: it is absent from theStateVarTypecatalog, and there is noStateVarType.register(...)call anywhere in the repository; yet scripting'sAnimatorBuilderApi.java:192Javadoc lists it as one of the built-in tokens. The two contradict, so for now it is written as "not currently available". If a downstream registration exists, this needs to be filled in. - Whether unknown keys inside a state machine element are likewise silently ignored: inferred from
StateMachineDefinition.CODEC(aRecordCodecBuilder) that they should be ignored, but only the clip side has a dedicated test for that. - The JSON examples on this page were only syntax-checked (
JSON.parse) and cross-checked field by field against the codecs; the Java loader was not executed. Decoding of build-time fields such asstate_vars[].typeis outside the scope of file-layer verification.