getDevices();
+
+ /**
+ * Schedules a rescan of the device bus.
+ *
+ * This will cause the internal device bus controller to discard the current bus
+ * state and scan for connected bus segments at an unspecified time in the future
+ * (typically during the next tick).
+ *
+ * This should be called on all neighboring {@link DeviceBus} instances when a
+ * {@link DeviceBus} is created, typically when a block is placed/runs its first
+ * update after a load.
+ *
+ * Technically this is a convenience method. It is equivalent to querying for a
+ * {@link DeviceBusElement}, checking if a controller is set and then scheduling
+ * a scan on the controller, if present. This way regular code will only ever
+ * have to interact with this interface.
+ */
+ void scheduleScan();
+
+// long addDevice(MemoryMappedDevice device);
+//
+// void addDevice(final long address, MemoryMappedDevice device);
+//
+// void removeDevice(MemoryMappedDevice device);
+}
diff --git a/src/main/java/li/cil/oc2/api/bus/DeviceBusController.java b/src/main/java/li/cil/oc2/api/bus/DeviceBusController.java
new file mode 100644
index 00000000..247ade15
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/bus/DeviceBusController.java
@@ -0,0 +1,58 @@
+package li.cil.oc2.api.bus;
+
+import li.cil.oc2.api.device.Device;
+
+import java.util.Collection;
+
+/**
+ * For each device bus there can be exactly one controller. The controller performs the
+ * actual scan for adjacent {@link DeviceBusElement}s and registers itself with them via
+ * {@link DeviceBusElement#setController(DeviceBusController)}.
+ *
+ * This interface is usually provided by VM containers and used to collect connected
+ * {@link Device}s by aggregating the devices that were added to the device bus elements
+ * via {@link DeviceBusElement#addDevice(Device)}.
+ *
+ * The only way for {@link DeviceBusElement}s to be added to a bus is for a
+ * {@link DeviceBusController} to detect them during a scan.
+ *
+ * This interface is only of relevance when implementing a VM container or a bus element,
+ * i.e. something that acts as a "cable" or otherwise extends the bus itself.
+ *
+ * @see DeviceBusElement
+ */
+public interface DeviceBusController {
+ /**
+ * Schedules a scan.
+ *
+ * This will immediately invalidate the current bus, i.e. all {@link DeviceBusElement}s
+ * will be removed from the controller and {@link #getDevices()} will return an empty
+ * list after this call.
+ *
+ * Multiple sequential calls to this method do nothing, the actual scan will be performed
+ * in the next update.
+ */
+ void scheduleBusScan();
+
+ /**
+ * Forces a device map rebuild.
+ *
+ * This causes the controller to query all registered {@link DeviceBusElement}s for their
+ * current devices and update the aggregated list of devices. Unlike {@link #scheduleBusScan()}
+ * this operation runs synchronously. The list of devices known to the controller will be
+ * updated when this method returns.
+ *
+ * This should be called when the list of devices of a {@link DeviceBusElement} changes.
+ */
+ void scanDevices();
+
+ /**
+ * The list of all devices currently known to this controller.
+ *
+ * This is the aggregation of all {@link Device} added to all {@link DeviceBusElement}s known
+ * to the controller as found during the last scan scheduled via {@link #scheduleBusScan()}.
+ *
+ * @return the list of all devices on the bus managed by this controller.
+ */
+ Collection getDevices();
+}
diff --git a/src/main/java/li/cil/oc2/api/bus/DeviceBusElement.java b/src/main/java/li/cil/oc2/api/bus/DeviceBusElement.java
new file mode 100644
index 00000000..c496372e
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/bus/DeviceBusElement.java
@@ -0,0 +1,52 @@
+package li.cil.oc2.api.bus;
+
+import li.cil.oc2.api.device.Device;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.Optional;
+
+/**
+ * Represents a single connection point on a device bus.
+ *
+ * The only way for {@link DeviceBusElement}s to be added to a bus is for a
+ * {@link DeviceBusController} to detect them during a scan.
+ *
+ * When discovered during a scan, the controller will then use the devices
+ * connected to this element.
+ *
+ * This interface is only relevant when implementing it, e.g. to provide a custom
+ * "cable" or other means to extend a bus.
+ */
+public interface DeviceBusElement extends DeviceBus {
+ /**
+ * The controller this bus element is currently registered with, if any.
+ *
+ * @return the current controller.
+ */
+ Optional getController();
+
+ /**
+ * Sets the controller this bus element is now registered with, if any.
+ *
+ * This will be called by {@link DeviceBusController}s when scanning.
+ *
+ * @param controller the new controller.
+ */
+ void setController(@Nullable final DeviceBusController controller);
+
+ /**
+ * Returns the list of devices connected specifically by this element.
+ *
+ * This differs from {@link #getDevices()} in such that {@link #getDevices()} will
+ * return all devices connected to the controller, if this element is registered
+ * with a controller.
+ *
+ * This method is called by the {@link DeviceBusController} the element is registered
+ * with when the global list of devices is rebuilt, e.g. after a call to
+ * {@link DeviceBusController#scanDevices()}.
+ *
+ * @return the devices that have been added to this element.
+ */
+ Collection getLocalDevices();
+}
diff --git a/src/main/java/li/cil/oc2/api/bus/package-info.java b/src/main/java/li/cil/oc2/api/bus/package-info.java
new file mode 100644
index 00000000..5313524c
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/bus/package-info.java
@@ -0,0 +1,7 @@
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+package li.cil.oc2.api.bus;
+
+import mcp.MethodsReturnNonnullByDefault;
+
+import javax.annotation.ParametersAreNonnullByDefault;
\ No newline at end of file
diff --git a/src/main/java/li/cil/oc2/api/device/AbstractDevice.java b/src/main/java/li/cil/oc2/api/device/AbstractDevice.java
new file mode 100644
index 00000000..290c8bd3
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/AbstractDevice.java
@@ -0,0 +1,56 @@
+package li.cil.oc2.api.device;
+
+import net.minecraft.nbt.CompoundNBT;
+import net.minecraftforge.common.util.INBTSerializable;
+
+import java.util.*;
+
+/**
+ * Convenience base class for {@link Device} implementations.
+ *
+ * In particular, this implements the logic needed for generating and
+ * storing the unique ID for this device.
+ */
+public abstract class AbstractDevice implements Device, INBTSerializable {
+ protected static final String UUID_NBT_TAG_NAME = "uuid";
+
+ protected final List typeNames;
+ protected UUID uuid;
+
+ protected AbstractDevice() {
+ this(Collections.emptyList());
+ }
+
+ protected AbstractDevice(final Collection typeNames) {
+ this.typeNames = new ArrayList<>(typeNames);
+ this.uuid = java.util.UUID.randomUUID();
+ }
+
+ @Override
+ public UUID getUniqueId() {
+ return uuid;
+ }
+
+ public void setUniqueId(final UUID uuid) {
+ this.uuid = uuid;
+ }
+
+ @Override
+ public List getTypeNames() {
+ return typeNames;
+ }
+
+ @Override
+ public CompoundNBT serializeNBT() {
+ final CompoundNBT nbt = new CompoundNBT();
+ nbt.putUniqueId(UUID_NBT_TAG_NAME, uuid);
+ return nbt;
+ }
+
+ @Override
+ public void deserializeNBT(final CompoundNBT nbt) {
+ if (nbt.hasUniqueId(UUID_NBT_TAG_NAME)) {
+ uuid = nbt.getUniqueId(UUID_NBT_TAG_NAME);
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/AbstractDeviceMethod.java b/src/main/java/li/cil/oc2/api/device/AbstractDeviceMethod.java
new file mode 100644
index 00000000..4e42b79c
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/AbstractDeviceMethod.java
@@ -0,0 +1,35 @@
+package li.cil.oc2.api.device;
+
+/**
+ * Convenience base class for {@link DeviceMethod} implementations.
+ */
+public abstract class AbstractDeviceMethod implements DeviceMethod {
+ protected final String name;
+ protected final Class> returnType;
+ protected final DeviceMethodParameter[] parameters;
+
+ protected AbstractDeviceMethod(final String name, final Class> returnType, final DeviceMethodParameter... parameters) {
+ this.name = name;
+ this.returnType = returnType;
+ this.parameters = parameters;
+ }
+
+ protected AbstractDeviceMethod(final String name, final DeviceMethodParameter... parameters) {
+ this(name, void.class, parameters);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Class> getReturnType() {
+ return returnType;
+ }
+
+ @Override
+ public DeviceMethodParameter[] getParameters() {
+ return parameters;
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/CompoundDevice.java b/src/main/java/li/cil/oc2/api/device/CompoundDevice.java
new file mode 100644
index 00000000..f82c1557
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/CompoundDevice.java
@@ -0,0 +1,95 @@
+package li.cil.oc2.api.device;
+
+import net.minecraft.nbt.CompoundNBT;
+import net.minecraft.nbt.INBT;
+import net.minecraftforge.common.util.INBTSerializable;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * A utility device type that allows grouping multiple {@link Device} instances.
+ *
+ * Serialization of contained devices requires the added devices' unique id to
+ * have been restored prior to calling {@link #deserializeNBT(CompoundNBT)}.
+ */
+public class CompoundDevice extends AbstractDevice {
+ private final ArrayList devices;
+
+ public CompoundDevice(final Collection devices) {
+ this.devices = new ArrayList<>(devices);
+ }
+
+ public CompoundDevice(final Device... devices) {
+ this(Arrays.asList(devices));
+ }
+
+ public CompoundDevice() {
+ this(Collections.emptyList());
+ }
+
+ /**
+ * The list of devices grouped in this device.
+ *
+ * Use this in case you need to inspect the current list of devices, add new
+ * devices or remove existing devices.
+ */
+ public List getDevices() {
+ return devices;
+ }
+
+ @Override
+ public List getTypeNames() {
+ return devices.stream()
+ .map(Device::getTypeNames)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List getMethods() {
+ return devices.stream()
+ .map(Device::getMethods)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public CompoundNBT serializeNBT() {
+ final CompoundNBT nbt = super.serializeNBT();
+
+ final CompoundNBT devicesNbt = new CompoundNBT();
+ for (final Device device : devices) {
+ if (device instanceof INBTSerializable) {
+ final INBTSerializable serializable = (INBTSerializable) device;
+ final String uuid = device.getUniqueId().toString();
+ final INBT deviceNbt = serializable.serializeNBT();
+ devicesNbt.put(uuid, deviceNbt);
+ }
+ }
+ nbt.put("devices", devicesNbt);
+
+ return nbt;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ public void deserializeNBT(final CompoundNBT nbt) {
+ super.deserializeNBT(nbt);
+
+ final CompoundNBT devicesNbt = nbt.getCompound("devices");
+ for (final Device device : devices) {
+ final String uuid = device.getUniqueId().toString();
+ if (!devicesNbt.contains(uuid)) {
+ continue;
+ }
+
+ if (device instanceof INBTSerializable) {
+ final INBTSerializable serializable = (INBTSerializable) device;
+ final INBT deviceNbt = devicesNbt.get(uuid);
+ serializable.deserializeNBT(deviceNbt);
+ }
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/Device.java b/src/main/java/li/cil/oc2/api/device/Device.java
new file mode 100644
index 00000000..1e3d4d30
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/Device.java
@@ -0,0 +1,43 @@
+package li.cil.oc2.api.device;
+
+import li.cil.oc2.api.bus.DeviceBus;
+import li.cil.oc2.api.device.object.ObjectDevice;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Defines a device that may be added to a {@link DeviceBus}.
+ *
+ * The easiest and hence recommended way of implementing this interface is to use
+ * the {@link ObjectDevice} class.
+ *
+ * @see ObjectDevice
+ */
+public interface Device {
+ /**
+ * An id unique to this device.
+ *
+ * This id must persist over save/load to prevent code in a running VM losing
+ * track of the device.
+ */
+ UUID getUniqueId();
+
+ /**
+ * A list of device type names for this device.
+ *
+ * Devices may be identified by multiple type names. Although every atomic
+ * implementation will usually only have one, when compounding such modular
+ * devices into a {@link CompoundDevice} all the underlying type names can
+ * thus be retained.
+ *
+ * In a more general sense, these can be considered tags the device can be
+ * referenced by inside a VM.
+ */
+ List getTypeNames();
+
+ /**
+ * The list of methods implemented by this device.
+ */
+ List getMethods();
+}
diff --git a/src/main/java/li/cil/oc2/api/device/DeviceMethod.java b/src/main/java/li/cil/oc2/api/device/DeviceMethod.java
new file mode 100644
index 00000000..9f8ecf50
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/DeviceMethod.java
@@ -0,0 +1,76 @@
+package li.cil.oc2.api.device;
+
+import li.cil.oc2.api.bus.DeviceBusController;
+import li.cil.oc2.api.device.object.ObjectDevice;
+
+import javax.annotation.Nullable;
+import java.util.Optional;
+
+/**
+ * Represents a single method that can be exposed by a {@link Device}.
+ *
+ * The easiest and hence recommended way of implementing this interface is to use
+ * the {@link ObjectDevice} class.
+ *
+ * @see ObjectDevice
+ */
+public interface DeviceMethod {
+ /**
+ * The name of the method.
+ *
+ * When invoked through a {@link DeviceBusController} this is what the method
+ * will be referenced by, so the name should be unlikely to be duplicated in
+ * another device to avoid ambiguity when devices are combined, e.g. in a
+ * {@link CompoundDevice}.
+ */
+ String getName();
+
+ /**
+ * The type of the values returned by this method.
+ */
+ Class> getReturnType();
+
+ /**
+ * The list of parameters this method accepts.
+ */
+ DeviceMethodParameter[] getParameters();
+
+ /**
+ * Called to run this method.
+ *
+ * Implementations should expect the passed {@code parameters} to match the
+ * declared parameters returned by {@link #getParameters()}. If the parameters
+ * do not match, and exception should be raised.
+ *
+ * Important: methods are expected to not irrevocably corrupt internal
+ * state, even when they throw an exception. As such, implementations should
+ * perform internal error handling to prevent state corruption and only throw
+ * exceptions to communicate that an error happened during the invocation.
+ *
+ * @param parameters the parameters for the method.
+ * @return the return value, or {@code null} if none.
+ * @throws Throwable if the parameters did not match or something inside the
+ * method caused an exception. The caller is responsible for
+ * catching these and passing them on appropriately.
+ */
+ @Nullable
+ Object invoke(Object... parameters) throws Throwable;
+
+ /**
+ * An optional description of the method.
+ *
+ * May be used inside VMs to generate documentation.
+ */
+ default Optional getDescription() {
+ return Optional.empty();
+ }
+
+ /**
+ * An optional description of the return value of this method.
+ *
+ * May be used inside VMs to generate documentation.
+ */
+ default Optional getReturnValueDescription() {
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/DeviceMethodParameter.java b/src/main/java/li/cil/oc2/api/device/DeviceMethodParameter.java
new file mode 100644
index 00000000..f5765a35
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/DeviceMethodParameter.java
@@ -0,0 +1,40 @@
+package li.cil.oc2.api.device;
+
+import li.cil.oc2.api.bus.DeviceBusController;
+
+import java.util.Optional;
+
+/**
+ * Describes a single parameter of a {@link DeviceMethod}.
+ */
+public interface DeviceMethodParameter {
+ /**
+ * The type of this parameter.
+ *
+ * This is used by {@link DeviceBusController}s to convert parameters from a lower
+ * level representation before passing it to {@link DeviceMethod#invoke(Object...)}.
+ * As such, the types used must be kept simple. As a rule of thumb, only primitives
+ * and POJOs should be used.
+ *
+ * @return the type of the parameter.
+ */
+ Class> getType();
+
+ /**
+ * An optional name of the parameter.
+ *
+ * May be used inside VMs to generate documentation.
+ */
+ default Optional getName() {
+ return Optional.empty();
+ }
+
+ /**
+ * An optional description of the parameter.
+ *
+ * May be used inside VMs to generate documentation.
+ */
+ default Optional getDescription() {
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/object/Callback.java b/src/main/java/li/cil/oc2/api/device/object/Callback.java
new file mode 100644
index 00000000..bfa0b4bd
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/object/Callback.java
@@ -0,0 +1,31 @@
+package li.cil.oc2.api.device.object;
+
+import li.cil.oc2.api.device.DeviceMethod;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Utility annotation to allow generating lists of {@link DeviceMethod}s using
+ * {@link Callbacks#collectMethods(Object)}.
+ *
+ * Intended to be used in classes instances of which are used in combination with
+ * {@link ObjectDevice} and subclasses of {@link ObjectDevice}.
+ *
+ * For
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Callback {
+ /**
+ * Option VM visible documentation of this method.
+ */
+ String description() default "";
+
+ /**
+ * Optional VM visible documentation of the values returned by this method.
+ */
+ String returnValueDescription() default "";
+}
diff --git a/src/main/java/li/cil/oc2/api/device/object/Callbacks.java b/src/main/java/li/cil/oc2/api/device/object/Callbacks.java
new file mode 100644
index 00000000..b6f5741b
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/object/Callbacks.java
@@ -0,0 +1,61 @@
+package li.cil.oc2.api.device.object;
+
+import li.cil.oc2.api.device.DeviceMethod;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Provides automated extraction of {@link DeviceMethod}s from instances of
+ * class with methods annotated with the {@link Callback} annotation.
+ *
+ * Prefer using {@link ObjectDevice} instead of using this class directly.
+ */
+public final class Callbacks {
+ private static final Logger LOGGER = LogManager.getLogger();
+ private static final HashMap, List> METHOD_BY_TYPE = new HashMap<>();
+
+ /**
+ * Collects all methods annotated with {@link Callback} in the specified object
+ * and generated {@link DeviceMethod}s for each one.
+ *
+ * The generated {@link DeviceMethod} will be bound to the passed object and
+ * can be called without needing to pass the object.
+ *
+ * For example:
+ *
+ * class Example {
+ * @Callback
+ * public void f(String a) { }
+ * }
+ *
+ * List methods = Callbacks.collectMethods(new Example());
+ * methods.get(0).invoke("argument");
+ *
+ *
+ * @param methodContainer an instance of a class with annotated methods.
+ * @return the list of methods extracted from the specified object.
+ */
+ public static List collectMethods(final Object methodContainer) {
+ final List reflectedMethods = METHOD_BY_TYPE.computeIfAbsent(methodContainer.getClass(), c -> Arrays.stream(c.getMethods())
+ .filter(m -> m.isAnnotationPresent(Callback.class))
+ .collect(Collectors.toList()));
+
+ final List methods = new ArrayList<>();
+ for (final Method method : reflectedMethods) {
+ try {
+ methods.add(new ObjectDeviceMethod(methodContainer, method));
+ } catch (final IllegalAccessException e) {
+ LOGGER.error("Failed accessing method [{}].", method);
+ }
+ }
+
+ return methods;
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/object/ObjectDevice.java b/src/main/java/li/cil/oc2/api/device/object/ObjectDevice.java
new file mode 100644
index 00000000..ef810a4e
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/object/ObjectDevice.java
@@ -0,0 +1,57 @@
+package li.cil.oc2.api.device.object;
+
+import li.cil.oc2.api.device.AbstractDevice;
+import li.cil.oc2.api.device.Device;
+import li.cil.oc2.api.device.DeviceMethod;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A reflection based implementation of {@link Device} using the {@link Callback}
+ * annotation to discover {@link DeviceMethod} in a target object via
+ * {@link Callbacks#collectMethods(Object)}.
+ *
+ * This class was designed targeting two possible use-cases:
+ *
+ * - Wrapping some separate object containing the annotated method.
+ * - Subclassing this type and implementing annotated methods in the subclass.
+ *
+ * The two sets of constructors are designed for these use cases, with the constructors
+ * targeting the workflow using an external object being {@code public}, the ones targeting
+ * subclassing being {@code protected}.
+ */
+public class ObjectDevice extends AbstractDevice {
+ private final List methods;
+
+ public ObjectDevice(final Object object, final List typeNames) {
+ super(typeNames);
+ this.methods = Callbacks.collectMethods(object);
+ }
+
+ public ObjectDevice(final Object object, final String typeName) {
+ this(object, Collections.singletonList(typeName));
+ }
+
+ public ObjectDevice(final Object object) {
+ this(object, Collections.emptyList());
+ }
+
+ protected ObjectDevice(final List typeNames) {
+ super(typeNames);
+ this.methods = Callbacks.collectMethods(this);
+ }
+
+ protected ObjectDevice(final String typeName) {
+ this(Collections.singletonList(typeName));
+ }
+
+ protected ObjectDevice() {
+ this(Collections.emptyList());
+ }
+
+ @Override
+ public List getMethods() {
+ return methods;
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/object/ObjectDeviceMethod.java b/src/main/java/li/cil/oc2/api/device/object/ObjectDeviceMethod.java
new file mode 100644
index 00000000..e6a8a389
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/object/ObjectDeviceMethod.java
@@ -0,0 +1,90 @@
+package li.cil.oc2.api.device.object;
+
+import li.cil.oc2.api.device.AbstractDeviceMethod;
+import li.cil.oc2.api.device.DeviceMethodParameter;
+import org.apache.logging.log4j.util.Strings;
+
+import javax.annotation.Nullable;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Not intended to be instantiated directly, see {@link Callbacks}.
+ *
+ * @see Callbacks
+ */
+public final class ObjectDeviceMethod extends AbstractDeviceMethod {
+ private final MethodHandle handle;
+ private final String description;
+ private final String returnValueDescription;
+
+ public ObjectDeviceMethod(final Object target, final Method method) throws IllegalAccessException {
+ super(method.getName(), method.getReturnType(), getParameters(method));
+
+ final Callback annotation = method.getAnnotation(Callback.class);
+ if (annotation == null) {
+ throw new IllegalArgumentException("Method without Callback annotation.");
+ }
+
+ this.handle = MethodHandles.lookup().unreflect(method).bindTo(target);
+ this.description = Strings.isNotBlank(annotation.description()) ? annotation.description() : null;
+ this.returnValueDescription = Strings.isNotBlank(annotation.returnValueDescription()) ? annotation.returnValueDescription() : null;
+ }
+
+ @Nullable
+ @Override
+ public Object invoke(final Object... parameters) throws Throwable {
+ return handle.invokeWithArguments(parameters);
+ }
+
+ @Override
+ public Optional getDescription() {
+ return Optional.ofNullable(description);
+ }
+
+ @Override
+ public Optional getReturnValueDescription() {
+ return Optional.ofNullable(returnValueDescription);
+ }
+
+ private static DeviceMethodParameter[] getParameters(final Method method) {
+ return Arrays.stream(method.getParameters())
+ .map(ReflectionParameter::new)
+ .toArray(DeviceMethodParameter[]::new);
+ }
+
+ private static final class ReflectionParameter implements DeviceMethodParameter {
+ private final Class> type;
+ @Nullable private final String name;
+ @Nullable private final String description;
+
+ public ReflectionParameter(final java.lang.reflect.Parameter parameter) {
+ this.type = parameter.getType();
+
+ final li.cil.oc2.api.device.object.Parameter annotation = parameter.getAnnotation(li.cil.oc2.api.device.object.Parameter.class);
+ final boolean hasName = annotation != null && Strings.isNotBlank(annotation.value());
+ final boolean hasDescription = annotation != null && Strings.isNotBlank(annotation.description());
+
+ this.name = hasName ? annotation.value() : null;
+ this.description = hasDescription ? annotation.description() : null;
+ }
+
+ @Override
+ public Class> getType() {
+ return type;
+ }
+
+ @Override
+ public Optional getName() {
+ return Optional.ofNullable(name);
+ }
+
+ @Override
+ public Optional getDescription() {
+ return Optional.ofNullable(description);
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/api/device/object/Parameter.java b/src/main/java/li/cil/oc2/api/device/object/Parameter.java
new file mode 100644
index 00000000..05299017
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/object/Parameter.java
@@ -0,0 +1,30 @@
+package li.cil.oc2.api.device.object;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation may be used to propagate the name of parameters and to
+ * provide VM visible documentation of this parameter for methods annotated
+ * with the {@link Callback} annotation.
+ *
+ * Java strips parameter names in non-debug builds, so the actual method
+ * parameter names cannot be retrieved directly.
+ *
+ * If this is not present, parameters will appear unnamed to the VM.
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Parameter {
+ /**
+ * The name of the parameter as seen by the VM.
+ */
+ String value();
+
+ /**
+ * Optional VM visible documentation of this parameter.
+ */
+ String description() default "";
+}
diff --git a/src/main/java/li/cil/oc2/api/device/object/package-info.java b/src/main/java/li/cil/oc2/api/device/object/package-info.java
new file mode 100644
index 00000000..f6655977
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/object/package-info.java
@@ -0,0 +1,7 @@
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+package li.cil.oc2.api.device.object;
+
+import mcp.MethodsReturnNonnullByDefault;
+
+import javax.annotation.ParametersAreNonnullByDefault;
\ No newline at end of file
diff --git a/src/main/java/li/cil/oc2/api/device/package-info.java b/src/main/java/li/cil/oc2/api/device/package-info.java
new file mode 100644
index 00000000..4f366c3a
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/device/package-info.java
@@ -0,0 +1,7 @@
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+package li.cil.oc2.api.device;
+
+import mcp.MethodsReturnNonnullByDefault;
+
+import javax.annotation.ParametersAreNonnullByDefault;
\ No newline at end of file
diff --git a/src/main/java/li/cil/oc2/common/CommonSetup.java b/src/main/java/li/cil/oc2/common/CommonSetup.java
index d20c35d4..29ae9bb3 100644
--- a/src/main/java/li/cil/oc2/common/CommonSetup.java
+++ b/src/main/java/li/cil/oc2/common/CommonSetup.java
@@ -1,5 +1,6 @@
package li.cil.oc2.common;
+import li.cil.oc2.common.capabilities.DeviceBusElementCapability;
import li.cil.oc2.common.network.Network;
import li.cil.oc2.common.vm.Allocator;
import li.cil.oc2.serialization.BlobStorage;
@@ -10,6 +11,8 @@ import net.minecraftforge.fml.event.server.FMLServerStoppedEvent;
public final class CommonSetup {
public static void run(final FMLCommonSetupEvent event) {
+ DeviceBusElementCapability.register();
+
Network.setup();
MinecraftForge.EVENT_BUS.addListener(CommonSetup::handleServerAboutToStart);
diff --git a/src/main/java/li/cil/oc2/common/capabilities/Capabilities.java b/src/main/java/li/cil/oc2/common/capabilities/Capabilities.java
new file mode 100644
index 00000000..a5cdc394
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/capabilities/Capabilities.java
@@ -0,0 +1,12 @@
+package li.cil.oc2.common.capabilities;
+
+import li.cil.oc2.api.bus.DeviceBus;
+import li.cil.oc2.api.bus.DeviceBusElement;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.CapabilityInject;
+
+public final class Capabilities {
+ @CapabilityInject(DeviceBus.class) @SuppressWarnings("FieldMayBeFinal")
+ public static Capability DEVICE_BUS_ELEMENT_CAPABILITY = null;
+
+}
diff --git a/src/main/java/li/cil/oc2/common/capabilities/DeviceBusElementCapability.java b/src/main/java/li/cil/oc2/common/capabilities/DeviceBusElementCapability.java
new file mode 100644
index 00000000..4a774e52
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/capabilities/DeviceBusElementCapability.java
@@ -0,0 +1,11 @@
+package li.cil.oc2.common.capabilities;
+
+import li.cil.oc2.api.bus.DeviceBusElement;
+import li.cil.oc2.common.vm.DeviceBusElementImpl;
+import net.minecraftforge.common.capabilities.CapabilityManager;
+
+public final class DeviceBusElementCapability {
+ public static void register() {
+ CapabilityManager.INSTANCE.register(DeviceBusElement.class, new NullStorage<>(), DeviceBusElementImpl::new);
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/capabilities/NullStorage.java b/src/main/java/li/cil/oc2/common/capabilities/NullStorage.java
new file mode 100644
index 00000000..a32802fb
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/capabilities/NullStorage.java
@@ -0,0 +1,19 @@
+package li.cil.oc2.common.capabilities;
+
+import net.minecraft.nbt.INBT;
+import net.minecraft.util.Direction;
+import net.minecraftforge.common.capabilities.Capability;
+
+import javax.annotation.Nullable;
+
+public class NullStorage implements Capability.IStorage {
+ @Nullable
+ @Override
+ public INBT writeNBT(final Capability capability, final T instance, final Direction side) {
+ return null;
+ }
+
+ @Override
+ public void readNBT(final Capability capability, final T instance, final Direction side, final INBT nbt) {
+ }
+}
diff --git a/src/test/java/li/cil/oc2/bus/DeviceBusTests.java b/src/test/java/li/cil/oc2/bus/DeviceBusTests.java
new file mode 100644
index 00000000..98b81c86
--- /dev/null
+++ b/src/test/java/li/cil/oc2/bus/DeviceBusTests.java
@@ -0,0 +1,79 @@
+package li.cil.oc2.bus;
+
+import li.cil.oc2.api.bus.DeviceBusElement;
+import li.cil.oc2.api.device.Device;
+import li.cil.oc2.common.capabilities.Capabilities;
+import li.cil.oc2.common.vm.DeviceBusControllerImpl;
+import li.cil.sedna.api.device.serial.SerialDevice;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.util.LazyOptional;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+
+import java.util.Collections;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+public class DeviceBusTests {
+ @Mock
+ private static Capability busElementCapability;
+
+ private World world;
+ private SerialDevice serialDevice;
+ private DeviceBusControllerImpl controller;
+
+ @BeforeAll
+ public static void setup() {
+ Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY = busElementCapability;
+ }
+
+ @BeforeEach
+ public void setupEach() {
+ world = mock(World.class);
+ serialDevice = mock(SerialDevice.class);
+ controller = new DeviceBusControllerImpl(serialDevice);
+ }
+
+ @Test
+ public void scanPendingWhenTileEntityNotLoaded() {
+ Assertions.assertEquals(DeviceBusControllerImpl.State.SCAN_PENDING,
+ controller.scan(world, new BlockPos(0, 0, 0)));
+ }
+
+ @Test
+ public void scanCompletesWhenNoNeighbors() {
+ when(world.chunkExists(anyInt(), anyInt())).thenReturn(true);
+ Assertions.assertEquals(DeviceBusControllerImpl.State.READY,
+ controller.scan(world, new BlockPos(0, 0, 0)));
+ }
+
+ @Test
+ public void scanSuccessfulWithLocalElement() {
+ when(world.chunkExists(anyInt(), anyInt())).thenReturn(true);
+
+ final TileEntity tileEntity = mock(TileEntity.class);
+ when(world.getTileEntity(eq(new BlockPos(0, 0, 0)))).thenReturn(tileEntity);
+
+ final DeviceBusElement busElement = mock(DeviceBusElement.class);
+ when(tileEntity.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElement));
+
+ final Device device = mock(Device.class);
+ when(busElement.getDevices()).thenReturn(Collections.singletonList(device));
+
+ when(device.getUniqueId()).thenReturn(UUID.randomUUID());
+
+ Assertions.assertEquals(DeviceBusControllerImpl.State.READY,
+ controller.scan(world, new BlockPos(0, 0, 0)));
+
+ verify(busElement).setController(controller);
+ Assertions.assertTrue(controller.getDevices().contains(device));
+ }
+}
diff --git a/src/test/java/li/cil/oc2/vm/AbstractTestMethod.java b/src/test/java/li/cil/oc2/vm/AbstractTestMethod.java
new file mode 100644
index 00000000..df13b7bf
--- /dev/null
+++ b/src/test/java/li/cil/oc2/vm/AbstractTestMethod.java
@@ -0,0 +1,54 @@
+package li.cil.oc2.vm;
+
+import li.cil.oc2.api.device.DeviceMethod;
+import li.cil.oc2.api.device.DeviceMethodParameter;
+
+import java.util.Optional;
+
+abstract class AbstractTestMethod implements DeviceMethod {
+ private final Class> returnType;
+ private final DeviceMethodParameter[] parameters;
+
+ protected AbstractTestMethod(final Class> returnType, final Class>... parameterTypes) {
+ this.returnType = returnType;
+ parameters = new DeviceMethodParameter[parameterTypes.length];
+ for (int i = 0; i < parameterTypes.length; i++) {
+ parameters[i] = new TestParameter("arg" + i, parameterTypes[i]);
+ }
+ }
+
+ @Override
+ public String getName() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public Class> getReturnType() {
+ return returnType;
+ }
+
+ @Override
+ public DeviceMethodParameter[] getParameters() {
+ return parameters;
+ }
+
+ private static final class TestParameter implements DeviceMethodParameter {
+ private final String name;
+ private final Class> type;
+
+ public TestParameter(final String name, final Class> type) {
+ this.name = name;
+ this.type = type;
+ }
+
+ @Override
+ public Optional getName() {
+ return Optional.ofNullable(name);
+ }
+
+ @Override
+ public Class> getType() {
+ return type;
+ }
+ }
+}
diff --git a/src/test/java/li/cil/oc2/vm/ObjectDeviceProtocolTests.java b/src/test/java/li/cil/oc2/vm/ObjectDeviceProtocolTests.java
new file mode 100644
index 00000000..75df9146
--- /dev/null
+++ b/src/test/java/li/cil/oc2/vm/ObjectDeviceProtocolTests.java
@@ -0,0 +1,256 @@
+package li.cil.oc2.vm;
+
+import com.google.gson.*;
+import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue;
+import li.cil.oc2.api.bus.DeviceBusElement;
+import li.cil.oc2.api.device.AbstractDevice;
+import li.cil.oc2.api.device.Device;
+import li.cil.oc2.api.device.DeviceMethod;
+import li.cil.oc2.api.device.object.Callback;
+import li.cil.oc2.api.device.object.ObjectDevice;
+import li.cil.oc2.api.device.object.Parameter;
+import li.cil.oc2.common.capabilities.Capabilities;
+import li.cil.oc2.common.vm.DeviceBusControllerImpl;
+import li.cil.oc2.common.vm.DeviceBusElementImpl;
+import li.cil.sedna.api.device.serial.SerialDevice;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.util.LazyOptional;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import javax.annotation.Nullable;
+import java.io.ByteArrayOutputStream;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ObjectDeviceProtocolTests {
+ private static final BlockPos CONTROLLER_POS = new BlockPos(0, 0, 0);
+
+ @Mock private Capability busElementCapability;
+ private World world;
+ private TestSerialDevice serialDevice;
+ private DeviceBusControllerImpl controller;
+ private DeviceBusElement busElement;
+
+ @BeforeEach
+ public void setupEach() {
+ MockitoAnnotations.initMocks(this);
+ Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY = busElementCapability;
+
+ serialDevice = new TestSerialDevice();
+ controller = new DeviceBusControllerImpl(serialDevice);
+ busElement = new DeviceBusElementImpl();
+
+ world = mock(World.class);
+ when(world.chunkExists(anyInt(), anyInt())).thenReturn(true);
+
+ final TileEntity tileEntity = mock(TileEntity.class);
+ when(world.getTileEntity(any())).thenReturn(tileEntity);
+
+ when(tileEntity.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElement));
+ }
+
+ @Test
+ public void resetAndReadDescriptor() {
+ final VoidIntMethod method = new VoidIntMethod();
+
+ busElement.addDevice(new TestDevice(method));
+ controller.scan(world, CONTROLLER_POS);
+
+ final JsonObject request = new JsonObject();
+ request.addProperty("type", "status");
+ serialDevice.putAsVM(request.toString());
+ controller.step(0); // process message
+
+ final String message = serialDevice.readMessageAsVM();
+ Assertions.assertNotNull(message);
+
+ final JsonObject json = new JsonParser().parse(message).getAsJsonObject();
+
+ final JsonArray devices = json.getAsJsonArray("data");
+ Assertions.assertEquals(1, devices.size());
+
+ final JsonObject device = devices.get(0).getAsJsonObject();
+
+ final JsonArray methods = device.getAsJsonArray("methods");
+ Assertions.assertEquals(1, methods.size());
+ }
+
+ @Test
+ public void simpleMethod() {
+ final VoidIntMethod method = new VoidIntMethod();
+ final TestDevice device = new TestDevice(method);
+
+ busElement.addDevice(device);
+ controller.scan(world, CONTROLLER_POS);
+
+ invokeMethod(device, method.getName(), 0xdeadbeef);
+
+ Assertions.assertEquals(0xdeadbeef, method.passedValue);
+ }
+
+ @Test
+ public void returningMethod() {
+ final IntLongMethod method = new IntLongMethod();
+ final TestDevice device = new TestDevice(method);
+
+ busElement.addDevice(device);
+ controller.scan(world, CONTROLLER_POS);
+
+ final JsonElement result = invokeMethod(device, method.getName(), 0xdeadbeefcafebabeL);
+ Assertions.assertNotNull(result);
+ Assertions.assertTrue(result.isJsonPrimitive());
+ Assertions.assertEquals(0xcafebabe, result.getAsInt());
+ }
+
+ @Test
+ public void annotatedObject() {
+ final SimpleObject object = new SimpleObject();
+ final ObjectDevice device = new ObjectDevice(object);
+
+ busElement.addDevice(device);
+ controller.scan(world, CONTROLLER_POS);
+
+ Assertions.assertEquals(42 + 23, invokeMethod(device, "add", 42, 23).getAsInt());
+ }
+
+ private JsonElement invokeMethod(final Device device, final String name, final Object... parameters) {
+ final JsonObject request = new JsonObject();
+ request.addProperty("type", "invoke");
+ final JsonObject methodInvocation = new JsonObject();
+ methodInvocation.addProperty("deviceId", device.getUniqueId().toString());
+ methodInvocation.addProperty("name", name);
+ final JsonArray parametersJson = new JsonArray();
+ methodInvocation.add("parameters", parametersJson);
+ for (final Object parameter : parameters) {
+ parametersJson.add(new Gson().toJson(parameter));
+ }
+ request.add("data", methodInvocation);
+ serialDevice.putAsVM(request.toString());
+
+ controller.step(0);
+
+ final String result = serialDevice.readMessageAsVM();
+ Assertions.assertNotNull(result);
+ final JsonObject resultJson = new JsonParser().parse(result).getAsJsonObject();
+ Assertions.assertEquals("result", resultJson.get("type").getAsString());
+ return resultJson.get("data");
+ }
+
+ private static final class VoidIntMethod extends AbstractTestMethod {
+ public int passedValue;
+
+ VoidIntMethod() {
+ super(void.class, int.class);
+ }
+
+ @Override
+ public Object invoke(final Object... parameters) {
+ passedValue = (int) parameters[0];
+ return 0;
+ }
+ }
+
+ private static final class IntLongMethod extends AbstractTestMethod {
+ public long passedValue;
+
+ IntLongMethod() {
+ super(int.class, long.class);
+ }
+
+ @Override
+ public Object invoke(final Object... parameters) {
+ passedValue = (long) parameters[0];
+ return (int) passedValue;
+ }
+ }
+
+ public static final class SimpleObject {
+ @Callback
+ public int add(@Parameter("a") final int a,
+ @Parameter("b") final int b) {
+ return a + b;
+ }
+
+ @Callback
+ public int div(@Parameter("a") final long a,
+ @Parameter("b") final long b) {
+ return (int) (a / b);
+ }
+ }
+
+ private static final class TestSerialDevice implements SerialDevice {
+ private final ByteArrayFIFOQueue transmit = new ByteArrayFIFOQueue();
+ private final ByteArrayFIFOQueue receive = new ByteArrayFIFOQueue();
+
+ public void putAsVM(final String data) {
+ final byte[] bytes = data.getBytes();
+ for (int i = 0; i < bytes.length; i++) {
+ transmit.enqueue(bytes[i]);
+ }
+ transmit.enqueue((byte) 0);
+ }
+
+ @Nullable
+ public String readMessageAsVM() {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ while (!receive.isEmpty()) {
+ final byte value = receive.dequeueByte();
+
+ if (value == 0) {
+ if (bytes.size() == 0) {
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ bytes.write(value);
+ }
+
+ if (bytes.size() > 0) {
+ return new String(bytes.toByteArray());
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int read() {
+ return transmit.isEmpty() ? -1 : transmit.dequeueByte();
+ }
+
+ @Override
+ public boolean canPutByte() {
+ return true;
+ }
+
+ @Override
+ public void putByte(final byte value) {
+ receive.enqueue(value);
+ }
+ }
+
+ private static final class TestDevice extends AbstractDevice {
+ private final DeviceMethod method;
+
+ public TestDevice(final DeviceMethod method) {
+ this.method = method;
+ }
+
+ @Override
+ public List getMethods() {
+ return Collections.singletonList(method);
+ }
+ }
+}
diff --git a/src/test/java/li/cil/oc2/vm/package-info.java b/src/test/java/li/cil/oc2/vm/package-info.java
new file mode 100644
index 00000000..2e7ece1f
--- /dev/null
+++ b/src/test/java/li/cil/oc2/vm/package-info.java
@@ -0,0 +1,7 @@
+@ParametersAreNonnullByDefault
+@MethodsReturnNonnullByDefault
+package li.cil.oc2.vm;
+
+import mcp.MethodsReturnNonnullByDefault;
+
+import javax.annotation.ParametersAreNonnullByDefault;
\ No newline at end of file