Do I need this page?

Prerequisite: Your First Addon โ€” complete a basic addon first!

On this page

When Do You Need an Extension?

You don't need a context extension if:

You do need a context extension if:

โ„น๏ธ Built-in context is extensive
Before creating an extension, check the ISpcExecutionContext reference. It already provides: digital/analog/FE I/O, virtual signals, world time, weather, light levels, entity counting, block detection, biome, moon phase, note blocks, particles, chat messages, item I/O, and data logging.

How Extensions Work

Registration Every tick Node execution SpcContextExtensionRegistry.register(IMyExt.class, new MyExtImpl()) SPC calls onTickStart(level, machinePos) on every registered extension, before nodes run node calls context.getExtension(IMyExt.class) โ†’ Optional<IMyExt> โ†’ call custom methods

Key points:

Step 1 Define an Extension Interface

Create an interface that extends ISpcExecutionContextExtension. This defines what your nodes can do through the extension.

ITemperatureContext.java
package com.example.myaddon.extension;

import com.hypernova.spc.api.execution.ISpcExecutionContextExtension;

/**
 * Context extension providing temperature reading/control capabilities.
 * Used by temperature sensor and heater control nodes.
 */
public interface ITemperatureContext extends ISpcExecutionContextExtension {

    /**
     * Read the temperature at a position relative to the machine.
     *
     * @param offsetX X offset from machine position
     * @param offsetY Y offset from machine position
     * @param offsetZ Z offset from machine position
     * @return temperature in degrees, or 0 if no sensor at that position
     */
    float readTemperature(int offsetX, int offsetY, int offsetZ);

    /**
     * Set the heater power on a specific channel.
     *
     * @param channel heater channel (0-based)
     * @param power   power level 0.0โ€“1.0
     */
    void setHeaterPower(int channel, float power);

    /**
     * Check if a heater block exists at the given offset.
     */
    boolean hasHeaterAt(int offsetX, int offsetY, int offsetZ);
}

Step 2 Implement the Extension

The implementation holds the current machine's ServerLevel and BlockPos, refreshed every tick via onTickStart().

TemperatureContextImpl.java
package com.example.myaddon.extension;

import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;

public class TemperatureContextImpl implements ITemperatureContext {

    // Updated each tick via onTickStart()
    private ServerLevel level;
    private BlockPos machinePos;

    @Override
    public void onTickStart(ServerLevel level, BlockPos machinePos) {
        // Cache the machine's world and position for this tick.
        // This is called BEFORE any nodes execute.
        this.level = level;
        this.machinePos = machinePos;
    }

    @Override
    public float readTemperature(int offsetX, int offsetY, int offsetZ) {
        if (level == null || machinePos == null) return 0f;

        BlockPos target = machinePos.offset(offsetX, offsetY, offsetZ);

        // Example: look for your mod's temperature sensor block entity
        // var be = level.getBlockEntity(target);
        // if (be instanceof MyTempSensorBlockEntity sensor) {
        //     return sensor.getTemperature();
        // }

        return 0f;
    }

    @Override
    public void setHeaterPower(int channel, float power) {
        if (level == null || machinePos == null) return;

        // Example: find and control your mod's heater block entity
        // ...
    }

    @Override
    public boolean hasHeaterAt(int offsetX, int offsetY, int offsetZ) {
        if (level == null || machinePos == null) return false;

        BlockPos target = machinePos.offset(offsetX, offsetY, offsetZ);
        // return level.getBlockEntity(target) instanceof MyHeaterBlockEntity;
        return false;
    }
}
โš ๏ธ Extensions are singletons
onTickStart() is called for each machine every tick. Since there's only one extension instance, the level and machinePos fields get overwritten each time. This is fine because all nodes for a given machine execute between that machine's onTickStart() and the next machine's. Never cache data across machines.

Step 3 Register the Extension

In your @Mod constructor, alongside your node registrations:

MyAddon.java (updated)
import com.hypernova.spc.api.registry.SpcContextExtensionRegistry;
import com.example.myaddon.extension.ITemperatureContext;
import com.example.myaddon.extension.TemperatureContextImpl;

@Mod("my_spc_addon")
public class MyAddon {
    public MyAddon(IEventBus modEventBus) {

        // Register the context extension
        SpcContextExtensionRegistry.register(
            ITemperatureContext.class,      // the interface (lookup key)
            new TemperatureContextImpl()    // the implementation
        );

        // Register nodes as before...
        SpcNodeRegistry.register(...);
    }
}

Step 4 Use It in Your Node

Inside your compiled node's execute(), retrieve the extension:

TemperatureSensorNode.java (execute method)
@Override
public void execute(ISpcExecutionContext context, ISpcRuntimeState state) {

    // Retrieve our extension โ€” returns Optional<ITemperatureContext>
    ITemperatureContext tempCtx = context.getExtension(ITemperatureContext.class)
        .orElse(null);

    if (tempCtx == null) {
        // Extension not available โ€” output defaults
        state.setSignal(outputQ, SpcSignalValue.DIGITAL_FALSE);
        state.setSignal(outputAQ, SpcSignalValue.DECIMAL_ZERO);
        return;
    }

    // Read temperature at configured offset
    float temp = tempCtx.readTemperature(offsetX, offsetY, offsetZ);

    // Output: Q = above threshold?, AQ = raw temperature
    state.setSignal(outputQ, SpcSignalValue.digital(temp > threshold));
    state.setSignal(outputAQ, SpcSignalValue.decimal(temp));
}
๐Ÿ’ก Always handle the missing case
getExtension() returns Optional.empty() if the extension isn't registered. This can happen if the player removes the addon mod that provides the implementation but kept a program that references it. Output sensible defaults.

The onTickStart Lifecycle

Here's exactly when onTickStart() is called in the execution flow:

For each active LOGO machine, every server tick: 1 Read machine's ServerLevel and BlockPos 2 notifyTickStart(level, pos) โ†’ extension.onTickStart() 3 Run INPUT_READ phase nodes 4 Run LOGIC_EVALUATION phase nodes 5 Run STATE_UPDATE phase nodes 6 Run OUTPUT_APPLY phase nodes 7 Commit pending state

This means that by the time any node calls getExtension(), the extension's onTickStart() has already been called with the correct level and machinePos.

Best Practices

DoDon't
Use the interface as the registry key: register(IMyExt.class, impl) Don't use the implementation class as the key
Check for null level/machinePos in extension methods Don't assume onTickStart was called before your methods
Handle Optional.empty() gracefully in nodes Don't call .get() without checking โ€” it throws
Keep extension methods fast (they run 20x/second per machine) Don't do heavy computation or I/O in extension methods
Register in @Mod constructor Don't register after FMLCommonSetupEvent
Namespace your extension interface in your package Don't put extension interfaces in the SPC API package