Add option to clean up stuff (via unmount) when devices go missing.

For block devices, this can happen when the block is broken while the computer is unloaded, for example. For items this is more esoteric, but it theoretically means items disappear while the container managing the item as a device is unloaded.
This commit is contained in:
Florian Nücke
2022-01-25 19:57:43 +01:00
parent 57a1dc9522
commit 0c271732cb
10 changed files with 202 additions and 26 deletions

View File

@@ -1,7 +1,10 @@
package li.cil.oc2.api.bus.device.provider;
import li.cil.oc2.api.bus.device.Device;
import li.cil.oc2.api.bus.device.rpc.RPCDevice;
import li.cil.oc2.api.bus.device.vm.VMDevice;
import li.cil.oc2.api.util.Invalidatable;
import net.minecraft.nbt.CompoundTag;
import net.minecraftforge.registries.IForgeRegistryEntry;
/**
@@ -48,4 +51,24 @@ public interface BlockDeviceProvider extends IForgeRegistryEntry<BlockDeviceProv
* @return a device for the specified query, if available.
*/
Invalidatable<Device> getDevice(BlockDeviceQuery query);
/**
* Last-resort cleanup method for devices provided by this provider.
* <p>
* This is the equivalent of {@link RPCDevice#unmount()} or {@link VMDevice#unmount()},
* for devices that have gone missing unexpectedly, so this method could no longer be
* called on the actual device.
* <p>
* For block devices, this can happen if the block the device was created for has been
* removed while the connected computer was unloaded, or the cable connecting the block
* the device was provided for with the computer was broken while the computer was unloaded.
* <p>
* Implementing this is only necessary, if the device holds some out-of-NBT serialized
* data, or does something similar.
*
* @param query the query that resulted in the device when it still existed, if available.
* @param tag data last serialized by the device that went missing.
*/
default void unmount(final BlockDeviceQuery query, final CompoundTag tag) {
}
}

View File

@@ -1,6 +1,9 @@
package li.cil.oc2.api.bus.device.provider;
import li.cil.oc2.api.bus.device.ItemDevice;
import li.cil.oc2.api.bus.device.rpc.RPCDevice;
import li.cil.oc2.api.bus.device.vm.VMDevice;
import net.minecraft.nbt.CompoundTag;
import net.minecraftforge.registries.IForgeRegistryEntry;
import java.util.Optional;
@@ -57,4 +60,22 @@ public interface ItemDeviceProvider extends IForgeRegistryEntry<ItemDeviceProvid
default int getEnergyConsumption(final ItemDeviceQuery query) {
return 0;
}
/**
* Last-resort cleanup method for devices provided by this provider.
* <p>
* This is the equivalent of {@link RPCDevice#unmount()} or {@link VMDevice#unmount()},
* for devices that have gone missing unexpectedly, so this method could no longer be
* called on the actual device.
* <p>
* For item devices this is rather unlikely. It means an item disappeared while the
* block managing the item device was unloaded.
* <p>
* Implementing this is only necessary, if the device holds some out-of-NBT serialized
* data, or does something similar.
*
* @param tag the data last serialized by the device went missing.
*/
default void unmount(final CompoundTag tag) {
}
}

View File

