diff --git a/src/main/java/li/cil/oc2/api/bus/DeviceBusController.java b/src/main/java/li/cil/oc2/api/bus/DeviceBusController.java index 21bcae7a..ba5bd538 100644 --- a/src/main/java/li/cil/oc2/api/bus/DeviceBusController.java +++ b/src/main/java/li/cil/oc2/api/bus/DeviceBusController.java @@ -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 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 getDevice(final UUID uuid); } diff --git a/src/main/java/li/cil/oc2/common/bus/DeviceBusControllerImpl.java b/src/main/java/li/cil/oc2/common/bus/RPCAdapter.java similarity index 56% rename from src/main/java/li/cil/oc2/common/bus/DeviceBusControllerImpl.java rename to src/main/java/li/cil/oc2/common/bus/RPCAdapter.java index 6cb6d0d7..2ad3b92d 100644 --- a/src/main/java/li/cil/oc2/common/bus/DeviceBusControllerImpl.java +++ b/src/main/java/li/cil/oc2/common/bus/RPCAdapter.java @@ -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 elements = new HashSet<>(); - private final ConcurrentHashMap 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> 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 group : groupedDevices.values()) { - final Device device = selectDeviceDeterministically(group); - devices.putIfAbsent(device.getUniqueIdentifier(), device); - } - } - - @Override - public Collection 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 queue = new Stack<>(); - final HashSet seenEdges = new HashSet<>(); // to avoid duplicate edge scans - final HashSet 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 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 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 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 = 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"; diff --git a/src/main/java/li/cil/oc2/common/bus/TileEntityDeviceBusController.java b/src/main/java/li/cil/oc2/common/bus/TileEntityDeviceBusController.java new file mode 100644 index 00000000..f200f698 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/bus/TileEntityDeviceBusController.java @@ -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 elements = new HashSet<>(); + private final ConcurrentHashMap 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> 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 group : groupedDevices.values()) { + final Device device = selectDeviceDeterministically(group); + devices.putIfAbsent(device.getUniqueIdentifier(), device); + } + } + + @Override + public Collection getDevices() { + return devices.values(); + } + + @Override + public Optional 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 queue = new Stack<>(); + final HashSet seenEdges = new HashSet<>(); // to avoid duplicate edge scans + final HashSet 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 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 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 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); + } + } +} diff --git a/src/main/java/li/cil/oc2/common/capabilities/DeviceBusControllerCapability.java b/src/main/java/li/cil/oc2/common/capabilities/DeviceBusControllerCapability.java index a1d50ace..beb4c4d2 100644 --- a/src/main/java/li/cil/oc2/common/capabilities/DeviceBusControllerCapability.java +++ b/src/main/java/li/cil/oc2/common/capabilities/DeviceBusControllerCapability.java @@ -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 getDevices() { return Collections.emptyList(); } + + @Override + public Optional getDevice(final UUID uuid) { + return Optional.empty(); + } } } diff --git a/src/main/java/li/cil/oc2/common/capabilities/DeviceBusElementCapability.java b/src/main/java/li/cil/oc2/common/capabilities/DeviceBusElementCapability.java index 1fae7e17..e3dd0340 100644 --- a/src/main/java/li/cil/oc2/common/capabilities/DeviceBusElementCapability.java +++ b/src/main/java/li/cil/oc2/common/capabilities/DeviceBusElementCapability.java @@ -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 oldControllers = new ArrayList<>(controllers); + for (final DeviceBusController controller : oldControllers) { controller.scheduleBusScan(); } + assert controllers.isEmpty(); } } } diff --git a/src/main/java/li/cil/oc2/serialization/serializers/MessageJsonDeserializer.java b/src/main/java/li/cil/oc2/serialization/serializers/MessageJsonDeserializer.java index 326f1ef5..98e4a440 100644 --- a/src/main/java/li/cil/oc2/serialization/serializers/MessageJsonDeserializer.java +++ b/src/main/java/li/cil/oc2/serialization/serializers/MessageJsonDeserializer.java @@ -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 { +public final class MessageJsonDeserializer implements JsonDeserializer { @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); } } diff --git a/src/main/java/li/cil/oc2/serialization/serializers/MethodInvocationJsonDeserializer.java b/src/main/java/li/cil/oc2/serialization/serializers/MethodInvocationJsonDeserializer.java index 13a76d6e..4f3472a0 100644 --- a/src/main/java/li/cil/oc2/serialization/serializers/MethodInvocationJsonDeserializer.java +++ b/src/main/java/li/cil/oc2/serialization/serializers/MethodInvocationJsonDeserializer.java @@ -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 { +public final class MethodInvocationJsonDeserializer implements JsonDeserializer { @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()); } } diff --git a/src/test/java/li/cil/oc2/bus/DeviceBusTests.java b/src/test/java/li/cil/oc2/bus/DeviceBusTests.java index 4e949936..8647d77e 100644 --- a/src/test/java/li/cil/oc2/bus/DeviceBusTests.java +++ b/src/test/java/li/cil/oc2/bus/DeviceBusTests.java @@ -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 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; } } diff --git a/src/test/java/li/cil/oc2/vm/ObjectDeviceProtocolTests.java b/src/test/java/li/cil/oc2/vm/RPCAdapterTests.java similarity index 76% rename from src/test/java/li/cil/oc2/vm/ObjectDeviceProtocolTests.java rename to src/test/java/li/cil/oc2/vm/RPCAdapterTests.java index 5e30d523..285f9bb8 100644 --- a/src/test/java/li/cil/oc2/vm/ObjectDeviceProtocolTests.java +++ b/src/test/java/li/cil/oc2/vm/RPCAdapterTests.java @@ -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 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);