First device API draft.

This commit is contained in:
Florian Nücke
2020-11-28 23:05:24 +01:00
parent 82d4e0b863
commit 3839ee5da4
25 changed files with 1253 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
package li.cil.oc2.api.bus;
import li.cil.oc2.api.device.Device;
import java.util.Collection;
/**
* A device bus provides the interface by which {@link Device} can be made available
* to a {@link DeviceBusController}, which is usually used by VMs to access devices.
* <p>
*/
public interface DeviceBus {
/**
* Adds a device to this device bus.
* <p>
* Adding a device to the bus does <em>not</em> transfer ownership. In particular,
* the bus will not handle persisting devices that have been added to it. Also,
* the bus does not persist the list of devices (that would imply it persisting
* the devices, as well). Instead, all devices that have been added to the bus
* must be added again after a load, at the latest during the first update/tick
* after the load.
*
* @param device the device to add to the bus.
*/
void addDevice(Device device);
/**
* Removes a device from this device bus.
* <p>
* If the device has not been added with {@link #addDevice(Device)} before calling
* this method, this method is a no-op.
*
* @param device the device to remove from the bus.
*/
void removeDevice(Device device);
/**
* The list of all devices currently registered with this device bus.
*
* @return the list of all devices that are currently on this bus.
*/
Collection<Device> getDevices();
/**
* Schedules a rescan of the device bus.
* <p>
* 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).
* <p>
* 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.
* <p>
* 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);
}

View File

@@ -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)}.
* <p>
* 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)}.
* <p>
* The only way for {@link DeviceBusElement}s to be added to a bus is for a
* {@link DeviceBusController} to detect them during a scan.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<Device> getDevices();
}

View File

@@ -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.
* <p>
* The only way for {@link DeviceBusElement}s to be added to a bus is for a
* {@link DeviceBusController} to detect them during a scan.
* <p>
* When discovered during a scan, the controller will then use the devices
* connected to this element.
* <p>
* 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<DeviceBusController> getController();
/**
* Sets the controller this bus element is now registered with, if any.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<Device> getLocalDevices();
}

View File

@@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package li.cil.oc2.api.bus;
import mcp.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -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.
* <p>
* In particular, this implements the logic needed for generating and
* storing the unique ID for this device.
*/
public abstract class AbstractDevice implements Device, INBTSerializable<CompoundNBT> {
protected static final String UUID_NBT_TAG_NAME = "uuid";
protected final List<String> typeNames;
protected UUID uuid;
protected AbstractDevice() {
this(Collections.emptyList());
}
protected AbstractDevice(final Collection<String> 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<String> 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);
}
}
}

View File

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

View File

@@ -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.
* <p>
* 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<Device> devices;
public CompoundDevice(final Collection<Device> 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.
* <p>
* Use this in case you need to inspect the current list of devices, add new
* devices or remove existing devices.
*/
public List<Device> getDevices() {
return devices;
}
@Override
public List<String> getTypeNames() {
return devices.stream()
.map(Device::getTypeNames)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
@Override
public List<DeviceMethod> 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);
}
}
}
}

View File

@@ -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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* In a more general sense, these can be considered tags the device can be
* referenced by inside a VM.
*/
List<String> getTypeNames();
/**
* The list of methods implemented by this device.
*/
List<DeviceMethod> getMethods();
}

View File

@@ -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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* <b>Important:</b> 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.
* <p>
* May be used inside VMs to generate documentation.
*/
default Optional<String> getDescription() {
return Optional.empty();
}
/**
* An optional description of the return value of this method.
* <p>
* May be used inside VMs to generate documentation.
*/
default Optional<String> getReturnValueDescription() {
return Optional.empty();
}
}

View File

@@ -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.
* <p>
* 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.
* <p>
* May be used inside VMs to generate documentation.
*/
default Optional<String> getName() {
return Optional.empty();
}
/**
* An optional description of the parameter.
* <p>
* May be used inside VMs to generate documentation.
*/
default Optional<String> getDescription() {
return Optional.empty();
}
}

View File

@@ -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)}.
* <p>
* Intended to be used in classes instances of which are used in combination with
* {@link ObjectDevice} and subclasses of {@link ObjectDevice}.
* <p>
* 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 "";
}

View File

@@ -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.
* <p>
* 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<Class<?>, List<Method>> METHOD_BY_TYPE = new HashMap<>();
/**
* Collects all methods annotated with {@link Callback} in the specified object
* and generated {@link DeviceMethod}s for each one.
* <p>
* The generated {@link DeviceMethod} will be bound to the passed object and
* can be called without needing to pass the object.
* <p>
* For example:
* <pre>
* class Example {
* &#64;Callback
* public void f(String a) { }
* }
*
* List<DeviceMethod> methods = Callbacks.collectMethods(new Example());
* methods.get(0).invoke("argument");
* </pre>
*
* @param methodContainer an instance of a class with annotated methods.
* @return the list of methods extracted from the specified object.
*/
public static List<DeviceMethod> collectMethods(final Object methodContainer) {
final List<Method> reflectedMethods = METHOD_BY_TYPE.computeIfAbsent(methodContainer.getClass(), c -> Arrays.stream(c.getMethods())
.filter(m -> m.isAnnotationPresent(Callback.class))
.collect(Collectors.toList()));
final List<DeviceMethod> 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;
}
}

View File

@@ -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)}.
* <p>
* This class was designed targeting two possible use-cases:
* <ul>
* <li>Wrapping some separate object containing the annotated method.</li>
* <li>Subclassing this type and implementing annotated methods in the subclass.</li>
* </ul>
* 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<DeviceMethod> methods;
public ObjectDevice(final Object object, final List<String> 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<String> 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<DeviceMethod> getMethods() {
return methods;
}
}

View File

@@ -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<String> getDescription() {
return Optional.ofNullable(description);
}
@Override
public Optional<String> 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<String> getName() {
return Optional.ofNullable(name);
}
@Override
public Optional<String> getDescription() {
return Optional.ofNullable(description);
}
}
}

View File

@@ -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.
* <p>
* Java strips parameter names in non-debug builds, so the actual method
* parameter names cannot be retrieved directly.
* <p>
* 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 "";
}

View File

@@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package li.cil.oc2.api.device.object;
import mcp.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package li.cil.oc2.api.device;
import mcp.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;

View File

@@ -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);

View File

@@ -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<DeviceBusElement> DEVICE_BUS_ELEMENT_CAPABILITY = null;
}

View File

@@ -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);
}
}

View File

@@ -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<T> implements Capability.IStorage<T> {
@Nullable
@Override
public INBT writeNBT(final Capability<T> capability, final T instance, final Direction side) {
return null;
}
@Override
public void readNBT(final Capability<T> capability, final T instance, final Direction side, final INBT nbt) {
}
}

View File

@@ -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<DeviceBusElement> 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));
}
}

View File

@@ -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<String> getName() {
return Optional.ofNullable(name);
}
@Override
public Class<?> getType() {
return type;
}
}
}

View File

@@ -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<DeviceBusElement> 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<DeviceMethod> getMethods() {
return Collections.singletonList(method);
}
}
}

View File

@@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package li.cil.oc2.vm;
import mcp.MethodsReturnNonnullByDefault;
import javax.annotation.ParametersAreNonnullByDefault;