Split up bus controller implementation and RPC logic and properly catch multiple controllers on one bus.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package li.cil.oc2.api.bus;
|
||||
|
||||
import li.cil.oc2.api.device.DeviceInterface;
|
||||
import li.cil.oc2.api.device.Device;
|
||||
import li.cil.oc2.api.device.DeviceInterface;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* For each device bus there can be exactly one controller. The controller performs the
|
||||
@@ -56,4 +58,12 @@ public interface DeviceBusController {
|
||||
* @return the list of all devices on the bus managed by this controller.
|
||||
*/
|
||||
Collection<Device> getDevices();
|
||||
|
||||
/**
|
||||
* Get the device with the specified unique identifier, if possible.
|
||||
*
|
||||
* @param uuid the id of the device to get.
|
||||
* @return the device with the specified id, if possible.
|
||||
*/
|
||||
Optional<Device> getDevice(final UUID uuid);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,10 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import li.cil.ceres.api.Serialized;
|
||||
import li.cil.oc2.api.bus.DeviceBusController;
|
||||
import li.cil.oc2.api.bus.DeviceBusElement;
|
||||
import li.cil.oc2.api.device.Device;
|
||||
import li.cil.oc2.api.device.DeviceInterface;
|
||||
import li.cil.oc2.api.device.DeviceMethod;
|
||||
import li.cil.oc2.api.device.DeviceMethodParameter;
|
||||
import li.cil.oc2.common.capabilities.Capabilities;
|
||||
import li.cil.oc2.common.device.DeviceMethodParameterTypeAdapters;
|
||||
import li.cil.oc2.serialization.serializers.DeviceJsonSerializer;
|
||||
import li.cil.oc2.serialization.serializers.DeviceMethodJsonSerializer;
|
||||
@@ -17,29 +15,16 @@ import li.cil.oc2.serialization.serializers.MessageJsonDeserializer;
|
||||
import li.cil.oc2.serialization.serializers.MethodInvocationJsonDeserializer;
|
||||
import li.cil.sedna.api.device.Steppable;
|
||||
import li.cil.sedna.api.device.serial.SerialDevice;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.Direction;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraftforge.common.util.LazyOptional;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
public enum State {
|
||||
SCAN_PENDING,
|
||||
TOO_COMPLEX,
|
||||
MULTIPLE_CONTROLLERS,
|
||||
READY,
|
||||
}
|
||||
|
||||
private static final int MAX_BUS_ELEMENT_COUNT = 128;
|
||||
public final class RPCAdapter implements Steppable {
|
||||
private static final int DEFAULT_MAX_MESSAGE_SIZE = 4 * 1024;
|
||||
private static final byte[] MESSAGE_DELIMITER = "\0".getBytes();
|
||||
|
||||
@@ -49,22 +34,21 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
public static final String ERROR_UNKNOWN_METHOD = "unknown method";
|
||||
public static final String ERROR_INVALID_PARAMETER_SIGNATURE = "invalid parameter signature";
|
||||
|
||||
private final Set<DeviceBusElement> elements = new HashSet<>();
|
||||
private final ConcurrentHashMap<UUID, Device> devices = new ConcurrentHashMap<>();
|
||||
private final DeviceBusController controller;
|
||||
|
||||
private final SerialDevice serialDevice;
|
||||
private final Gson gson;
|
||||
private int scanDelay;
|
||||
|
||||
@Serialized private final ByteBuffer transmitBuffer; // for data written to device by VM
|
||||
@Serialized private ByteBuffer receiveBuffer; // for data written by device to VM
|
||||
@Serialized private MethodInvocation synchronizedInvocation; // pending main thread invocation
|
||||
|
||||
public DeviceBusControllerImpl(final SerialDevice serialDevice) {
|
||||
this(serialDevice, DEFAULT_MAX_MESSAGE_SIZE);
|
||||
public RPCAdapter(final DeviceBusController controller, final SerialDevice serialDevice) {
|
||||
this(controller, serialDevice, DEFAULT_MAX_MESSAGE_SIZE);
|
||||
}
|
||||
|
||||
public DeviceBusControllerImpl(final SerialDevice serialDevice, final int maxMessageSize) {
|
||||
public RPCAdapter(final DeviceBusController controller, final SerialDevice serialDevice, final int maxMessageSize) {
|
||||
this.controller = controller;
|
||||
this.serialDevice = serialDevice;
|
||||
this.transmitBuffer = ByteBuffer.allocate(maxMessageSize);
|
||||
this.gson = DeviceMethodParameterTypeAdapters.beginBuildGson()
|
||||
@@ -75,138 +59,6 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleBusScan() {
|
||||
for (final DeviceBusElement element : elements) {
|
||||
element.removeController(this);
|
||||
}
|
||||
|
||||
elements.clear();
|
||||
devices.clear();
|
||||
|
||||
scanDelay = 0; // scan as soon as possible
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanDevices() {
|
||||
devices.clear();
|
||||
|
||||
final HashMap<DeviceInterface, ArrayList<Device>> groupedDevices = new HashMap<>();
|
||||
|
||||
for (final DeviceBusElement element : elements) {
|
||||
for (final Device device : element.getLocalDevices()) {
|
||||
groupedDevices.computeIfAbsent(device.getIdentifiedDevice(), d -> new ArrayList<>()).add(device);
|
||||
}
|
||||
}
|
||||
|
||||
for (final ArrayList<Device> group : groupedDevices.values()) {
|
||||
final Device device = selectDeviceDeterministically(group);
|
||||
devices.putIfAbsent(device.getUniqueIdentifier(), device);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Device> getDevices() {
|
||||
return devices.values();
|
||||
}
|
||||
|
||||
public State scan(final World world, final BlockPos start) {
|
||||
if (scanDelay < 0) {
|
||||
return State.READY;
|
||||
}
|
||||
|
||||
if (scanDelay-- > 0) {
|
||||
return State.SCAN_PENDING;
|
||||
}
|
||||
|
||||
assert scanDelay == -1;
|
||||
|
||||
final Stack<ScanEdge> queue = new Stack<>();
|
||||
final HashSet<ScanEdge> seenEdges = new HashSet<>(); // to avoid duplicate edge scans
|
||||
final HashSet<BlockPos> busPositions = new HashSet<>(); // to track number of seen blocks for limit
|
||||
|
||||
final Direction[] faces = Direction.values();
|
||||
for (final Direction face : faces) {
|
||||
final ScanEdge edgeIn = new ScanEdge(start, face);
|
||||
queue.add(edgeIn);
|
||||
seenEdges.add(edgeIn);
|
||||
}
|
||||
|
||||
// When we belong to a bus with multiple controllers we finish the scan and register
|
||||
// with all bus elements so that an element can easily trigger a scan on all connected
|
||||
// controllers -- without having to scan through the bus itself.
|
||||
boolean hasMultipleControllers = false;
|
||||
while (!queue.isEmpty()) {
|
||||
final ScanEdge edge = queue.pop();
|
||||
assert seenEdges.contains(edge);
|
||||
|
||||
final ChunkPos chunkPos = new ChunkPos(edge.position);
|
||||
if (!world.chunkExists(chunkPos.x, chunkPos.z)) {
|
||||
// If we have an unloaded chunk neighbor we cannot know whether our neighbor in that
|
||||
// chunk would cause a scan once it is loaded, so we'll just retry every so often.
|
||||
scanDelay = 20;
|
||||
elements.clear();
|
||||
return State.SCAN_PENDING;
|
||||
}
|
||||
|
||||
final TileEntity tileEntity = world.getTileEntity(edge.position);
|
||||
if (tileEntity == null) {
|
||||
for (final Direction face : faces) {
|
||||
seenEdges.add(new ScanEdge(edge.position, face));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tileEntity.getCapability(Capabilities.DEVICE_BUS_CONTROLLER_CAPABILITY, edge.face)
|
||||
.map(controller -> Objects.equals(controller, this)).orElse(false)) {
|
||||
hasMultipleControllers = true;
|
||||
}
|
||||
|
||||
final LazyOptional<DeviceBusElement> capability = tileEntity.getCapability(Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY, edge.face);
|
||||
if (capability.isPresent()) {
|
||||
if (busPositions.add(edge.position) && busPositions.size() > MAX_BUS_ELEMENT_COUNT) {
|
||||
elements.clear();
|
||||
return State.TOO_COMPLEX; // This return is the reason this is not in the ifPresent below.
|
||||
}
|
||||
}
|
||||
|
||||
capability.ifPresent(element -> {
|
||||
elements.add(element);
|
||||
|
||||
for (final Direction face : faces) {
|
||||
final LazyOptional<DeviceBusElement> otherCapability = tileEntity.getCapability(Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY, face);
|
||||
otherCapability.ifPresent(otherElement -> {
|
||||
final boolean isConnectedToIncomingEdge = Objects.equals(otherElement, element);
|
||||
if (!isConnectedToIncomingEdge) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ScanEdge edgeIn = new ScanEdge(edge.position, face);
|
||||
seenEdges.add(edgeIn);
|
||||
|
||||
final ScanEdge edgeOut = new ScanEdge(edge.position.offset(face), face.getOpposite());
|
||||
if (seenEdges.add(edgeOut)) {
|
||||
queue.add(edgeOut);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (final DeviceBusElement element : elements) {
|
||||
element.addController(this);
|
||||
}
|
||||
|
||||
if (hasMultipleControllers) {
|
||||
return State.MULTIPLE_CONTROLLERS;
|
||||
}
|
||||
|
||||
scanDevices();
|
||||
|
||||
return State.READY;
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
if (synchronizedInvocation != null) {
|
||||
final MethodInvocation methodInvocation = synchronizedInvocation;
|
||||
@@ -220,18 +72,6 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
writeToDevice();
|
||||
}
|
||||
|
||||
private static Device selectDeviceDeterministically(final ArrayList<Device> devices) {
|
||||
Device deviceWithLowestUuid = devices.get(0);
|
||||
for (int i = 1; i < devices.size(); i++) {
|
||||
final Device device = devices.get(i);
|
||||
if (device.getUniqueIdentifier().compareTo(deviceWithLowestUuid.getUniqueIdentifier()) < 0) {
|
||||
deviceWithLowestUuid = device;
|
||||
}
|
||||
}
|
||||
|
||||
return deviceWithLowestUuid;
|
||||
}
|
||||
|
||||
private void readFromDevice() {
|
||||
// Only ever allow one pending message to avoid giving the VM the
|
||||
// power of uncontrollably inflating memory usage. Basically any
|
||||
@@ -305,8 +145,8 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
}
|
||||
|
||||
private void processMethodInvocation(final MethodInvocation methodInvocation, final boolean isMainThread) {
|
||||
final DeviceInterface device = devices.get(methodInvocation.deviceId);
|
||||
if (device == null) {
|
||||
final Optional<Device> device = controller.getDevice(methodInvocation.deviceId);
|
||||
if (!device.isPresent()) {
|
||||
writeError(ERROR_UNKNOWN_DEVICE);
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +157,7 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
// flexibility for free (devices may dynamically change their methods).
|
||||
String error = ERROR_UNKNOWN_METHOD;
|
||||
outer:
|
||||
for (final DeviceMethod method : device.getMethods()) {
|
||||
for (final DeviceMethod method : device.get().getMethods()) {
|
||||
if (!Objects.equals(method.getName(), methodInvocation.methodName)) {
|
||||
continue;
|
||||
}
|
||||
@@ -358,7 +198,7 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
}
|
||||
|
||||
private void writeStatus() {
|
||||
writeMessage(Message.MESSAGE_TYPE_STATUS, devices.values().toArray(new DeviceInterface[0]));
|
||||
writeMessage(Message.MESSAGE_TYPE_STATUS, controller.getDevices().toArray(new DeviceInterface[0]));
|
||||
}
|
||||
|
||||
private void writeError(final String message) {
|
||||
@@ -386,30 +226,6 @@ public class DeviceBusControllerImpl implements DeviceBusController, Steppable {
|
||||
receiveBuffer.flip();
|
||||
}
|
||||
|
||||
private static final class ScanEdge {
|
||||
public final BlockPos position;
|
||||
public final Direction face;
|
||||
|
||||
public ScanEdge(final BlockPos position, final Direction face) {
|
||||
this.position = position;
|
||||
this.face = face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final ScanEdge scanEdge = (ScanEdge) o;
|
||||
return position.equals(scanEdge.position) &&
|
||||
face == scanEdge.face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(position, face);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Message {
|
||||
// Device -> VM
|
||||
public static final String MESSAGE_TYPE_STATUS = "status";
|
||||
@@ -0,0 +1,216 @@
|
||||
package li.cil.oc2.common.bus;
|
||||
|
||||
import li.cil.oc2.api.bus.DeviceBusController;
|
||||
import li.cil.oc2.api.bus.DeviceBusElement;
|
||||
import li.cil.oc2.api.device.Device;
|
||||
import li.cil.oc2.api.device.DeviceInterface;
|
||||
import li.cil.oc2.common.capabilities.Capabilities;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.Direction;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraftforge.common.util.LazyOptional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class TileEntityDeviceBusController implements DeviceBusController {
|
||||
public enum State {
|
||||
SCAN_PENDING,
|
||||
TOO_COMPLEX,
|
||||
MULTIPLE_CONTROLLERS,
|
||||
READY,
|
||||
}
|
||||
|
||||
private static final int MAX_BUS_ELEMENT_COUNT = 128;
|
||||
|
||||
private final TileEntity tileEntity;
|
||||
|
||||
private final Set<DeviceBusElement> elements = new HashSet<>();
|
||||
private final ConcurrentHashMap<UUID, Device> devices = new ConcurrentHashMap<>();
|
||||
|
||||
private int scanDelay;
|
||||
|
||||
public TileEntityDeviceBusController(final TileEntity tileEntity) {
|
||||
this.tileEntity = tileEntity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scheduleBusScan() {
|
||||
for (final DeviceBusElement element : elements) {
|
||||
element.removeController(this);
|
||||
}
|
||||
|
||||
elements.clear();
|
||||
devices.clear();
|
||||
|
||||
scanDelay = 0; // scan as soon as possible
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanDevices() {
|
||||
devices.clear();
|
||||
|
||||
final HashMap<DeviceInterface, ArrayList<Device>> groupedDevices = new HashMap<>();
|
||||
|
||||
for (final DeviceBusElement element : elements) {
|
||||
for (final Device device : element.getLocalDevices()) {
|
||||
groupedDevices.computeIfAbsent(device.getIdentifiedDevice(), d -> new ArrayList<>()).add(device);
|
||||
}
|
||||
}
|
||||
|
||||
for (final ArrayList<Device> group : groupedDevices.values()) {
|
||||
final Device device = selectDeviceDeterministically(group);
|
||||
devices.putIfAbsent(device.getUniqueIdentifier(), device);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Device> getDevices() {
|
||||
return devices.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Device> getDevice(final UUID uuid) {
|
||||
return Optional.ofNullable(devices.get(uuid));
|
||||
}
|
||||
|
||||
public State scan() {
|
||||
if (scanDelay < 0) {
|
||||
return State.READY;
|
||||
}
|
||||
|
||||
if (scanDelay-- > 0) {
|
||||
return State.SCAN_PENDING;
|
||||
}
|
||||
|
||||
assert scanDelay == -1;
|
||||
|
||||
final World world = tileEntity.getWorld();
|
||||
if (world == null || world.isRemote()) {
|
||||
return State.SCAN_PENDING;
|
||||
}
|
||||
|
||||
final Stack<ScanEdge> queue = new Stack<>();
|
||||
final HashSet<ScanEdge> seenEdges = new HashSet<>(); // to avoid duplicate edge scans
|
||||
final HashSet<BlockPos> busPositions = new HashSet<>(); // to track number of seen blocks for limit
|
||||
|
||||
final Direction[] faces = Direction.values();
|
||||
for (final Direction face : faces) {
|
||||
final ScanEdge edgeIn = new ScanEdge(tileEntity.getPos(), face);
|
||||
queue.add(edgeIn);
|
||||
seenEdges.add(edgeIn);
|
||||
}
|
||||
|
||||
// When we belong to a bus with multiple controllers we finish the scan and register
|
||||
// with all bus elements so that an element can easily trigger a scan on all connected
|
||||
// controllers -- without having to scan through the bus itself.
|
||||
boolean hasMultipleControllers = false;
|
||||
while (!queue.isEmpty()) {
|
||||
final ScanEdge edge = queue.pop();
|
||||
assert seenEdges.contains(edge);
|
||||
|
||||
final ChunkPos chunkPos = new ChunkPos(edge.position);
|
||||
if (!world.chunkExists(chunkPos.x, chunkPos.z)) {
|
||||
// If we have an unloaded chunk neighbor we cannot know whether our neighbor in that
|
||||
// chunk would cause a scan once it is loaded, so we'll just retry every so often.
|
||||
scanDelay = 20;
|
||||
elements.clear();
|
||||
return State.SCAN_PENDING;
|
||||
}
|
||||
|
||||
final TileEntity tileEntity = world.getTileEntity(edge.position);
|
||||
if (tileEntity == null) {
|
||||
for (final Direction face : faces) {
|
||||
seenEdges.add(new ScanEdge(edge.position, face));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tileEntity.getCapability(Capabilities.DEVICE_BUS_CONTROLLER_CAPABILITY, edge.face)
|
||||
.map(controller -> !Objects.equals(controller, this)).orElse(false)) {
|
||||
hasMultipleControllers = true;
|
||||
}
|
||||
|
||||
final LazyOptional<DeviceBusElement> capability = tileEntity.getCapability(Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY, edge.face);
|
||||
if (capability.isPresent()) {
|
||||
if (busPositions.add(edge.position) && busPositions.size() > MAX_BUS_ELEMENT_COUNT) {
|
||||
elements.clear();
|
||||
return State.TOO_COMPLEX; // This return is the reason this is not in the ifPresent below.
|
||||
}
|
||||
}
|
||||
|
||||
capability.ifPresent(element -> {
|
||||
elements.add(element);
|
||||
|
||||
for (final Direction face : faces) {
|
||||
final LazyOptional<DeviceBusElement> otherCapability = tileEntity.getCapability(Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY, face);
|
||||
otherCapability.ifPresent(otherElement -> {
|
||||
final boolean isConnectedToIncomingEdge = Objects.equals(otherElement, element);
|
||||
if (!isConnectedToIncomingEdge) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ScanEdge edgeIn = new ScanEdge(edge.position, face);
|
||||
seenEdges.add(edgeIn);
|
||||
|
||||
final ScanEdge edgeOut = new ScanEdge(edge.position.offset(face), face.getOpposite());
|
||||
if (seenEdges.add(edgeOut)) {
|
||||
queue.add(edgeOut);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (final DeviceBusElement element : elements) {
|
||||
element.addController(this);
|
||||
}
|
||||
|
||||
if (hasMultipleControllers) {
|
||||
return State.MULTIPLE_CONTROLLERS;
|
||||
}
|
||||
|
||||
scanDevices();
|
||||
|
||||
return State.READY;
|
||||
}
|
||||
|
||||
private static Device selectDeviceDeterministically(final ArrayList<Device> devices) {
|
||||
Device deviceWithLowestUuid = devices.get(0);
|
||||
for (int i = 1; i < devices.size(); i++) {
|
||||
final Device device = devices.get(i);
|
||||
if (device.getUniqueIdentifier().compareTo(deviceWithLowestUuid.getUniqueIdentifier()) < 0) {
|
||||
deviceWithLowestUuid = device;
|
||||
}
|
||||
}
|
||||
|
||||
return deviceWithLowestUuid;
|
||||
}
|
||||
|
||||
private static final class ScanEdge {
|
||||
public final BlockPos position;
|
||||
public final Direction face;
|
||||
|
||||
public ScanEdge(final BlockPos position, final Direction face) {
|
||||
this.position = position;
|
||||
this.face = face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final ScanEdge scanEdge = (ScanEdge) o;
|
||||
return position.equals(scanEdge.position) &&
|
||||
face == scanEdge.face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(position, face);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import net.minecraftforge.common.capabilities.CapabilityManager;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class DeviceBusControllerCapability {
|
||||
public static void register() {
|
||||
@@ -25,5 +27,10 @@ public final class DeviceBusControllerCapability {
|
||||
public Collection<Device> getDevices() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Device> getDevice(final UUID uuid) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +61,12 @@ public final class DeviceBusElementCapability {
|
||||
|
||||
@Override
|
||||
public void scheduleScan() {
|
||||
for (final DeviceBusController controller : controllers) {
|
||||
// Controllers are expected to remove themselves when a scan is scheduled.
|
||||
final ArrayList<DeviceBusController> oldControllers = new ArrayList<>(controllers);
|
||||
for (final DeviceBusController controller : oldControllers) {
|
||||
controller.scheduleBusScan();
|
||||
}
|
||||
assert controllers.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
package li.cil.oc2.serialization.serializers;
|
||||
|
||||
import com.google.gson.*;
|
||||
import li.cil.oc2.common.bus.DeviceBusControllerImpl;
|
||||
import li.cil.oc2.common.bus.RPCAdapter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public final class MessageJsonDeserializer implements JsonDeserializer<DeviceBusControllerImpl.Message> {
|
||||
public final class MessageJsonDeserializer implements JsonDeserializer<RPCAdapter.Message> {
|
||||
@Override
|
||||
public DeviceBusControllerImpl.Message deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
|
||||
public RPCAdapter.Message deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
|
||||
final JsonObject jsonObject = json.getAsJsonObject();
|
||||
final String messageType = jsonObject.get("type").getAsString();
|
||||
final Object messageData;
|
||||
switch (messageType) {
|
||||
case DeviceBusControllerImpl.Message.MESSAGE_TYPE_STATUS: {
|
||||
case RPCAdapter.Message.MESSAGE_TYPE_STATUS: {
|
||||
messageData = null;
|
||||
break;
|
||||
}
|
||||
case DeviceBusControllerImpl.Message.MESSAGE_TYPE_INVOKE_METHOD: {
|
||||
messageData = context.deserialize(jsonObject.getAsJsonObject("data"), DeviceBusControllerImpl.MethodInvocation.class);
|
||||
case RPCAdapter.Message.MESSAGE_TYPE_INVOKE_METHOD: {
|
||||
messageData = context.deserialize(jsonObject.getAsJsonObject("data"), RPCAdapter.MethodInvocation.class);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new JsonParseException(DeviceBusControllerImpl.ERROR_UNKNOWN_MESSAGE_TYPE);
|
||||
throw new JsonParseException(RPCAdapter.ERROR_UNKNOWN_MESSAGE_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
return new DeviceBusControllerImpl.Message(messageType, messageData);
|
||||
return new RPCAdapter.Message(messageType, messageData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package li.cil.oc2.serialization.serializers;
|
||||
|
||||
import com.google.gson.*;
|
||||
import li.cil.oc2.common.bus.DeviceBusControllerImpl;
|
||||
import li.cil.oc2.common.bus.RPCAdapter;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class MethodInvocationJsonDeserializer implements JsonDeserializer<DeviceBusControllerImpl.MethodInvocation> {
|
||||
public final class MethodInvocationJsonDeserializer implements JsonDeserializer<RPCAdapter.MethodInvocation> {
|
||||
@Override
|
||||
public DeviceBusControllerImpl.MethodInvocation deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
|
||||
public RPCAdapter.MethodInvocation deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
|
||||
final JsonObject jsonObject = json.getAsJsonObject();
|
||||
final UUID deviceId = context.deserialize(jsonObject.get("deviceId"), UUID.class);
|
||||
final String methodName = jsonObject.get("name").getAsString();
|
||||
final JsonElement parameters = jsonObject.get("parameters");
|
||||
return new DeviceBusControllerImpl.MethodInvocation(deviceId, methodName, parameters != null && parameters.isJsonArray() ? parameters.getAsJsonArray() : new JsonArray());
|
||||
return new RPCAdapter.MethodInvocation(deviceId, methodName, parameters != null && parameters.isJsonArray() ? parameters.getAsJsonArray() : new JsonArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ package li.cil.oc2.bus;
|
||||
|
||||
import li.cil.oc2.api.bus.DeviceBusElement;
|
||||
import li.cil.oc2.api.device.Device;
|
||||
import li.cil.oc2.common.bus.DeviceBusControllerImpl;
|
||||
import li.cil.oc2.common.bus.TileEntityDeviceBusController;
|
||||
import li.cil.oc2.common.capabilities.Capabilities;
|
||||
import li.cil.sedna.api.device.serial.SerialDevice;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.world.World;
|
||||
@@ -28,8 +27,9 @@ public class DeviceBusTests {
|
||||
@Mock
|
||||
private Capability<DeviceBusElement> busElementCapability;
|
||||
private World world;
|
||||
private SerialDevice serialDevice;
|
||||
private DeviceBusControllerImpl controller;
|
||||
private TileEntity busControllerTileEntity;
|
||||
private TileEntityDeviceBusController busController;
|
||||
private DeviceBusElement busControllerBusElement;
|
||||
|
||||
@BeforeEach
|
||||
public void setupEach() {
|
||||
@@ -37,73 +37,70 @@ public class DeviceBusTests {
|
||||
Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY = busElementCapability;
|
||||
|
||||
world = mock(World.class);
|
||||
serialDevice = mock(SerialDevice.class);
|
||||
controller = new DeviceBusControllerImpl(serialDevice);
|
||||
|
||||
busControllerTileEntity = mock(TileEntity.class);
|
||||
when(busControllerTileEntity.getWorld()).thenReturn(world);
|
||||
when(busControllerTileEntity.getPos()).thenReturn(CONTROLLER_POS);
|
||||
when(busControllerTileEntity.getCapability(any(), any())).thenReturn(LazyOptional.empty());
|
||||
|
||||
when(world.getTileEntity(CONTROLLER_POS)).thenReturn(busControllerTileEntity);
|
||||
|
||||
busControllerBusElement = mock(DeviceBusElement.class);
|
||||
when(busControllerTileEntity.getCapability(eq(busElementCapability), any()))
|
||||
.thenReturn(LazyOptional.of(() -> busControllerBusElement));
|
||||
when(busControllerBusElement.getLocalDevices()).thenReturn(Collections.emptyList());
|
||||
|
||||
busController = new TileEntityDeviceBusController(busControllerTileEntity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scanPendingWhenTileEntityNotLoaded() {
|
||||
Assertions.assertEquals(DeviceBusControllerImpl.State.SCAN_PENDING,
|
||||
controller.scan(world, CONTROLLER_POS));
|
||||
Assertions.assertEquals(TileEntityDeviceBusController.State.SCAN_PENDING, busController.scan());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scanCompletesWhenNoNeighbors() {
|
||||
when(world.chunkExists(anyInt(), anyInt())).thenReturn(true);
|
||||
Assertions.assertEquals(DeviceBusControllerImpl.State.READY,
|
||||
controller.scan(world, CONTROLLER_POS));
|
||||
Assertions.assertEquals(TileEntityDeviceBusController.State.READY, busController.scan());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scanSuccessfulWithLocalElement() {
|
||||
when(world.chunkExists(anyInt(), anyInt())).thenReturn(true);
|
||||
|
||||
final TileEntity tileEntity = mock(TileEntity.class);
|
||||
when(world.getTileEntity(eq(CONTROLLER_POS))).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.getLocalDevices()).thenReturn(Collections.singletonList(device));
|
||||
when(busControllerBusElement.getLocalDevices()).thenReturn(Collections.singletonList(device));
|
||||
|
||||
when(device.getUniqueIdentifier()).thenReturn(UUID.randomUUID());
|
||||
|
||||
Assertions.assertEquals(DeviceBusControllerImpl.State.READY,
|
||||
controller.scan(world, CONTROLLER_POS));
|
||||
Assertions.assertEquals(TileEntityDeviceBusController.State.READY, busController.scan());
|
||||
|
||||
verify(busElement).addController(controller);
|
||||
Assertions.assertTrue(controller.getDevices().contains(device));
|
||||
verify(busControllerBusElement).addController(busController);
|
||||
Assertions.assertTrue(busController.getDevices().contains(device));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void scanSuccessfulWithMultipleElements() {
|
||||
when(world.chunkExists(anyInt(), anyInt())).thenReturn(true);
|
||||
|
||||
final TileEntity tileEntityController = mock(TileEntity.class);
|
||||
when(world.getTileEntity(eq(CONTROLLER_POS))).thenReturn(tileEntityController);
|
||||
final DeviceBusElement busElement1 = mockBusElement(CONTROLLER_POS.west());
|
||||
final DeviceBusElement busElement2 = mockBusElement(CONTROLLER_POS.west().west());
|
||||
|
||||
final DeviceBusElement busElementController = mock(DeviceBusElement.class);
|
||||
when(tileEntityController.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElementController));
|
||||
Assertions.assertEquals(TileEntityDeviceBusController.State.READY, busController.scan());
|
||||
|
||||
final TileEntity tileEntityBusElement1 = mock(TileEntity.class);
|
||||
when(world.getTileEntity(eq(CONTROLLER_POS.west()))).thenReturn(tileEntityBusElement1);
|
||||
verify(busElement1).addController(busController);
|
||||
verify(busElement2).addController(busController);
|
||||
}
|
||||
|
||||
final DeviceBusElement busElement1 = mock(DeviceBusElement.class);
|
||||
when(tileEntityBusElement1.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElement1));
|
||||
when(busElement1.getLocalDevices()).thenReturn(Collections.emptyList());
|
||||
private DeviceBusElement mockBusElement(final BlockPos pos) {
|
||||
final TileEntity tileEntity = mock(TileEntity.class);
|
||||
when(world.getTileEntity(pos)).thenReturn(tileEntity);
|
||||
when(tileEntity.getCapability(any(), any())).thenReturn(LazyOptional.empty());
|
||||
|
||||
final TileEntity tileEntityBusElement2 = mock(TileEntity.class);
|
||||
when(world.getTileEntity(eq(CONTROLLER_POS.west().west()))).thenReturn(tileEntityBusElement2);
|
||||
final DeviceBusElement busElement = mock(DeviceBusElement.class);
|
||||
when(tileEntity.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElement));
|
||||
when(busElement.getLocalDevices()).thenReturn(Collections.emptyList());
|
||||
|
||||
final DeviceBusElement busElement2 = mock(DeviceBusElement.class);
|
||||
when(tileEntityBusElement2.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElement2));
|
||||
when(busElement2.getLocalDevices()).thenReturn(Collections.emptyList());
|
||||
|
||||
Assertions.assertEquals(DeviceBusControllerImpl.State.READY,
|
||||
controller.scan(world, CONTROLLER_POS));
|
||||
|
||||
verify(busElement1).addController(controller);
|
||||
verify(busElement2).addController(controller);
|
||||
return busElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,98 +2,72 @@ 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.DeviceMethod;
|
||||
import li.cil.oc2.api.bus.DeviceBusController;
|
||||
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.ObjectDeviceInterface;
|
||||
import li.cil.oc2.api.device.object.Parameter;
|
||||
import li.cil.oc2.common.bus.DeviceBusControllerImpl;
|
||||
import li.cil.oc2.common.capabilities.Capabilities;
|
||||
import li.cil.oc2.common.capabilities.DeviceBusElementCapability;
|
||||
import li.cil.oc2.common.bus.RPCAdapter;
|
||||
import li.cil.oc2.common.device.DeviceImpl;
|
||||
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 java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
public class RPCAdapterTests {
|
||||
private TestSerialDevice serialDevice;
|
||||
private DeviceBusControllerImpl controller;
|
||||
private DeviceBusElement busElement;
|
||||
private DeviceBusController busController;
|
||||
private RPCAdapter rpcAdapter;
|
||||
|
||||
@BeforeEach
|
||||
public void setupEach() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
Capabilities.DEVICE_BUS_ELEMENT_CAPABILITY = busElementCapability;
|
||||
|
||||
serialDevice = new TestSerialDevice();
|
||||
controller = new DeviceBusControllerImpl(serialDevice);
|
||||
busElement = new DeviceBusElementCapability.Implementation();
|
||||
|
||||
world = mock(World.class);
|
||||
when(world.chunkExists(anyInt(), anyInt())).thenReturn(true);
|
||||
|
||||
final TileEntity tileEntity = mock(TileEntity.class);
|
||||
when(world.getTileEntity(eq(CONTROLLER_POS))).thenReturn(tileEntity);
|
||||
|
||||
when(tileEntity.getCapability(eq(busElementCapability), any())).thenReturn(LazyOptional.of(() -> busElement));
|
||||
busController = mock(DeviceBusController.class);
|
||||
rpcAdapter = new RPCAdapter(busController, serialDevice);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetAndReadDescriptor() {
|
||||
final VoidIntMethod method = new VoidIntMethod();
|
||||
|
||||
busElement.addDevice(new TestDeviceInterface(method));
|
||||
controller.scan(world, CONTROLLER_POS);
|
||||
final TestDeviceInterface device = new TestDeviceInterface(method);
|
||||
setDevice(device);
|
||||
|
||||
final JsonObject request = new JsonObject();
|
||||
request.addProperty("type", "status");
|
||||
serialDevice.putAsVM(request.toString());
|
||||
controller.step(0); // process message
|
||||
rpcAdapter.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 JsonArray devicesJson = json.getAsJsonArray("data");
|
||||
Assertions.assertEquals(1, devicesJson.size());
|
||||
|
||||
final JsonObject device = devices.get(0).getAsJsonObject();
|
||||
final JsonObject deviceJson = devicesJson.get(0).getAsJsonObject();
|
||||
|
||||
final JsonArray methods = device.getAsJsonArray("methods");
|
||||
Assertions.assertEquals(1, methods.size());
|
||||
final JsonArray methodsJson = deviceJson.getAsJsonArray("methods");
|
||||
Assertions.assertEquals(1, methodsJson.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleMethod() {
|
||||
final VoidIntMethod method = new VoidIntMethod();
|
||||
final TestDeviceInterface device = new TestDeviceInterface(method);
|
||||
|
||||
busElement.addDevice(device);
|
||||
controller.scan(world, CONTROLLER_POS);
|
||||
setDevice(device);
|
||||
|
||||
invokeMethod(device, method.getName(), 0xdeadbeef);
|
||||
|
||||
@@ -104,9 +78,7 @@ public class ObjectDeviceProtocolTests {
|
||||
public void returningMethod() {
|
||||
final IntLongMethod method = new IntLongMethod();
|
||||
final TestDeviceInterface device = new TestDeviceInterface(method);
|
||||
|
||||
busElement.addDevice(device);
|
||||
controller.scan(world, CONTROLLER_POS);
|
||||
setDevice(device);
|
||||
|
||||
final JsonElement result = invokeMethod(device, method.getName(), 0xdeadbeefcafebabeL);
|
||||
Assertions.assertNotNull(result);
|
||||
@@ -119,13 +91,16 @@ public class ObjectDeviceProtocolTests {
|
||||
final SimpleObject object = new SimpleObject();
|
||||
final ObjectDeviceInterface device = new ObjectDeviceInterface(object);
|
||||
final DeviceImpl identifiableDevice = new DeviceImpl(LazyOptional.of(() -> device), UUID.randomUUID());
|
||||
|
||||
busElement.addDevice(identifiableDevice);
|
||||
controller.scan(world, CONTROLLER_POS);
|
||||
setDevice(identifiableDevice);
|
||||
|
||||
Assertions.assertEquals(42 + 23, invokeMethod(identifiableDevice, "add", 42, 23).getAsInt());
|
||||
}
|
||||
|
||||
private void setDevice(final Device device) {
|
||||
when(busController.getDevices()).thenReturn(Collections.singletonList(device));
|
||||
when(busController.getDevice(device.getUniqueIdentifier())).thenReturn(Optional.of(device));
|
||||
}
|
||||
|
||||
private JsonElement invokeMethod(final Device device, final String name, final Object... parameters) {
|
||||
final JsonObject request = new JsonObject();
|
||||
request.addProperty("type", "invoke");
|
||||
@@ -140,7 +115,7 @@ public class ObjectDeviceProtocolTests {
|
||||
request.add("data", methodInvocation);
|
||||
serialDevice.putAsVM(request.toString());
|
||||
|
||||
controller.step(0);
|
||||
rpcAdapter.step(0);
|
||||
|
||||
final String result = serialDevice.readMessageAsVM();
|
||||
Assertions.assertNotNull(result);
|
||||
Reference in New Issue
Block a user