TL;DR

Read signals with state.read*Signal(addr), transform them in execute(), and write results with state.setSignal(addr, SpcSignalValue.*(value)). Use SpcSignalValue factory methods to create typed values.

On This Page

Reading Signals

Every typed read method returns a sensible default if the signal isn't set:

MethodReturnsDefault if not set
state.readDigitalSignal(addr)booleanfalse
state.readIntegerSignal(addr)int0
state.readDecimalSignal(addr)double0.0
state.readTextSignal(addr)String""
state.readSignalValue(addr)SpcSignalValueITEM_EMPTY
💡 Use the typed methods
Prefer readIntegerSignal() over readSignalValue().asInteger(). The typed methods are clearer and let the runtime optimize.

Writing Signals

Always use SpcSignalValue factory methods — never call the record constructor directly:

// ✅ Correct — use factory methods:
state.setSignal(addr, SpcSignalValue.digital(true));
state.setSignal(addr, SpcSignalValue.integer(42));
state.setSignal(addr, SpcSignalValue.decimal(3.14));
state.setSignal(addr, SpcSignalValue.text("hello"));
state.setSignal(addr, SpcSignalValue.item("minecraft:diamond", 64));
state.setSignal(addr, SpcSignalValue.itemId("minecraft:diamond"));

Pre-defined constants

For zero/empty values, use the constants — they avoid allocations:

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

Type Conversion

SpcSignalValue provides methods to convert between types. These follow common-sense rules:

From \ ToasDigital()asInteger()asDecimal()asText()
DIGITALvaluetrue→1, false→0true→1.0, false→0.0"true" / "false"
INTEGER≠0 → truevalue(double) valueString.valueOf
DECIMAL≠0.0 → trueMath.roundvalueString.valueOf
TEXT!blank → trueparseInt or 0parseDouble or 0.0value
// Reading a signal as a different type than it was written:
SpcSignalValue signal = state.readSignalValue(addr);

boolean b = signal.asDigital();   // works for any source type
int     i = signal.asInteger();   // decimal → rounded, text → parsed
double  d = signal.asDecimal();   // integer → promoted, text → parsed
String  s = signal.asText();      // everything has a string form

Example: Scaler (Multiply + Offset)

Transforms a decimal signal: output = input × gain + offset. Useful for unit conversions (e.g., RPM to rad/s, Celsius to Fahrenheit).

What this produces in-game
A math node with a decimal input, decimal output, and two parameters (gain, offset). To convert Celsius to Fahrenheit: set gain=1.8, offset=32.
public static final SpcNodeSchema SCALER = new SpcNodeSchema(
    "mymod:scaler",
    SpcExecutionPhase.LOGIC_EVALUATION,
    List.of(
        new SpcPortSpec("I", INPUT,  DECIMAL, true),
        new SpcPortSpec("Q", OUTPUT, DECIMAL, true)
    ),
    List.of(
        new SpcParameterSpec("gain",   DECIMAL, false),
        new SpcParameterSpec("offset", DECIMAL, false)
    ),
    false
);

public record ScalerNode(
    UUID nodeId, SpcSignalAddress in, SpcSignalAddress out,
    double gain, double offset
) implements ISpcCompiledNode {

    @Override public String typeId() { return "mymod:scaler"; }
    @Override public SpcExecutionPhase phase() { return LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        double value = state.readDecimalSignal(in);
        double result = value * gain + offset;
        state.setSignal(out, SpcSignalValue.decimal(result));
    }
}

// Factory:
ctx -> new ScalerNode(ctx.nodeId(), ctx.inputAddress("I"),
    ctx.outputAddress("Q"), ctx.doubleParameter("gain", 1.0),
    ctx.doubleParameter("offset", 0.0))

Example: Value Clamp

Restricts an integer value to a min–max range. Values below min are raised to min; values above max are lowered to max.

What this produces in-game
Feed any integer signal in, get a clamped value out. Set min=0 and max=15 to keep values in redstone range. Set min=0 and max=100 for percentage clamping.
public record ClampNode(
    UUID nodeId, SpcSignalAddress in, SpcSignalAddress out,
    int min, int max
) implements ISpcCompiledNode {

    @Override public String typeId() { return "mymod:clamp"; }
    @Override public SpcExecutionPhase phase() { return LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        int value = state.readIntegerSignal(in);
        int clamped = Math.max(min, Math.min(max, value));
        state.setSignal(out, SpcSignalValue.integer(clamped));
    }
}

Example: Signal Multiplexer

Selects one of two integer inputs based on a digital selector signal. When selector is LOW → output = A. When selector is HIGH → output = B.

What this produces in-game
A signal switch controlled by a digital signal. Wire two different sensor values to A and B, wire a toggle to SEL, and the output switches between them.
public static final SpcNodeSchema MUX = new SpcNodeSchema(
    "mymod:mux",
    SpcExecutionPhase.LOGIC_EVALUATION,
    List.of(
        new SpcPortSpec("A",   INPUT,  INTEGER, true),
        new SpcPortSpec("B",   INPUT,  INTEGER, true),
        new SpcPortSpec("SEL", INPUT,  DIGITAL, true),
        new SpcPortSpec("Q",   OUTPUT, INTEGER, true)
    ),
    List.of(),
    false
);

