Questions

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);
    }
}
Do not register in event handlers
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:

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.

Slot naming
Slot names are scoped per node instance (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:

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:

SourceasDigital()asInteger()asDecimal()asText()
DIGITAL truetrue11.0"1"
DIGITAL falsefalse00.0"0"
INTEGER 42true4242.0"42"
INTEGER 0false00.0"0"
DECIMAL 3.14true33.14"3.14"
TEXT "hello"true00.0"hello"
TEXT ""false00.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?

  1. Data logging: Use ctx.recordDataLog(nodeId, map) to publish values to SPC's data recorder.
  2. Display messages: Use ctx.publishDisplayMessage() to show debug output on the machine display.
  3. Breakpoints: Standard Java debugger works โ€” run Minecraft via IDE debug config and set breakpoints in execute().
  4. Chat messages: Use ctx.sendChatMessage(nodeId, "Debug", "value=" + v) for quick runtime inspection.
Data logging example
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?

PhaseUse forExamples
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
โ„น๏ธ Rule of thumb
If your node reads from the world โ†’ INPUT_READ.
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.