index architecture stat-reference template-mode config-schema hardware-sensors build+ci | repo↗ homepage↗

template mode / TemplateEngine

hud/TemplateEngine.java. Parses free-text lines with interpolated {stat} tokens into a List<Token>, cached per-list, re-rendered every frame. This page documents the actual parser behavior, not a spec someone wrote separately from it — read it next to the source if you're modifying the grammar.

token grammar

line     := (literal | token)*
token    := "{" body "}"
body     := name (":" modifiers)?
modifiers:= modifier ("," modifier)*
modifier := decimals | "graph=" bool | "color=" hexcolor
decimals := [0-9]+          ; 0–6 inclusive
hexcolor := "#" (hex{3} | hex{6})
bool     := "true" | "false"

name is matched case-insensitively against StatRegistry.byToken() after lowercasing (String.toLowerCase(Locale.ROOT)). Whitespace around a modifier is trimmed before matching.

escaping

inputoutput
{{literal {
}}literal }
lone } (no preceding matched {)literal }, passed through as-is
{ with no closing } anywhere in the linerest of the line from that point is literal text

modifiers

modifiereffectexample
:Ndecimal places (bare integer, no key). Only valid on stats where supportsDecimals() is true.{tps:2}
graph=truerender as rolling graph instead of text. Only valid where supportsGraph() is true. Must be the line's only content in practice, though the parser itself doesn't enforce that — layout does.{fps:graph=true}
color=#RRGGBBcolors just this token, independent of the line's base color. 3-digit shorthand accepted and expanded (#5F5#55FF55).{tps:color=#55FF55}

Modifiers combine via commas: {cpu:0,color=#FF5555}. color= and graph=true on the same token is not a parse error, but functionally a graph draws its own coloring regardless — the two don't compose visually even though nothing stops you from writing it.

failure modes — every one returns null from tryParseTokenBody

A malformed token is never silently dropped. On parse failure the whole {body} — braces included — is appended back into the surrounding literal text verbatim, and a one-time-per-list warning is logged (WARNED_BAD_TOKENS, a Map<listId, Set<badBody>>, so the same typo doesn't spam the log every frame it's rendered).

inputwhy it fails
{}empty body
{tpss}unknown token name — no match in StatRegistry.byToken()
{tps:}colon present, nothing after it (modifiers.isBlank())
{tps:1,,graph=true}empty modifier between two commas
{tps:abc}bare modifier isn't a parseable integer
{tps:99}decimals outside 0–6
{entities:2}decimals given, but ENTITIES.supportsDecimals() == false
{biome:graph=true}graph requested, but BIOME.supportsGraph() == false
{tps:graph=maybe}graph= value isn't literally true/false
{tps:color=notahex}fails the hex regex [0-9a-fA-F]{3}|[0-9a-fA-F]{6}
{tps:unknownkey=1}modifier key isn't graph or color

rendering

render(tokens) — plain string concatenation, used where color doesn't matter.

renderRuns(tokens, baseColor) — the one the HUD actually uses. Walks the token list and coalesces adjacent same-colored output into ColoredRun records, so a line with no color= modifiers anywhere renders as exactly one run, not one run per token. Literal text always takes the line's base color; a stat token takes its own color() if set, else falls back to base.

renderStat(token) calls StatDefinition.rawValue(decimals) — never format() — since template lines are label-free by design (you type your own label as literal text if you want one). As a safety net against a stat forgetting to override rawValue(), it strips a leading "Label: " prefix if one slipped through (same logic MineTunerRenderer uses for classic mode's showPrefix=false).

caching

getParsedLines(cfg) joins cfg.templateLines with a NUL separator (\u0000 — chosen because it can't appear in a normal template string) and compares against the cached join for that list id. Cache hit → return the cached List<List<Token>> unchanged. Miss → reparse every line, replace the cache entry. Keyed by cfg.id, so parsing happens once per edit, not once per frame. invalidate(listId) — called on list delete — drops both the parse cache and the per-list warned-tokens set for that id.