Signal Processing Intermediate
Patterns for converting, combining, filtering, and transforming signals. Complete examples for the most common signal processing tasks addon developers encounter.
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.
Reading Signals
Every typed read method returns a sensible default if the signal isn't set:
| Method | Returns | Default if not set |
|---|---|---|
state.readDigitalSignal(addr) | boolean | false |
state.readIntegerSignal(addr) | int | 0 |
state.readDecimalSignal(addr) | double | 0.0 |
state.readTextSignal(addr) | String | "" |
state.readSignalValue(addr) | SpcSignalValue | ITEM_EMPTY |
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 \ To | asDigital() | asInteger() | asDecimal() | asText() |
|---|---|---|---|---|
| DIGITAL | value | true→1, false→0 | true→1.0, false→0.0 | "true" / "false" |
| INTEGER | ≠0 → true | value | (double) value | String.valueOf |
| DECIMAL | ≠0.0 → true | Math.round | value | String.valueOf |
| TEXT | !blank → true | parseInt or 0 | parseDouble or 0.0 | value |
// 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).
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.
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.
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.
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.
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.
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));
}
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;
isInverted() and flip the signal. If you don't handle it,
the negation bubble is visual-only and has no effect.