TL;DR โ€” The 3 things you need to know

1. A node = a Schema (what it is) + a Factory (how to create it) + a CompiledNode (what it does each tick).
2. Nodes execute in 4 phases per tick: Read Inputs โ†’ Logic โ†’ Update State โ†’ Apply Outputs.
3. Data flows through wires as typed signals (digital, integer, decimal, text, item, item_id).

On this page

What is a Node?

A node (or "function block") is the basic building block in SPC programs. Players place nodes on the programming canvas, connect them with wires, and the LOGO runtime executes them every server tick (20 times per second).

Each node has three aspects:

AspectAPI TypePurpose
Schema SpcNodeSchema Static declaration: type ID, ports, parameters, phase. Defines what the node is.
Factory ISpcNodeFactory Creates compiled instances during program compilation.
Compiled Node ISpcCompiledNode Executable logic that runs every tick. Reads inputs โ†’ processes โ†’ writes outputs.
Programming Editor (Design Time) LOGO Runtime (Execution Time) SpcNodeSchema typeId ports parameters phase requiresDisplay compile ISpcNodeFactory create(ctx) creates ISpcCompiledNode execute(ctx, state) runs each tick

Node Schema

SpcNodeSchema is a Java record that declares everything about a node type. Think of it as the "blueprint" โ€” it tells the editor what ports to show, what parameters to offer, and when the node runs.

Schema fields

FieldTypeDescription
typeId String Globally unique identifier. Use namespace format: "mymod:my_node". Built-in nodes use simple IDs like "and", "or".
phase SpcExecutionPhase When this node runs within each tick (see Execution Phases).
ports List<SpcPortSpec> Input and output port declarations (see Ports).
parameters List<SpcParameterSpec> User-configurable settings (see Parameters).
requiresDisplay boolean Whether this node needs a display block in the multiblock to function.
new SpcNodeSchema(
    "mymod:temperature_sensor",     // typeId
    SpcExecutionPhase.INPUT_READ,    // phase
    List.of( /* ports */ ),              // ports
    List.of( /* params */ ),             // parameters
    false                              // requiresDisplay
);

Ports & Connections

Ports are the inputs and outputs of a node. In the editor, they appear as connection points on the node's left (inputs) and right (outputs) sides. Wires connect an output port of one node to an input port of another.

SpcPortSpec fields

FieldTypeDescription
portIdStringUnique within the node: "I1", "I2", "Q", "AQ"
directionSpcPortDirectionINPUT or OUTPUT
signalTypeSpcSignalTypeThe data type flowing through this port
requiredbooleanMust the user connect a wire to this port?

Common port naming conventions

ConventionMeaningExample
I1, I2, I3โ€ฆDigital inputsAND gate inputs
AI1, AI2โ€ฆAnalog inputsThreshold comparator
QDigital outputGate result
AQAnalog outputMath result
TrgTrigger inputTimer trigger
RReset inputCounter reset
// A node with 2 digital inputs, 1 digital output, and 1 integer output
List.of(
    new SpcPortSpec("I1",  SpcPortDirection.INPUT,  SpcSignalType.DIGITAL, false),
    new SpcPortSpec("I2",  SpcPortDirection.INPUT,  SpcSignalType.DIGITAL, false),
    new SpcPortSpec("Q",   SpcPortDirection.OUTPUT, SpcSignalType.DIGITAL, false),
    new SpcPortSpec("AQ",  SpcPortDirection.OUTPUT, SpcSignalType.INTEGER, false)
)

Inverted inputs

Players can place a negation bubble on any digital input in the editor. At compile time, your factory can check ctx.isInverted("I1") and apply ! to the value.

Signal Types & Values

Signals are the data flowing through wires between nodes. Each signal has a type and a value. The SpcSignalValue record carries both.

The six signal types

