Validation
Minecraft 1.21.1Fabric + NeoForgeTenet 0.2.0
A problem in a quest file is cheap to make and expensive to find — a typo'd field name, a dependency that points at a quest that was renamed, a cycle nobody can unlock. Tenet's answer is to read the files before anything tries to trust them, and to say what is wrong in the format a compiler uses:
quests/first_light/first_steps/punch_a_tree.json:14:9: error: unknown field "titl" - did you mean "title"?
valid fields here: description, icon, id, titleFile, line, column, severity, message — and, for a field nobody declares, the suggestions on the same line
plus a second line naming the fields that would have worked. That is DataProblem.render() and the
message beside it, and it is what the log carries for every problem a reload finds.
Two passes, because they catch different mistakes#
The per-file validatorValidatorThe pass that reads a quest file before anything tries to decode it. It reports file, line and column, and a file that fails is skipped rather than fatal — the other files still load. runs before the codecs do. It checks structure and values: every
required field present, every field some type or the common set declares, every enum name valid, every
id well-formed, every item that exists. Codecs alone cannot do this job — they ignore a field they do not
recognise, so "titl": "Punch a Tree" produces a quest with a blank title, no error, and nothing in
any log. And a codec failure names the file but not the line, which is most of what an author needs.
Because a file with a structural error is not decoded at all, one mistake produces one message
rather than a validator complaint followed by a codec complaint about the same thing. The same holds
one level down: a broken entry in a task's or reward's conditions list is reported at the entry's
own line, and the enclosing task's codec does not repeat it — a condition whose type no build knows
is one warning listing the condition types, not two messages saying it twice.
Cross-file checks happen when the whole tree is known. A file on its own cannot know that an id is
duplicated in another file — or that it collides with another thing's aliases, which the loader claims
in the same namespace per kind — or that a dependsOn names a quest nobody wrote, or that two quests
depend on each other in a circle. Those are checked against the assembled tree, after every file has been
read.
A chapter's own gate is checked in both passes, for the same split. The per-file validator checks
the five fields' shape — a well-formed id in dependsOn and completesWhen, a known
prerequisiteMode, a minRequired inside its bounds, a boolean for hideUntilDependenciesComplete and
for the two defaultHideUntilDependencies* fields beside it — as well as the two reveal flags on a
quest, which are booleans when present and absent when the chapter decides. The cross-file pass is what
can say the thing that matters, because a chapter reference, a completes-quest and the graph they form
are only visible once every file is read:
- a
dependsOnnaming a chapter that does not exist — reported with a "did you mean", like a quest's; - a chapter waiting on itself, and
minRequiredabove the size of its own list; - a
completesWhennaming a quest that does not exist, since such a chapter never reports completed; - a dependency that asks for completed while the chapter it points at declares no
completesWhen— counted rather than reported per edge, becauseone_completedover three chapters needs one of them to be completable andminRequired: 2needs two. Each of these is an error rather than a warning: every one of them produces a chapter that can never be opened, and a chapter that never opens is the quietest failure a questline has; - a cycle in the chapter graph, printed as a chain like a quest cycle. That includes the one no single file shows: chapter A waits on chapter B while B is finished by a quest inside A.
What happens when a file is wrong#
The file is skipped, not fatal, and the scope of the skip is that file's own contents. A quest file with an error costs that quest: the chapter it sits in still opens with the rest of its list. The reload reports the tally:
/tenet reloadTwo things cost more than one file, and both are said in the log rather than left to be discovered:
- A name with nothing behind it. A
chaptersorquestsentry that resolves to no folder or file is an error against that name, not against the manifest that listed it — so a quest file deleted by hand, or a chapter folder renamed on one side only, costs exactly the one thing that is missing. It used to be reported against the manifest, and a manifest carrying an error is refused, which took the chapter — or the whole group — with it. - A manifest that cannot say what it holds. A
group.jsonorchapter.jsonthat will not parse, names noid, or has achapters/queststhat is not a list of names cannot be read at all, so its subtree does not load and the error is against that file. That is the only case where one file costs more than itself.
A root index.json is a third, and it is the one that is read around rather than skipped: one that
cannot declare the root is read as if it were not there, and the tree loads by folder name — see
The quest folder.
Ghosts, crowding and duplicates#
A quest with no tasks and no rewards is only a warning when nothing depends on it — a ghost,
almost always a quest someone started and did not finish writing. A taskless quest with a dependant
is a junction, a milestone or a chapter gate, and it loads quietly: FTB authors use empty quests
that way, and warning about them would be noise on every converted pack. Quest links and image
gates pointing at the quest do not count — only dependsOn makes a dependant — and a taskless
quest completes on the tick its gate opens, with no press on it.
Two quests at exactly the same position warn as a duplicate — one will be drawn over the other. Two titled quests in one row warn when they are closer than 64 pixels, the grid a converted pack is laid out on: below that even short titles collide, while at 64 and above the book truncates what does not fit. Pairs with no drawn titles never warn, and a gap of zero is reported once, as a duplicate rather than as crowding.
Unknown task, reward and condition types#
A type no build here registers — an addon that is not installed, or a third-party type like
eternalcurrencies:currency, questsadditions:* or quest_loot — is a warning, and the node is
kept as an unknown placeholder that carries the id. The file still loads, and the row says which mod
is missing. An unknown task can never satisfy and offers no button; an unknown reward is never
auto-granted, so an automatic path leaves it for the claim rather than marking it collected unpaid;
an unknown condition reads as unmet, so a gate it guards stays shut. An unknown field on a known
type is still an error — a typo must fail loudly, and there is no way to tell a misspelling from a
field a future build adds.
Stale translations and retired fields#
A lang/ key for a quest that was deleted, or written against the wrong id, warns rather than failing
the load — one warning per file, naming the keys and, where the spelling is close, the id that was
meant. A key written against an alias or a tag warns too: readers build their lookups from the
object's own id, so the warning names the id the key should use. Keys in no book namespace are left
alone — the overlay serves any key, and a script may own the ones no reader looks up. See
Languages for the key shapes.
A version in a per-kind file, and an id or loot_crate in a reward-table file, warn the same way:
leftovers the codec never reads, from an older schema or a converted file, which is why refusing the
file over them would be wrong. Anything else unknown is still an error. The editor schemas describe
what to write rather than what loads — so a leftover shows an editor hint as well as the load warning,
and both say the same thing.
The in-game editor refuses a write that would not validate, and reports the first problem's line, so the round trip through the book cannot produce a file the loader would reject.
Finding the problem when the message is not enough#
/tenet quest <id> prints one quest as the engine sees it — where it is, what it depends on, which
flags are set — which is what tells you whether a file loaded as written or was overridden by an
inherited default:
/tenet quest punch_a_tree/tenet progress prints the tree from the player's point of view: each quest's state, and every task
with its recorded count against what it needs. A task that shows 0/8 while the player carries eight
logs is a matching or consumption question, not a loading one.