On this page

Example 1 โ€” AND Gate (Pure Logic)

The simplest possible node: read two digital inputs, output their logical AND. No parameters, no state.

AND Gate I1 I2 Q digital digital digital

Schema

public static final SpcNodeSchema SCHEMA = new SpcNodeSchema(
    "mymod:and_gate",
    SpcExecutionPhase.LOGIC_EVALUATION,
    List.of(
        new SpcPortSpec("I1", SpcPortDirection.INPUT,  SpcSignalType.DIGITAL, true),
        new SpcPortSpec("I2", SpcPortDirection.INPUT,  SpcSignalType.DIGITAL, true),
        new SpcPortSpec("Q",  SpcPortDirection.OUTPUT, SpcSignalType.DIGITAL, true)
    ),
    List.of(),   // no parameters
    false         // no display
);

Compiled Node

public class AndGateNode implements ISpcCompiledNode {
    private final UUID nodeId;
    private final SpcSignalAddress in1, in2, out;

    public AndGateNode(UUID nodeId, SpcSignalAddress in1,
                       SpcSignalAddress in2, SpcSignalAddress out) {
        this.nodeId = nodeId;
        this.in1 = in1;
        this.in2 = in2;
        this.out = out;
    }

    @Override public UUID nodeId()            { return nodeId; }
    @Override public String typeId()          { return "mymod:and_gate"; }
    @Override public SpcExecutionPhase phase() { return SpcExecutionPhase.LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        boolean a = state.readDigitalSignal(in1);
        boolean b = state.readDigitalSignal(in2);
        state.setSignal(out, SpcSignalValue.digital(a && b));
    }
}

Factory + Registration

// Lambda factory โ€” no separate class needed for simple nodes
ISpcNodeFactory factory = ctx -> new AndGateNode(
    ctx.nodeId(),
    ctx.inputAddress("I1"),
    ctx.inputAddress("I2"),
    ctx.outputAddress("Q")
);

SpcNodeCategory category = new SpcNodeCategory("mymod:logic", "My Logic");
SpcNodeRegistry.register(SCHEMA, factory, category);

Example 2 โ€” Tick Counter (Stateful)

A node that counts how many ticks its input has been HIGH, using persistent integer slots. The counter resets when input goes LOW.

Tick Counter [count slot] IN COUNT integer ACTIVE digital digital

Schema

public static final SpcNodeSchema SCHEMA = new SpcNodeSchema(
    "mymod:tick_counter",
    SpcExecutionPhase.STATE_UPDATE,
    List.of(
        new SpcPortSpec("IN",     SpcPortDirection.INPUT,  SpcSignalType.DIGITAL,  true),
        new SpcPortSpec("COUNT",  SpcPortDirection.OUTPUT, SpcSignalType.INTEGER,  true),
        new SpcPortSpec("ACTIVE", SpcPortDirection.OUTPUT, SpcSignalType.DIGITAL,  false)
    ),
    List.of(),
    false
);

Compiled Node

public class TickCounterNode implements ISpcCompiledNode {
    private static final String SLOT_COUNT = "count";

    private final UUID nodeId;
    private final SpcSignalAddress inAddr, countAddr, activeAddr;

    public TickCounterNode(UUID id, SpcSignalAddress in,
                           SpcSignalAddress count, SpcSignalAddress active) {
        this.nodeId = id;
        this.inAddr = in;
        this.countAddr = count;
        this.activeAddr = active;
    }

    @Override public UUID nodeId()            { return nodeId; }
    @Override public String typeId()          { return "mymod:tick_counter"; }
    @Override public SpcExecutionPhase phase() { return SpcExecutionPhase.STATE_UPDATE; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        boolean inputHigh = state.readDigitalSignal(inAddr);

        if (inputHigh) {
            // Increment counter from persistent slot
            int current = state.getIntegerSlot(nodeId, SLOT_COUNT);
            int next = current + 1;
            state.setPendingIntegerSlot(nodeId, SLOT_COUNT, next);

            // Publish outputs
            state.setSignal(countAddr, SpcSignalValue.integer(next));
            if (activeAddr != null) {
                state.setSignal(activeAddr, SpcSignalValue.DIGITAL_TRUE);
            }
        } else {
            // Reset counter when input is LOW
            state.setPendingIntegerSlot(nodeId, SLOT_COUNT, 0);
            state.setSignal(countAddr, SpcSignalValue.INTEGER_ZERO);
            if (activeAddr != null) {
                state.setSignal(activeAddr, SpcSignalValue.DIGITAL_FALSE);
            }
        }
    }
}
Key pattern
Use getIntegerSlot() / setPendingIntegerSlot() for values that must persist across ticks. The "pending" prefix means the value is applied after the current tick completes (double buffering).

Example 3 โ€” Rain Sensor (Context Methods)

