Split up bus controller implementation and RPC logic and properly catch multiple controllers on one bus.

This commit is contained in:
Florian Nücke
2020-12-02 01:43:45 +01:00
parent 47c4486a1b
commit c9931123c8
9 changed files with 327 additions and 303 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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