Functions and macros
Verified against Minecraft 26.3 · Part XIII · A macro function in
#minecraft:tickis reached with no arguments: it fails, silently, twenty times a second, forever — nothing logged, nothing printed, nothing counted.
Put a $-prefixed line in a function, add that function to
#minecraft:tick, and the game will call it every tick with no argument
compound at all. Instantiating it raises a
FunctionInstantiationException, ServerFunctionManager.execute catches
that one exception with an empty body, and the tick moves on. Any
other exception from the same call is logged at warn; this one is the
single case the manager was written to swallow, because the /function
command is expected to report it instead — and #minecraft:tick is not the
/function command.
That is the sharp end of a model that is otherwise very tidy: a
.mcfunction file becomes a runnable thing in two steps, and for the
overwhelming majority of functions the second step does nothing at all. A
third stage below is the hand-off — what the engine does with the result —
and it belongs to another page.
The pipeline
The figure is the whole model: which kind of function a file becomes, and where the one silent failure leaves it.
flowchart TD
F[(".mcfunction file")]:::disk --> C["1 · compile, at reload"]
C --> K{"a dollar line?"}
K -- no --> PT["PlainTextFunction"]
K -- yes --> MF["MacroFunction"]
PT -- "2 · instantiate: itself" --> R["InstantiatedFunction"]
MF -- "2 · instantiate" --> X{"arguments given, parses?"}
X -- yes --> R
X -- no --> EX["FunctionInstantiationException"]
EX --> SW["a command reports it,<br/>ServerFunctionManager swallows it"]
R --> Q["3 · queued: CallFunction opens a frame"]
Q --> E["the execution engine runs the lines"]
A function’s two steps and the hand-off — compiled once at reload, instantiated on every call, and only the macro branch can fail there; the failure is reported or swallowed depending on who made the call.
The two halves of that live in net/minecraft/commands/functions (the model)
and net/minecraft/server (the two managers), and both are server-side but for StringTemplate, which a dialog’s command template also runs on the client (dialogs).
The cast
| class | what it decides |
|---|---|
ServerFunctionLibrary | the reload listener: parses every file, holds the volatile function and tag maps, and the compile-time PermissionSet |
ServerFunctionManager | the runtime face: the two tags, the source they run as, and the one empty catch |
CommandFunction | the compiled, uninstantiated function — CommandFunction.fromLines is the compiler |
PlainTextFunction | a macro-free function. It is both the compiled and the instantiated form, so instantiating one allocates nothing |
MacroFunction | the other case: parameters, an eight-entry LRU, and a re-parse per miss |
StringTemplate | the $(name) syntax and what a valid variable name is |
InstantiatedFunction | an id and an ordered list of UnboundEntryActions. That is the entire runnable representation |
FunctionInstantiationException | carries a Component, which is what /function shows when it reports the failure |
1 · Compile, at reload
In: the raw lines of one file. Out: a CommandFunction, or nothing
at all.
CommandFunction.fromLines walks the lines. A trailing backslash joins the
next one (CommandFunction.shouldConcatenateNextLine), and a continuation
at end of file is an error; blank lines and # comments are skipped; a
leading / is a hard error with two different messages depending on whether
you wrote one slash or two; a leading $ is a macro line, kept as a template; and
anything else goes through CommandFunction.parseCommand, which produces a
BuildContexts.Unbound. So a compiled function line is literally a parsed
context chain plus its input string, waiting for a source.
CommandFunction.checkCommandLineLength caps a line at two million
characters.
A syntax error on any plain line fails the whole file, which is logged at error and then simply absent from the map. There is no partial function.
Two constraints make this stage unusual, and they are the same constraint
seen twice. ServerFunctionLibrary parses every file in parallel on the
reload’s background executor, and it does so against a source built by
Commands.createCompilationContext with a null level and a null server.
Only the last stage — collecting the parses, logging the files that failed, resolving the function tags and swapping in the maps — runs on the thread that applies the reload, and the maps are volatile, the library object being built on a background thread. An argument type that dereferenced the world during parsing would fail every function that used it — which is exactly why FunctionArgument reads an id and
defers the lookup. No argument type tests the constraint:
the four argument types whose parse reads the source at all consult only
its permissions, and those come from the dedicated server’s function-permission-level property, gamemaster by default, gamemaster always in singleplayer, and owner on the game test server.
2 · Instantiate, per call
In: a CommandFunction and an optional argument compound. Out: an
InstantiatedFunction, or a FunctionInstantiationException.
For a PlainTextFunction this step returns the very same object: nothing
is allocated, nothing is parsed, and the overwhelming majority of functions
in the world take this path.
A MacroFunction looks up each declared parameter in the argument compound,
stringifies it, and uses the ordered list of strings as a cache key over
an eight-entry LRU (MacroFunction.MAX_CACHE_ENTRIES). On a miss it
substitutes into every macro line and re-parses it, and a parse failure
there is the exception above. Note what the cache is keyed on: the values,
in parameter order — so nine distinct argument tuples cycling round will
miss every time.
Stringification is where macros surprise people, because it is not SNBT for
everything. MacroFunction has explicit cases for float and double (a
decimal format with up to fifteen fraction digits, so 1.0 becomes 1),
for byte, short and long, and for strings (the unquoted value).
Everything else — integers, compounds, lists — falls through to SNBT.
Integers merely happen to render bare; byte, short and long need their own
cases precisely because SNBT would suffix them.
StringTemplate also decides what a parameter may be called: inside $(…), letters, digits and underscore and nothing else — any letter or digit in Unicode’s basic plane counts — checked by StringTemplate.isValidVariableName at compile time.
That rule is borrowed — a dialog’s input keys obey it too, which is what lets
a dialog substitute its inputs into a command (dialogs).
Three smaller rules complete the model. One $ line anywhere makes the
whole file a macro function, though its non-macro lines keep their
already-compiled form and are never re-parsed. A $ line containing no
substitution at all is a load-time error, not a plain command. And a
macro function’s instantiated variants all share one synthetic id,
derived from the parameter names rather than the values.
Then: queued, and gone
In: an InstantiatedFunction and a source. Out: entries on the
execution queue.
CallFunction spends one cost unit, opens a frame at the next depth, and
hands the function’s line list to ContinuationTask.schedule — the same
call the fan-out over an entity selector makes. A hundred-line function
and a hundred-player fork are therefore the same shape in the queue, which
is what makes /return cheap: discarding one self-entry abandons every line
not yet materialised. Everything after this point is
the execution engine.
What calls a function, and when
ServerFunctionManager.tick runs in the first profiler zone
MinecraftServer.tickChildren opens — commandFunctions, with only the
suspension of every player’s packet flushing ahead of it — and so before the
clocks, before the time sync, before any level ticks, and long before
connections and players tick, which happen after the levels (the
server tick). It no-ops entirely when the
tick-rate manager is not running normally, so /tick freeze suspends the tick and load tags. #minecraft:load runs once after a reload or start, and
#minecraft:tick runs every tick from a list snapshotted at reload and
never consulted again, so nothing can join or leave the tick loop between
reloads.
That list is where the failure at the top of this page lives. Nothing in
ServerFunctionManager.tick carries an argument compound, so a macro
function on the tag can never be instantiated; the manager’s one empty catch
swallows the exception; and the snapshot means the function is tried again
on the next tick and on every tick after it, for as long as the pack is
loaded. There is no counter and no log line, and the way to see it is to call the same function from a command — /function, /debug function or execute if function — each of which reports what the tick tag does not.
Each function in the tick or load tag gets its own ExecutionContext — its own copy of the budget the engine spends, rather than one shared across the tag
(the execution engine).
/schedule is the one way out of the current tick, and it books its callback
into the server-wide TimerQueue that only the overworld’s clock advances
(the level tick) —
so a scheduled function fires once per tick, not once per dimension, and
stands still while the world is frozen. What belongs to functions rather than
to the clock is what ScheduleCommand refuses outright: a delay of zero, and a macro function named on its own — a tag holding one is accepted, and fails when it fires as silently as the tick tag’s.
Everything else that runs a function is a short list: /function itself, with /return run function; execute if function and /debug function, the two commands that are part of the engine (the execution engine);
AdvancementRewards, whose CacheableFunction holds the id and resolves it
once (advancements); RunFunction,
one of the EnchantmentEntityEffects
(enchantments); and
TestEnvironmentDefinition.Functions for a game test’s environment setup
(game tests).
The two permission verbs
A function body is one of the places a command source’s permission set is deliberately rewritten, and the game reaches the same answer down two differently-named routes.
ServerFunctionManager.getGameLoopSender takes the server’s own source —
which is LevelBasedPermissionSet.OWNER — and calls
CommandSourceStack.withPermission with gamemaster. That is a flat
replacement: the tick and load tags run at gamemaster.
FunctionCommand (for /function and execute if function) and DebugCommand instead call CommandSourceStack.withMaximumPermission at gamemaster, which is
PermissionSet.union — and for the level-based sets a player or the console carries, that union returns the lower of the two rather than the
higher (permissions). So an
owner’s source comes out at gamemaster on this route as well. Both verbs land on the same rung, and no route runs a function body’s source above gamemaster. Which commands the body’s lines may run was settled earlier, when they were parsed against the compile-time permissions above: Brigadier does not ask a node’s requirement again when it runs.
Where to look
CommandFunction for the compiler, and MacroFunction for the only part of
it that runs per call. ServerFunctionLibrary for what a reload does off the thread that applies it, ServerFunctionManager for the tick hook and the one empty
catch, and StringTemplate for the substitution rules a pack author will trip over.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.