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.
- Gradle (Kotlin)
- Gradle (Groovy)
- Maven
repositories {
maven("https://1robie.github.io/maven/")
}
dependencies {
compileOnly("com.github.1robie.MachineEngine:machineengine-api:0.1.0")
}
repositories {
maven { url 'https://1robie.github.io/maven/' }
}
dependencies {
compileOnly 'com.github.1robie.MachineEngine:machineengine-api:0.1.0'
}
<repositories>
<repository>
<id>1robie</id>
<url>https://1robie.github.io/maven/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.1robie.MachineEngine</groupId>
<artifactId>machineengine-api</artifactId>
<version>0.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
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:
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.
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();
}
}
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.
requiresApiis 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.
- Gradle (Kotlin)
- Gradle (Groovy)
- Maven
repositories {
maven("https://1robie.github.io/maven/")
}
dependencies {
testImplementation("com.github.1robie.MachineEngine:machineengine-test:0.1.0")
}
repositories {
maven { url 'https://1robie.github.io/maven/' }
}
dependencies {
testImplementation 'com.github.1robie.MachineEngine:machineengine-test:0.1.0'
}
<repositories>
<repository>
<id>1robie</id>
<url>https://1robie.github.io/maven/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.1robie.MachineEngine</groupId>
<artifactId>machineengine-test</artifactId>
<version>0.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
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).
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.