TL;DR โ€” What you'll create

3 Java files (Schema + CompiledNode + @Mod class) โ†’ a Daylight Sensor that appears in the LOGO editor palette. Outputs a digital "is daytime?" signal and an integer "world time ticks" value. Copy-paste ready โ€” each step explains what it does and why.

On this page

โ„น๏ธ Before you start
Make sure you've completed the Getting Started guide and have a compiling NeoForge project with the SPC API jar as a dependency.

What We're Building

Our Daylight Sensor node will:

Daylight Sensor offset: 0 Q digital (is daytime?) AQ integer (world time ticks)

Step 1 Define the Schema

The schema declares the structure of our node: its unique ID, when it executes, what ports it has, and what parameters the player can configure.

DaylightSensorSchema.java
package com.example.myaddon.node;

import com.hypernova.spc.api.execution.SpcExecutionPhase;
import com.hypernova.spc.api.node.SpcNodeSchema;
import com.hypernova.spc.api.node.SpcParameterSpec;
import com.hypernova.spc.api.node.SpcParameterValueType;
import com.hypernova.spc.api.node.SpcPortDirection;
import com.hypernova.spc.api.node.SpcPortSpec;
import com.hypernova.spc.api.signal.SpcSignalType;
import java.util.List;

public final class DaylightSensorSchema {

    public static final SpcNodeSchema SCHEMA = new SpcNodeSchema(
        "myaddon:daylight_sensor",          // 1. Unique type ID (namespaced!)
        SpcExecutionPhase.INPUT_READ,       // 2. Runs first โ€” it reads world data
        List.of(                              // 3. Ports: no inputs, two outputs
            new SpcPortSpec(
                "Q",                          // port ID
                SpcPortDirection.OUTPUT,        // direction
                SpcSignalType.DIGITAL,          // data type
                false                          // not required to connect
            ),
            new SpcPortSpec(
                "AQ",                         // port ID
                SpcPortDirection.OUTPUT,        // direction
                SpcSignalType.INTEGER,          // data type
                false                          // not required
            )
        ),
        List.of(                              // 4. Parameters
            new SpcParameterSpec(
                "offset",                     // parameter ID
                SpcParameterValueType.INTEGER, // data type
                false                          // not required (optional)
            )
        ),
        false                                // 5. Does not require a display block
    );

    private DaylightSensorSchema() {} // prevent instantiation
}
๐Ÿ’ก Why namespace the type ID?
Using "myaddon:daylight_sensor" instead of just "daylight_sensor" prevents collisions if another addon creates a node with the same name. Always use "yourmodid:node_name" format.

Step 2 Implement the Compiled Node

This class is the executable logic. It implements ISpcCompiledNode and runs once per tick during the INPUT_READ phase.

DaylightSensorNode.java
package com.example.myaddon.node;

import com.hypernova.spc.api.execution.ISpcExecutionContext;
import com.hypernova.spc.api.execution.SpcExecutionPhase;
import com.hypernova.spc.api.node.ISpcCompiledNode;
import com.hypernova.spc.api.signal.SpcSignalAddress;
import com.hypernova.spc.api.signal.SpcSignalValue;
import com.hypernova.spc.api.state.ISpcRuntimeState;
import java.util.UUID;

public class DaylightSensorNode implements ISpcCompiledNode {

    // โ”€โ”€ Immutable fields set during compilation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    private final UUID nodeId;
    private final SpcSignalAddress outputQ;   // digital output
    private final SpcSignalAddress outputAQ;  // integer output
    private final int offset;                // user parameter

    public DaylightSensorNode(UUID nodeId, SpcSignalAddress outputQ,
                              SpcSignalAddress outputAQ, int offset) {
        this.nodeId = nodeId;
        this.outputQ = outputQ;
        this.outputAQ = outputAQ;
        this.offset = offset;
    }

    // โ”€โ”€ ISpcCompiledNode implementation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    @Override
    public UUID nodeId() {
        return nodeId;
    }

    @Override
    public String typeId() {
        return "myaddon:daylight_sensor";
    }

    @Override
    public SpcExecutionPhase phase() {
        return SpcExecutionPhase.INPUT_READ;
    }

    @Override
    public void execute(ISpcExecutionContext context, ISpcRuntimeState state) {
        // 1. Read the world time from the execution context
        long rawTime = context.worldTimeTicks();

        // 2. Apply the user-configured offset
        long adjustedTime = Math.floorMod(rawTime + offset, 24000L);

        // 3. Determine if it's daytime (ticks 0โ€“11999 are day)
        boolean isDaytime = adjustedTime < 12000L;

        // 4. Write outputs to the signal bus
        state.setSignal(outputQ, SpcSignalValue.digital(isDaytime));
        state.setSignal(outputAQ, SpcSignalValue.integer((int) adjustedTime));
    }
}
โš ๏ธ Keep nodes stateless
Notice we don't store any mutable state in the class fields. The nodeId, outputQ, outputAQ, and offset are all final and set during compilation. If you need state that persists across ticks, use ISpcRuntimeState persistent slots (see Runtime State).