SpcSignalTypeJava TypeDescriptionDefault
DIGITALbooleanOn/off, true/falsefalse
INTEGERintWhole numbers (redstone levels, counts, etc.)0
DECIMALdoubleFloating-point values (temperatures, ratios)0.0
TEXTStringText strings (messages, labels)""
ITEMString + intItem ID + count pair"" + 0
ITEM_IDStringItem registry ID only""

Creating signal values

// Factory methods on SpcSignalValue:
SpcSignalValue.digital(true);                      // DIGITAL
SpcSignalValue.integer(42);                        // INTEGER
SpcSignalValue.decimal(3.14);                      // DECIMAL
SpcSignalValue.text("hello");                      // TEXT
SpcSignalValue.item("minecraft:diamond", 64);       // ITEM
SpcSignalValue.itemId("minecraft:diamond");         // ITEM_ID

Cross-type conversion

You can read any signal as any type using conversion methods. This is useful when different node types need to interoperate:

MethodReturnsConversion rule
.asDigital()booleanNon-zero/non-empty = true
.asInteger()intRounds decimals; parses text; true=1, false=0
.asDecimal()doubleParses text; true=1.0, false=0.0
.asText()StringConverts numbers to string; digital โ†’ "1"/"0"
.asItemId()StringExtracts item ID from ITEM/ITEM_ID/TEXT
.asItemCount()intExtracts count from ITEM; integer from INTEGER

Pre-defined constants

To avoid allocations, use the static constants for common values:

SpcSignalValue.DIGITAL_FALSE   // false
SpcSignalValue.DIGITAL_TRUE    // true
SpcSignalValue.INTEGER_ZERO    // 0
SpcSignalValue.DECIMAL_ZERO    // 0.0
SpcSignalValue.TEXT_EMPTY      // ""
SpcSignalValue.ITEM_EMPTY      // empty item
SpcSignalValue.ITEM_ID_EMPTY   // empty item ID

Execution Phases

Every server tick, the LOGO runtime processes all nodes in a fixed 4-phase order. Each node declares which phase it belongs to. This ensures deterministic execution: inputs are always read before logic runs, logic always before state updates, etc.

Every Server Tick (50 ms) onTickStart(level, pos) extensions notified Phase 1: INPUT_READ sample inputs Phase 2: LOGIC_EVALUATION AND, OR, math Phase 3: STATE_UPDATE timers, counters Phase 4: OUTPUT_APPLY write outputs Commit pending state slots finalized

Which phase should I use?

PhaseEnum ValueUse forExamples
1. Input Read INPUT_READ Reading data from the world Redstone sensors, FE meters, temperature probes, entity counters
2. Logic Evaluation LOGIC_EVALUATION Pure computation AND/OR/XOR gates, comparisons, math, multiplexers
3. State Update STATE_UPDATE Stateful nodes that change over time Timers, counters, shift registers, latches, edge detectors
4. Output Apply OUTPUT_APPLY Writing data to the world Redstone outputs, FE outputs, displays, particle effects
โš ๏ธ Choose carefully
If your sensor node runs in OUTPUT_APPLY, it reads stale data (current tick's outputs haven't been written yet by other output nodes). Sensors should always be INPUT_READ.

Parameters

Parameters are user-configurable values set in the editor's properties dialog. Unlike signals (which change every tick), parameters are set once and remain constant during program execution.

SpcParameterSpec fields

FieldTypeDescription
parameterIdStringUnique within the node: "radius", "threshold"
valueTypeSpcParameterValueTypeBOOLEAN, INTEGER, DECIMAL, or TEXT
requiredbooleanMust the user set this before running?

SpcParameterValueType enum

TypeJavaExample use
BOOLEANbooleanFeature toggle, invert mode
INTEGERintRadius, delay ticks, channel number
DECIMALdoubleThreshold, multiplier, coefficient
TEXTStringEntity type filter, block ID, message

In your factory, read parameters from the NodeCompilationContext:

