Skip to main content

Developer API

For plugin developers building content on MachineEngine. Server owners do not need anything in this section — and much of what an addon does can be done without Java, in YAML definitions.

Add the dependency​

MachineEngine is on the server at runtime, so depend on its API with compileOnly: shading it in would load a second copy of every class.

build.gradle.kts
repositories {
maven("https://1robie.github.io/maven/")
}

dependencies {
compileOnly("com.github.1robie.MachineEngine:machineengine-api:0.1.0")
}

Compile against Paper's API too (the newest; MachineEngine targets Paper 1.20.6 – 26.3 from one jar) and, if you use CraftEngine classes, CraftEngine's.

Declare the dependency​

MachineEngine must enable before your plugin's content is used, and you register in your onLoad:

paper-plugin.yml
dependencies:
server:
MachineEngine:
load: BEFORE
required: true
join-classpath: true

With a plugin.yml, use depend: [MachineEngine].

Register your content​

Register in onLoad. MachineEngine runs every addon's callback when it enables — in dependency order, before worlds load — then freezes its registries. A machine type cannot be added later.

A complete addon: a generator that burns lava into energy.

LavaGenerator.java
import fr.robie.machineengine.api.addon.AddonRegistration;
import fr.robie.machineengine.api.machine.MachineType;
import fr.robie.machineengine.api.machine.PortDefinition;
import fr.robie.machineengine.api.machine.PortDirection;
import fr.robie.machineengine.api.machine.ProcessingBehavior;
import fr.robie.machineengine.api.machine.StorageDefinition;
import fr.robie.machineengine.api.process.ProcessBinding;
import fr.robie.machineengine.api.recipe.ProcessingRecipe;
import fr.robie.machineengine.api.recipe.RecipeType;
import fr.robie.machineengine.api.resource.Energy;
import fr.robie.machineengine.api.resource.Fluids;
import fr.robie.machineengine.api.resource.match.ResourceMatcher;
import fr.robie.machineengine.api.resource.match.ResourcePredicate;
import fr.robie.machineengine.api.world.RelativeSide;
import net.kyori.adventure.key.Key;

public final class LavaGenerator {
public static final RecipeType<ProcessingRecipe> BURNING =
RecipeType.of(Key.key("lavagen", "burning"), ProcessingRecipe.class);

public static final MachineType GENERATOR = MachineType.builder(Key.key("lavagen", "generator"))
.storage(StorageDefinition.tank("fuel", 4_000).filter(ResourcePredicate.type(Fluids.lava())))
.storage(StorageDefinition.tank("energy", 100_000).filter(Energy.is()))
.port(PortDefinition.builder("fuel_in", "fuel").direction(PortDirection.INPUT).build())
.port(PortDefinition.builder("power_out", "energy").direction(PortDirection.OUTPUT)
.sides(RelativeSide.BACK).build())
.behavior(ProcessingBehavior.of(BURNING, ProcessBinding.of("fuel", "energy")))
.build();

/** What the addon registers, run by MachineEngine when it enables. */
public static AddonRegistration registration(String name, String version) {
return AddonRegistration.builder(name, version)
.requiresApi("[0.1,1.0)")
.register(ctx -> {
ctx.recipeTypes().register(BURNING);
ctx.machineTypes().register(GENERATOR);
// Recipes are reloadable (/machineengine reload recipes): given by a provider.
ctx.recipes(reload -> reload.register(
ProcessingRecipe.builder(Key.key("lavagen", "burn_lava"), BURNING)
.input(ResourceMatcher.of(ResourcePredicate.type(Fluids.lava()), 100))
.output(Energy.type().stack(2_000))
.duration(40)
.build()));
})
.build();
}
}
LavaGeneratorPlugin.java
import fr.robie.machineengine.api.MachineEngine;
import org.bukkit.plugin.java.JavaPlugin;

public final class LavaGeneratorPlugin extends JavaPlugin {
@Override
public void onLoad() {
MachineEngine.register(LavaGenerator.registration(getName(), getPluginMeta().getVersion()));
}
}

Then a CraftEngine block for it (template: machineengine:block/machine on a block named lavagen:generator) — see Blocks and items.

  • requiresApi is the MachineEngine API range your addon works with; an addon outside it is disabled at startup with a clear message.
  • after("OtherAddon") on the builder orders your callback after another addon's, to use its resources or types.
  • Everything the API registers can also come from YAML: a machine type defined in Java can use recipes from a definitions file, and the reverse.

While the server runs​

MachineEngine.get() returns the running engine (after MachineEngine has enabled): its registries, the event bus, transactions, the machine at a position (machineAt), and player data (players()).

MachineEngine.scheduler(plugin) gives a scheduler that runs on the right thread on Paper and on Folia (a location's region, an entity's thread), and stops with your plugin.

Test it headless​

machineengine-test starts a real engine without a server, with a fake world: place machines, fill them, tick, and check.

build.gradle.kts
repositories {
maven("https://1robie.github.io/maven/")
}

dependencies {
testImplementation("com.github.1robie.MachineEngine:machineengine-test:0.1.0")
}

It needs MachineEngine's own repositories as well: Paper (https://repo.papermc.io/repository/maven-public/), CraftEngine (https://repo.momirealms.net/snapshots/), zMenu (https://repo.groupez.dev/releases), FoliaLib (https://repo.tcoded.com/releases) and JitPack (Paper-Dispatch).

LavaGeneratorTest.java
import fr.robie.machineengine.api.resource.Fluids;
import fr.robie.machineengine.api.transfer.StorageView;
import fr.robie.machineengine.api.transfer.Transaction;
import fr.robie.machineengine.api.world.Direction;
import fr.robie.machineengine.api.world.WorldPosition;
import fr.robie.machineengine.core.lifecycle.CoreMachineEngine;
import fr.robie.machineengine.core.machine.MachineInstanceImpl;
import fr.robie.machineengine.test.FakeWorld;
import fr.robie.machineengine.test.TestEngine;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class LavaGeneratorTest {
@Test
void burnsLavaIntoEnergy() {
CoreMachineEngine engine = TestEngine.builder()
.addon(LavaGenerator.registration("lavagen", "1.0.0"))
.start();
FakeWorld world = FakeWorld.singleRegion(engine);
WorldPosition here = FakeWorld.at(0, 64, 0);
MachineInstanceImpl generator = world.place(LavaGenerator.GENERATOR, here, Direction.NORTH);

try (Transaction transaction = engine.transactions().openOuter()) {
generator.context().storage("fuel").insert(Fluids.lava().variant(), 100, transaction);
transaction.commit();
}
world.tick(60);

long energy = generator.context().storage("energy").views().stream().mapToLong(StorageView::amount).sum();
assertEquals(2_000, energy);
}
}

The harness reaches engine internals (CoreMachineEngine, MachineInstanceImpl); that is fine in tests, but your plugin's own code should use the API only.

Going further​

  • Events: react to machines.
  • Javadoc: every type of the API.
  • Mekanized: a full addon built on the API only — machines, cables, multiblocks, gear and menus.