What happens in execute()?

  1. Read โ€” we call context.worldTimeTicks() to get the current world time
  2. Compute โ€” we apply the offset and determine daytime/night
  3. Write โ€” we call state.setSignal() to put our results on the signal bus

Downstream nodes connected to our Q and AQ ports will read these values in later phases.

Step 3 Create the Factory

The factory bridges the schema (design-time) and the compiled node (runtime). It receives a NodeCompilationContext with all resolved data and constructs a DaylightSensorNode instance.

DaylightSensorFactory.java
package com.example.myaddon.node;

import com.hypernova.spc.api.node.ISpcCompiledNode;
import com.hypernova.spc.api.node.ISpcNodeFactory;

public class DaylightSensorFactory implements ISpcNodeFactory {

    @Override
    public ISpcCompiledNode create(NodeCompilationContext ctx) {
        return new DaylightSensorNode(
            ctx.nodeId(),                        // unique ID for this instance
            ctx.outputAddress("Q"),                // signal address for digital output
            ctx.outputAddress("AQ"),               // signal address for integer output
            ctx.intParameter("offset", 0)          // user's offset, default 0
        );
    }
}
๐Ÿ’ก Inline factory with a lambda
For simple nodes, you can skip the separate factory class and use a lambda:
// Instead of new DaylightSensorFactory(), you can write:
ISpcNodeFactory factory = ctx -> new DaylightSensorNode(
    ctx.nodeId(),
    ctx.outputAddress("Q"),
    ctx.outputAddress("AQ"),
    ctx.intParameter("offset", 0)
);

Step 4 Register Everything

Now we put it all together in the @Mod constructor. We register the schema, factory, and a palette category.

MyAddon.java
package com.example.myaddon;

import com.example.myaddon.node.DaylightSensorFactory;
import com.example.myaddon.node.DaylightSensorSchema;
import com.hypernova.spc.api.category.SpcNodeCategory;
import com.hypernova.spc.api.registry.SpcNodeRegistry;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import org.slf4j.Logger;
import com.mojang.logging.LogUtils;

@Mod("my_spc_addon")
public class MyAddon {

    private static final Logger LOGGER = LogUtils.getLogger();

    // Define the palette category our nodes will appear in
    private static final SpcNodeCategory SENSORS = new SpcNodeCategory(
        "myaddon:sensors",       // unique category ID
        "My Addon: Sensors",     // display name in the palette
        5000                      // sort order (built-in uses 0-1000)
    );

    public MyAddon(IEventBus modEventBus) {
        LOGGER.info("My SPC Addon loading โ€” registering custom nodes");

        // Register the daylight sensor node
        SpcNodeRegistry.register(
            DaylightSensorSchema.SCHEMA,    // schema (what the node IS)
            new DaylightSensorFactory(),     // factory (how to compile it)
            SENSORS                          // category (where it appears)
        );

        LOGGER.info("My SPC Addon loaded โ€” {} nodes registered", 1);
    }
}
๐Ÿ’ก That's it โ€” your addon is complete!
When a player opens the LOGO programming editor, they'll see "My Addon: Sensors" in the palette sidebar with the Daylight Sensor node ready to be placed and wired.

Testing Your Addon

  1. Build your project: ./gradlew build
  2. Place the SPC mod jar in run/mods/
  3. Launch: ./gradlew runClient
  4. In Minecraft:
    1. Build a LOGO multiblock machine (Processing Unit + I/O blocks)
    2. Right-click the Programming Block to open the editor
    3. Look for "My Addon: Sensors" in the palette sidebar
    4. Drag the Daylight Sensor onto the canvas
    5. Wire its Q output to a digital output block
    6. Click "Run" โ€” the output should toggle with daytime!

Troubleshooting

ProblemLikely causeFix
Node doesn't appear in palette Registration happened too late Register in @Mod constructor, not in event handlers
IllegalStateException: frozen Registration after FMLCommonSetupEvent Move SpcNodeRegistry.register() to constructor
IllegalArgumentException: Duplicate Another mod uses the same typeId Use a unique namespace: "myaddon:node_name"
Node appears but doesn't work Wrong execution phase or null output address Verify phase and check outputAddress() port IDs match schema

Next Steps