Frequently Asked Questions
Common questions and answers about SPC Addon API development.
Questions
- When should I register nodes?
- Can I access the Level / World object?
- What happens with unconnected optional ports?
- How do inverted inputs work?
- How do I persist data across ticks?
- Why "setPending" instead of "set" for state slots?
- What if SPC is not installed?
- Will my addon break saves?
- How do I handle API version changes?
- How does signal type conversion work?
- Can I register nodes in different categories?
- How do I debug my nodes?
- What if my context extension isn't available?
- Which execution phase should I use?
When should I register nodes?
Register in your @Mod constructor. Both SpcNodeRegistry and
SpcContextExtensionRegistry freeze after mod loading completes.
Any registration after the freeze throws an exception.
@Mod("mymod")
public class MyMod {
public MyMod() {
// โ
Register here โ before freeze
SpcNodeRegistry.register(schema, factory, category);
}
}
FMLCommonSetupEvent or other deferred events fire too late.
Always use the constructor.
Can I access the Level / World object?
Not directly โ and that's by design. Nodes execute in a sandboxed context.
Use the methods on ISpcExecutionContext for world interaction:
isRaining(),isThundering()โ weathergetLightLevel(x, y, z)โ light at offsetworldTimeTicks(),isDaytime()โ timeisBlockAt(x, y, z, blockId)โ block checkscountEntitiesInRadius(radius, type)โ entity countsgetBiomeId(),getMoonPhase()โ biome/sky
If you need access not covered by these methods, create a
context extension that samples the world in
onTickStart() (which does receive the ServerLevel).
What happens with unconnected optional ports?
inputAddress(portId) returns null for unconnected ports. Always null-check:
SpcSignalAddress addr = ctx.inputAddress("I3");
if (addr != null) {
boolean val = state.readDigitalSignal(addr);
// use val...
}
For numberedInputAddresses(prefix, count), unconnected entries are null in the list.
How do inverted inputs work?
The editor allows users to place negation bubbles on inputs (like LOGO! Soft Comfort). Check inversions in your factory or node:
// In factory:
boolean inv1 = ctx.isInverted("I1");
// In execute():
boolean raw = state.readDigitalSignal(in1Addr);
boolean val = invertI1 ? !raw : raw;
Inversions are typically baked into the compiled node at compile time (read once in the factory, stored as a field).
How do I persist data across ticks?
Use the persistent slot methods on ISpcRuntimeState.
Available types: Boolean, Integer, Long, Double, Text.
// Read current value
int counter = state.getIntegerSlot(nodeId, "counter");
// Write new value (applied after tick)
state.setPendingIntegerSlot(nodeId, "counter", counter + 1);
For a complete guide with 5 examples (timers, counters, toggles), see State & Persistence.
nodeId + slotName).
Use descriptive names like "counter", "lastValue", "active".
Why "setPending" instead of "set" for state slots?
SPC uses double buffering: writes go to a pending buffer that is applied after all nodes in the current tick have executed. This prevents execution-order dependencies โ every node in the same tick sees the same state.
// During execute():
state.getIntegerSlot(id, "x"); // reads from CURRENT buffer
state.setPendingIntegerSlot(id, "x", 42); // writes to PENDING buffer
// After tick completes: pending โ current
What if SPC is not installed?
Your addon should declare SPC as a required dependency in neoforge.mods.toml.
NeoForge will prevent loading your mod without SPC present.
[[dependencies.mymod]]
modId = "spc_core"
type = "required"
versionRange = "[0.2.0,)"
ordering = "AFTER"
side = "BOTH"
If you want an optional soft dependency, use type = "optional" and guard
all SPC API calls behind a class-existence check.
Will my addon break saves?
SPC identifies nodes by their typeId string. As long as you:
- Never change a registered
typeId - Only add new optional ports (don't remove or rename existing ones)
- Keep parameter IDs stable
Existing programs will continue to load. Removed nodes will show as "unknown" in the editor.
How do I handle API version changes?
Check SpcApi.API_VERSION at startup if needed:
if (SpcApi.API_VERSION < 1) {
// Unsupported โ log warning and skip registration
}
The API version increments only on breaking changes. Minor additions (new context methods, new signal types) are backward compatible.
How does signal type conversion work?
SpcSignalValue provides asDigital(), asInteger(),
asDecimal(), and asText() for safe cross-type conversion:
| Source | asDigital() | asInteger() | asDecimal() | asText() |
|---|---|---|---|---|
| DIGITAL true | true | 1 | 1.0 | "1" |
| DIGITAL false | false | 0 | 0.0 | "0" |
| INTEGER 42 | true | 42 | 42.0 | "42" |
| INTEGER 0 | false | 0 | 0.0 | "0" |
| DECIMAL 3.14 | true | 3 | 3.14 | "3.14" |
| TEXT "hello" | true | 0 | 0.0 | "hello" |
| TEXT "" | false | 0 | 0.0 | "" |
Rule of thumb: non-zero / non-empty = true. Numeric parsing falls back to 0.
Can I register nodes in different categories?
Yes. Each SpcNodeRegistry.register() call takes its own category.
You can reuse category instances or create new ones:
SpcNodeCategory sensors = new SpcNodeCategory("mymod:sensors", "Sensors");
SpcNodeCategory logic = new SpcNodeCategory("mymod:logic", "Logic");
SpcNodeRegistry.register(rainSchema, rainFactory, sensors);
SpcNodeRegistry.register(thresholdSchema, thresholdFactory, logic);
Nodes sharing the same categoryId are grouped together in the palette.
Use sortOrder (3rd constructor parameter, default 5000) to control ordering.
Built-in categories use 0โ1000. See Node Categories for a full guide.
How do I debug my nodes?
- Data logging: Use
ctx.recordDataLog(nodeId, map)to publish values to SPC's data recorder. - Display messages: Use
ctx.publishDisplayMessage()to show debug output on the machine display. - Breakpoints: Standard Java debugger works โ run Minecraft via IDE debug config and set breakpoints in
execute(). - Chat messages: Use
ctx.sendChatMessage(nodeId, "Debug", "value=" + v)for quick runtime inspection.
ctx.recordDataLog(nodeId, Map.of(
"temperature", SpcSignalValue.integer(temp),
"active", SpcSignalValue.digital(isActive)
));
What if my context extension isn't available?
getExtension() returns Optional.empty() if the extension class wasn't registered.
Always handle the missing case:
int temp = ctx.getExtension(ITemperatureContext.class)
.map(ITemperatureContext::getTemperature)
.orElse(0); // safe fallback
This makes your node resilient even if the extension provider mod is removed.
Which execution phase should I use?
| Phase | Use for | Examples |
|---|---|---|
INPUT_READ |
Sampling world state, reading physical inputs | Sensors, digital input readers, real-time clocks |
LOGIC_EVALUATION |
Pure logic, math, comparisons | AND/OR gates, multiplexers, thresholds, arithmetic |
STATE_UPDATE |
Stateful operations with persistent slots | Timers, counters, latches, shift registers |
OUTPUT_APPLY |
Writing physical outputs, effects, displays | Output blocks, display messages, chat, particles |
If your node does math or logic โ LOGIC_EVALUATION.
If your node keeps a counter or timer โ STATE_UPDATE.
If your node writes to the world โ OUTPUT_APPLY.