@@ -335,7 +335,7 @@ public final class ComputerBlockEntity extends ModBlockEntity implements Termina
public void addOwnDevices() {
assert level != null;
for (final BlockEntry info : collectDevices(level, getPosition(), null)) {
for (final BlockEntry info : collectDevices(level, getPosition(), null).getEntries()) {
devices.add(info.getDevice());
super.addDevice(info.getDevice());
}

View File

@@ -5,13 +5,21 @@ import li.cil.oc2.common.util.NBTTagIds;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import javax.annotation.Nullable;
import java.util.*;
public abstract class AbstractGroupingDeviceBusElement<T extends AbstractGroupingDeviceBusElement.Entry> extends AbstractDeviceBusElement {
public abstract class AbstractGroupingDeviceBusElement<TEntry extends AbstractGroupingDeviceBusElement.Entry, TQuery> extends AbstractDeviceBusElement {
private static final String GROUPS_TAG_NAME = "groups";
private static final String GROUP_ID_TAG_NAME = "groupId";
private static final String GROUP_DATA_TAG_NAME = "groupData";
protected abstract class QueryResult {
@Nullable
public abstract TQuery getQuery();
public abstract Set<TEntry> getEntries();
}
protected interface Entry {
Optional<String> getDeviceDataKey();
@@ -23,7 +31,7 @@ public abstract class AbstractGroupingDeviceBusElement<T extends AbstractGroupin
///////////////////////////////////////////////////////////////////
protected final int groupCount;
protected final ArrayList<HashSet<T>> groups;
protected final ArrayList<HashSet<TEntry>> groups;
///////////////////////////////////////////////////////////////////
@@ -80,7 +88,7 @@ public abstract class AbstractGroupingDeviceBusElement<T extends AbstractGroupin
}
// Immediately load data into devices, if we already have some.
for (final T entry : groups.get(i)) {
for (final TEntry entry : groups.get(i)) {
final CompoundTag devicesTag = groupData[i];
entry.getDeviceDataKey().ifPresent(key -> {
if (devicesTag.contains(key, NBTTagIds.TAG_COMPOUND)) {
@@ -94,8 +102,8 @@ public abstract class AbstractGroupingDeviceBusElement<T extends AbstractGroupin
@Override
public Optional<UUID> getDeviceIdentifier(final Device device) {
for (int i = 0; i < groupCount; i++) {
final HashSet<T> group = groups.get(i);
for (final T deviceInfo : group) {
final HashSet<TEntry> group = groups.get(i);
for (final TEntry deviceInfo : group) {
if (Objects.equals(device, deviceInfo.getDevice())) {
return Optional.of(groupIds[i]);
}
@@ -106,22 +114,23 @@ public abstract class AbstractGroupingDeviceBusElement<T extends AbstractGroupin
///////////////////////////////////////////////////////////////////
protected final void setEntriesForGroup(final int index, final Set<T> newEntries) {
final HashSet<T> oldEntries = groups.get(index);
protected final void setEntriesForGroup(final int index, final QueryResult queryResult) {
final Set<TEntry> newEntries = queryResult.getEntries();
final HashSet<TEntry> oldEntries = groups.get(index);
if (Objects.equals(newEntries, oldEntries)) {
return;
}
final HashSet<T> removedEntries = new HashSet<>(oldEntries);
final HashSet<TEntry> removedEntries = new HashSet<>(oldEntries);
removedEntries.removeAll(newEntries);
for (final T entry : removedEntries) {
for (final TEntry entry : removedEntries) {
devices.removeInt(entry.getDevice());
onEntryRemoved(entry);
}
final HashSet<T> addedEntries = new HashSet<>(newEntries);
final HashSet<TEntry> addedEntries = new HashSet<>(newEntries);
addedEntries.removeAll(oldEntries);
for (final T entry : addedEntries) {
for (final TEntry entry : addedEntries) {
devices.put(entry.getDevice(), entry.getDeviceEnergyConsumption().orElse(0));
onEntryAdded(entry);
}
@@ -130,31 +139,48 @@ public abstract class AbstractGroupingDeviceBusElement<T extends AbstractGroupin
oldEntries.addAll(newEntries);
final CompoundTag devicesTag = groupData[index];
for (final T entry : removedEntries) {
for (final TEntry entry : removedEntries) {
entry.getDeviceDataKey().ifPresent(devicesTag::remove);
}
for (final T entry : addedEntries) {
final HashSet<String> invalidDataKeys = new HashSet<>(devicesTag.getAllKeys());
for (final TEntry entry : addedEntries) {
entry.getDeviceDataKey().ifPresent(key -> {
invalidDataKeys.remove(key);
if (devicesTag.contains(key, NBTTagIds.TAG_COMPOUND)) {
entry.getDevice().deserializeNBT(devicesTag.getCompound(key));
} else {
devicesTag.remove(key);
}
});
}
final TQuery query = queryResult.getQuery();
for (final String invalidDataKey : invalidDataKeys) {
if (devicesTag.contains(invalidDataKey, NBTTagIds.TAG_COMPOUND)) {
final CompoundTag tag = devicesTag.getCompound(invalidDataKey);
onEntryRemoved(invalidDataKey, tag, query);
}
devicesTag.remove(invalidDataKey);
}
scanDevices();
}
protected void onEntryRemoved(final T entry) {
protected void onEntryAdded(final TEntry entry) {
}
protected void onEntryAdded(final T entry) {
protected void onEntryRemoved(final TEntry entry) {
}
protected void onEntryRemoved(final String dataKey, final CompoundTag data, @Nullable final TQuery query) {
}
///////////////////////////////////////////////////////////////////
private void saveGroup(final int index) {
final CompoundTag devicesTag = new CompoundTag();
for (final T entry : groups.get(index)) {
for (final TEntry entry : groups.get(index)) {
entry.getDeviceDataKey().ifPresent(key -> {
final CompoundTag deviceTag = entry.getDevice().serializeNBT();
if (!deviceTag.isEmpty()) {

View File

@@ -4,9 +4,11 @@ import li.cil.oc2.api.bus.BlockDeviceBusElement;
import li.cil.oc2.api.bus.DeviceBus;
import li.cil.oc2.api.bus.DeviceBusElement;
import li.cil.oc2.api.bus.device.Device;
import li.cil.oc2.api.bus.device.provider.BlockDeviceProvider;
import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery;
import li.cil.oc2.api.util.Invalidatable;
import li.cil.oc2.common.Constants;
import li.cil.oc2.common.bus.device.provider.Providers;
import li.cil.oc2.common.bus.device.rpc.TypeNameRPCDevice;
import li.cil.oc2.common.bus.device.util.BlockDeviceInfo;
import li.cil.oc2.common.bus.device.util.Devices;
@@ -15,11 +17,14 @@ import li.cil.oc2.common.util.LevelUtils;
import li.cil.oc2.common.util.ServerScheduler;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.registries.IForgeRegistry;
import javax.annotation.Nullable;
import java.util.*;
@@ -27,7 +32,7 @@ import java.util.*;
import static java.util.Objects.requireNonNull;
import static li.cil.oc2.common.util.RegistryUtils.optionalKey;
public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElement<BlockEntityDeviceBusElement.BlockEntry> implements BlockDeviceBusElement {
public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElement<BlockEntityDeviceBusElement.BlockEntry, BlockDeviceQuery> implements BlockDeviceBusElement {
private final BlockEntity blockEntity;
///////////////////////////////////////////////////////////////////
@@ -96,10 +101,10 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElemen
return;
}
final HashSet<BlockEntry> newDevices = collectDevices(level, pos, direction);
final BlockQueryResult queryResult = collectDevices(level, pos, direction);
final int index = direction.get3DDataValue();
setEntriesForGroup(index, newDevices);
setEntriesForGroup(index, queryResult);
}
public void initialize() {
@@ -124,10 +129,11 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElemen
return canScanContinueTowards(direction);
}
protected HashSet<BlockEntry> collectDevices(final Level level, final BlockPos pos, @Nullable final Direction direction) {
protected BlockQueryResult collectDevices(final Level level, final BlockPos pos, @Nullable final Direction direction) {
final BlockDeviceQuery query = Devices.makeQuery(level, pos, direction != null ? direction.getOpposite() : null);
final HashSet<BlockEntry> entries = new HashSet<>();
if (canDetectDevicesTowards(direction)) {
final BlockDeviceQuery query = Devices.makeQuery(level, pos, direction != null ? direction.getOpposite() : null);
for (final Invalidatable<BlockDeviceInfo> deviceInfo : Devices.getDevices(query)) {
if (deviceInfo.isPresent()) {
entries.add(new BlockEntry(deviceInfo, pos));
@@ -137,7 +143,7 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElemen
collectSyntheticDevices(level, pos, direction, entries);
return entries;
return new BlockQueryResult(query, entries);
}
protected void collectSyntheticDevices(final Level level, final BlockPos pos, @Nullable final Direction direction, final HashSet<BlockEntry> entries) {
@@ -159,6 +165,17 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElemen
entry.removeListener();
}
@Override
protected void onEntryRemoved(final String dataKey, final CompoundTag tag, @Nullable final BlockDeviceQuery query) {
assert query != null : "Passed null query for block device bus element.";
super.onEntryRemoved(dataKey, tag, query);
final IForgeRegistry<BlockDeviceProvider> registry = Providers.BLOCK_DEVICE_PROVIDER_REGISTRY.get();
final BlockDeviceProvider provider = registry.getValue(new ResourceLocation(dataKey));
if (provider != null) {
provider.unmount(query, tag);
}
}
///////////////////////////////////////////////////////////////////
private void scanNeighborsForDevices() {
@@ -185,6 +202,25 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElemen
///////////////////////////////////////////////////////////////////
protected final class BlockQueryResult extends QueryResult {
private final BlockDeviceQuery query;
private final Set<BlockEntry> entries;
public BlockQueryResult(final BlockDeviceQuery query, final Set<BlockEntry> entries) {
this.query = query;
this.entries = entries;
}
public BlockDeviceQuery getQuery() {
return query;
}
@Override
public Set<BlockEntry> getEntries() {
return entries;
}
}
protected final class BlockEntry implements Entry {
private final Invalidatable<BlockDeviceInfo> deviceInfo;
@Nullable private final String dataKey;

View File

@@ -2,8 +2,10 @@ package li.cil.oc2.common.bus;
import li.cil.oc2.api.bus.device.Device;
import li.cil.oc2.api.bus.device.ItemDevice;
import li.cil.oc2.api.bus.device.provider.ItemDeviceProvider;
import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery;
import li.cil.oc2.api.bus.device.rpc.RPCDevice;
import li.cil.oc2.common.bus.device.provider.Providers;
import li.cil.oc2.common.bus.device.rpc.TypeNameRPCDevice;
import li.cil.oc2.common.bus.device.util.Devices;
import li.cil.oc2.common.bus.device.util.ItemDeviceInfo;
@@ -12,13 +14,15 @@ import li.cil.oc2.common.util.NBTTagIds;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.registries.IForgeRegistry;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
import static li.cil.oc2.common.util.RegistryUtils.optionalKey;
public final class ItemHandlerDeviceBusElement extends AbstractGroupingDeviceBusElement<ItemHandlerDeviceBusElement.ItemEntry> {
public final class ItemHandlerDeviceBusElement extends AbstractGroupingDeviceBusElement<ItemHandlerDeviceBusElement.ItemEntry, ItemDeviceQuery> {
private final Function<ItemStack, ItemDeviceQuery> queryFactory;
public ItemHandlerDeviceBusElement(final int slotCount, final Function<ItemStack, ItemDeviceQuery> queryFactory) {
@@ -44,9 +48,9 @@ public final class ItemHandlerDeviceBusElement extends AbstractGroupingDeviceBus
final HashSet<ItemEntry> newDevices = new HashSet<>(Devices.getDevices(query).stream().map(ItemEntry::new).toList());
insertItemNameDevice(stack, newDevices);
importDeviceDataFromItemStack(stack, newDevices);
setEntriesForGroup(slot, newDevices);
setEntriesForGroup(slot, new ItemQueryResult(query, newDevices));
} else {
setEntriesForGroup(slot, Collections.emptySet());
setEntriesForGroup(slot, new ItemQueryResult(null, Collections.emptySet()));
}
}
@@ -73,6 +77,18 @@ public final class ItemHandlerDeviceBusElement extends AbstractGroupingDeviceBus
///////////////////////////////////////////////////////////////////
@Override
protected void onEntryRemoved(final String dataKey, final CompoundTag tag, @Nullable final ItemDeviceQuery query) {
super.onEntryRemoved(dataKey, tag, query);
final IForgeRegistry<ItemDeviceProvider> registry = Providers.ITEM_DEVICE_PROVIDER_REGISTRY.get();
final ItemDeviceProvider provider = registry.getValue(new ResourceLocation(dataKey));
if (provider != null) {
provider.unmount(query, tag);
}
}
///////////////////////////////////////////////////////////////////
private void importDeviceDataFromItemStack(final ItemStack stack, final HashSet<ItemEntry> entries) {
final CompoundTag exportedTag = ItemDeviceUtils.getItemDeviceData(stack);
if (!exportedTag.isEmpty()) {
@@ -97,6 +113,27 @@ public final class ItemHandlerDeviceBusElement extends AbstractGroupingDeviceBus
///////////////////////////////////////////////////////////////////
protected final class ItemQueryResult extends QueryResult {
@Nullable private final ItemDeviceQuery query;
private final Set<ItemEntry> entries;
public ItemQueryResult(@Nullable final ItemDeviceQuery query, final Set<ItemEntry> entries) {
this.query = query;
this.entries = entries;
}
@Nullable
@Override
public ItemDeviceQuery getQuery() {
return query;
}
@Override
public Set<ItemEntry> getEntries() {
return entries;
}
}
protected record ItemEntry(ItemDeviceInfo deviceInfo) implements Entry {
@Override
public Optional<String> getDeviceDataKey() {

View File

@@ -151,6 +151,13 @@ public abstract class AbstractBlockDeviceVMDevice<TBlock extends BlockDevice, TI
}
}
public static void unmount(final CompoundTag tag) {
if (tag.hasUUID(BLOB_HANDLE_TAG_NAME)) {
final UUID blobHandle = tag.getUUID(BLOB_HANDLE_TAG_NAME);
BlobStorage.close(blobHandle);
}
}
///////////////////////////////////////////////////////////////
protected abstract TBlock createBlockDevice() throws IOException;

View File

@@ -14,6 +14,12 @@ public final class DiskDriveDeviceProvider extends AbstractBlockEntityDeviceProv
super(BlockEntities.DISK_DRIVE.get());
}
///////////////////////////////////////////////////////////////////
// NB: Does *not* need an unmount() implementation, because the blob UUID is stored on the item.
///////////////////////////////////////////////////////////////////
@Override
protected Invalidatable<Device> getBlockDevice(final BlockDeviceQuery query, final DiskDriveBlockEntity blockEntity) {
// We only allow connecting to exactly one face of the disk drive to ensure only one

View File

@@ -8,9 +8,11 @@ import li.cil.oc2.common.bus.device.item.HardDriveVMDevice;
import li.cil.oc2.common.bus.device.provider.util.AbstractItemDeviceProvider;
import li.cil.oc2.common.item.HardDriveItem;
import li.cil.oc2.common.util.LocationSupplierUtils;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.Optional;
public final class HardDriveItemDeviceProvider extends AbstractItemDeviceProvider {
@@ -20,6 +22,14 @@ public final class HardDriveItemDeviceProvider extends AbstractItemDeviceProvide
///////////////////////////////////////////////////////////////////
@Override
public void unmount(@Nullable final ItemDeviceQuery query, final CompoundTag tag) {
super.unmount(query, tag);
HardDriveVMDevice.unmount(tag);
}
///////////////////////////////////////////////////////////////////
@Override
protected Optional<ItemDevice> getItemDevice(final ItemDeviceQuery query) {
return Optional.of(new HardDriveVMDevice(query.getItemStack(), getCapacity(query), false, LocationSupplierUtils.of(query)));

View File

@@ -9,8 +9,10 @@ import li.cil.oc2.common.bus.device.item.HardDriveVMDeviceWithInitialData;
import li.cil.oc2.common.bus.device.provider.util.AbstractItemDeviceProvider;
import li.cil.oc2.common.item.HardDriveWithExternalDataItem;
import li.cil.oc2.common.util.LocationSupplierUtils;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.Optional;
public final class HardDriveWithExternalDataItemDeviceProvider extends AbstractItemDeviceProvider {
@@ -20,6 +22,14 @@ public final class HardDriveWithExternalDataItemDeviceProvider extends AbstractI
///////////////////////////////////////////////////////////////////
@Override
public void unmount(@Nullable final ItemDeviceQuery query, final CompoundTag tag) {
super.unmount(query, tag);
HardDriveVMDeviceWithInitialData.unmount(tag);
}
///////////////////////////////////////////////////////////////////
@Override
protected Optional<ItemDevice> getItemDevice(final ItemDeviceQuery query) {
final ItemStack stack = query.getItemStack();