Uses ISpcExecutionContext to read world state. Outputs a digital signal indicating whether it's raining.

Schema

public static final SpcNodeSchema SCHEMA = new SpcNodeSchema(
    "mymod:rain_sensor",
    SpcExecutionPhase.INPUT_READ,
    List.of(
        new SpcPortSpec("RAIN",    SpcPortDirection.OUTPUT, SpcSignalType.DIGITAL, true),
        new SpcPortSpec("THUNDER", SpcPortDirection.OUTPUT, SpcSignalType.DIGITAL, false)
    ),
    List.of(),
    false
);

Compiled Node

public class RainSensorNode implements ISpcCompiledNode {
    private final UUID nodeId;
    private final SpcSignalAddress rainAddr, thunderAddr;

    public RainSensorNode(UUID id, SpcSignalAddress rain, SpcSignalAddress thunder) {
        this.nodeId = id;
        this.rainAddr = rain;
        this.thunderAddr = thunder;
    }

    @Override public UUID nodeId()            { return nodeId; }
    @Override public String typeId()          { return "mymod:rain_sensor"; }
    @Override public SpcExecutionPhase phase() { return SpcExecutionPhase.INPUT_READ; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        // Read world weather from context
        state.setSignal(rainAddr, SpcSignalValue.digital(ctx.isRaining()));

        if (thunderAddr != null) {
            state.setSignal(thunderAddr, SpcSignalValue.digital(ctx.isThundering()));
        }
    }
}
โ„น๏ธ Phase choice
Sensor nodes that sample world state should use INPUT_READ so logic nodes in the same tick see fresh data.

Example 4 โ€” Analog Threshold (Parameters)

Reads an integer input and compares it against a configurable threshold parameter. Outputs HIGH when the value exceeds the threshold.

Analog Threshold threshold: 50 VAL integer Q digital

Schema

public static final SpcNodeSchema SCHEMA = new SpcNodeSchema(
    "mymod:threshold",
    SpcExecutionPhase.LOGIC_EVALUATION,
    List.of(
        new SpcPortSpec("VAL", SpcPortDirection.INPUT,  SpcSignalType.INTEGER, true),
        new SpcPortSpec("Q",   SpcPortDirection.OUTPUT, SpcSignalType.DIGITAL, true)
    ),
    List.of(
        new SpcParameterSpec("threshold", SpcParameterValueType.INTEGER, true)
    ),
    false
);

Compiled Node

public class ThresholdNode implements ISpcCompiledNode {
    private final UUID nodeId;
    private final SpcSignalAddress valAddr, outAddr;
    private final int threshold;

    public ThresholdNode(UUID id, SpcSignalAddress val,
                         SpcSignalAddress out, int threshold) {
        this.nodeId = id;
        this.valAddr = val;
        this.outAddr = out;
        this.threshold = threshold;
    }

    @Override public UUID nodeId()            { return nodeId; }
    @Override public String typeId()          { return "mymod:threshold"; }
    @Override public SpcExecutionPhase phase() { return SpcExecutionPhase.LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        int value = state.readIntegerSignal(valAddr);
        state.setSignal(outAddr, SpcSignalValue.digital(value > threshold));
    }
}

Factory

// Parameters are read at compile time, not at runtime.
// intParameter() provides a safe default if parsing fails.
ISpcNodeFactory factory = ctx -> new ThresholdNode(
    ctx.nodeId(),
    ctx.inputAddress("VAL"),
    ctx.outputAddress("Q"),
    ctx.intParameter("threshold", 0)
);

Example 5 โ€” Multi-Input OR (Numbered Inputs)

A node with a variable number of digital inputs. Uses the numberedInputAddresses() helper to collect I1 through I8.

Schema

public static final SpcNodeSchema SCHEMA;
static {
    List<SpcPortSpec> ports = new ArrayList<>();
    for (int i = 1; i <= 8; i++) {
        ports.add(new SpcPortSpec(
            "I" + i, SpcPortDirection.INPUT, SpcSignalType.DIGITAL,
            i <= 2  // first two required
        ));
    }
    ports.add(new SpcPortSpec("Q", SpcPortDirection.OUTPUT, SpcSignalType.DIGITAL, true));
    SCHEMA = new SpcNodeSchema("mymod:or8", SpcExecutionPhase.LOGIC_EVALUATION,
                               ports, List.of(), false);
}

Compiled Node

public class Or8Node implements ISpcCompiledNode {
    private final UUID nodeId;
    private final List<SpcSignalAddress> inputs;
    private final SpcSignalAddress outAddr;

    public Or8Node(UUID id, List<SpcSignalAddress> inputs, SpcSignalAddress out) {
        this.nodeId = id;
        this.inputs = inputs;
        this.outAddr = out;
    }

