Core Concepts Beginner
Before writing your first addon, understand how the SPC runtime works: how nodes are organized, when they execute, and how data flows between them.
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:
| Aspect | API Type | Purpose |
|---|---|---|
| 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. |
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
| Field | Type | Description |
|---|---|---|
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
| Field | Type | Description |
|---|---|---|
portId | String | Unique within the node: "I1", "I2", "Q", "AQ" |
direction | SpcPortDirection | INPUT or OUTPUT |
signalType | SpcSignalType | The data type flowing through this port |
required | boolean | Must the user connect a wire to this port? |
Common port naming conventions
| Convention | Meaning | Example |
|---|---|---|
I1, I2, I3โฆ | Digital inputs | AND gate inputs |
AI1, AI2โฆ | Analog inputs | Threshold comparator |
Q | Digital output | Gate result |
AQ | Analog output | Math result |
Trg | Trigger input | Timer trigger |
R | Reset input | Counter 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
| SpcSignalType | Java Type | Description | Default |
|---|---|---|---|
DIGITAL | boolean | On/off, true/false | false |
INTEGER | int | Whole numbers (redstone levels, counts, etc.) | 0 |
DECIMAL | double | Floating-point values (temperatures, ratios) | 0.0 |
TEXT | String | Text strings (messages, labels) | "" |
ITEM | String + int | Item ID + count pair | "" + 0 |
ITEM_ID | String | Item 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:
| Method | Returns | Conversion rule |
|---|---|---|
.asDigital() | boolean | Non-zero/non-empty = true |
.asInteger() | int | Rounds decimals; parses text; true=1, false=0 |
.asDecimal() | double | Parses text; true=1.0, false=0.0 |
.asText() | String | Converts numbers to string; digital โ "1"/"0" |
.asItemId() | String | Extracts item ID from ITEM/ITEM_ID/TEXT |
.asItemCount() | int | Extracts 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.
Which phase should I use?
| Phase | Enum Value | Use for | Examples |
|---|---|---|---|
| 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 |
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
| Field | Type | Description |
|---|---|---|
parameterId | String | Unique within the node: "radius", "threshold" |
valueType | SpcParameterValueType | BOOLEAN, INTEGER, DECIMAL, or TEXT |
required | boolean | Must the user set this before running? |
SpcParameterValueType enum
| Type | Java | Example use |
|---|---|---|
BOOLEAN | boolean | Feature toggle, invert mode |
INTEGER | int | Radius, delay ticks, channel number |
DECIMAL | double | Threshold, multiplier, coefficient |
TEXT | String | Entity 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:
| Method | Returns | Description |
|---|---|---|
nodeId() | UUID | This node's unique instance ID |
typeId() | String | The schema type ID |
inputAddress("I1") | SpcSignalAddress | Where to read an input signal from (null if unconnected) |
outputAddress("Q") | SpcSignalAddress | Where to write an output signal to |
isInverted("I1") | boolean | Whether the input has a negation bubble |
intParameter("id", def) | int | Integer parameter value with fallback |
longParameter("id", def) | long | Long parameter value with fallback |
doubleParameter("id", def) | double | Double parameter value with fallback |
stringParameter("id", def) | String | String parameter value with fallback |
booleanParameter("id", def) | boolean | Boolean 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.
| Type | Read | Write |
|---|---|---|
| Boolean | getBooleanSlot(nodeId, key) | setPendingBooleanSlot(nodeId, key, val) |
| Integer | getIntegerSlot(nodeId, key) | setPendingIntegerSlot(nodeId, key, val) |
| Long | getLongSlot(nodeId, key) | setPendingLongSlot(nodeId, key, val) |
| Double | getDoubleSlot(nodeId, key) | setPendingDoubleSlot(nodeId, key, val) |
| Text | getTextSlot(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());
FMLCommonSetupEvent.
Any registration after that throws IllegalStateException.
Always register in your @Mod constructor.