diff --git a/build.gradle b/build.gradle index 1a6d0f66..c7b8588e 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,6 @@ minecraft { source sourceSets.main } } - - arg "-mixin.config=oc2.mixins.json" } client { @@ -119,6 +117,10 @@ minecraft { mixin { add sourceSets.main, "oc2.refmap.json" + + config 'oc2.mixins.json' + + quiet } task copyGeneratedResources(type: Copy) { diff --git a/src/main/java/li/cil/oc2/api/bus/device/object/LifecycleAwareDevice.java b/src/main/java/li/cil/oc2/api/bus/device/object/LifecycleAwareDevice.java index 2badd061..5c6fdfc7 100644 --- a/src/main/java/li/cil/oc2/api/bus/device/object/LifecycleAwareDevice.java +++ b/src/main/java/li/cil/oc2/api/bus/device/object/LifecycleAwareDevice.java @@ -11,23 +11,20 @@ import net.minecraft.world.level.block.entity.BlockEntity; */ public interface LifecycleAwareDevice { /** - * This method corresponds to {@link RPCDevice#mount()}. It is called when the device is initialized, either - * because its virtual machine starts running, or because it is added to a running virtual machine. + * This method corresponds to {@link RPCDevice#mount()}. */ default void onDeviceMounted() { } /** - * This method corresponds to {@link RPCDevice#unmount()}. It is called when the device is disposed, either - * because its virtual machine stops running, or because it is removed from a running virtual machine. + * This method corresponds to {@link RPCDevice#unmount()}. */ default void onDeviceUnmounted() { } /** - * This method corresponds to {@link RPCDevice#suspend()}. It is called when its virtual machine is suspended, - * either due to the containing chunk being unloaded, or the containing world being unloaded. + * This method corresponds to {@link RPCDevice#dispose()}. */ - default void onDeviceSuspended() { + default void onDeviceDisposed() { } } diff --git a/src/main/java/li/cil/oc2/api/bus/device/object/ObjectDevice.java b/src/main/java/li/cil/oc2/api/bus/device/object/ObjectDevice.java index e5f1885e..afda5cd5 100644 --- a/src/main/java/li/cil/oc2/api/bus/device/object/ObjectDevice.java +++ b/src/main/java/li/cil/oc2/api/bus/device/object/ObjectDevice.java @@ -102,9 +102,9 @@ public final class ObjectDevice implements RPCDevice { } @Override - public void suspend() { + public void dispose() { if (object instanceof LifecycleAwareDevice device) { - device.onDeviceSuspended(); + device.onDeviceDisposed(); } } diff --git a/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java b/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java index f650d71c..f35f45d4 100644 --- a/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java +++ b/src/main/java/li/cil/oc2/api/bus/device/rpc/RPCDevice.java @@ -20,35 +20,27 @@ import java.util.List; *

* The lifecycle for {@link RPCDevice}s is as follows: *

- *    ┌──────────────────────────────────┐
- *    │VirtualMachine.isRunning() = false◄──────────────────────┐
- *    └────────────────┬─────────────────┘                      │
- *                     │                                        │
- *          ┌──────────▼───────────┐                            │
- *          │VirtualMachine.start()│                            │
- *          └──────────┬───────────┘                            │
- *                     │                                        │
- *                     │   ┌──────────┐                         │
- *                     │   │Chunk Load│   ┌───────────────────┐ │
- *                     ├───┼──────────◄───┤RPCDevice.suspend()│ │
- *                     │   │World Load│   └──────▲────────────┘ │
- *                     │   └──────────┘          │              │
- *                     │                         │              │
- *            ┌────────▼────────┐          ┌─────┴──────┐       │
- * ┌──────────►RPCDevice.mount()│          │Chunk Unload│       │
- * │          └────────┬────────┘        ┌─►────────────┤       │
- * │                   │                 │ │World Unload│       │
- * │ ┌─────────────────▼───────────────┐ │ └────────────┘       │
- * │ │VirtualMachine.isRunning() = true├─┤                      │
- * │ └─────┬───────────────────┬───────┘ │ ┌──────────────────┐ │
- * │       │                   │         │ │Computer Shutdown │ │
- * │ ┌─────▼──────┐     ┌──────▼───────┐ └─►──────────────────┤ │
- * └─┤Device Added│     │Device Removed│   │Computer Destroyed│ │
- *   └────────────┘     └──────┬───────┘   └─────┬────────────┘ │
- *                             │                 │              │
- *                  ┌──────────▼────────┐ ┌──────▼────────────┐ │
- *                  │RPCDevice.unmount()│ │RPCDevice.unmount()├─┘
- *                  └───────────────────┘ └───────────────────┘
+ * ┌──────────────┐ ┌────────────────┐
+ * │serializeNBT()│ │deserializeNBT()◄─────────────────┐
+ * └──────────────┘ └───────┬────────┘                 │
+ *   May be called          │VM starts or              │
+ *   at any time,           │resumes after             │
+ *   except while           │load                      │
+ *   unloaded...        ┌───▼───┐                      │
+ *                      │mount()│                      │
+ *                      └───┬───┘                      │
+ *                          │VM stops or               │
+ *                          │is unloaded               │
+ *                          │                          │
+ *                     ┌────▼────┐Chunk unloaded       │
+ *                     │unmount()├─────────────────────┤
+ *                     └────┬────┘                     │
+ *                          │VM stopped or             │
+ *                          │device removed            │
+ *                          │                          │
+ *                     ┌────▼────┐                     │
+ *                     │dispose()├─────────────────────┘
+ *                     └─────────┘
  * 
* * @see ObjectDevice @@ -78,7 +70,7 @@ public interface RPCDevice extends Device { List getMethodGroups(); /** - * Called to initialize this device. + * Called to start this device. *

* This is called when the connected virtual machine starts, or when the device is added to an already running * virtual machine. @@ -87,22 +79,28 @@ public interface RPCDevice extends Device { } /** - * Called to dispose this device. + * Called to stop this device. *

- * Called when the connected virtual machine stops, or when the device is removed from a currently running - * virtual machine. + * Called when the connected virtual machine stops, the device is removed from a currently running + * virtual machine, or the connected virtual machine is suspended (chunk unload/server stopped/...). + *

+ * If {@link #mount()} was called, this is guaranteed to be called. */ default void unmount() { } /** - * Called when the device is suspended. + * Called to dispose this device. *

- * This can happen when the level area containing the context the device was loaded in is unloaded, - * e.g. due to player moving too far away from the area. + * Called when the connected virtual machine stops or the device is removed from a currently running + * virtual machine. *

- * Intended for soft-releasing unmanaged resource, i.e. non-persisted unmanaged resources. + * Will only be called on unmounted devices (i.e. will always be called after {@link #unmount()} if + * {@link #mount()} was called before). May be called without intermediary {@link #mount()} calls, e.g. + * virtual machine stops, then device is disconnected from the virtual machine. + *

+ * Intended for releasing persistent unmanaged resources. */ - default void suspend() { + default void dispose() { } } diff --git a/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java b/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java index bd72d899..2b537f88 100644 --- a/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java +++ b/src/main/java/li/cil/oc2/common/bus/RPCDeviceBusAdapter.java @@ -36,7 +36,7 @@ public final class RPCDeviceBusAdapter implements Steppable { private final SerialDevice serialDevice; private final Gson gson; - private final ArrayList devices = new ArrayList<>(); + private final ArrayList devicesWithId = new ArrayList<>(); private final HashMap devicesById = new HashMap<>(); private final Set unmountedDevices = new HashSet<>(); private final Set mountedDevices = new HashSet<>(); @@ -82,14 +82,18 @@ public final class RPCDeviceBusAdapter implements Steppable { for (final RPCDevice device : mountedDevices) { device.unmount(); } - unmountedDevices.addAll(mountedDevices); - mountedDevices.clear(); } - public void suspend() { - for (final RPCDeviceWithIdentifier info : devices) { - info.device.suspend(); + public void dispose() { + for (final RPCDevice device : mountedDevices) { + device.unmount(); + device.dispose(); } + for (final RPCDeviceList device : unmountedDevices) { + device.dispose(); + } + unmountedDevices.addAll(mountedDevices); + mountedDevices.clear(); } public void reset() { @@ -115,10 +119,6 @@ public final class RPCDeviceBusAdapter implements Steppable { return; } - devices.clear(); - devicesById.clear(); - unmountedDevices.clear(); - // How device grouping works: // Each device can have multiple UUIDs due to being attached to multiple bus elements. // There is no guarantee that for each device D1 present on bus elements E1 and E2, @@ -162,31 +162,38 @@ public final class RPCDeviceBusAdapter implements Steppable { .add(identifier); }); - final Set newDevices = new HashSet<>(); + // Rebuild devices lists. + devicesWithId.clear(); + devicesById.clear(); + + final Set devices = new HashSet<>(); identifiersByDevice.forEach((device, identifiers) -> { final UUID identifier = selectIdentifierDeterministically(identifiers); - devices.add(new RPCDeviceWithIdentifier(identifier, device)); + devicesWithId.add(new RPCDeviceWithIdentifier(identifier, device)); devicesById.put(identifier, device); - newDevices.add(device); + devices.add(device); + + // Add to set of unmounted devices if we don't already track it. It's a set, so + // there won't be duplicates in the unmounted set due to this. + if (!mountedDevices.contains(device)) { + unmountedDevices.add(device); + } }); - // Add new devices to list of unmounted devices. List was cleared, so removed devices previously in - // list of unmounted devices are already gone. - for (final RPCDeviceList newDevice : newDevices) { - if (!mountedDevices.contains(newDevice)) { - unmountedDevices.add(newDevice); - } - } + // Remove devices from mounted set, call appropriate callbacks. + final HashSet removedMountedDevices = new HashSet<>(mountedDevices); + removedMountedDevices.removeAll(devices); + mountedDevices.removeAll(removedMountedDevices); + removedMountedDevices.forEach(device -> { + device.unmount(); + device.dispose(); + }); - // Remove removed devices from list of mounted devices. - final Iterator mountedDeviceIterator = mountedDevices.iterator(); - while (mountedDeviceIterator.hasNext()) { - final RPCDeviceList device = mountedDeviceIterator.next(); - if (!newDevices.contains(device)) { - device.unmount(); - mountedDeviceIterator.remove(); - } - } + // Remove devices from unmounted set, call appropriate callbacks. + final HashSet removedUnmountedDevices = new HashSet<>(unmountedDevices); + removedUnmountedDevices.removeAll(devices); + unmountedDevices.removeAll(removedUnmountedDevices); + removedUnmountedDevices.forEach(RPCDeviceList::dispose); } public void tick() { @@ -355,7 +362,7 @@ public final class RPCDeviceBusAdapter implements Steppable { } private void writeDeviceList() { - writeMessage(Message.MESSAGE_TYPE_LIST, devices); + writeMessage(Message.MESSAGE_TYPE_LIST, devicesWithId); } private void writeDeviceMethods(final UUID deviceId) { diff --git a/src/main/java/li/cil/oc2/common/bus/device/item/AbstractItemRPCDevice.java b/src/main/java/li/cil/oc2/common/bus/device/item/AbstractItemRPCDevice.java index 9293f412..17ef1558 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/item/AbstractItemRPCDevice.java +++ b/src/main/java/li/cil/oc2/common/bus/device/item/AbstractItemRPCDevice.java @@ -42,7 +42,7 @@ public abstract class AbstractItemRPCDevice extends IdentityProxy imp } @Override - public void suspend() { - device.suspend(); + public void dispose() { + device.dispose(); } } diff --git a/src/main/java/li/cil/oc2/common/bus/device/item/FileImportExportCardItemDevice.java b/src/main/java/li/cil/oc2/common/bus/device/item/FileImportExportCardItemDevice.java index 666f15ed..0a59c03f 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/item/FileImportExportCardItemDevice.java +++ b/src/main/java/li/cil/oc2/common/bus/device/item/FileImportExportCardItemDevice.java @@ -134,7 +134,7 @@ public final class FileImportExportCardItemDevice extends AbstractItemRPCDevice /////////////////////////////////////////////////////////////////// @Override - public void suspend() { + public void unmount() { reset(); } diff --git a/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java b/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java index d0e9cd16..087680b9 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java +++ b/src/main/java/li/cil/oc2/common/bus/device/rpc/RPCDeviceList.java @@ -42,9 +42,9 @@ public record RPCDeviceList(ArrayList devices) implements RPCDevice { } @Override - public void suspend() { + public void dispose() { for (final RPCDevice device : devices) { - device.suspend(); + device.dispose(); } } diff --git a/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java b/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java index a874f452..0e37586c 100644 --- a/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java +++ b/src/main/java/li/cil/oc2/common/vm/AbstractVirtualMachine.java @@ -91,7 +91,7 @@ public abstract class AbstractVirtualMachine implements VirtualMachine { public void suspend() { joinWorkerThread(); state.vmAdapter.suspend(); - state.rpcAdapter.suspend(); + state.rpcAdapter.unmount(); } @Override @@ -203,7 +203,7 @@ public abstract class AbstractVirtualMachine implements VirtualMachine { state.board.setRunning(false); state.board.reset(); state.rpcAdapter.reset(); - state.rpcAdapter.unmount(); + state.rpcAdapter.dispose(); state.vmAdapter.unmount(); runner = null; diff --git a/src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java b/src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java index 9ad35b03..c3be390d 100644 --- a/src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java +++ b/src/test/java/li/cil/oc2/common/bus/RPCDeviceTests.java @@ -30,134 +30,94 @@ public final class RPCDeviceTests { } @Test - public void emptyDevicesAreNotMounted() { - final RPCDevice device1 = mock(RPCDevice.class); - addDevice(device1); + public void resumeDoesNotMountDirectly() { + final RPCDevice device1 = addDevice(); adapter.resume(controller, true); - adapter.mount(); verify(device1, never()).mount(); } + @Test + public void emptyDevicesAreNotMounted() { + final RPCDevice device = addEmptyDevice(); + adapter.resume(controller, true); + + adapter.mount(); + verify(device, never()).mount(); + } + @Test public void addedDevicesHaveMountCalled() { - final RPCDevice device1 = mock(RPCDevice.class); - when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - addDevice(device1); - + final RPCDevice device = addDevice(); adapter.resume(controller, true); - verify(device1, never()).mount(); adapter.mount(); - verify(device1).mount(); + verify(device).mount(); } @Test - public void mountedDevicesAreUnmountedWhenRemoved() { - final RPCDevice device1 = mock(RPCDevice.class); - when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - addDevice(device1); - + public void mountedDevicesAreUnmountedAndDisposedWhenRemoved() { + final RPCDevice device = addDevice(); adapter.resume(controller, true); - verify(device1, never()).mount(); - adapter.mount(); - verify(device1).mount(); - removeDevice(device1); + removeDevice(device); adapter.resume(controller, true); - verify(device1).unmount(); + verify(device).unmount(); + verify(device).dispose(); } @Test - public void mountedDevicesAreUnmountedOnGlobalUnmount() { - final RPCDevice device1 = mock(RPCDevice.class); - when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - addDevice(device1); - + public void unmountedDevicesAreDisposedWhenRemoved() { + final RPCDevice device = addDevice(); adapter.resume(controller, true); - verify(device1, never()).mount(); + removeDevice(device); + adapter.resume(controller, true); + verify(device, never()).unmount(); + verify(device).dispose(); + } + + @Test + public void mountedDevicesAreUnmountedButNotDisposedOnGlobalUnmount() { + final RPCDevice device = addDevice(); + adapter.resume(controller, true); adapter.mount(); - verify(device1).mount(); adapter.unmount(); - verify(device1).unmount(); + verify(device).unmount(); + verify(device, never()).dispose(); } @Test - public void unmountedDevicesAreNotUnmountedOnGlobalUnmount() { - final RPCDevice device1 = mock(RPCDevice.class); - when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - addDevice(device1); - + public void unmountedDevicesAreNotUnmountedAndNotDisposedOnGlobalUnmount() { + final RPCDevice device = addDevice(); adapter.resume(controller, true); - verify(device1, never()).mount(); adapter.unmount(); - verify(device1, never()).unmount(); + verify(device, never()).unmount(); + verify(device, never()).dispose(); } @Test - public void deviceListForwardsMount() { - final RPCDevice device1 = mock(RPCDevice.class); - final RPCDevice device2 = mock(RPCDevice.class); - final RPCDevice listDevice = new RPCDeviceList(new ArrayList<>(Arrays.asList(device1, device2))); - when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - when(device2.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - addDevice(listDevice); - + public void mountedDevicesAreUnmountedAndDisposedOnGlobalDispose() { + final RPCDevice device = addDevice(); adapter.resume(controller, true); - verify(device1, never()).mount(); - verify(device2, never()).mount(); - adapter.mount(); - verify(device1).mount(); - verify(device2).mount(); + + adapter.dispose(); + verify(device).unmount(); + verify(device).dispose(); } @Test - public void deviceListForwardsUnmount() { - final RPCDevice device1 = mock(RPCDevice.class); - final RPCDevice device2 = mock(RPCDevice.class); - final RPCDevice listDevice = new RPCDeviceList(new ArrayList<>(Arrays.asList(device1, device2))); - when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - when(device2.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - addDevice(listDevice); - + public void unmountedDevicesAreNotUnmountedButDisposedOnGlobalDispose() { + final RPCDevice device = addDevice(); adapter.resume(controller, true); - verify(device1, never()).mount(); - verify(device2, never()).mount(); - adapter.mount(); - verify(device1).mount(); - verify(device2).mount(); - - adapter.unmount(); - verify(device1).unmount(); - verify(device2).unmount(); - } - - @Test - public void deviceListForwardsSuspend() { - final RPCDevice device1 = mock(RPCDevice.class); - final RPCDevice device2 = mock(RPCDevice.class); - final RPCDevice listDevice = new RPCDeviceList(new ArrayList<>(Arrays.asList(device1, device2))); - when(device1.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - when(device2.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); - addDevice(listDevice); - - adapter.resume(controller, true); - verify(device1, never()).mount(); - verify(device2, never()).mount(); - - adapter.mount(); - verify(device1).mount(); - verify(device2).mount(); - - adapter.suspend(); - verify(device1).suspend(); - verify(device2).suspend(); + adapter.dispose(); + verify(device, never()).unmount(); + verify(device).dispose(); } @Test @@ -187,6 +147,19 @@ public final class RPCDeviceTests { verify(device2, atMostOnce()).mount(); } + private RPCDevice addEmptyDevice() { + final RPCDevice device = mock(RPCDevice.class); + addDevice(device); + return device; + } + + private RPCDevice addDevice() { + final RPCDevice device = mock(RPCDevice.class); + when(device.getMethodGroups()).thenReturn(Collections.singletonList(mock(RPCMethod.class))); + addDevice(device); + return device; + } + private void addDevice(final Device device, UUID... identifiers) { if (identifiers.length == 0) { identifiers = new UUID[]{UUID.randomUUID()};