From b656e27f43d49faa337f97e8858874bd7ee7f686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 2 Jan 2022 15:11:53 +0100 Subject: [PATCH] Put some more effort into avoiding listeners keeping references to things that may otherwise be garbage collected. --- .../device/provider/BlockDeviceProvider.java | 4 +- .../li/cil/oc2/api/util/Invalidatable.java | 107 ++++++++++++++++++ .../common/block/NetworkConnectorBlock.java | 2 +- .../blockentity/BusCableBlockEntity.java | 6 +- .../blockentity/ComputerBlockEntity.java | 7 +- .../common/blockentity/ModBlockEntity.java | 9 +- .../NetworkConnectorBlockEntity.java | 30 ++--- .../blockentity/NetworkHubBlockEntity.java | 16 ++- ...AbstractGroupingBlockDeviceBusElement.java | 10 -- .../bus/AbstractGroupingDeviceBusElement.java | 75 ++++++------ .../AbstractGroupingItemDeviceBusElement.java | 10 -- .../bus/BlockEntityDeviceBusElement.java | 99 +++++++++++++--- .../common/bus/CommonDeviceBusController.java | 5 +- .../bus/ItemHandlerDeviceBusElement.java | 64 ++++++++--- .../block/BlockEntityDeviceProvider.java | 8 +- .../block/BlockStateDeviceProvider.java | 10 +- .../block/DiskDriveDeviceProvider.java | 8 +- .../EnergyStorageBlockDeviceProvider.java | 6 +- .../FluidHandlerBlockDeviceProvider.java | 6 +- .../block/ItemHandlerBlockDeviceProvider.java | 6 +- ...ctBlockEntityCapabilityDeviceProvider.java | 15 ++- .../AbstractBlockEntityDeviceProvider.java | 13 +-- .../oc2/common/bus/device/util/Devices.java | 12 +- .../serializers/JsonArraySerializer.java | 2 +- .../cil/oc2/common/util/ItemDeviceUtils.java | 18 --- .../oc2/common/util/LazyOptionalUtils.java | 36 ++++++ .../li/cil/oc2/common/util/RegistryUtils.java | 16 +++ .../vm/AbstractVMItemStackHandlers.java | 20 ++-- 28 files changed, 428 insertions(+), 192 deletions(-) create mode 100644 src/main/java/li/cil/oc2/api/util/Invalidatable.java delete mode 100644 src/main/java/li/cil/oc2/common/bus/AbstractGroupingBlockDeviceBusElement.java delete mode 100644 src/main/java/li/cil/oc2/common/bus/AbstractGroupingItemDeviceBusElement.java create mode 100644 src/main/java/li/cil/oc2/common/util/LazyOptionalUtils.java diff --git a/src/main/java/li/cil/oc2/api/bus/device/provider/BlockDeviceProvider.java b/src/main/java/li/cil/oc2/api/bus/device/provider/BlockDeviceProvider.java index 6a80e7c3..3a66ee39 100644 --- a/src/main/java/li/cil/oc2/api/bus/device/provider/BlockDeviceProvider.java +++ b/src/main/java/li/cil/oc2/api/bus/device/provider/BlockDeviceProvider.java @@ -1,7 +1,7 @@ package li.cil.oc2.api.bus.device.provider; import li.cil.oc2.api.bus.device.Device; -import net.minecraftforge.common.util.LazyOptional; +import li.cil.oc2.api.util.Invalidatable; import net.minecraftforge.registries.IForgeRegistryEntry; /** @@ -47,5 +47,5 @@ public interface BlockDeviceProvider extends IForgeRegistryEntry getDevice(BlockDeviceQuery query); + Invalidatable getDevice(BlockDeviceQuery query); } diff --git a/src/main/java/li/cil/oc2/api/util/Invalidatable.java b/src/main/java/li/cil/oc2/api/util/Invalidatable.java new file mode 100644 index 00000000..c350fcbd --- /dev/null +++ b/src/main/java/li/cil/oc2/api/util/Invalidatable.java @@ -0,0 +1,107 @@ +package li.cil.oc2.api.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Wrapper for objects which may become invalid, such as {@link li.cil.oc2.api.bus.device.Device}s. + *

+ * This implementation allows listeners added via {@link #addListener(Consumer)} to be removed again + * using the returned token. This allows avoiding memory leaks due to inversion of reference ownership, + * where an observable keeps observers alive due to its listener list. + * + * @param The type of the underlying value. + */ +public final class Invalidatable { + @FunctionalInterface + public interface ListenerToken { + void removeListener(); + } + + @SuppressWarnings("rawtypes") + private static final Invalidatable EMPTY = new Invalidatable(); + + @SuppressWarnings("unchecked") + public static Invalidatable empty() { + return EMPTY; + } + + public static Invalidatable of(final T value) { + return new Invalidatable<>(value); + } + + private final List>> listeners = new ArrayList<>(); + private T value; + private boolean isValid = true; + + public Invalidatable(final T value) { + this.value = value; + } + + private Invalidatable() { + this.value = null; + this.isValid = false; + } + + public T get() { + if (isValid) { + assert value != null; + return value; + } else { + throw new IllegalStateException(); + } + } + + public boolean isPresent() { + return isValid; + } + + public void ifPresent(final Consumer consumer) { + if (isValid) { + consumer.accept(value); + } + } + + public Invalidatable mapWithDependency(final Function mapper) { + if (!isValid) { + return empty(); + } + + // Map to new type. + final Invalidatable mapped = new Invalidatable<>(mapper.apply(value)); + + // When this instance gets invalidated, invalidate mapped value. + final ListenerToken token = this.addListener(unused -> mapped.invalidate()); + + // When mapped value gets invalidated, remove listener. + mapped.addListener(unused -> token.removeListener()); + + return mapped; + } + + public void invalidate() { + if (isValid) { + isValid = false; + value = null; + listeners.forEach(listener -> listener.accept(this)); + listeners.clear(); + } + } + + public ListenerToken addListener(final Consumer> listener) { + if (isValid) { + listeners.add(listener); + return () -> { + if (isValid) { + listeners.remove(listener); + } + }; + } else { + listener.accept(this); + return () -> { + }; + } + } +} diff --git a/src/main/java/li/cil/oc2/common/block/NetworkConnectorBlock.java b/src/main/java/li/cil/oc2/common/block/NetworkConnectorBlock.java index 518930de..5655749c 100644 --- a/src/main/java/li/cil/oc2/common/block/NetworkConnectorBlock.java +++ b/src/main/java/li/cil/oc2/common/block/NetworkConnectorBlock.java @@ -56,7 +56,7 @@ public final class NetworkConnectorBlock extends FaceAttachedHorizontalDirection if (Objects.equals(changedBlockPos, pos.relative(getFacing(state).getOpposite()))) { final BlockEntity blockEntity = level.getBlockEntity(pos); if (blockEntity instanceof final NetworkConnectorBlockEntity networkConnector) { - networkConnector.setLocalInterfaceChanged(); + networkConnector.setNeighborChanged(); } } } diff --git a/src/main/java/li/cil/oc2/common/blockentity/BusCableBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/BusCableBlockEntity.java index e1cbf0da..2c283ff6 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/BusCableBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/BusCableBlockEntity.java @@ -265,12 +265,12 @@ public final class BusCableBlockEntity extends ModBlockEntity { } @Override - protected void collectSyntheticDevices(final Level level, final BlockPos pos, @Nullable final Direction direction, final HashSet devices) { - super.collectSyntheticDevices(level, pos, direction, devices); + protected void collectSyntheticDevices(final Level level, final BlockPos pos, @Nullable final Direction direction, final HashSet entries) { + super.collectSyntheticDevices(level, pos, direction, entries); if (direction != null) { final String interfaceName = interfaceNames[direction.get3DDataValue()]; if (!StringUtil.isNullOrEmpty(interfaceName)) { - devices.add(new BlockDeviceInfo(null, new TypeNameRPCDevice(interfaceName))); + entries.add(new BlockEntry(new BlockDeviceInfo(null, new TypeNameRPCDevice(interfaceName)), pos)); } } } diff --git a/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java index 74d7d722..a2fb627f 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/ComputerBlockEntity.java @@ -11,7 +11,6 @@ import li.cil.oc2.common.block.ComputerBlock; import li.cil.oc2.common.bus.BlockEntityDeviceBusController; import li.cil.oc2.common.bus.BlockEntityDeviceBusElement; import li.cil.oc2.common.bus.CommonDeviceBusController; -import li.cil.oc2.common.bus.device.util.BlockDeviceInfo; import li.cil.oc2.common.bus.device.util.Devices; import li.cil.oc2.common.capabilities.Capabilities; import li.cil.oc2.common.container.ComputerInventoryContainer; @@ -338,9 +337,9 @@ public final class ComputerBlockEntity extends ModBlockEntity implements Termina public void addOwnDevices() { assert level != null; - for (final BlockDeviceInfo info : collectDevices(level, getPosition(), null)) { - devices.add(info.device); - super.addDevice(info.device); + for (final BlockEntry info : collectDevices(level, getPosition(), null)) { + devices.add(info.getDevice()); + super.addDevice(info.getDevice()); } } diff --git a/src/main/java/li/cil/oc2/common/blockentity/ModBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/ModBlockEntity.java index 52e605e9..1c7b9461 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/ModBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/ModBlockEntity.java @@ -1,5 +1,6 @@ package li.cil.oc2.common.blockentity; +import li.cil.oc2.common.util.LazyOptionalUtils; import li.cil.oc2.common.util.ServerScheduler; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -36,10 +37,14 @@ public abstract class ModBlockEntity extends BlockEntity { } final CapabilityCacheKey key = new CapabilityCacheKey(capability, side); - final LazyOptional value; + LazyOptional value; if (capabilityCache.containsKey(key)) { value = capabilityCache.get(key); } else { + value = LazyOptional.empty(); + } + + if (!value.isPresent()) { final ArrayList list = new ArrayList<>(); collectCapabilities(new CapabilityCollector() { @SuppressWarnings("unchecked") @@ -60,7 +65,7 @@ public abstract class ModBlockEntity extends BlockEntity { if (value.isPresent()) { capabilityCache.put(key, value); - value.addListener(optional -> capabilityCache.remove(key, optional)); + LazyOptionalUtils.addWeakListener(value, capabilityCache, (map, optional) -> map.remove(key, optional)); } } diff --git a/src/main/java/li/cil/oc2/common/blockentity/NetworkConnectorBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/NetworkConnectorBlockEntity.java index 120474c6..8e6dee08 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/NetworkConnectorBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/NetworkConnectorBlockEntity.java @@ -9,6 +9,7 @@ import li.cil.oc2.common.item.Items; import li.cil.oc2.common.network.Network; import li.cil.oc2.common.network.message.NetworkConnectorConnectionsMessage; import li.cil.oc2.common.util.ItemStackUtils; +import li.cil.oc2.common.util.LazyOptionalUtils; import li.cil.oc2.common.util.NBTTagIds; import li.cil.oc2.common.util.ServerScheduler; import net.minecraft.client.Minecraft; @@ -62,8 +63,8 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { private final NetworkConnectorNetworkInterface networkInterface = new NetworkConnectorNetworkInterface(); - private LazyOptional localInterface = LazyOptional.empty(); - private boolean isLocalConnectionDirty = true; + private LazyOptional adjacentInterface = LazyOptional.empty(); + private boolean isAdjacentInterfaceDirty = true; private final HashSet connectorPositions = new HashSet<>(); private final HashSet ownedCables = new HashSet<>(); @@ -161,8 +162,8 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { return connectorPositions; } - public void setLocalInterfaceChanged() { - isLocalConnectionDirty = true; + public void setNeighborChanged() { + isAdjacentInterfaceDirty = true; } @OnlyIn(Dist.CLIENT) @@ -181,8 +182,8 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { return; } - if (isLocalConnectionDirty) { - isLocalConnectionDirty = false; + if (isAdjacentInterfaceDirty) { + isAdjacentInterfaceDirty = false; resolveLocalInterface(); } @@ -194,7 +195,7 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { } } - final NetworkInterface src = localInterface.orElse(NullNetworkInterface.INSTANCE); + final NetworkInterface src = adjacentInterface.orElse(NullNetworkInterface.INSTANCE); int byteBudget = BYTES_PER_TICK; byte[] frame; @@ -324,7 +325,7 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { private void resolveLocalInterface() { assert level != null; - localInterface = LazyOptional.empty(); + adjacentInterface = LazyOptional.empty(); if (isRemoved()) { return; @@ -333,9 +334,8 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { final Direction facing = NetworkConnectorBlock.getFacing(getBlockState()); final BlockPos sourcePos = getBlockPos().relative(facing.getOpposite()); - final ChunkPos sourceChunk = new ChunkPos(sourcePos); - if (!level.hasChunk(sourceChunk.x, sourceChunk.z)) { - ServerScheduler.schedule(level, this::setLocalInterfaceChanged, RETRY_UNLOADED_CHUNK_INTERVAL); + if (!level.isLoaded(sourcePos)) { + ServerScheduler.schedule(level, this::setNeighborChanged, RETRY_UNLOADED_CHUNK_INTERVAL); return; } @@ -344,9 +344,9 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { return; } - localInterface = blockEntity.getCapability(Capabilities.NETWORK_INTERFACE, facing); - if (localInterface.isPresent()) { - localInterface.addListener(unused -> setLocalInterfaceChanged()); + adjacentInterface = blockEntity.getCapability(Capabilities.NETWORK_INTERFACE, facing); + if (adjacentInterface.isPresent()) { + LazyOptionalUtils.addWeakListener(adjacentInterface, this, (connector, unused) -> connector.setNeighborChanged()); } } @@ -449,7 +449,7 @@ public final class NetworkConnectorBlockEntity extends ModBlockEntity { return; } - localInterface.ifPresent(dst -> { + adjacentInterface.ifPresent(dst -> { if (dst == source) { return; } diff --git a/src/main/java/li/cil/oc2/common/blockentity/NetworkHubBlockEntity.java b/src/main/java/li/cil/oc2/common/blockentity/NetworkHubBlockEntity.java index 0fd3009f..13084695 100644 --- a/src/main/java/li/cil/oc2/common/blockentity/NetworkHubBlockEntity.java +++ b/src/main/java/li/cil/oc2/common/blockentity/NetworkHubBlockEntity.java @@ -3,6 +3,8 @@ package li.cil.oc2.common.blockentity; import li.cil.oc2.api.capabilities.NetworkInterface; import li.cil.oc2.common.Constants; import li.cil.oc2.common.capabilities.Capabilities; +import li.cil.oc2.common.util.LazyOptionalUtils; +import li.cil.oc2.common.util.LevelUtils; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.level.block.entity.BlockEntity; @@ -61,6 +63,10 @@ public final class NetworkHubBlockEntity extends ModBlockEntity implements Netwo return; } + for (final Direction side : Constants.DIRECTIONS) { + adjacentInterfaces[side.get3DDataValue()] = null; + } + areAdjacentInterfacesDirty = false; if (level == null || level.isClientSide()) { @@ -69,14 +75,12 @@ public final class NetworkHubBlockEntity extends ModBlockEntity implements Netwo final BlockPos pos = getBlockPos(); for (final Direction side : Constants.DIRECTIONS) { - adjacentInterfaces[side.get3DDataValue()] = null; - - final BlockEntity neighborBlockEntity = level.getBlockEntity(pos.relative(side)); + final BlockEntity neighborBlockEntity = LevelUtils.getBlockEntityIfChunkExists(level, pos.relative(side)); if (neighborBlockEntity != null) { - final LazyOptional capability = neighborBlockEntity.getCapability(Capabilities.NETWORK_INTERFACE, side.getOpposite()); - capability.ifPresent(adjacentInterface -> { + final LazyOptional optional = neighborBlockEntity.getCapability(Capabilities.NETWORK_INTERFACE, side.getOpposite()); + optional.ifPresent(adjacentInterface -> { adjacentInterfaces[side.get3DDataValue()] = adjacentInterface; - capability.addListener(unused -> handleNeighborChanged()); + LazyOptionalUtils.addWeakListener(optional, this, (hub, unused) -> hub.handleNeighborChanged()); }); } } diff --git a/src/main/java/li/cil/oc2/common/bus/AbstractGroupingBlockDeviceBusElement.java b/src/main/java/li/cil/oc2/common/bus/AbstractGroupingBlockDeviceBusElement.java deleted file mode 100644 index cb41afbb..00000000 --- a/src/main/java/li/cil/oc2/common/bus/AbstractGroupingBlockDeviceBusElement.java +++ /dev/null @@ -1,10 +0,0 @@ -package li.cil.oc2.common.bus; - -import li.cil.oc2.api.bus.device.provider.BlockDeviceProvider; -import li.cil.oc2.common.bus.device.util.BlockDeviceInfo; - -public abstract class AbstractGroupingBlockDeviceBusElement extends AbstractGroupingDeviceBusElement { - public AbstractGroupingBlockDeviceBusElement(final int groupCount) { - super(groupCount); - } -} diff --git a/src/main/java/li/cil/oc2/common/bus/AbstractGroupingDeviceBusElement.java b/src/main/java/li/cil/oc2/common/bus/AbstractGroupingDeviceBusElement.java index 2351c00b..d85a2bbd 100644 --- a/src/main/java/li/cil/oc2/common/bus/AbstractGroupingDeviceBusElement.java +++ b/src/main/java/li/cil/oc2/common/bus/AbstractGroupingDeviceBusElement.java @@ -1,24 +1,29 @@ package li.cil.oc2.common.bus; import li.cil.oc2.api.bus.device.Device; -import li.cil.oc2.common.bus.device.util.AbstractDeviceInfo; -import li.cil.oc2.common.util.ItemDeviceUtils; import li.cil.oc2.common.util.NBTTagIds; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; -import net.minecraftforge.registries.IForgeRegistryEntry; import java.util.*; -public abstract class AbstractGroupingDeviceBusElement, TDeviceInfo extends AbstractDeviceInfo> extends AbstractDeviceBusElement { +public abstract class AbstractGroupingDeviceBusElement 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 interface Entry { + Optional getDeviceDataKey(); + + OptionalInt getDeviceEnergyConsumption(); + + Device getDevice(); + } + /////////////////////////////////////////////////////////////////// protected final int groupCount; - protected final ArrayList> groups; + protected final ArrayList> groups; /////////////////////////////////////////////////////////////////// @@ -42,10 +47,6 @@ public abstract class AbstractGroupingDeviceBusElement getDeviceGroup(final int index) { - return groups.get(index); - } - public CompoundTag save() { final ListTag listTag = new ListTag(); for (int i = 0; i < groupCount; i++) { @@ -83,9 +84,9 @@ public abstract class AbstractGroupingDeviceBusElement getDeviceIdentifier(final Device device) { for (int i = 0; i < groupCount; i++) { - final HashSet group = groups.get(i); - for (final TDeviceInfo deviceInfo : group) { - if (Objects.equals(device, deviceInfo.device)) { + final HashSet group = groups.get(i); + for (final T deviceInfo : group) { + if (Objects.equals(device, deviceInfo.getDevice())) { return Optional.of(groupIds[i]); } } @@ -95,35 +96,37 @@ public abstract class AbstractGroupingDeviceBusElement newDevices) { - final HashSet oldDevices = groups.get(index); - if (Objects.equals(newDevices, oldDevices)) { + protected final void setEntriesForGroup(final int index, final Set newEntries) { + final HashSet oldEntries = groups.get(index); + if (Objects.equals(newEntries, oldEntries)) { return; } - final HashSet removedDevices = new HashSet<>(oldDevices); - removedDevices.removeAll(newDevices); - for (final TDeviceInfo info : removedDevices) { - devices.removeInt(info.device); + final HashSet removedEntries = new HashSet<>(oldEntries); + removedEntries.removeAll(newEntries); + for (final T entry : removedEntries) { + devices.removeInt(entry.getDevice()); + onEntryRemoved(entry); } - final HashSet addedDevices = new HashSet<>(newDevices); - addedDevices.removeAll(oldDevices); - for (final TDeviceInfo info : addedDevices) { - devices.put(info.device, info.getEnergyConsumption()); + final HashSet addedEntries = new HashSet<>(newEntries); + addedEntries.removeAll(oldEntries); + for (final T entry : addedEntries) { + devices.put(entry.getDevice(), entry.getDeviceEnergyConsumption().orElse(0)); + onEntryAdded(entry); } - oldDevices.removeAll(removedDevices); - oldDevices.addAll(newDevices); + oldEntries.removeAll(removedEntries); + oldEntries.addAll(newEntries); final CompoundTag devicesTag = groupData[index]; - for (final TDeviceInfo deviceInfo : removedDevices) { - ItemDeviceUtils.getItemDeviceDataKey(deviceInfo.provider).ifPresent(devicesTag::remove); + for (final T entry : removedEntries) { + entry.getDeviceDataKey().ifPresent(devicesTag::remove); } - for (final TDeviceInfo deviceInfo : addedDevices) { - ItemDeviceUtils.getItemDeviceDataKey(deviceInfo.provider).ifPresent(key -> { + for (final T entry : addedEntries) { + entry.getDeviceDataKey().ifPresent(key -> { if (devicesTag.contains(key, NBTTagIds.TAG_COMPOUND)) { - deviceInfo.device.deserializeNBT(devicesTag.getCompound(key)); + entry.getDevice().deserializeNBT(devicesTag.getCompound(key)); } }); } @@ -131,13 +134,19 @@ public abstract class AbstractGroupingDeviceBusElement { - final CompoundTag deviceTag = deviceInfo.device.serializeNBT(); + for (final T entry : groups.get(index)) { + entry.getDeviceDataKey().ifPresent(key -> { + final CompoundTag deviceTag = entry.getDevice().serializeNBT(); if (!deviceTag.isEmpty()) { devicesTag.put(key, deviceTag); } diff --git a/src/main/java/li/cil/oc2/common/bus/AbstractGroupingItemDeviceBusElement.java b/src/main/java/li/cil/oc2/common/bus/AbstractGroupingItemDeviceBusElement.java deleted file mode 100644 index e002b956..00000000 --- a/src/main/java/li/cil/oc2/common/bus/AbstractGroupingItemDeviceBusElement.java +++ /dev/null @@ -1,10 +0,0 @@ -package li.cil.oc2.common.bus; - -import li.cil.oc2.api.bus.device.provider.ItemDeviceProvider; -import li.cil.oc2.common.bus.device.util.ItemDeviceInfo; - -public abstract class AbstractGroupingItemDeviceBusElement extends AbstractGroupingDeviceBusElement { - public AbstractGroupingItemDeviceBusElement(final int groupCount) { - super(groupCount); - } -} diff --git a/src/main/java/li/cil/oc2/common/bus/BlockEntityDeviceBusElement.java b/src/main/java/li/cil/oc2/common/bus/BlockEntityDeviceBusElement.java index d66e964a..d31f8b44 100644 --- a/src/main/java/li/cil/oc2/common/bus/BlockEntityDeviceBusElement.java +++ b/src/main/java/li/cil/oc2/common/bus/BlockEntityDeviceBusElement.java @@ -3,7 +3,9 @@ package li.cil.oc2.common.bus; 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.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.Constants; import li.cil.oc2.common.bus.device.rpc.TypeNameRPCDevice; import li.cil.oc2.common.bus.device.util.BlockDeviceInfo; @@ -20,14 +22,12 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraftforge.common.util.LazyOptional; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Optional; +import java.util.*; import static java.util.Objects.requireNonNull; +import static li.cil.oc2.common.util.RegistryUtils.optionalKey; -public class BlockEntityDeviceBusElement extends AbstractGroupingBlockDeviceBusElement implements BlockDeviceBusElement { +public class BlockEntityDeviceBusElement extends AbstractGroupingDeviceBusElement implements BlockDeviceBusElement { private final BlockEntity blockEntity; /////////////////////////////////////////////////////////////////// @@ -86,7 +86,7 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingBlockDeviceBusE public void handleNeighborChanged(final BlockPos pos) { final Level level = blockEntity.getLevel(); - if (level == null || level.isClientSide()) { + if (level == null || level.isClientSide() || !level.isLoaded(pos)) { return; } @@ -96,10 +96,10 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingBlockDeviceBusE return; } - final HashSet newDevices = collectDevices(level, pos, direction); + final HashSet newDevices = collectDevices(level, pos, direction); final int index = direction.get3DDataValue(); - setDevicesForGroup(index, newDevices); + setEntriesForGroup(index, newDevices); } public void initialize() { @@ -124,28 +124,41 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingBlockDeviceBusE return canScanContinueTowards(direction); } - protected HashSet collectDevices(final Level level, final BlockPos pos, @Nullable final Direction direction) { - final HashSet newDevices = new HashSet<>(); + protected HashSet collectDevices(final Level level, final BlockPos pos, @Nullable final Direction direction) { + final HashSet entries = new HashSet<>(); if (canDetectDevicesTowards(direction)) { final BlockDeviceQuery query = Devices.makeQuery(level, pos, direction); - for (final LazyOptional deviceInfo : Devices.getDevices(query)) { - deviceInfo.ifPresent(newDevices::add); - deviceInfo.addListener(unused -> handleNeighborChanged(pos)); + for (final Invalidatable deviceInfo : Devices.getDevices(query)) { + if (deviceInfo.isPresent()) { + entries.add(new BlockEntry(deviceInfo, pos)); + } } } - collectSyntheticDevices(level, pos, direction, newDevices); + collectSyntheticDevices(level, pos, direction, entries); - return newDevices; + return entries; } - protected void collectSyntheticDevices(final Level level, final BlockPos pos, @Nullable final Direction direction, final HashSet devices) { + protected void collectSyntheticDevices(final Level level, final BlockPos pos, @Nullable final Direction direction, final HashSet entries) { final String blockName = LevelUtils.getBlockName(level, pos); if (blockName != null) { - devices.add(new BlockDeviceInfo(null, new TypeNameRPCDevice(blockName))); + entries.add(new BlockEntry(new BlockDeviceInfo(null, new TypeNameRPCDevice(blockName)), pos)); } } + @Override + protected void onEntryAdded(final BlockEntry entry) { + super.onEntryAdded(entry); + entry.addListener(); + } + + @Override + protected void onEntryRemoved(final BlockEntry entry) { + super.onEntryRemoved(entry); + entry.removeListener(); + } + /////////////////////////////////////////////////////////////////// private void scanNeighborsForDevices() { @@ -169,4 +182,56 @@ public class BlockEntityDeviceBusElement extends AbstractGroupingBlockDeviceBusE capability.ifPresent(DeviceBus::scheduleScan); } } + + /////////////////////////////////////////////////////////////////// + + protected final class BlockEntry implements Entry { + private final Invalidatable deviceInfo; + @Nullable private final String dataKey; + private final Device device; + private final BlockPos pos; + private Invalidatable.ListenerToken token; + + public BlockEntry(final Invalidatable deviceInfo, final BlockPos pos) { + this.deviceInfo = deviceInfo; + this.pos = pos; + + // Grab these while the device info has not yet been invalidated. We still need to access + // these even after the device has been invalidated to clean up. + this.dataKey = optionalKey(deviceInfo.get().provider).orElse(null); + this.device = deviceInfo.get().device; + } + + public BlockEntry(final BlockDeviceInfo deviceInfo, final BlockPos pos) { + this(Invalidatable.of(deviceInfo), pos); + } + + @Override + public Optional getDeviceDataKey() { + return Optional.ofNullable(dataKey); + } + + @Override + public OptionalInt getDeviceEnergyConsumption() { + return deviceInfo.isPresent() ? OptionalInt.of(deviceInfo.get().getEnergyConsumption()) : OptionalInt.empty(); + } + + @Override + public Device getDevice() { + return device; + } + + public void addListener() { + if (token == null) { + token = deviceInfo.addListener(unused -> handleNeighborChanged(pos)); + } + } + + public void removeListener() { + if (token != null) { + token.removeListener(); + token = null; + } + } + } } diff --git a/src/main/java/li/cil/oc2/common/bus/CommonDeviceBusController.java b/src/main/java/li/cil/oc2/common/bus/CommonDeviceBusController.java index 78774cc2..4600ca56 100644 --- a/src/main/java/li/cil/oc2/common/bus/CommonDeviceBusController.java +++ b/src/main/java/li/cil/oc2/common/bus/CommonDeviceBusController.java @@ -5,6 +5,7 @@ import li.cil.oc2.api.bus.DeviceBusElement; import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.common.Constants; import li.cil.oc2.common.util.Event; +import li.cil.oc2.common.util.LazyOptionalUtils; import li.cil.oc2.common.util.ParameterizedEvent; import net.minecraftforge.common.util.LazyOptional; @@ -207,7 +208,9 @@ public class CommonDeviceBusController implements DeviceBusController { // Rescan if any bus element gets invalidated. for (final LazyOptional optional : optionals) { assert optional.isPresent(); - optional.addListener(unused -> scheduleBusScan()); + + // Don't have bus elements keep this instance alive, only notify us on change if we still exist. + LazyOptionalUtils.addWeakListener(optional, this, (controller, unused) -> controller.scheduleBusScan()); } onAfterBusScan(); diff --git a/src/main/java/li/cil/oc2/common/bus/ItemHandlerDeviceBusElement.java b/src/main/java/li/cil/oc2/common/bus/ItemHandlerDeviceBusElement.java index 1e327796..7d09f669 100644 --- a/src/main/java/li/cil/oc2/common/bus/ItemHandlerDeviceBusElement.java +++ b/src/main/java/li/cil/oc2/common/bus/ItemHandlerDeviceBusElement.java @@ -1,5 +1,7 @@ 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.ItemDeviceQuery; import li.cil.oc2.api.bus.device.rpc.RPCDevice; import li.cil.oc2.common.bus.device.rpc.TypeNameRPCDevice; @@ -11,11 +13,12 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; -import java.util.Collections; -import java.util.HashSet; +import java.util.*; import java.util.function.Function; -public final class ItemHandlerDeviceBusElement extends AbstractGroupingItemDeviceBusElement { +import static li.cil.oc2.common.util.RegistryUtils.optionalKey; + +public final class ItemHandlerDeviceBusElement extends AbstractGroupingDeviceBusElement { private final Function queryFactory; public ItemHandlerDeviceBusElement(final int slotCount, final Function queryFactory) { @@ -25,15 +28,25 @@ public final class ItemHandlerDeviceBusElement extends AbstractGroupingItemDevic /////////////////////////////////////////////////////////////////// + public boolean groupContains(final int groupIndex, final Device device) { + for (final ItemEntry entry : groups.get(groupIndex)) { + if (Objects.equals(entry.getDevice(), device)) { + return true; + } + } + + return false; + } + public void updateDevices(final int slot, final ItemStack stack) { if (!stack.isEmpty()) { final ItemDeviceQuery query = queryFactory.apply(stack); - final HashSet newDevices = new HashSet<>(Devices.getDevices(query)); + final HashSet newDevices = new HashSet<>(Devices.getDevices(query).stream().map(ItemEntry::new).toList()); insertItemNameDevice(stack, newDevices); importDeviceDataFromItemStack(stack, newDevices); - setDevicesForGroup(slot, newDevices); + setEntriesForGroup(slot, newDevices); } else { - setDevicesForGroup(slot, Collections.emptySet()); + setEntriesForGroup(slot, Collections.emptySet()); } } @@ -43,10 +56,10 @@ public final class ItemHandlerDeviceBusElement extends AbstractGroupingItemDevic } final CompoundTag exportedTag = new CompoundTag(); - for (final ItemDeviceInfo info : groups.get(slot)) { - ItemDeviceUtils.getItemDeviceDataKey(info.provider).ifPresent(key -> { + for (final ItemEntry entry : groups.get(slot)) { + entry.getDeviceDataKey().ifPresent(key -> { final CompoundTag deviceTag = new CompoundTag(); - info.device.exportToItemStack(deviceTag); + entry.getDevice().exportToItemStack(deviceTag); if (!deviceTag.isEmpty()) { exportedTag.put(key, deviceTag); } @@ -60,25 +73,44 @@ public final class ItemHandlerDeviceBusElement extends AbstractGroupingItemDevic /////////////////////////////////////////////////////////////////// - private void importDeviceDataFromItemStack(final ItemStack stack, final HashSet devices) { + private void importDeviceDataFromItemStack(final ItemStack stack, final HashSet entries) { final CompoundTag exportedTag = ItemDeviceUtils.getItemDeviceData(stack); if (!exportedTag.isEmpty()) { - for (final ItemDeviceInfo info : devices) { - ItemDeviceUtils.getItemDeviceDataKey(info.provider).ifPresent(key -> { + for (final ItemEntry entry : entries) { + entry.getDeviceDataKey().ifPresent(key -> { if (exportedTag.contains(key, NBTTagIds.TAG_COMPOUND)) { - info.device.importFromItemStack(exportedTag.getCompound(key)); + entry.deviceInfo.device.importFromItemStack(exportedTag.getCompound(key)); } }); } } } - private void insertItemNameDevice(final ItemStack stack, final HashSet devices) { - if (devices.stream().anyMatch(info -> info.device instanceof RPCDevice)) { + private void insertItemNameDevice(final ItemStack stack, final HashSet entries) { + if (entries.stream().anyMatch(entry -> entry.getDevice() instanceof RPCDevice)) { final ResourceLocation registryName = stack.getItem().getRegistryName(); if (registryName != null) { - devices.add(new ItemDeviceInfo(null, new TypeNameRPCDevice(registryName.toString()), 0)); + entries.add(new ItemEntry(new ItemDeviceInfo(null, new TypeNameRPCDevice(registryName.toString()), 0))); } } } + + /////////////////////////////////////////////////////////////////// + + protected record ItemEntry(ItemDeviceInfo deviceInfo) implements Entry { + @Override + public Optional getDeviceDataKey() { + return optionalKey(deviceInfo.provider); + } + + @Override + public OptionalInt getDeviceEnergyConsumption() { + return OptionalInt.of(deviceInfo.getEnergyConsumption()); + } + + @Override + public ItemDevice getDevice() { + return deviceInfo.device; + } + } } diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockEntityDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockEntityDeviceProvider.java index 774bcd1f..5307a777 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockEntityDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockEntityDeviceProvider.java @@ -4,17 +4,17 @@ import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.object.Callbacks; import li.cil.oc2.api.bus.device.object.ObjectDevice; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.bus.device.provider.util.AbstractBlockEntityDeviceProvider; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.common.util.LazyOptional; public final class BlockEntityDeviceProvider extends AbstractBlockEntityDeviceProvider { @Override - public LazyOptional getBlockDevice(final BlockDeviceQuery query, final BlockEntity blockEntity) { + public Invalidatable getBlockDevice(final BlockDeviceQuery query, final BlockEntity blockEntity) { if (Callbacks.hasMethods(blockEntity)) { - return LazyOptional.of(() -> new ObjectDevice(blockEntity)); + return Invalidatable.of(new ObjectDevice(blockEntity)); } else { - return LazyOptional.empty(); + return Invalidatable.empty(); } } } diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockStateDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockStateDeviceProvider.java index 21d56568..f3571537 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockStateDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/block/BlockStateDeviceProvider.java @@ -4,30 +4,30 @@ import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.object.Callbacks; import li.cil.oc2.api.bus.device.object.ObjectDevice; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.bus.device.provider.util.AbstractBlockDeviceProvider; import net.minecraft.core.BlockPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; -import net.minecraftforge.common.util.LazyOptional; public final class BlockStateDeviceProvider extends AbstractBlockDeviceProvider { @Override - public LazyOptional getDevice(final BlockDeviceQuery query) { + public Invalidatable getDevice(final BlockDeviceQuery query) { final Level level = query.getLevel(); final BlockPos position = query.getQueryPosition(); final BlockState blockState = level.getBlockState(position); if (blockState.isAir()) { - return LazyOptional.empty(); + return Invalidatable.empty(); } final Block block = blockState.getBlock(); if (!Callbacks.hasMethods(block)) { - return LazyOptional.empty(); + return Invalidatable.empty(); } - return LazyOptional.of(() -> new ObjectDevice(block)); + return Invalidatable.of(new ObjectDevice(block)); } } diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/block/DiskDriveDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/block/DiskDriveDeviceProvider.java index 2c2637ec..4223c85b 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/block/DiskDriveDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/block/DiskDriveDeviceProvider.java @@ -2,11 +2,11 @@ package li.cil.oc2.common.bus.device.provider.block; import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.blockentity.BlockEntities; import li.cil.oc2.common.blockentity.DiskDriveBlockEntity; import li.cil.oc2.common.bus.device.provider.util.AbstractBlockEntityDeviceProvider; import net.minecraft.world.level.block.HorizontalDirectionalBlock; -import net.minecraftforge.common.util.LazyOptional; public final class DiskDriveDeviceProvider extends AbstractBlockEntityDeviceProvider { public DiskDriveDeviceProvider() { @@ -14,13 +14,13 @@ public final class DiskDriveDeviceProvider extends AbstractBlockEntityDeviceProv } @Override - protected LazyOptional getBlockDevice(final BlockDeviceQuery query, final DiskDriveBlockEntity blockEntity) { + protected Invalidatable getBlockDevice(final BlockDeviceQuery query, final DiskDriveBlockEntity blockEntity) { // We only allow connecting to exactly one face of the disk drive to ensure only one // bus (and thus, one VM) will access the device at any single time. if (query.getQuerySide() != blockEntity.getBlockState().getValue(HorizontalDirectionalBlock.FACING)) { - return LazyOptional.empty(); + return Invalidatable.empty(); } - return LazyOptional.of(blockEntity::getDevice); + return Invalidatable.of(blockEntity.getDevice()); } } diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/block/EnergyStorageBlockDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/block/EnergyStorageBlockDeviceProvider.java index 002eff11..3e575ead 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/block/EnergyStorageBlockDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/block/EnergyStorageBlockDeviceProvider.java @@ -4,11 +4,11 @@ import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.object.Callback; import li.cil.oc2.api.bus.device.object.ObjectDevice; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.bus.device.provider.util.AbstractBlockEntityCapabilityDeviceProvider; import li.cil.oc2.common.bus.device.util.IdentityProxy; import li.cil.oc2.common.capabilities.Capabilities; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.energy.IEnergyStorage; public final class EnergyStorageBlockDeviceProvider extends AbstractBlockEntityCapabilityDeviceProvider { @@ -19,8 +19,8 @@ public final class EnergyStorageBlockDeviceProvider extends AbstractBlockEntityC /////////////////////////////////////////////////////////////////// @Override - protected LazyOptional getBlockDevice(final BlockDeviceQuery query, final IEnergyStorage value) { - return LazyOptional.of(() -> new ObjectDevice(new EnergyStorageDevice(value), "energy_storage")); + protected Invalidatable getBlockDevice(final BlockDeviceQuery query, final IEnergyStorage value) { + return Invalidatable.of(new ObjectDevice(new EnergyStorageDevice(value), "energy_storage")); } /////////////////////////////////////////////////////////////////// diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/block/FluidHandlerBlockDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/block/FluidHandlerBlockDeviceProvider.java index c2e71d34..41c394b5 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/block/FluidHandlerBlockDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/block/FluidHandlerBlockDeviceProvider.java @@ -4,11 +4,11 @@ import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.object.Callback; import li.cil.oc2.api.bus.device.object.ObjectDevice; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.bus.device.provider.util.AbstractBlockEntityCapabilityDeviceProvider; import li.cil.oc2.common.bus.device.util.IdentityProxy; import li.cil.oc2.common.capabilities.Capabilities; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.capability.IFluidHandler; @@ -20,8 +20,8 @@ public final class FluidHandlerBlockDeviceProvider extends AbstractBlockEntityCa /////////////////////////////////////////////////////////////////// @Override - protected LazyOptional getBlockDevice(final BlockDeviceQuery query, final IFluidHandler value) { - return LazyOptional.of(() -> new ObjectDevice(new FluidHandlerDevice(value), "fluid_handler")); + protected Invalidatable getBlockDevice(final BlockDeviceQuery query, final IFluidHandler value) { + return Invalidatable.of(new ObjectDevice(new FluidHandlerDevice(value), "fluid_handler")); } /////////////////////////////////////////////////////////////////// diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/block/ItemHandlerBlockDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/block/ItemHandlerBlockDeviceProvider.java index 3bcb4faf..6c01d808 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/block/ItemHandlerBlockDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/block/ItemHandlerBlockDeviceProvider.java @@ -4,12 +4,12 @@ import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.object.Callback; import li.cil.oc2.api.bus.device.object.ObjectDevice; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.bus.device.provider.util.AbstractBlockEntityCapabilityDeviceProvider; import li.cil.oc2.common.bus.device.util.IdentityProxy; import li.cil.oc2.common.capabilities.Capabilities; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.items.IItemHandler; public final class ItemHandlerBlockDeviceProvider extends AbstractBlockEntityCapabilityDeviceProvider { @@ -20,8 +20,8 @@ public final class ItemHandlerBlockDeviceProvider extends AbstractBlockEntityCap /////////////////////////////////////////////////////////////////// @Override - protected LazyOptional getBlockDevice(final BlockDeviceQuery query, final IItemHandler value) { - return LazyOptional.of(() -> new ObjectDevice(new ItemHandlerDevice(value), "item_handler")); + protected Invalidatable getBlockDevice(final BlockDeviceQuery query, final IItemHandler value) { + return Invalidatable.of(new ObjectDevice(new ItemHandlerDevice(value), "item_handler")); } /////////////////////////////////////////////////////////////////// diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityCapabilityDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityCapabilityDeviceProvider.java index 393a1275..c812765a 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityCapabilityDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityCapabilityDeviceProvider.java @@ -2,6 +2,8 @@ package li.cil.oc2.common.bus.device.provider.util; import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; +import li.cil.oc2.common.util.LazyOptionalUtils; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraftforge.common.capabilities.Capability; @@ -26,19 +28,22 @@ public abstract class AbstractBlockEntityCapabilityDeviceProvider getBlockDevice(final BlockDeviceQuery blockQuery, final BlockEntity blockEntity) { + protected final Invalidatable getBlockDevice(final BlockDeviceQuery blockQuery, final BlockEntity blockEntity) { final Capability capability = capabilitySupplier.get(); if (capability == null) throw new IllegalStateException(); final LazyOptional optional = blockEntity.getCapability(capability, blockQuery.getQuerySide()); if (!optional.isPresent()) { - return LazyOptional.empty(); + return Invalidatable.empty(); } final TCapability value = optional.orElseThrow(AssertionError::new); - final LazyOptional device = getBlockDevice(blockQuery, value); - optional.addListener(ignored -> device.invalidate()); + final Invalidatable device = getBlockDevice(blockQuery, value); + + // When capability gets invalidated, invalidate device. But don't keep device alive via capability. + LazyOptionalUtils.addWeakListener(optional, device, (invalidatable, unused) -> invalidatable.invalidate()); + return device; } - protected abstract LazyOptional getBlockDevice(final BlockDeviceQuery query, final TCapability value); + protected abstract Invalidatable getBlockDevice(final BlockDeviceQuery query, final TCapability value); } diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityDeviceProvider.java index acd27c56..3abc5e1e 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityDeviceProvider.java +++ b/src/main/java/li/cil/oc2/common/bus/device/provider/util/AbstractBlockEntityDeviceProvider.java @@ -2,10 +2,9 @@ package li.cil.oc2.common.bus.device.provider.util; import li.cil.oc2.api.bus.device.Device; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; -import li.cil.oc2.common.util.LevelUtils; +import li.cil.oc2.api.util.Invalidatable; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; -import net.minecraftforge.common.util.LazyOptional; public abstract class AbstractBlockEntityDeviceProvider extends AbstractBlockDeviceProvider { private final BlockEntityType blockEntityType; @@ -24,14 +23,14 @@ public abstract class AbstractBlockEntityDeviceProvider e @SuppressWarnings("unchecked") @Override - public final LazyOptional getDevice(final BlockDeviceQuery query) { - final BlockEntity blockEntity = LevelUtils.getBlockEntityIfChunkExists(query.getLevel(), query.getQueryPosition()); + public final Invalidatable getDevice(final BlockDeviceQuery query) { + final BlockEntity blockEntity = query.getLevel().getBlockEntity(query.getQueryPosition()); if (blockEntity == null) { - return LazyOptional.empty(); + return Invalidatable.empty(); } if (blockEntityType != null && blockEntity.getType() != blockEntityType) { - return LazyOptional.empty(); + return Invalidatable.empty(); } return getBlockDevice(query, (T) blockEntity); @@ -39,5 +38,5 @@ public abstract class AbstractBlockEntityDeviceProvider e /////////////////////////////////////////////////////////////////// - protected abstract LazyOptional getBlockDevice(final BlockDeviceQuery query, final T blockEntity); + protected abstract Invalidatable getBlockDevice(final BlockDeviceQuery query, final T blockEntity); } diff --git a/src/main/java/li/cil/oc2/common/bus/device/util/Devices.java b/src/main/java/li/cil/oc2/common/bus/device/util/Devices.java index a0da0f3f..9e9f6b75 100644 --- a/src/main/java/li/cil/oc2/common/bus/device/util/Devices.java +++ b/src/main/java/li/cil/oc2/common/bus/device/util/Devices.java @@ -7,6 +7,7 @@ import li.cil.oc2.api.bus.device.provider.BlockDeviceProvider; import li.cil.oc2.api.bus.device.provider.BlockDeviceQuery; import li.cil.oc2.api.bus.device.provider.ItemDeviceProvider; import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery; +import li.cil.oc2.api.util.Invalidatable; import li.cil.oc2.common.bus.device.provider.Providers; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -14,7 +15,6 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.registries.IForgeRegistry; import javax.annotation.Nullable; @@ -45,19 +45,17 @@ public final class Devices { return new ItemQuery(entity, stack); } - public static List> getDevices(final BlockDeviceQuery query) { + public static List> getDevices(final BlockDeviceQuery query) { if (!query.getLevel().isLoaded(query.getQueryPosition())) { return Collections.emptyList(); } final IForgeRegistry registry = Providers.BLOCK_DEVICE_PROVIDER_REGISTRY.get(); - final ArrayList> devices = new ArrayList<>(); + final ArrayList> devices = new ArrayList<>(); for (final BlockDeviceProvider provider : registry.getValues()) { - final LazyOptional device = provider.getDevice(query); + final Invalidatable device = provider.getDevice(query); if (device.isPresent()) { - final LazyOptional info = device.lazyMap(d -> new BlockDeviceInfo(provider, d)); - device.addListener(unused -> info.invalidate()); - devices.add(info); + devices.add(device.mapWithDependency(d -> new BlockDeviceInfo(provider, d))); } } return devices; diff --git a/src/main/java/li/cil/oc2/common/serialization/serializers/JsonArraySerializer.java b/src/main/java/li/cil/oc2/common/serialization/serializers/JsonArraySerializer.java index 96af5a72..fea2f788 100644 --- a/src/main/java/li/cil/oc2/common/serialization/serializers/JsonArraySerializer.java +++ b/src/main/java/li/cil/oc2/common/serialization/serializers/JsonArraySerializer.java @@ -37,7 +37,7 @@ public final class JsonArraySerializer implements Serializer { array.remove(array.size() - 1); } - array.addAll(new JsonParser().parse(jsonString).getAsJsonArray()); + array.addAll(JsonParser.parseString(jsonString).getAsJsonArray()); return array; } diff --git a/src/main/java/li/cil/oc2/common/util/ItemDeviceUtils.java b/src/main/java/li/cil/oc2/common/util/ItemDeviceUtils.java index 3b41e103..8ab040ac 100644 --- a/src/main/java/li/cil/oc2/common/util/ItemDeviceUtils.java +++ b/src/main/java/li/cil/oc2/common/util/ItemDeviceUtils.java @@ -1,12 +1,7 @@ package li.cil.oc2.common.util; import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; -import net.minecraftforge.registries.IForgeRegistryEntry; - -import javax.annotation.Nullable; -import java.util.Optional; public final class ItemDeviceUtils { private static final String ITEM_DEVICE_DATA_TAG_NAME = "item_device"; @@ -20,17 +15,4 @@ public final class ItemDeviceUtils { public static void setItemDeviceData(final ItemStack stack, final CompoundTag data) { ItemStackUtils.getOrCreateModDataTag(stack).put(ITEM_DEVICE_DATA_TAG_NAME, data); } - - public static Optional getItemDeviceDataKey(@Nullable final IForgeRegistryEntry provider) { - if (provider == null) { - return Optional.empty(); - } - - final ResourceLocation providerName = provider.getRegistryName(); - if (providerName == null) { - return Optional.empty(); - } - - return Optional.of(providerName.toString()); - } } diff --git a/src/main/java/li/cil/oc2/common/util/LazyOptionalUtils.java b/src/main/java/li/cil/oc2/common/util/LazyOptionalUtils.java new file mode 100644 index 00000000..eee9fa60 --- /dev/null +++ b/src/main/java/li/cil/oc2/common/util/LazyOptionalUtils.java @@ -0,0 +1,36 @@ +package li.cil.oc2.common.util; + +import net.minecraftforge.common.util.LazyOptional; +import net.minecraftforge.common.util.NonNullConsumer; + +import java.lang.ref.WeakReference; +import java.util.function.BiConsumer; + +public final class LazyOptionalUtils { + /** + * Adds a weak listener to a {@link LazyOptional}. + *

+ * {@link LazyOptional}s do not allow removing listeners again, so once added, they will prevent garbage collection + * of listeners until they go out of scope themselves. This may result in undesired "memory leaks". To work around + * this, we register a light-weight proxy listener instead, which only keeps a weak reference to the actual + * listener implementation context. + * + * @param optional the optional to add the listener to. + * @param weakValue the value that should only be referenced weakly, allowing garbage collection. + * @param listener the listener proxy which will take the weak value as context, if it still exists. + * @param the type of the optional. + * @param the type of the listener context. + */ + public static void addWeakListener(final LazyOptional optional, final U weakValue, final BiConsumer> listener) { + optional.addListener(buildListener(new WeakReference(weakValue), listener)); + } + + private static NonNullConsumer> buildListener(final WeakReference weakValue, final BiConsumer> listener) { + return capability -> { + final U value = weakValue.get(); + if (value != null) { + listener.accept(value, capability); + } + }; + } +} diff --git a/src/main/java/li/cil/oc2/common/util/RegistryUtils.java b/src/main/java/li/cil/oc2/common/util/RegistryUtils.java index 5a97716f..8f0bcd26 100644 --- a/src/main/java/li/cil/oc2/common/util/RegistryUtils.java +++ b/src/main/java/li/cil/oc2/common/util/RegistryUtils.java @@ -1,14 +1,17 @@ package li.cil.oc2.common.util; import li.cil.oc2.api.API; +import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.IForgeRegistry; import net.minecraftforge.registries.IForgeRegistryEntry; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; public abstract class RegistryUtils { private enum Phase { @@ -56,6 +59,19 @@ public abstract class RegistryUtils { return Objects.requireNonNull(registryEntry.getRegistryName()).toString(); } + public static Optional optionalKey(@Nullable final IForgeRegistryEntry registryEntry) { + if (registryEntry == null) { + return Optional.empty(); + } + + final ResourceLocation providerName = registryEntry.getRegistryName(); + if (providerName == null) { + return Optional.empty(); + } + + return Optional.of(providerName.toString()); + } + private RegistryUtils() { } } diff --git a/src/main/java/li/cil/oc2/common/vm/AbstractVMItemStackHandlers.java b/src/main/java/li/cil/oc2/common/vm/AbstractVMItemStackHandlers.java index 6d7652ac..25a71b6e 100644 --- a/src/main/java/li/cil/oc2/common/vm/AbstractVMItemStackHandlers.java +++ b/src/main/java/li/cil/oc2/common/vm/AbstractVMItemStackHandlers.java @@ -6,7 +6,6 @@ import li.cil.oc2.api.bus.device.DeviceTypes; import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery; import li.cil.oc2.api.bus.device.vm.VMDevice; import li.cil.oc2.common.bus.AbstractDeviceBusElement; -import li.cil.oc2.common.bus.device.util.ItemDeviceInfo; import li.cil.oc2.common.container.DeviceItemStackHandler; import li.cil.oc2.common.container.TypedDeviceItemStackHandler; import net.minecraft.nbt.CompoundTag; @@ -76,17 +75,14 @@ public abstract class AbstractVMItemStackHandlers implements VMItemStackHandlers final DeviceItemStackHandler handler = entry.getValue(); for (int i = 0; i < handler.getSlots(); i++) { - final Collection devices = handler.getBusElement().getDeviceGroup(i); - for (final ItemDeviceInfo info : devices) { - if (Objects.equals(info.device, wrapper)) { - // Ahhh, such special casing, much wow. Honestly I don't expect this - // special case to ever be needed for anything other than physical - // memory, so it's fine. Prove me wrong. - if (deviceType == DeviceTypes.MEMORY) { - return OptionalLong.empty(); - } else { - return OptionalLong.of(address); - } + if (handler.getBusElement().groupContains(i, wrapper)) { + // Ahhh, such special casing, much wow. Honestly I don't expect this + // special case to ever be needed for anything other than physical + // memory, so it's fine. Prove me wrong. + if (deviceType == DeviceTypes.MEMORY) { + return OptionalLong.empty(); + } else { + return OptionalLong.of(address); } }