mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-07-15 16:32:52 +00:00

Due to the asynchronous nature of main-thread tasks, it's possible for them to be executed on peripherals which have been detached. This has been known for a long time (#893 was opened back in 2021), but finding a good solution here is tricky. Most of the time the method will silently succeed, but if we try to interact with an IComputerAccess (such as in inventory methods, as seen in #1750), we throw a NotAttachedException exception and spam the logs! This is an initial step towards fixing this - when calling a peripheral method via peripheral.call/modem.callRemote, we now wrap any enqueued main-thread tasks and silently skip them if the peripheral has been detached since. This means that peripheral methods may start to return nil when they didn't before. I think this is *fine* (though not ideal for sure!) - we return nil if the peripheral has been detached, so it's largely equivalent to that.
335 lines
11 KiB
Java
335 lines
11 KiB
Java
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
|
//
|
|
// SPDX-License-Identifier: LicenseRef-CCPL
|
|
|
|
package dan200.computercraft.core.apis;
|
|
|
|
import dan200.computercraft.api.filesystem.Mount;
|
|
import dan200.computercraft.api.filesystem.WritableMount;
|
|
import dan200.computercraft.api.lua.*;
|
|
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;
|
|
import dan200.computercraft.core.util.LuaUtil;
|
|
|
|
import javax.annotation.Nullable;
|
|
import java.util.*;
|
|
|
|
/**
|
|
* CC's "native" peripheral API. This is wrapped within CraftOS to provide a version which works with modems.
|
|
*
|
|
* @cc.module peripheral
|
|
* @hidden
|
|
*/
|
|
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener {
|
|
private class PeripheralWrapper extends ComputerAccess implements GuardedLuaContext.Guard {
|
|
private final String side;
|
|
private final IPeripheral peripheral;
|
|
|
|
private final String type;
|
|
private final Set<String> additionalTypes;
|
|
private final Map<String, PeripheralMethod> methodMap;
|
|
private boolean attached = false;
|
|
|
|
private @Nullable GuardedLuaContext contextWrapper;
|
|
|
|
PeripheralWrapper(IPeripheral peripheral, String side) {
|
|
super(environment);
|
|
this.side = side;
|
|
this.peripheral = peripheral;
|
|
|
|
type = Objects.requireNonNull(peripheral.getType(), "Peripheral type cannot be null");
|
|
additionalTypes = peripheral.getAdditionalTypes();
|
|
|
|
methodMap = peripheralMethods.getSelfMethods(peripheral);
|
|
}
|
|
|
|
public IPeripheral getPeripheral() {
|
|
return peripheral;
|
|
}
|
|
|
|
public String getType() {
|
|
return type;
|
|
}
|
|
|
|
public Set<String> getAdditionalTypes() {
|
|
return additionalTypes;
|
|
}
|
|
|
|
public Collection<String> getMethods() {
|
|
return methodMap.keySet();
|
|
}
|
|
|
|
public synchronized boolean isAttached() {
|
|
return attached;
|
|
}
|
|
|
|
public synchronized void attach() {
|
|
attached = true;
|
|
peripheral.attach(this);
|
|
}
|
|
|
|
public void detach() {
|
|
// Call detach
|
|
peripheral.detach(this);
|
|
|
|
synchronized (this) {
|
|
// Unmount everything the detach function forgot to do
|
|
unmountAll();
|
|
}
|
|
|
|
attached = false;
|
|
}
|
|
|
|
public MethodResult call(ILuaContext context, String methodName, IArguments arguments) throws LuaException {
|
|
PeripheralMethod method;
|
|
synchronized (this) {
|
|
method = methodMap.get(methodName);
|
|
}
|
|
|
|
if (method == null) throw new LuaException("No such method " + methodName);
|
|
|
|
// 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
|
|
|
|
@Nullable
|
|
@Override
|
|
public synchronized String mount(String desiredLoc, Mount mount, String driveName) {
|
|
if (!attached) throw new NotAttachedException();
|
|
return super.mount(desiredLoc, mount, driveName);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public synchronized String mountWritable(String desiredLoc, WritableMount mount, String driveName) {
|
|
if (!attached) throw new NotAttachedException();
|
|
return super.mountWritable(desiredLoc, mount, driveName);
|
|
}
|
|
|
|
@Override
|
|
public synchronized void unmount(@Nullable String location) {
|
|
if (!attached) throw new NotAttachedException();
|
|
super.unmount(location);
|
|
}
|
|
|
|
@Override
|
|
public int getID() {
|
|
if (!attached) throw new NotAttachedException();
|
|
return super.getID();
|
|
}
|
|
|
|
@Override
|
|
public void queueEvent(String event, @Nullable Object... arguments) {
|
|
if (!attached) throw new NotAttachedException();
|
|
super.queueEvent(event, arguments);
|
|
}
|
|
|
|
@Override
|
|
public String getAttachmentName() {
|
|
if (!attached) throw new NotAttachedException();
|
|
return side;
|
|
}
|
|
|
|
@Override
|
|
public Map<String, IPeripheral> getAvailablePeripherals() {
|
|
if (!attached) throw new NotAttachedException();
|
|
|
|
Map<String, IPeripheral> peripherals = new HashMap<>();
|
|
for (var wrapper : PeripheralAPI.this.peripherals) {
|
|
if (wrapper != null && wrapper.isAttached()) {
|
|
peripherals.put(wrapper.getAttachmentName(), wrapper.getPeripheral());
|
|
}
|
|
}
|
|
|
|
return Collections.unmodifiableMap(peripherals);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public IPeripheral getAvailablePeripheral(String name) {
|
|
if (!attached) throw new NotAttachedException();
|
|
|
|
for (var wrapper : peripherals) {
|
|
if (wrapper != null && wrapper.isAttached() && wrapper.getAttachmentName().equals(name)) {
|
|
return wrapper.getPeripheral();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public WorkMonitor getMainThreadMonitor() {
|
|
if (!attached) throw new NotAttachedException();
|
|
return super.getMainThreadMonitor();
|
|
}
|
|
}
|
|
|
|
private final IAPIEnvironment environment;
|
|
private final MethodSupplier<PeripheralMethod> peripheralMethods;
|
|
private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6];
|
|
private boolean running;
|
|
|
|
public PeripheralAPI(IAPIEnvironment environment, MethodSupplier<PeripheralMethod> peripheralMethods) {
|
|
this.environment = environment;
|
|
this.peripheralMethods = peripheralMethods;
|
|
this.environment.setPeripheralChangeListener(this);
|
|
running = false;
|
|
}
|
|
|
|
// IPeripheralChangeListener
|
|
|
|
@Override
|
|
public void onPeripheralChanged(ComputerSide side, @Nullable IPeripheral newPeripheral) {
|
|
synchronized (peripherals) {
|
|
var index = side.ordinal();
|
|
if (peripherals[index] != null) {
|
|
// Queue a detachment
|
|
final var wrapper = peripherals[index];
|
|
if (wrapper.isAttached()) wrapper.detach();
|
|
|
|
// Queue a detachment event
|
|
environment.queueEvent("peripheral_detach", side.getName());
|
|
}
|
|
|
|
// Assign the new peripheral
|
|
peripherals[index] = newPeripheral == null ? null
|
|
: new PeripheralWrapper(newPeripheral, side.getName());
|
|
|
|
if (peripherals[index] != null) {
|
|
// Queue an attachment
|
|
final var wrapper = peripherals[index];
|
|
if (running && !wrapper.isAttached()) wrapper.attach();
|
|
|
|
// Queue an attachment event
|
|
environment.queueEvent("peripheral", side.getName());
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String[] getNames() {
|
|
return new String[]{ "peripheral" };
|
|
}
|
|
|
|
@Override
|
|
public void startup() {
|
|
synchronized (peripherals) {
|
|
running = true;
|
|
for (var i = 0; i < 6; i++) {
|
|
var wrapper = peripherals[i];
|
|
if (wrapper != null && !wrapper.isAttached()) wrapper.attach();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void shutdown() {
|
|
synchronized (peripherals) {
|
|
running = false;
|
|
for (var i = 0; i < 6; i++) {
|
|
var wrapper = peripherals[i];
|
|
if (wrapper != null && wrapper.isAttached()) {
|
|
wrapper.detach();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@LuaFunction
|
|
public final boolean isPresent(String sideName) {
|
|
var side = ComputerSide.valueOfInsensitive(sideName);
|
|
if (side != null) {
|
|
synchronized (peripherals) {
|
|
var p = peripherals[side.ordinal()];
|
|
if (p != null) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Nullable
|
|
@LuaFunction
|
|
public final Object[] getType(String sideName) {
|
|
var side = ComputerSide.valueOfInsensitive(sideName);
|
|
if (side == null) return null;
|
|
|
|
synchronized (peripherals) {
|
|
var p = peripherals[side.ordinal()];
|
|
return p == null ? null : LuaUtil.consArray(p.getType(), p.getAdditionalTypes());
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
@LuaFunction
|
|
public final Object[] hasType(String sideName, String type) {
|
|
var side = ComputerSide.valueOfInsensitive(sideName);
|
|
if (side == null) return null;
|
|
|
|
synchronized (peripherals) {
|
|
var p = peripherals[side.ordinal()];
|
|
if (p != null) {
|
|
return new Object[]{ p.getType().equals(type) || p.getAdditionalTypes().contains(type) };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Nullable
|
|
@LuaFunction
|
|
public final Object[] getMethods(String sideName) {
|
|
var side = ComputerSide.valueOfInsensitive(sideName);
|
|
if (side == null) return null;
|
|
|
|
synchronized (peripherals) {
|
|
var p = peripherals[side.ordinal()];
|
|
if (p != null) return new Object[]{ p.getMethods() };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@LuaFunction
|
|
public final MethodResult call(ILuaContext context, IArguments args) throws LuaException {
|
|
var side = ComputerSide.valueOfInsensitive(args.getString(0));
|
|
var methodName = args.getString(1);
|
|
var methodArgs = args.drop(2);
|
|
|
|
if (side == null) throw new LuaException("No peripheral attached");
|
|
|
|
PeripheralWrapper p;
|
|
synchronized (peripherals) {
|
|
p = peripherals[side.ordinal()];
|
|
}
|
|
if (p == null) throw new LuaException("No peripheral attached");
|
|
|
|
try {
|
|
return p.call(context, methodName, methodArgs).adjustError(1);
|
|
} catch (LuaException e) {
|
|
// We increase the error level by one in order to shift the error level to where peripheral.call was
|
|
// invoked. It would be possible to do it in Lua code, but would add significantly more overhead.
|
|
if (e.getLevel() > 0) throw new FastLuaException(e.getMessage(), e.getLevel() + 1);
|
|
throw e;
|
|
}
|
|
}
|
|
}
|