Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Functions and macros

Verified against Minecraft 26.3 · Part XIII · A macro function in #minecraft:tick is 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

classwhat it decides
ServerFunctionLibrarythe reload listener: parses every file, holds the volatile function and tag maps, and the compile-time PermissionSet
ServerFunctionManagerthe runtime face: the two tags, the source they run as, and the one empty catch
CommandFunctionthe compiled, uninstantiated function — CommandFunction.fromLines is the compiler
PlainTextFunctiona macro-free function. It is both the compiled and the instantiated form, so instantiating one allocates nothing
MacroFunctionthe other case: parameters, an eight-entry LRU, and a re-parse per miss
StringTemplatethe $(name) syntax and what a valid variable name is
InstantiatedFunctionan id and an ordered list of UnboundEntryActions. That is the entire runnable representation
FunctionInstantiationExceptioncarries 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.