public record MuxNode(
    UUID nodeId,
    SpcSignalAddress a, SpcSignalAddress b,
    SpcSignalAddress sel, SpcSignalAddress out
) implements ISpcCompiledNode {

    @Override public String typeId() { return "mymod:mux"; }
    @Override public SpcExecutionPhase phase() { return LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        boolean select = state.readDigitalSignal(sel);
        int value = select
            ? state.readIntegerSignal(b)
            : state.readIntegerSignal(a);
        state.setSignal(out, SpcSignalValue.integer(value));
    }
}

Example: Hysteresis Threshold

Outputs HIGH when input exceeds an upper threshold, LOW when it drops below a lower threshold. Prevents rapid on/off cycling when a signal hovers near a threshold.

What this produces in-game
A smart comparator. Set upper=80 and lower=20: output turns on at 80, stays on until the value drops below 20, then turns off. No flickering even if the value bounces around 50.
public record HysteresisNode(
    UUID nodeId,
    SpcSignalAddress in, SpcSignalAddress out,
    int upper, int lower
) implements ISpcCompiledNode {

    @Override public String typeId() { return "mymod:hysteresis"; }
    @Override public SpcExecutionPhase phase() { return STATE_UPDATE; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        int value = state.readIntegerSignal(in);
        boolean wasOn = state.getBooleanSlot(nodeId, "on");

        boolean isOn;
        if (wasOn) {
            isOn = value >= lower;   // stay on until below lower
        } else {
            isOn = value >= upper;   // turn on at upper
        }

        state.setPendingBooleanSlot(nodeId, "on", isOn);
        state.setSignal(out, SpcSignalValue.digital(isOn));
    }
}

Example: Number Formatter

Converts an integer signal to a formatted text signal with a prefix and suffix. Useful for display nodes.

What this produces in-game
Turn the number 42 into the text "Temperature: 42 °C". Wire the output to a display node for human-readable labels on your LOGO machine.
public record FormatterNode(
    UUID nodeId,
    SpcSignalAddress in, SpcSignalAddress out,
    String prefix, String suffix
) implements ISpcCompiledNode {

    @Override public String typeId() { return "mymod:formatter"; }
    @Override public SpcExecutionPhase phase() { return LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        int value = state.readIntegerSignal(in);
        String formatted = prefix + value + suffix;
        state.setSignal(out, SpcSignalValue.text(formatted));
    }
}

// Factory: prefix defaults to "", suffix defaults to ""
ctx -> new FormatterNode(ctx.nodeId(), ctx.inputAddress("I"),
    ctx.outputAddress("Q"),
    ctx.stringParameter("prefix", ""),
    ctx.stringParameter("suffix", ""))

Example: Item Signal Builder

Combines a text signal (item ID) and an integer signal (count) into a single ITEM signal.

What this produces in-game
Feed an item ID string from one node and a count from another. The output is a single ITEM signal that downstream nodes can use for inventory checks or crafting logic.
public static final SpcNodeSchema ITEM_BUILDER = new SpcNodeSchema(
    "mymod:item_builder",
    SpcExecutionPhase.LOGIC_EVALUATION,
    List.of(
        new SpcPortSpec("ID",    INPUT,  TEXT,    true),
        new SpcPortSpec("COUNT", INPUT,  INTEGER, true),
        new SpcPortSpec("Q",     OUTPUT, ITEM,    true)
    ),
    List.of(),
    false
);

public record ItemBuilderNode(
    UUID nodeId,
    SpcSignalAddress idIn, SpcSignalAddress countIn,
    SpcSignalAddress out
) implements ISpcCompiledNode {

    @Override public String typeId() { return "mymod:item_builder"; }
    @Override public SpcExecutionPhase phase() { return LOGIC_EVALUATION; }

    @Override
    public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
        String itemId = state.readTextSignal(idIn);
        int count = state.readIntegerSignal(countIn);
        state.setSignal(out, SpcSignalValue.item(itemId, count));
    }
}

Null Safety for Optional Ports

When a port is marked required: false, the player can leave it unconnected. In that case, ctx.inputAddress("portId") returns null. You must check for this:

@Override
public void execute(ISpcExecutionContext ctx, ISpcRuntimeState state) {
    // Required input — always safe to read directly
    int mainValue = state.readIntegerSignal(mainInput);

    // Optional input — check for null first
    int offset = 0;
    if (optionalInput != null) {
        offset = state.readIntegerSignal(optionalInput);
    }

    state.setSignal(out, SpcSignalValue.integer(mainValue + offset));
}
⚠ NullPointerException trap
Calling state.readDigitalSignal(null) will crash. Always guard optional inputs with a null check. This is the #1 addon bug.

Handling Negation Bubbles

Players can add a negation bubble to any digital input in the editor. If you want your node to respect this, read the inversion in the factory and bake it in:

// In the factory:
boolean invertI1 = ctx.isInverted("I1");

// In the compiled node's execute():
boolean raw = state.readDigitalSignal(i1Addr);
boolean effective = invertI1 ? !raw : raw;
💡 Inversion is opt-in
The runtime doesn't automatically apply negation — your node must read isInverted() and flip the signal. If you don't handle it, the negation bubble is visual-only and has no effect.