    @Override public UUID nodeId()            { return nodeId; }
    @Override public String typeId()          { return "mymod:or8"; }
    @Override public SpcExecutionPhase phase() { return SpcExecutionPhase.LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        boolean result = false;
        for (SpcSignalAddress addr : inputs) {
            if (addr != null && state.readDigitalSignal(addr)) {
                result = true;
                break;
            }
        }
        state.setSignal(outAddr, SpcSignalValue.digital(result));
    }
}

Factory

ISpcNodeFactory factory = ctx -> new Or8Node(
    ctx.nodeId(),
    ctx.numberedInputAddresses("I", 8),  // collects I1..I8 (null if unconnected)
    ctx.outputAddress("Q")
);
numberedInputAddresses
Returns a list of length count. Unconnected ports yield null entries. Always null-check before reading.

Example 6 โ€” Message Display

When input is HIGH, publishes a parameterized text message to the machine display. Demonstrates requiresDisplay = true and the display API.

Schema

public static final SpcNodeSchema SCHEMA = new SpcNodeSchema(
    "mymod:display_msg",
    SpcExecutionPhase.OUTPUT_APPLY,
    List.of(
        new SpcPortSpec("EN", SpcPortDirection.INPUT, SpcSignalType.DIGITAL, true)
    ),
    List.of(
        new SpcParameterSpec("message", SpcParameterValueType.TEXT, true)
    ),
    true   // requiresDisplay = true
);

Compiled Node

public class DisplayMsgNode implements ISpcCompiledNode {
    private final UUID nodeId;
    private final SpcSignalAddress enAddr;
    private final String message;

    public DisplayMsgNode(UUID id, SpcSignalAddress en, String msg) {
        this.nodeId = id;
        this.enAddr = en;
        this.message = msg;
    }

    @Override public UUID nodeId()            { return nodeId; }
    @Override public String typeId()          { return "mymod:display_msg"; }
    @Override public SpcExecutionPhase phase() { return SpcExecutionPhase.OUTPUT_APPLY; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        boolean enabled = state.readDigitalSignal(enAddr);
        ctx.publishDisplayMessage(nodeId, message, enabled);
    }
}

Factory

ISpcNodeFactory factory = ctx -> new DisplayMsgNode(
    ctx.nodeId(),
    ctx.inputAddress("EN"),
    ctx.stringParameter("message", "Hello!")
);

Example 7 โ€” Cross-Mod Extension Node

Full two-part example: register a context extension AND a node that uses it. The extension tracks an external temperature; the node outputs temperature as an integer value.

Step 1 โ€” Extension interface

public interface ITemperatureContext extends ISpcExecutionContextExtension {
    int getTemperature();
}

Step 2 โ€” Extension implementation

public class TemperatureContextImpl implements ITemperatureContext {
    private int temperature = 0;

    @Override
    public void onTickStart(ServerLevel level, BlockPos machinePos) {
        // Sample biome temperature and convert to integer scale
        var biome = level.getBiome(machinePos);
        this.temperature = (int) (biome.value().getBaseTemperature() * 100);
    }

    @Override
    public int getTemperature() {
        return temperature;
    }
}

Step 3 โ€” Temperature node

public class TemperatureNode implements ISpcCompiledNode {
    private final UUID nodeId;
    private final SpcSignalAddress tempAddr;

    public TemperatureNode(UUID id, SpcSignalAddress temp) {
        this.nodeId = id;
        this.tempAddr = temp;
    }

    @Override public UUID nodeId()            { return nodeId; }
    @Override public String typeId()          { return "mymod:temperature"; }
    @Override public SpcExecutionPhase phase() { return SpcExecutionPhase.INPUT_READ; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        // Safely retrieve extension โ€” graceful fallback to 0
        int temp = ctx.getExtension(ITemperatureContext.class)
                      .map(ITemperatureContext::getTemperature)
                      .orElse(0);
        state.setSignal(tempAddr, SpcSignalValue.integer(temp));
    }
}

Step 4 โ€” Registration (all in @Mod constructor)

@Mod("mymod")
public class MyMod {
    public MyMod() {
        // 1. Register context extension
        SpcContextExtensionRegistry.register(
            ITemperatureContext.class,
            new TemperatureContextImpl()
        );

        // 2. Define schema
        SpcNodeSchema tempSchema = new SpcNodeSchema(
            "mymod:temperature",
            SpcExecutionPhase.INPUT_READ,
            List.of(
                new SpcPortSpec("TEMP", SpcPortDirection.OUTPUT,
                                SpcSignalType.INTEGER, true)
            ),
            List.of(),
            false
        );

        // 3. Register node
        SpcNodeRegistry.register(
            tempSchema,
            ctx -> new TemperatureNode(ctx.nodeId(), ctx.outputAddress("TEMP")),
            new SpcNodeCategory("mymod:environment", "Environment")
        );
    }
}
Registration order
Register context extensions before registering nodes that depend on them. Both must happen in the @Mod constructor (before registry freeze).