Do I need this page?

Prerequisites: Core Concepts + Your First Addon โ€” complete a basic addon first!

On this page

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/InterfacePackagePurpose
SpcModuleTypeapi.multiblockDeclares a module type (ID, name, position, I/O flag)
SpcMultiblockPositionapi.multiblockEnum of valid positions in the structure
ISpcMultiblockModuleapi.multiblockInterface your Block class implements
ISpcPhysicalIoHandlerapi.multiblockReads inputs / writes outputs for your module
SpcModuleTypeRegistryapi.multiblockRegistration point for all custom module types

Multiblock Layout

The LOGO multiblock is a 3-tall grid that extends rightward from the processing unit:

Col 0 Col 1 Col 2+ y=+1 y= 0 y=โˆ’1 POWER CPU OUTPUT INPUT MIDDLE OUTPUT INPUT MIDDLE OUTPUT โ€ฆ extends โ†’

SpcMultiblockPosition โ€” Where Modules Go

Enum ValueY OffsetColumn RuleBuilt-in BlocksTypical 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)
โš ๏ธ Column 0 is special
Column 0 is the processing unit column. Only 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

ComponentTypeRequiredDescription
moduleTypeIdStringYesUnique namespaced ID (e.g., "mymod:rotational_input")
displayNameStringYesHuman-readable name shown in structure preview
positionSpcMultiblockPositionYesWhere this module type is valid in the structure
isIoModulebooleanYestrue = provides I/O channels; false = structural only

Constructors

ConstructorDescription
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.

MethodReturnsDescription
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.

MethodReturnsParametersDefaultCalled 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

MethodReturnsParametersDescription
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)

ComponentTypeDescription
moduleTypeSpcModuleTypeThe module type definition
ioHandlerISpcPhysicalIoHandlerThe I/O handler (null for non-I/O modules)

Step-by-Step: Custom Input Module

1

Define the module type

SpcModuleType ROT_INPUT = new SpcModuleType(
    "mymod:rotational_input",
    "Rotational Input",
    SpcMultiblockPosition.INPUT_ROW
);
2

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;
    }
}
3

Create the block class

public class RotationalInputBlock extends Block implements ISpcMultiblockModule {
    @Override
    public String getModuleTypeId() {
        return "mymod:rotational_input";
    }
}
4

Register everything in your @Mod constructor

SpcModuleTypeRegistry.register(ROT_INPUT, new RotationalIoHandler());
5

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:

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).

OperationContext MethodWhen
Read from custom inputcontext.readCustomInput("modTypeId", channel)INPUT_READ phase
Write to custom outputcontext.applyCustomOutputs("modTypeId", Map.of(ch, value))OUTPUT_APPLY phase