Examples
Complete, copy-pasteable addon node examples covering the most common patterns.
Every example is self-contained and can be registered in a single @Mod class.
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.
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.
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);
}
}
}
}
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()));
}
}
}
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.
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")
);
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")
);
}
}
@Mod constructor (before registry freeze).