int radius = ctx.intParameter("radius", 16);         // default: 16
double threshold = ctx.doubleParameter("thresh", 0.5); // default: 0.5
String filter = ctx.stringParameter("filter", "");     // default: ""
boolean invert = ctx.booleanParameter("inv", false);  // default: false

Compilation & Factories

When a player runs a program, the LOGO compiler converts the visual canvas into an executable form. For each placed node, the compiler calls the node's ISpcNodeFactory.create(NodeCompilationContext) method.

NodeCompilationContext

The context provides everything your factory needs:

MethodReturnsDescription
nodeId()UUIDThis node's unique instance ID
typeId()StringThe schema type ID
inputAddress("I1")SpcSignalAddressWhere to read an input signal from (null if unconnected)
outputAddress("Q")SpcSignalAddressWhere to write an output signal to
isInverted("I1")booleanWhether the input has a negation bubble
intParameter("id", def)intInteger parameter value with fallback
longParameter("id", def)longLong parameter value with fallback
doubleParameter("id", def)doubleDouble parameter value with fallback
stringParameter("id", def)StringString parameter value with fallback
booleanParameter("id", def)booleanBoolean parameter value with fallback
numberedInputAddresses("I", 4)List<SpcSignalAddress>I1, I2, I3, I4 addresses for multi-input nodes

SpcSignalAddress

A SpcSignalAddress is a (UUID nodeId, String portId) pair that uniquely identifies a signal slot on the runtime bus. You receive them from the compilation context and pass them to your compiled node.

Runtime State

The ISpcRuntimeState interface is the signal bus and persistent storage. Every compiled node receives it in execute(context, state).

Signal bus

Read signals from upstream nodes and write your output signals:

// Read from upstream connections
boolean input = state.readDigitalSignal(myInputAddress);
int value = state.readIntegerSignal(myInputAddress);
double decimal = state.readDecimalSignal(myInputAddress);  
SpcSignalValue raw = state.readSignalValue(myInputAddress);

// Write your outputs
state.setSignal(myOutputAddress, SpcSignalValue.digital(true));
state.setSignal(myOutputAddress, SpcSignalValue.integer(42));

Persistent slots

For stateful nodes (timers, counters, latches), use persistent slots. Values survive across ticks. Use setPending* methods โ€” values are committed after each phase completes, preventing mid-tick race conditions.

TypeReadWrite
BooleangetBooleanSlot(nodeId, key)setPendingBooleanSlot(nodeId, key, val)
IntegergetIntegerSlot(nodeId, key)setPendingIntegerSlot(nodeId, key, val)
LonggetLongSlot(nodeId, key)setPendingLongSlot(nodeId, key, val)
DoublegetDoubleSlot(nodeId, key)setPendingDoubleSlot(nodeId, key, val)
TextgetTextSlot(nodeId, key)setPendingTextSlot(nodeId, key, val)

Physical output channels

Output nodes that drive real-world I/O use numbered channels:

state.setDigitalOutput(0, true);       // digital output channel 0
state.setAnalogOutput(1, 15);          // analog channel 1, strength 15
state.setFeOutput(0, 1000);             // FE output channel 0, 1000 FE/t

Markers (global variables)

Named global values accessible by all nodes in the program:

boolean flag = state.getMarkerValue("alarm_active");
state.setPendingMarkerValue("alarm_active", true);

int level = state.getAnalogMarkerValue("temp_setpoint");
state.setPendingAnalogMarkerValue("temp_setpoint", 100);

Registries

SPC provides two registries for addon content:

SpcNodeRegistry

Register your node schemas + factories + categories. The compiler and editor query this to find addon nodes.

SpcNodeRegistry.register(schema, factory, category);

SpcContextExtensionRegistry

Register execution context extensions for custom world interactions. See Context Extensions for details.

SpcContextExtensionRegistry.register(IMyExt.class, new MyExtImpl());
๐Ÿšซ Registry freeze
Both registries are frozen during FMLCommonSetupEvent. Any registration after that throws IllegalStateException. Always register in your @Mod constructor.