diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java index d57208b8f..ee4f1f299 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/generic/GenericPeripheral.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IDynamicPeripheral; import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.computer.GuardedLuaContext; import net.minecraft.core.Direction; import net.minecraft.world.level.block.entity.BlockEntity; @@ -26,12 +27,16 @@ public final class GenericPeripheral implements IDynamicPeripheral { private final Set additionalTypes; private final List methods; + private @Nullable GuardedLuaContext contextWrapper; + private final GuardedLuaContext.Guard guard; + GenericPeripheral(BlockEntity tile, Direction side, String type, Set additionalTypes, List methods) { this.side = side; this.tile = tile; this.type = type; this.additionalTypes = additionalTypes; this.methods = methods; + this.guard = () -> !tile.isRemoved() && tile.getLevel() != null && tile.getLevel().isLoaded(tile.getBlockPos()); } public Direction side() { @@ -47,7 +52,12 @@ public final class GenericPeripheral implements IDynamicPeripheral { @Override public MethodResult callMethod(IComputerAccess computer, ILuaContext context, int method, IArguments arguments) throws LuaException { - return methods.get(method).apply(context, computer, arguments); + var contextWrapper = this.contextWrapper; + if (contextWrapper == null || !contextWrapper.wraps(context)) { + contextWrapper = this.contextWrapper = new GuardedLuaContext(context, guard); + } + + return methods.get(method).apply(contextWrapper, computer, arguments); } @Override diff --git a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java index ac14bd962..ce1374b27 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java @@ -15,6 +15,7 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.NotAttachedException; import dan200.computercraft.api.peripheral.WorkMonitor; import dan200.computercraft.core.apis.PeripheralAPI; +import dan200.computercraft.core.computer.GuardedLuaContext; import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.core.util.LuaUtil; import dan200.computercraft.shared.computer.core.ServerContext; @@ -304,7 +305,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi return wrappers == null ? null : wrappers.get(remoteName); } - private static class RemotePeripheralWrapper implements IComputerAccess { + private static class RemotePeripheralWrapper implements IComputerAccess, GuardedLuaContext.Guard { private final WiredModemElement element; private final IPeripheral peripheral; private final IComputerAccess computer; @@ -317,6 +318,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi private volatile boolean attached; private final Set mounts = new HashSet<>(); + private @Nullable GuardedLuaContext contextWrapper; + RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map methods) { this.element = element; this.peripheral = peripheral; @@ -364,7 +367,19 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi public MethodResult callMethod(ILuaContext context, String methodName, IArguments arguments) throws LuaException { var method = methodMap.get(methodName); if (method == null) throw new LuaException("No such method " + methodName); - return method.apply(peripheral, context, this, arguments); + + // Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations. + var contextWrapper = this.contextWrapper; + if (contextWrapper == null || !contextWrapper.wraps(context)) { + contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this); + } + + return method.apply(peripheral, contextWrapper, this, arguments); + } + + @Override + public boolean checkValid() { + return attached; } // IComputerAccess implementation diff --git a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java index 70fdb0b1c..0b5307fcd 100644 --- a/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java +++ b/projects/core/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java @@ -11,6 +11,7 @@ import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.NotAttachedException; import dan200.computercraft.api.peripheral.WorkMonitor; import dan200.computercraft.core.computer.ComputerSide; +import dan200.computercraft.core.computer.GuardedLuaContext; import dan200.computercraft.core.methods.MethodSupplier; import dan200.computercraft.core.methods.PeripheralMethod; import dan200.computercraft.core.metrics.Metrics; @@ -26,7 +27,7 @@ import java.util.*; * @hidden */ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener { - private class PeripheralWrapper extends ComputerAccess { + private class PeripheralWrapper extends ComputerAccess implements GuardedLuaContext.Guard { private final String side; private final IPeripheral peripheral; @@ -35,6 +36,8 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange private final Map methodMap; private boolean attached = false; + private @Nullable GuardedLuaContext contextWrapper; + PeripheralWrapper(IPeripheral peripheral, String side) { super(environment); this.side = side; @@ -91,9 +94,21 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange if (method == null) throw new LuaException("No such method " + methodName); - try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) { - return method.apply(peripheral, context, this, arguments); + // Wrap the ILuaContext. We try to reuse the previous context where possible to avoid allocations - this + // should be pretty common as ILuaMachine uses a constant context. + var contextWrapper = this.contextWrapper; + if (contextWrapper == null || !contextWrapper.wraps(context)) { + contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this); } + + try (var ignored = environment.time(Metrics.PERIPHERAL_OPS)) { + return method.apply(peripheral, contextWrapper, this, arguments); + } + } + + @Override + public boolean checkValid() { + return isAttached(); } // IComputerAccess implementation diff --git a/projects/core/src/main/java/dan200/computercraft/core/computer/GuardedLuaContext.java b/projects/core/src/main/java/dan200/computercraft/core/computer/GuardedLuaContext.java new file mode 100644 index 000000000..06dc65235 --- /dev/null +++ b/projects/core/src/main/java/dan200/computercraft/core/computer/GuardedLuaContext.java @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.core.computer; + +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaTask; + +/** + * A {@link ILuaContext} which checks if context is valid when before executing + * {@linkplain #issueMainThreadTask(LuaTask) main-thread tasks}. + */ +public final class GuardedLuaContext implements ILuaContext { + private final ILuaContext original; + private final Guard guard; + + public GuardedLuaContext(ILuaContext original, Guard guard) { + this.original = original; + this.guard = guard; + } + + /** + * Determine if this {@link GuardedLuaContext} wraps another context. + *

+ * This may be used to avoid constructing new guarded contexts, in a pattern something like: + * + *

{@code
+     * var contextWrapper = this.contextWrapper;
+     * if(contextWrapper == null || !contextWrapper.wraps(context)) {
+     *     contextWrapper = this.contextWrapper = new GuardedLuaContext(context, this);
+     * }
+     * }
+ * + * @param context The original context. + * @return Whether {@code this} wraps {@code context}. + */ + public boolean wraps(ILuaContext context) { + return original == context; + } + + @Override + public long issueMainThreadTask(LuaTask task) throws LuaException { + return original.issueMainThreadTask(() -> guard.checkValid() ? task.execute() : null); + } + + /** + * The function which checks if the context is still valid. + */ + @FunctionalInterface + public interface Guard { + boolean checkValid(); + } +}