Skip to content

Template processors: generate methods for data ​

Expression macros process function calls. A template processor handles a data declaration and adds methods. For example, it can generate load() and save() from fields.

This page first generates an answer() method returning 42. It then explains field synchronization.

When the processor runs ​

StageCompiler action
Read the template declarationCollect fields, methods, and annotations
Invoke TemplateProcessorSupply template information to Java
Receive generated contentAdd methods to the template
Compile ordinary source callsCheck and compile calls such as reward.answer()

A processor can therefore choose methods from the available fields. During play, instances execute commands already generated for those methods.

Complete processor class ​

example/macros/AnswerProcessor.java:

java
package example.macros;

import java.util.List;
import top.mcfpp.mni.api.*;

public final class AnswerProcessor implements TemplateProcessor {
    @Override
    public DeclarationEdits expand(DeclarationContext context, TemplateView template) {
        FunctionSignature<IntT> signature = FunctionSignature.of(
            List.of(), context.types().intType());
        MethodEdit answer = context.declarations().method(
            template.type(), "answer", signature,
            method -> {
                CodeBuilder code = method.builder();
                code.yield(code.intLiteral(42));
            });
        return context.declarations().edits(List.of(answer));
    }
}

Read expand through its inputs and output:

NamePurpose here
templateRead information about the current data template
contextSupply method generation and diagnostic services
signatureDescribe answer(): int
answerHold one generated method addition
DeclarationEditsDeliver the requested additions to the compiler

template.type() identifies the method owner. declarations().method(...) creates a public instance method. method.builder() supplies the body builder. yield(intLiteral(42)) supplies its result.

Associate the annotation with the processor ​

Library loading calls namespace.registerTemplateProcessor(annotationName, processorClass) to establish the association. For example, a namespace can associate Answer with AnswerProcessor.

After that preparation, callers can write:

mcfpp
@Answer
data Reward {}

func inspectReward() {
    var reward = Reward()
    print(reward.answer())
}

Calling inspectReward displays 42. The processor supplies the public answer method. Read Macro introduction for annotations and library loading.

Read fields and generate instance access ​

TemplateView supplies this information:

InterfaceContent
fields()Field names, types, writability, and source locations
fieldDeclarations()Field owners, access levels, nullability, and annotations
methods()Method names, signatures, readonly parameters, and modifiers
annotations()Template annotation names and constant arguments
source()The template declaration location

Within a generated instance method, method.self() represents the current instance. code.fieldRef(method.self(), field) selects one of its fields. assign can then write it.

Place new behavior in new methods. Existing visible methods retain their definitions. The compiler checks field access and parameter types.

Complete case: synchronize fields with scores ​

The repository library examples/mni/score-mapping uses @ScoreMapping to select a fixed holder. @Score associates each field with an objective:

mcfpp
@ScoreMapping("#scores")
data Player {
    @Score("health_hp")
    var health: int = 0

    @Score("state_alive")
    var alive: bool = false
}

func inspectPlayer() {
    var player = Player()
    player.health = 7
    player.alive = true
    player.save()

    player.health = 0
    player.alive = false
    player.load()
    print(player.health)
    print(player.alive)
}

First create the health_hp and state_alive objectives in the game with dummy criteria. Run inspectPlayer with this example library loaded. save stores the fields as scores. load reads them back, producing 7 and true.

The processor generates both methods. Integers retain their values. Booleans use 0 or 1. The Java implementation is examples/mni/score-mapping/provider/java/example/mniscore/JavaScoreMappingProcessor.java.

Run ./gradlew test --tests top.mcfpp.test.MniScoreMappingTest from the compiler repository root to check generation and field access.