diff --git a/src/main/java/com/portingdeadmods/modjam/api/blockentities/MultiblockEntity.java b/src/main/java/com/portingdeadmods/modjam/api/blockentities/MultiblockEntity.java
new file mode 100644
index 00000000..405ee064
--- /dev/null
+++ b/src/main/java/com/portingdeadmods/modjam/api/blockentities/MultiblockEntity.java
@@ -0,0 +1,4 @@
+package com.portingdeadmods.modjam.api.blockentities;
+
+public interface MultiblockEntity {
+}
diff --git a/src/main/java/com/portingdeadmods/modjam/api/multiblocks/Multiblock.java b/src/main/java/com/portingdeadmods/modjam/api/multiblocks/Multiblock.java
new file mode 100644
index 00000000..297e26f1
--- /dev/null
+++ b/src/main/java/com/portingdeadmods/modjam/api/multiblocks/Multiblock.java
@@ -0,0 +1,208 @@
+package com.portingdeadmods.modjam.api.multiblocks;
+
+import com.portingdeadmods.modjam.api.blockentities.MultiblockEntity;
+import com.portingdeadmods.modjam.api.utils.HorizontalDirection;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraft.world.level.block.state.BlockState;
+import org.apache.commons.lang3.IntegerRange;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public interface Multiblock {
+ /**
+ * This method provides the controller block of your unformed multiblock.
+ * Your multiblock needs at least one of these in its structure.
+ *
+ *
+ * Example: {@link com.indref.industrial_reforged.registries.multiblocks.BlastFurnaceMultiblock#getUnformedController() BlastFurnaceMultiblock.getUnformedController()}
+ *
+ * @return The controller block of your unformed multiblock
+ */
+ Block getUnformedController();
+
+ /**
+ * This method provides the controller block of your formed multiblock.
+ * Your multiblock needs at least one of these in its structure.
+ *
+ *
+ * Example: {@link com.indref.industrial_reforged.registries.multiblocks.BlastFurnaceMultiblock#getUnformedController() BlastFurnaceMultiblock.getFormedController()}
+ *
+ * @return The controller block of your formed multiblock
+ */
+ Block getFormedController();
+
+ /**
+ * This method provides the layout of your unformed multiblock.
+ *
+ * It consists of an array of multiblock layers. Each layer
+ * is constructed with a method call.
+ *
+ * For this, you can use {@link Multiblock#layer(int...)}
+ *
+ * Each of these methods ask you to provide you a list of integers.
+ * These integers represent the actual blocks used.
+ * Nonetheless, you still need to provide the actual blocks using
+ * the {@link Multiblock#getDefinition()} method.
+ * This provides the minimum and maximum height for this multiblock.
+ *
+ * Example: {@code IntegerRange.of(1, 3)}
+ *
+ *
+ * Note: The first layer in this array also represents the bottom layer of the multiblock
+ *
+ *
+ * Example: {@link com.indref.industrial_reforged.registries.multiblocks.BlastFurnaceMultiblock#getLayout() BlastFurnaceMultiblock.getLayout()}.
+ * @return An array of multiblock layers that describes the layout of the multiblock
+ */
+ MultiblockLayer[] getLayout();
+
+ /**
+ * This method provides a definition map that can be used to look up
+ * an integer key in {@link Multiblock#getLayout()} and will return a block.
+ *
+ *
+ * The keyset of this map needs to include
+ * every key that is used in {@link Multiblock#getLayout()}.
+ *
+ *
+ * The values of this map need to contain the block for each
+ * integer key. If you do not care about a block you can use {@code null}
+ * instead of a value.
+ *
+ *
+ * Example: {@link com.indref.industrial_reforged.registries.multiblocks.BlastFurnaceMultiblock#getDefinition() BlastFurnaceMultiblock.getDefintion()}
+ * @return The integer to block map that provides the integer keys and their block values
+ */
+ Int2ObjectMap getDefinition();
+
+ /**
+ * This method provides the block entity type for the controller of your multiblock.
+ * @return the blockentity type of your controllers blockentity
+ */
+ BlockEntityType extends MultiblockEntity> getMultiBlockEntityType();
+
+ /**
+ * This method provides a list of widths for every layer
+ * of your multiblock.
+ *
+ *
+ * This method has a default implementation meaning that
+ * you do not have to override it, unless one of your
+ * multiblock layers is not quadratic. (And it's width
+ * can therefore not be determined by getting the
+ * square root of the integer arrays length)
+ *
+ *
+ * The size of this list needs to be {@link Multiblock#getMaxSize()}
+ * and needs to contain the widths for every possible layer, this also
+ * includes dynamic layers.
+ *
+ * @return a list of integer pairs where left is the x- and right is the z-width
+ */
+ default List getWidths() {
+ List widths = new ArrayList<>(getMaxSize());
+ for (MultiblockLayer layer : getLayout()) {
+ if (layer.dynamic()) {
+ for (int i = 0; i < layer.range().getMaximum(); i++) {
+ widths.add(layer.getWidths());
+ }
+ } else {
+ widths.add(layer.getWidths());
+ }
+ }
+ return widths;
+ }
+
+ /**
+ * This method is used to form a block. It is called for that block and also when unforming the multi.
+ * This is why this should only return the blockState, not perform any interactions on the level/player....
+ * For interactions with the world/player..., use {@link Multiblock#afterFormBlock(Level, BlockPos, BlockPos, int, int, MultiblockData, Player)}
+ * @param level Level of the multiblock, should only be used for reading things, not setting new things.
+ * @param blockPos BlockPos of the block that is being formed
+ * @param controllerPos BlockPos of this multiblocks controller
+ * @param layerIndex index of the current layers block (array of integer)
+ * @param layoutIndex index of the current multiblock layer (array of multiblock layer)
+ * @param multiblockData Information about the unformed multiblock, like the layers of the concrete multiblock and the direction it is formed in.
+ * @param player Player that is trying to form this multiblock. Note that there does not necessarily have to be a player that is responsible for forming the multiblock
+ * @return Formed BlockState. This will replace the unformed block in the multiblock. Return {@code null} if you do not want to change the block.
+ */
+ @Nullable BlockState formBlock(Level level, BlockPos blockPos, BlockPos controllerPos, int layerIndex, int layoutIndex, MultiblockData multiblockData, @Nullable Player player);
+
+ /**
+ * This method is called after the block is formed. It can be used to interact with the level/player...
+ * as it is only called, when the multiblock is formed.
+ * @param level Level of the multiblock
+ * @param blockPos BlockPos of the block that is being formed
+ * @param controllerPos BlockPos of this multiblocks controller
+ * @param layerIndex index of the current layers block (array of integer)
+ * @param layoutIndex index of the current multiblock layer (array of multiblock layer)
+ * @param multiblockData Information about the unformed multiblock, like the layers of the concrete multiblock and the direction it is formed in.
+ * @param player Player that is trying to form this multiblock. Note that there does not necessarily have to be a player that is responsible for forming the multiblock
+ */
+ default void afterFormBlock(Level level, BlockPos blockPos, BlockPos controllerPos, int layerIndex, int layoutIndex, MultiblockData multiblockData, @Nullable Player player) {
+ }
+
+ /**
+ * This method is called after the block is unformed. It can be used to interact with the level/player...
+ * as it is only called, when the multiblock is unformed.
+ * @param level Level of the multiblock
+ * @param direction Direction of the multiblock
+ * @param blockPos BlockPos of the block that is being unformed
+ * @param controllerPos BlockPos of this multiblocks controller
+ * @param layerIndex index of the current layers block (array of integer)
+ * @param layoutIndex index of the current multiblock layer (array of multiblock layer)
+ * @param player Player that is trying to unform this multiblock. Note that there does not necessarily have to be a player that is responsible for unforming the multiblock
+ */
+ default void afterUnformBlock(Level level, BlockPos blockPos, BlockPos controllerPos, int layerIndex, int layoutIndex, HorizontalDirection direction, @Nullable Player player) {
+ }
+
+ /**
+ * This method determines whether the block at the specified position
+ * is a formed part of this multiblock.
+ * @param level Level of the multiblock
+ * @param blockPos BlockPos that needs to be checked if it is formed.
+ * @return Whether the block at this position is formed
+ */
+ boolean isFormed(Level level, BlockPos blockPos);
+
+ /**
+ * This method can make the direction of this multiblock fixed. This only works,
+ * if the multiblock cannot be rotated, like the crucible or firebox.
+ * Providing a fixed direction can improve performance while forming the multiblock
+ * by a bit.
+ * @return a horizontal direction, if the direction can be fixed.
+ */
+ default @Nullable HorizontalDirection getFixedDirection() {
+ return null;
+ }
+
+ /**
+ * This method provides the maximum possible
+ * size for this multiblock.
+ * @return the maximum possible size
+ */
+ default int getMaxSize() {
+ int maxSize = 0;
+ for (MultiblockLayer layer : getLayout()) {
+ maxSize += layer.range().getMaximum();
+ }
+ return maxSize;
+ }
+
+ /**
+ * Create a new layer for your multiblock
+ * @param layer The block indices for your multiblock layer
+ * @return the newly created layer
+ */
+ default MultiblockLayer layer(int... layer) {
+ return new MultiblockLayer(false, IntegerRange.of(1, 1), layer);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/portingdeadmods/modjam/api/multiblocks/MultiblockData.java b/src/main/java/com/portingdeadmods/modjam/api/multiblocks/MultiblockData.java
new file mode 100644
index 00000000..a09aa20f
--- /dev/null
+++ b/src/main/java/com/portingdeadmods/modjam/api/multiblocks/MultiblockData.java
@@ -0,0 +1,34 @@
+package com.portingdeadmods.modjam.api.multiblocks;
+
+import com.portingdeadmods.modjam.api.utils.HorizontalDirection;
+import net.minecraft.nbt.CompoundTag;
+
+public record MultiblockData(boolean valid, HorizontalDirection direction, MultiblockLayer[] layers) {
+ public static final MultiblockData EMPTY = new MultiblockData(false, HorizontalDirection.NORTH, new MultiblockLayer[0]);
+
+ public CompoundTag serializeNBT() {
+ CompoundTag tag = new CompoundTag();
+ tag.putInt("layersLength", layers.length);
+ CompoundTag listTag = new CompoundTag();
+ for (int i = 0, expandedLayersLength = layers.length; i < expandedLayersLength; i++) {
+ MultiblockLayer layer = layers[i];
+ listTag.put(String.valueOf(i), layer.save());
+ }
+ tag.put("layersList", listTag);
+ tag.putInt("direction", this.direction.ordinal());
+ tag.putBoolean("valid", this.valid);
+ return tag;
+ }
+
+ public static MultiblockData deserializeNBT(CompoundTag nbt) {
+ int layersLength = nbt.getInt("layersLength");
+ CompoundTag listTag = nbt.getCompound("layersList");
+ MultiblockLayer[] layers = new MultiblockLayer[layersLength];
+ for (int i = 0; i < layers.length; i++) {
+ layers[i] = MultiblockLayer.load(listTag.getCompound(String.valueOf(i)));
+ }
+ HorizontalDirection direction = HorizontalDirection.values()[nbt.getInt("direction")];
+ boolean valid = nbt.getBoolean("valid");
+ return new MultiblockData(valid, direction, layers);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/portingdeadmods/modjam/api/multiblocks/MultiblockLayer.java b/src/main/java/com/portingdeadmods/modjam/api/multiblocks/MultiblockLayer.java
new file mode 100644
index 00000000..8c09fb8b
--- /dev/null
+++ b/src/main/java/com/portingdeadmods/modjam/api/multiblocks/MultiblockLayer.java
@@ -0,0 +1,54 @@
+package com.portingdeadmods.modjam.api.multiblocks;
+
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import net.minecraft.nbt.CompoundTag;
+import org.apache.commons.lang3.IntegerRange;
+
+import java.util.Arrays;
+
+public record MultiblockLayer(boolean dynamic, IntegerRange range, int[] layer, IntIntPair widths) {
+ public MultiblockLayer(boolean dynamic, IntegerRange range, int[] layer) {
+ this(dynamic, range, layer, IntIntPair.of((int) Math.sqrt(layer.length), (int) Math.sqrt(layer.length)));
+ }
+
+ public MultiblockLayer setDynamic(IntegerRange size) {
+ return new MultiblockLayer(true, size, layer, widths);
+ }
+
+ public MultiblockLayer setWidths(int xWidth, int zWidth) {
+ return new MultiblockLayer(dynamic, range, layer, IntIntPair.of(xWidth, zWidth));
+ }
+
+ public static MultiblockLayer load(CompoundTag tag) {
+ return new MultiblockLayer(
+ tag.getBoolean("dynamic"),
+ IntegerRange.of(tag.getInt("rangeMin"), tag.getInt("rangeMax")),
+ tag.getIntArray("layer"),
+ IntIntPair.of(tag.getInt("widthsX"), tag.getInt("widthsZ"))
+ );
+ }
+
+ public CompoundTag save() {
+ CompoundTag tag = new CompoundTag();
+ tag.putBoolean("dynamic", dynamic);
+ tag.putInt("rangeMin", range.getMinimum());
+ tag.putInt("rangeMax", range.getMaximum());
+ tag.putIntArray("layer", layer);
+ tag.putInt("widthsX", widths.leftInt());
+ tag.putInt("widthsZ", widths.rightInt());
+ return tag;
+ }
+
+ public IntIntPair getWidths() {
+ return widths;
+ }
+
+ @Override
+ public String toString() {
+ return "MultiblockLayer{" +
+ "dynamic=" + dynamic +
+ ", range=" + range +
+ ", layer=" + Arrays.toString(layer) +
+ '}';
+ }
+}
diff --git a/src/main/java/com/portingdeadmods/modjam/api/utils/HorizontalDirection.java b/src/main/java/com/portingdeadmods/modjam/api/utils/HorizontalDirection.java
new file mode 100644
index 00000000..9dfd6887
--- /dev/null
+++ b/src/main/java/com/portingdeadmods/modjam/api/utils/HorizontalDirection.java
@@ -0,0 +1,30 @@
+package com.portingdeadmods.modjam.api.utils;
+
+import net.minecraft.core.Direction;
+import org.jetbrains.annotations.Nullable;
+
+public enum HorizontalDirection {
+ NORTH,
+ EAST,
+ SOUTH,
+ WEST;
+
+ public Direction toRegularDirection() {
+ return switch (this) {
+ case NORTH -> Direction.NORTH;
+ case EAST -> Direction.EAST;
+ case SOUTH -> Direction.SOUTH;
+ case WEST -> Direction.WEST;
+ };
+ }
+
+ public static @Nullable HorizontalDirection fromRegularDirection(Direction direction) {
+ return switch (direction) {
+ case NORTH -> NORTH;
+ case EAST -> EAST;
+ case SOUTH -> SOUTH;
+ case WEST -> WEST;
+ default -> null;
+ };
+ }
+}
\ No newline at end of file