Multiblock Modules Advanced
Add custom physical blocks that validate with the LOGO multiblock structure โ custom input sources, output actuators, and structural modules.
Do I need this page?
- Yes if you want to add new physical block types to the LOGO machine structure (e.g., a rotation sensor block, a fluid valve block)
- No if you only want to add logic nodes to the programming editor โ use the tutorial instead
- No if you just need to read world data โ use Execution Context methods instead
Prerequisites: Core Concepts + Your First Addon โ complete a basic addon first!
On this page
- What are Multiblock Modules?
- Multiblock Layout
- SpcMultiblockPosition โ Where Modules Go
- SpcModuleType โ Declaring a Module
- ISpcMultiblockModule โ Block Interface
- ISpcPhysicalIoHandler โ Reading & Writing
- SpcModuleTypeRegistry โ Registration
- Step-by-Step: Custom Input Module
- Step-by-Step: Custom Output Module
- Step-by-Step: Non-I/O Module
- Reading Custom Modules in Nodes
What are Multiblock Modules?
The LOGO machine is a multiblock structure built from physical blocks. With the API, addon mods can register custom block types that the multiblock validator recognizes. This lets you add blocks that read new kinds of inputs (rotation, fluid levels, RF rates) or write new kinds of outputs (motor speeds, valve positions) โ all integrated into the standard LOGO programming environment.
What you need
| Class/Interface | Package | Purpose |
|---|---|---|
SpcModuleType | api.multiblock | Declares a module type (ID, name, position, I/O flag) |
SpcMultiblockPosition | api.multiblock | Enum of valid positions in the structure |
ISpcMultiblockModule | api.multiblock | Interface your Block class implements |
ISpcPhysicalIoHandler | api.multiblock | Reads inputs / writes outputs for your module |
SpcModuleTypeRegistry | api.multiblock | Registration point for all custom module types |
Multiblock Layout
The LOGO multiblock is a 3-tall grid that extends rightward from the processing unit:
SpcMultiblockPosition โ Where Modules Go
| Enum Value | Y Offset | Column Rule | Built-in Blocks | Typical Addon Use |
|---|---|---|---|---|
INPUT_ROW |
+1 |
Column โฅ 1 | Digital Input, Analog Input | Custom sensor inputs (rotational speed, fluid level, RF meter) |
OUTPUT_ROW |
-1 |
All columns | Digital Output, Analog Output, FE Output | Custom actuator outputs (motor control, valve, furnace speed) |
MIDDLE_ROW |
0 |
Column โฅ 1 | Casing, Display Unit | Custom structural blocks, addon display panels |
POWER_ROW |
+1 |
Column 0 only | Power Source (Transformator) | Custom power sources (solar, kinetic-to-FE) |
POWER_ROW modules go at (col 0, y+1),
and OUTPUT_ROW at (col 0, yโ1). The CPU itself occupies (col 0, y=0).
Custom modules should primarily target columns โฅ 1.
SpcModuleType โ Declaring a Module
SpcModuleType is a record that declares a custom module type's identity and placement rules.
Record components
| Component | Type | Required | Description |
|---|---|---|---|
moduleTypeId | String | Yes | Unique namespaced ID (e.g., "mymod:rotational_input") |
displayName | String | Yes | Human-readable name shown in structure preview |
position | SpcMultiblockPosition | Yes | Where this module type is valid in the structure |
isIoModule | boolean | Yes | true = provides I/O channels; false = structural only |
Constructors
| Constructor | Description |
|---|---|
new SpcModuleType(moduleTypeId, displayName, position, isIoModule) |
Full constructor |
new SpcModuleType(moduleTypeId, displayName, position) |
Convenience โ defaults isIoModule = true |
// Custom input module type:
SpcModuleType ROT_INPUT = new SpcModuleType(
"mymod:rotational_input",
"Rotational Input",
SpcMultiblockPosition.INPUT_ROW
);
// Custom structural module (no I/O):
SpcModuleType CUSTOM_CASING = new SpcModuleType(
"mymod:reinforced_casing",
"Reinforced Casing",
SpcMultiblockPosition.MIDDLE_ROW,
false // not an I/O module
);
ISpcMultiblockModule โ Block Interface
Implement this @FunctionalInterface on your Block subclass.
The multiblock BFS discovery recognizes blocks that implement this interface.
| Method | Returns | Description |
|---|---|---|
getModuleTypeId() |
String |
Must return the same ID registered in SpcModuleTypeRegistry |
public class RotationalInputBlock extends Block implements ISpcMultiblockModule {
@Override
public String getModuleTypeId() {
return "mymod:rotational_input";
}
}
ISpcPhysicalIoHandler โ Reading & Writing
The I/O handler bridges your physical block to the signal bus.
Registered alongside the SpcModuleType.
| Method | Returns | Parameters | Default | Called During |
|---|---|---|---|---|
readInput(ServerLevel, BlockPos) |
SpcSignalValue |
level โ server level; modulePos โ block position |
INTEGER_ZERO |
INPUT_READ phase |
applyOutput(ServerLevel, BlockPos, SpcSignalValue) |
void |
level; modulePos; value โ signal from bus |
no-op | OUTPUT_APPLY phase |
public class RotationalIoHandler implements ISpcPhysicalIoHandler {
@Override
public SpcSignalValue readInput(ServerLevel level, BlockPos modulePos) {
var be = level.getBlockEntity(modulePos);
if (be instanceof RotationalInputBlockEntity rot) {
return SpcSignalValue.integer(rot.getRotationalSpeed());
}
return SpcSignalValue.INTEGER_ZERO;
}
}
SpcModuleTypeRegistry โ Registration
Methods
| Method | Returns | Parameters | Description |
|---|---|---|---|
register(SpcModuleType, ISpcPhysicalIoHandler) |
void |
Module type + I/O handler (nullable for non-I/O modules) | Register a module type with its handler |
register(SpcModuleType) |
void |
Module type only | Register a non-I/O module (handler = null) |
find(String moduleTypeId) |
Optional<SpcModuleTypeRegistration> |
Module type ID | Look up a registration by ID |
all() |
Map<String, SpcModuleTypeRegistration> |
โ | Unmodifiable view of all registrations |
isFrozen() |
boolean |
โ | Whether the registry is locked |
freeze() |
void |
โ | Internal โ do not call from addon code |
SpcModuleTypeRegistration (inner record)
| Component | Type | Description |
|---|---|---|
moduleType | SpcModuleType | The module type definition |
ioHandler | ISpcPhysicalIoHandler | The I/O handler (null for non-I/O modules) |
Step-by-Step: Custom Input Module
Define the module type
SpcModuleType ROT_INPUT = new SpcModuleType(
"mymod:rotational_input",
"Rotational Input",
SpcMultiblockPosition.INPUT_ROW
);
Create the I/O handler
public class RotationalIoHandler implements ISpcPhysicalIoHandler {
@Override
public SpcSignalValue readInput(ServerLevel level, BlockPos pos) {
if (level.getBlockEntity(pos) instanceof RotInputBE be)
return SpcSignalValue.integer(be.getRpm());
return SpcSignalValue.INTEGER_ZERO;
}
}
Create the block class
public class RotationalInputBlock extends Block implements ISpcMultiblockModule {
@Override
public String getModuleTypeId() {
return "mymod:rotational_input";
}
}
Register everything in your @Mod constructor
SpcModuleTypeRegistry.register(ROT_INPUT, new RotationalIoHandler());
Read it from a compiled node
SpcSignalValue rpm = context.readCustomInput("mymod:rotational_input", 1);
int speed = rpm.asInteger();
Step-by-Step: Custom Output Module
Same process as input, but:
- Use
SpcMultiblockPosition.OUTPUT_ROW - Override
applyOutput()instead ofreadInput() - Called during
OUTPUT_APPLYphase
public class MotorOutputHandler implements ISpcPhysicalIoHandler {
@Override
public void applyOutput(ServerLevel level, BlockPos pos, SpcSignalValue value) {
if (level.getBlockEntity(pos) instanceof MotorOutputBE be) {
be.setTargetSpeed(value.asInteger());
}
}
}
// Registration:
SpcModuleType MOTOR_OUT = new SpcModuleType(
"mymod:motor_output", "Motor Output",
SpcMultiblockPosition.OUTPUT_ROW);
SpcModuleTypeRegistry.register(MOTOR_OUT, new MotorOutputHandler());
// Writing from a node:
context.applyCustomOutputs("mymod:motor_output",
Map.of(1, SpcSignalValue.integer(targetSpeed)));
Step-by-Step: Non-I/O Module
Structural modules (custom casings, decorative panels) don't provide I/O channels but still validate as part of the multiblock.
// Just register the type with isIoModule = false, no handler:
SpcModuleType CASING = new SpcModuleType(
"mymod:reinforced_casing", "Reinforced Casing",
SpcMultiblockPosition.MIDDLE_ROW, false);
SpcModuleTypeRegistry.register(CASING);
// Block still implements ISpcMultiblockModule:
public class ReinforcedCasingBlock extends Block implements ISpcMultiblockModule {
@Override
public String getModuleTypeId() {
return "mymod:reinforced_casing";
}
}
Reading Custom Modules in Nodes
Once registered, custom modules appear as channels on the execution context.
Use readCustomInput() and applyCustomOutputs() (see
Execution Context โ Custom Module I/O).
| Operation | Context Method | When |
|---|---|---|
| Read from custom input | context.readCustomInput("modTypeId", channel) | INPUT_READ phase |
| Write to custom output | context.applyCustomOutputs("modTypeId", Map.of(ch, value)) | OUTPUT_APPLY phase |