Context Extensions Intermediate
The built-in ISpcExecutionContext covers redstone, FE, displays, weather, and more.
But what if your addon needs data from another mod โ Create's rotational speed, Mekanism's gas pressure,
or your own custom block entities? That's where context extensions come in.
Do I need this page?
- โ Skip if: your node only does math/logic, or the built-in context methods (weather, time, light, entities) are enough โ check Execution Context first
- โ
Read if: you need Create rotational speed, Mekanism gas pressure, or any
custom
BlockEntitydata not covered by the built-in methods
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:
- Your node only does math/logic on its input signals
- Your node uses built-in context methods (
isRaining(),readFeInput(),getLightLevel(), etc.) - Your node only reads/writes to the signal bus and persistent slots
You do need a context extension if:
- Your node needs to read data from another mod's block entities
- Your node needs to interact with world data that
ISpcExecutionContextdoesn't expose - Your node writes to another mod's systems (e.g., setting rotation speed, gas pressure)
- You need access to
ServerLevelandBlockPosdirectly
How Extensions Work
Key points:
- Extensions are singletons โ one instance per extension type, shared across all machines
onTickStart()is called per machine per tick, so the extension knows which machine is running- Extensions are retrieved via
context.getExtension()โ type-safe, returnsOptional
Step 1 Define an Extension Interface
Create an interface that extends ISpcExecutionContextExtension.
This defines what your nodes can do through the extension.
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().
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;
}
}
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:
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:
@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));
}
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:
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
| Do | Don'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 |