mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2026-06-05 20:32:07 +00:00
Build a web-based emulator for the documentation site (#1597)
Historically we've used copy-cat to provide a web-based emulator for running example code on our documentation site. However, copy-cat is often out-of-date with CC:T, which means example snippets fail when you try to run them! This commit vendors in copy-cat (or rather an updated version of it) into CC:T itself, allowing us to ensure the emulator is always in sync with the mod. While the ARCHITECTURE.md documentation goes into a little bit more detail here, the general implementation is as follows - In project/src/main we implement the core of the emulator. This includes a basic reimplementation of some of CC's classes to work on the web (mostly the HTTP API and ComputerThread), and some additional code to expose the computers to Javascript. - This is all then compiled to Javascript using [TeaVM][1] (we actually use a [personal fork of it][2] as there's a couple of changes I've not upstreamed yet). - The Javascript side then pulls in the these compiled classes (and the CC ROM) and hooks them up to [cc-web-term][3] to display the actual computer. - As we're no longer pulling in copy-cat, we can simplify our bundling system a little - we now just compile to ESM modules directly. [1]: https://github.com/konsoletyper/teavm [2]: https://github.com/SquidDev/teavm/tree/squid-patches [3]: https://github.com/squiddev-cc/cc-web-term
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web;
|
||||
|
||||
import cc.tweaked.web.js.ComputerDisplay;
|
||||
import cc.tweaked.web.js.ComputerHandle;
|
||||
import cc.tweaked.web.js.JavascriptConv;
|
||||
import cc.tweaked.web.peripheral.SpeakerPeripheral;
|
||||
import cc.tweaked.web.peripheral.TickablePeripheral;
|
||||
import dan200.computercraft.api.filesystem.WritableMount;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFile;
|
||||
import dan200.computercraft.core.apis.transfer.TransferredFiles;
|
||||
import dan200.computercraft.core.computer.Computer;
|
||||
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
||||
import dan200.computercraft.core.metrics.Metric;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.core.JSString;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Manages the core lifecycle of an emulated {@link Computer}.
|
||||
* <p>
|
||||
* This is exposed to Javascript via the {@link ComputerHandle} interface.
|
||||
*/
|
||||
class EmulatedComputer implements ComputerEnvironment, ComputerHandle, MetricsObserver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EmulatedComputer.class);
|
||||
|
||||
private static final ComputerSide[] SIDES = ComputerSide.values();
|
||||
private boolean terminalChanged = false;
|
||||
private final Terminal terminal = new Terminal(51, 19, true, () -> terminalChanged = true);
|
||||
private final Computer computer;
|
||||
private final ComputerDisplay computerAccess;
|
||||
private boolean disposed = false;
|
||||
private final MemoryMount mount = new MemoryMount();
|
||||
|
||||
EmulatedComputer(ComputerContext context, ComputerDisplay computerAccess) {
|
||||
this.computerAccess = computerAccess;
|
||||
this.computer = new Computer(context, this, terminal, 0);
|
||||
|
||||
if (!disposed) computer.turnOn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick this computer.
|
||||
*
|
||||
* @return If this computer has been disposed of.
|
||||
*/
|
||||
public boolean tick() {
|
||||
if (disposed && computer.isOn()) computer.unload();
|
||||
|
||||
try {
|
||||
computer.tick();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("Error when ticking computer", e);
|
||||
}
|
||||
|
||||
if (computer.pollAndResetChanged()) {
|
||||
computerAccess.setState(computer.getLabel(), computer.isOn());
|
||||
}
|
||||
|
||||
for (var side : SIDES) {
|
||||
var peripheral = computer.getEnvironment().getPeripheral(side);
|
||||
if (peripheral instanceof TickablePeripheral toTick) toTick.tick();
|
||||
}
|
||||
|
||||
if (terminalChanged) {
|
||||
terminalChanged = false;
|
||||
computerAccess.updateTerminal(
|
||||
terminal.getWidth(), terminal.getHeight(),
|
||||
terminal.getCursorX(), terminal.getCursorY(),
|
||||
terminal.getCursorBlink(), terminal.getTextColour()
|
||||
);
|
||||
|
||||
for (var i = 0; i < terminal.getHeight(); i++) {
|
||||
computerAccess.setTerminalLine(i,
|
||||
terminal.getLine(i).toString(),
|
||||
terminal.getTextColourLine(i).toString(),
|
||||
terminal.getBackgroundColourLine(i).toString()
|
||||
);
|
||||
}
|
||||
|
||||
var palette = terminal.getPalette();
|
||||
for (var i = 0; i < 16; i++) {
|
||||
var colours = palette.getColour(i);
|
||||
computerAccess.setPaletteColour(15 - i, colours[0], colours[1], colours[2]);
|
||||
}
|
||||
|
||||
computerAccess.flushTerminal();
|
||||
}
|
||||
|
||||
return disposed && !computer.isOn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDay() {
|
||||
return (int) ((Main.getTicks() + 6000) / 24000) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getTimeOfDay() {
|
||||
return ((Main.getTicks() + 6000) % 24000) / 1000.0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public WritableMount createRootMount() {
|
||||
return mount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetricsObserver getMetrics() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observe(Metric.Counter counter) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observe(Metric.Event event, long value) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void event(String event, @Nullable JSObject[] args) {
|
||||
computer.queueEvent(event, JavascriptConv.toJava(args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
computer.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void turnOn() {
|
||||
computer.turnOn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reboot() {
|
||||
computer.reboot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferFiles(FileContents[] files) {
|
||||
computer.queueEvent(TransferredFiles.EVENT, new Object[]{
|
||||
new TransferredFiles(
|
||||
Arrays.stream(files)
|
||||
.map(x -> new TransferredFile(x.getName(), new ArrayByteChannel(bytesOfBuffer(x.getContents()))))
|
||||
.toList(),
|
||||
() -> {
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPeripheral(String sideName, @Nullable String kind) {
|
||||
var side = ComputerSide.valueOfInsensitive(sideName);
|
||||
if (side == null) throw new IllegalArgumentException("Unknown sideName");
|
||||
|
||||
IPeripheral peripheral;
|
||||
if (kind == null) {
|
||||
peripheral = null;
|
||||
} else if (kind.equals("speaker")) {
|
||||
peripheral = new SpeakerPeripheral();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown peripheral kind");
|
||||
}
|
||||
|
||||
computer.getEnvironment().setPeripheral(side, peripheral);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFile(String path, JSObject contents) {
|
||||
byte[] bytes;
|
||||
if (JavascriptConv.isArrayBuffer(contents)) {
|
||||
bytes = bytesOfBuffer(contents.cast());
|
||||
} else {
|
||||
JSString string = contents.cast();
|
||||
bytes = string.stringValue().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
mount.addFile(path, bytes);
|
||||
}
|
||||
|
||||
private byte[] bytesOfBuffer(ArrayBuffer buffer) {
|
||||
var oldBytes = JavascriptConv.asByteArray(buffer);
|
||||
return Arrays.copyOf(oldBytes, oldBytes.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web;
|
||||
|
||||
import cc.tweaked.web.js.Callbacks;
|
||||
import dan200.computercraft.api.filesystem.Mount;
|
||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* The {@link GlobalEnvironment} for all {@linkplain EmulatedComputer emulated computers}. This reads resources and
|
||||
* version information from {@linkplain Callbacks the resources module}.
|
||||
*/
|
||||
final class EmulatorEnvironment implements GlobalEnvironment {
|
||||
public static final EmulatorEnvironment INSTANCE = new EmulatorEnvironment();
|
||||
|
||||
private final String version = Callbacks.getModVersion();
|
||||
private @Nullable ResourceMount romMount;
|
||||
private @Nullable byte[] bios;
|
||||
|
||||
private EmulatorEnvironment() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHostString() {
|
||||
return "ComputerCraft " + version + " (tweaked.cc)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserAgent() {
|
||||
return "computercraft/" + version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mount createResourceMount(String domain, String subPath) {
|
||||
if (domain.equals("computercraft") && subPath.equals("lua/rom")) {
|
||||
return romMount != null ? romMount : (romMount = new ResourceMount());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown domain or subpath");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream createResourceFile(String domain, String subPath) {
|
||||
if (domain.equals("computercraft") && subPath.equals("lua/bios.lua")) {
|
||||
var biosContents = bios != null ? bios : (bios = Callbacks.getResource("bios.lua"));
|
||||
return new ByteArrayInputStream(biosContents);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown domain or subpath");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web;
|
||||
|
||||
import cc.tweaked.web.js.Callbacks;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import org.teavm.jso.browser.Window;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The main entrypoint to the emulator.
|
||||
*/
|
||||
public class Main {
|
||||
public static final String CORS_PROXY = "https://copy-cat-cors.vercel.app/?{}";
|
||||
|
||||
private static long ticks;
|
||||
|
||||
public static void main(String[] args) {
|
||||
var context = ComputerContext.builder(EmulatorEnvironment.INSTANCE).build();
|
||||
List<EmulatedComputer> computers = new ArrayList<>();
|
||||
|
||||
Callbacks.setup(access -> {
|
||||
var wrapper = new EmulatedComputer(context, access);
|
||||
computers.add(wrapper);
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
Window.setInterval(() -> {
|
||||
ticks++;
|
||||
var iterator = computers.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().tick()) iterator.remove();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
public static long getTicks() {
|
||||
return ticks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web;
|
||||
|
||||
import cc.tweaked.web.js.Callbacks;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.filesystem.AbstractInMemoryMount;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
/**
|
||||
* Mounts in files from JavaScript-supplied resources.
|
||||
*
|
||||
* @see Callbacks#listResources()
|
||||
* @see Callbacks#getResource(String)
|
||||
*/
|
||||
final class ResourceMount extends AbstractInMemoryMount<ResourceMount.FileEntry> {
|
||||
private static final String PREFIX = "rom/";
|
||||
|
||||
ResourceMount() {
|
||||
root = new FileEntry("");
|
||||
for (var file : Callbacks.listResources()) {
|
||||
if (file.startsWith(PREFIX)) getOrCreateChild(root, file.substring(PREFIX.length()), FileEntry::new);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long getSize(String path, FileEntry file) {
|
||||
return file.isDirectory() ? 0 : getContents(file).length;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SeekableByteChannel openForRead(String path, FileEntry file) {
|
||||
return new ArrayByteChannel(getContents(file));
|
||||
}
|
||||
|
||||
private byte[] getContents(FileEntry file) {
|
||||
return file.contents != null ? file.contents : (file.contents = Callbacks.getResource(PREFIX + file.path));
|
||||
}
|
||||
|
||||
protected static final class FileEntry extends AbstractInMemoryMount.FileEntry<FileEntry> {
|
||||
private final String path;
|
||||
private @Nullable byte[] contents;
|
||||
|
||||
FileEntry(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js;
|
||||
|
||||
import org.teavm.jso.JSBody;
|
||||
import org.teavm.jso.JSByRef;
|
||||
import org.teavm.jso.JSFunctor;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.browser.TimerHandler;
|
||||
|
||||
/**
|
||||
* Invoke functions in the {@code $javaCallbacks} object. This global is set up by the Javascript code before the
|
||||
* Java code is started.
|
||||
* <p>
|
||||
* This module is a bit of a hack - we should be able to do most of this with module imports/exports. However, handling
|
||||
* those within TeaVM is a bit awkward, so this ends up being much easier.
|
||||
*/
|
||||
public class Callbacks {
|
||||
@JSFunctor
|
||||
@FunctionalInterface
|
||||
public interface AddComputer extends JSObject {
|
||||
ComputerHandle addComputer(ComputerDisplay computer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the {@link AddComputer} function to the Javascript code.
|
||||
*
|
||||
* @param addComputer The function to add a computer.
|
||||
*/
|
||||
@JSBody(params = "setup", script = "return $javaCallbacks.setup(setup);")
|
||||
public static native void setup(AddComputer addComputer);
|
||||
|
||||
/**
|
||||
* Get the version of CC: Tweaked.
|
||||
*
|
||||
* @return The mod's version.
|
||||
*/
|
||||
@JSBody(script = "return $javaCallbacks.modVersion;")
|
||||
public static native String getModVersion();
|
||||
|
||||
/**
|
||||
* List all resources available in the ROM.
|
||||
*
|
||||
* @return All available resources.
|
||||
*/
|
||||
@JSBody(script = "return $javaCallbacks.listResources();")
|
||||
public static native String[] listResources();
|
||||
|
||||
/**
|
||||
* Load a resource from the ROM.
|
||||
*
|
||||
* @param resource The path to the resource to load.
|
||||
* @return The loaded resource.
|
||||
*/
|
||||
@JSByRef
|
||||
@JSBody(params = "name", script = "return $javaCallbacks.getResource(name);")
|
||||
public static native byte[] getResource(String resource);
|
||||
|
||||
/**
|
||||
* Call {@code setImmediate} (or rather a polyfill) to run an asynchronous task.
|
||||
* <p>
|
||||
* While it would be nicer to use something built-in like {@code queueMicrotask}, our computer execution definitely
|
||||
* doesn't count as a microtask, and doing so will stall the UI thread.
|
||||
*
|
||||
* @param task The task to run.
|
||||
*/
|
||||
@JSBody(params = "task", script = "return setImmediate(task);")
|
||||
public static native void setImmediate(TimerHandler task);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js;
|
||||
|
||||
import org.teavm.jso.JSObject;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The Javascript-side terminal which displays this computer.
|
||||
*
|
||||
* @see Callbacks.AddComputer#addComputer(ComputerDisplay)
|
||||
*/
|
||||
public interface ComputerDisplay extends JSObject {
|
||||
/**
|
||||
* Set this computer's current state.
|
||||
*
|
||||
* @param label This computer's label
|
||||
* @param on If this computer is on right now
|
||||
*/
|
||||
void setState(@Nullable String label, boolean on);
|
||||
|
||||
/**
|
||||
* Update the terminal's properties.
|
||||
*
|
||||
* @param width The terminal width
|
||||
* @param height The terminal height
|
||||
* @param x The X cursor
|
||||
* @param y The Y cursor
|
||||
* @param blink Whether the cursor is blinking
|
||||
* @param cursorColour The cursor's colour
|
||||
*/
|
||||
void updateTerminal(int width, int height, int x, int y, boolean blink, int cursorColour);
|
||||
|
||||
/**
|
||||
* Set a line on the terminal.
|
||||
*
|
||||
* @param line The line index to set
|
||||
* @param text The line's text
|
||||
* @param fore The line's foreground
|
||||
* @param back The line's background
|
||||
*/
|
||||
void setTerminalLine(int line, String text, String fore, String back);
|
||||
|
||||
/**
|
||||
* Set the palette colour for a specific index.
|
||||
*
|
||||
* @param colour The colour index to set
|
||||
* @param r The red value, between 0 and 1
|
||||
* @param g The green value, between 0 and 1
|
||||
* @param b The blue value, between 0 and 1
|
||||
*/
|
||||
void setPaletteColour(int colour, double r, double g, double b);
|
||||
|
||||
/**
|
||||
* Mark the terminal as having changed. Should be called after all other terminal methods.
|
||||
*/
|
||||
void flushTerminal();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js;
|
||||
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
import org.teavm.jso.core.JSString;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A Javascript-facing interface for controlling computers.
|
||||
*/
|
||||
public interface ComputerHandle extends JSObject {
|
||||
/**
|
||||
* Queue an event on the computer.
|
||||
*
|
||||
* @param event The name of the event.
|
||||
* @param args The arguments for this event.
|
||||
*/
|
||||
void event(String event, @Nullable JSObject[] args);
|
||||
|
||||
/**
|
||||
* Shut the computer down.
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* Turn the computer on.
|
||||
*/
|
||||
void turnOn();
|
||||
|
||||
/**
|
||||
* Reboot the computer.
|
||||
*/
|
||||
void reboot();
|
||||
|
||||
/**
|
||||
* Dispose of this computer, marking it as no longer running.
|
||||
*/
|
||||
void dispose();
|
||||
|
||||
/**
|
||||
* Transfer some files to this computer.
|
||||
*
|
||||
* @param files A list of files and their contents.
|
||||
*/
|
||||
void transferFiles(FileContents[] files);
|
||||
|
||||
/**
|
||||
* Set a peripheral on a particular side.
|
||||
*
|
||||
* @param side The side to set the peripheral on.
|
||||
* @param kind The kind of peripheral. For now, can only be "speaker".
|
||||
*/
|
||||
void setPeripheral(String side, @Nullable String kind);
|
||||
|
||||
/**
|
||||
* Add a file to this computer's filesystem.
|
||||
*
|
||||
* @param path The path of the file.
|
||||
* @param contents The contents of the file, either a {@link JSString} or {@link ArrayBuffer}.
|
||||
*/
|
||||
void addFile(String path, JSObject contents);
|
||||
|
||||
/**
|
||||
* A file to transfer to the computer.
|
||||
*/
|
||||
interface FileContents extends JSObject {
|
||||
@JSProperty
|
||||
String getName();
|
||||
|
||||
@JSProperty
|
||||
ArrayBuffer getContents();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js;
|
||||
|
||||
import org.teavm.jso.JSBody;
|
||||
import org.teavm.jso.JSObject;
|
||||
|
||||
/**
|
||||
* Wraps Javascript's {@code console} class.
|
||||
*/
|
||||
public final class Console {
|
||||
private Console() {
|
||||
}
|
||||
|
||||
@JSBody(params = "x", script = "console.log(x);")
|
||||
public static native void log(String message);
|
||||
|
||||
@JSBody(params = "x", script = "console.warn(x);")
|
||||
public static native void warn(String message);
|
||||
|
||||
@JSBody(params = "x", script = "console.error(x);")
|
||||
public static native void error(String message);
|
||||
|
||||
@JSBody(params = "x", script = "console.error(x);")
|
||||
public static native void error(JSObject object);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.teavm.jso.JSBody;
|
||||
import org.teavm.jso.JSByRef;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.core.JSBoolean;
|
||||
import org.teavm.jso.core.JSNumber;
|
||||
import org.teavm.jso.core.JSObjects;
|
||||
import org.teavm.jso.core.JSString;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
import org.teavm.jso.typedarrays.Int8Array;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utility methods for converting between Java and Javascript representations.
|
||||
*/
|
||||
public class JavascriptConv {
|
||||
/**
|
||||
* Convert an array of Javascript values to an equivalent array of Java values.
|
||||
*
|
||||
* @param value The value to convert.
|
||||
* @return The converted value.
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable Object[] toJava(@Nullable JSObject[] value) {
|
||||
if (value == null) return null;
|
||||
var out = new Object[value.length];
|
||||
for (var i = 0; i < value.length; i++) out[i] = toJava(value[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a primitive Javascript value to a boxed Java object.
|
||||
*
|
||||
* @param value The value to convert.
|
||||
* @return The converted value.
|
||||
*/
|
||||
public static @Nullable Object toJava(@Nullable JSObject value) {
|
||||
if (value == null) return null;
|
||||
return switch (JSObjects.typeOf(value)) {
|
||||
case "string" -> ((JSString) value).stringValue();
|
||||
case "number" -> ((JSNumber) value).doubleValue();
|
||||
case "boolean" -> ((JSBoolean) value).booleanValue();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an arbitrary object is a {@link ArrayBuffer}.
|
||||
*
|
||||
* @param object The object ot check
|
||||
* @return Whether this is an {@link ArrayBuffer}.
|
||||
*/
|
||||
@JSBody(params = "data", script = "return data instanceof ArrayBuffer;")
|
||||
public static native boolean isArrayBuffer(JSObject object);
|
||||
|
||||
/**
|
||||
* Wrap a JS {@link Int8Array} into a {@code byte[]}.
|
||||
*
|
||||
* @param view The array to wrap.
|
||||
* @return The wrapped array.
|
||||
*/
|
||||
@JSByRef
|
||||
@JSBody(params = "x", script = "return x;")
|
||||
public static native byte[] asByteArray(Int8Array view);
|
||||
|
||||
/**
|
||||
* Wrap a JS {@link ArrayBuffer} into a {@code byte[]}.
|
||||
*
|
||||
* @param view The array to wrap.
|
||||
* @return The wrapped array.
|
||||
*/
|
||||
public static byte[] asByteArray(ArrayBuffer view) {
|
||||
return asByteArray(Int8Array.create(view));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
@DefaultQualifier(value = NonNull.class, locations = {
|
||||
TypeUseLocation.RETURN,
|
||||
TypeUseLocation.PARAMETER,
|
||||
TypeUseLocation.FIELD,
|
||||
})
|
||||
package cc.tweaked.web;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
import org.checkerframework.framework.qual.TypeUseLocation;
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.peripheral;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaTable;
|
||||
import org.teavm.jso.webaudio.AudioBuffer;
|
||||
import org.teavm.jso.webaudio.AudioContext;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
|
||||
import static cc.tweaked.web.peripheral.SpeakerPeripheral.SAMPLE_RATE;
|
||||
|
||||
final class AudioState {
|
||||
/**
|
||||
* The minimum size of the client's audio buffer. Once we have less than this on the client, we should send another
|
||||
* batch of audio.
|
||||
*/
|
||||
private static final double CLIENT_BUFFER = 0.5;
|
||||
|
||||
private final AudioContext audioContext;
|
||||
private @Nullable AudioBuffer nextBuffer;
|
||||
private double nextTime;
|
||||
|
||||
AudioState(AudioContext audioContext) {
|
||||
this.audioContext = audioContext;
|
||||
nextTime = audioContext.getCurrentTime();
|
||||
}
|
||||
|
||||
boolean pushBuffer(LuaTable<?, ?> table, int size, Optional<Double> volume) throws LuaException {
|
||||
if (nextBuffer != null) return false;
|
||||
|
||||
var buffer = nextBuffer = audioContext.createBuffer(1, size, SAMPLE_RATE);
|
||||
var contents = buffer.getChannelData(0);
|
||||
|
||||
for (var i = 0; i < size; i++) contents.set(i, table.getInt(i + 1) / 128.0f);
|
||||
|
||||
// So we really should go via DFPWM here, but I do not have enough faith in our performance to do this properly.
|
||||
|
||||
if (shouldSendPending()) playNext();
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean isPlaying() {
|
||||
return nextTime >= audioContext.getCurrentTime();
|
||||
}
|
||||
|
||||
boolean shouldSendPending() {
|
||||
return nextBuffer != null && audioContext.getCurrentTime() >= nextTime - CLIENT_BUFFER;
|
||||
}
|
||||
|
||||
void playNext() {
|
||||
if (nextBuffer == null) throw new NullPointerException("Buffer is null");
|
||||
var source = audioContext.createBufferSource();
|
||||
source.setBuffer(nextBuffer);
|
||||
source.connect(audioContext.getDestination());
|
||||
source.start(nextTime);
|
||||
|
||||
nextTime += nextBuffer.getDuration();
|
||||
nextBuffer = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.peripheral;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.LuaTable;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.teavm.jso.webaudio.AudioContext;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||
|
||||
/**
|
||||
* A minimal speaker peripheral, which implements {@code playAudio} and nothing else.
|
||||
*/
|
||||
public class SpeakerPeripheral implements TickablePeripheral {
|
||||
public static final int SAMPLE_RATE = 48000;
|
||||
|
||||
private static @MonotonicNonNull AudioContext audioContext;
|
||||
|
||||
private @Nullable IComputerAccess computer;
|
||||
|
||||
private @Nullable AudioState state;
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "speaker";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attach(IComputerAccess computer) {
|
||||
this.computer = computer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach(IComputerAccess computer) {
|
||||
this.computer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (state != null && state.shouldSendPending()) {
|
||||
state.playNext();
|
||||
if (computer != null) computer.queueEvent("speaker_audio_empty", computer.getAttachmentName());
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final boolean playNote(String instrumentA, Optional<Double> volumeA, Optional<Double> pitchA) throws LuaException {
|
||||
throw new LuaException("Cannot play notes outside of Minecraft");
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final boolean playSound(String name, Optional<Double> volumeA, Optional<Double> pitchA) throws LuaException {
|
||||
throw new LuaException("Cannot play sounds outside of Minecraft");
|
||||
}
|
||||
|
||||
@LuaFunction(unsafe = true)
|
||||
public final boolean playAudio(LuaTable<?, ?> audio, Optional<Double> volume) throws LuaException {
|
||||
checkFinite(1, volume.orElse(0.0));
|
||||
|
||||
var length = audio.length();
|
||||
if (length <= 0) throw new LuaException("Cannot play empty audio");
|
||||
if (length > 128 * 1024) throw new LuaException("Audio data is too large");
|
||||
|
||||
if (audioContext == null) audioContext = AudioContext.create();
|
||||
if (state == null || !state.isPlaying()) state = new AudioState(audioContext);
|
||||
|
||||
return state.pushBuffer(audio, length, volume);
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final void stop() {
|
||||
// TODO: Not sure how to do this.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable IPeripheral other) {
|
||||
return other instanceof SpeakerPeripheral;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.peripheral;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
|
||||
/**
|
||||
* A peripheral which will be updated every time the computer ticks.
|
||||
*/
|
||||
public interface TickablePeripheral extends IPeripheral {
|
||||
void tick();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.stub;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
|
||||
/**
|
||||
* A stub for {@link java.nio.channels.FileChannel}. This is never constructed, only used in {@code instanceof} checks.
|
||||
*/
|
||||
public abstract class FileChannel implements SeekableByteChannel {
|
||||
private FileChannel() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract FileChannel position(long newPosition) throws IOException;
|
||||
|
||||
public abstract void force(boolean metadata) throws IOException;
|
||||
|
||||
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.stub;
|
||||
|
||||
/**
|
||||
* A no-op stub for {@link java.util.concurrent.locks.ReentrantLock}.
|
||||
*/
|
||||
public class ReentrantLock {
|
||||
public boolean tryLock() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void unlock() {
|
||||
}
|
||||
|
||||
public void lockInterruptibly() throws InterruptedException {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core;
|
||||
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule;
|
||||
|
||||
/**
|
||||
* Replaces {@link CoreConfig} with a slightly cut-down version.
|
||||
* <p>
|
||||
* This is mostly required to avoid pulling in {@link AddressRule}.
|
||||
*/
|
||||
public final class TCoreConfig {
|
||||
private TCoreConfig() {
|
||||
}
|
||||
|
||||
public static int maximumFilesOpen = 128;
|
||||
public static boolean disableLua51Features = false;
|
||||
public static String defaultComputerSettings = "";
|
||||
|
||||
public static boolean httpEnabled = true;
|
||||
public static boolean httpWebsocketEnabled = true;
|
||||
public static int httpMaxRequests = 16;
|
||||
public static int httpMaxWebsockets = 4;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* Replaces {@link CheckUrl} with an implementation which unconditionally returns true.
|
||||
*/
|
||||
public class TCheckUrl extends Resource<TCheckUrl> {
|
||||
private static final String EVENT = "http_check";
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final String address;
|
||||
|
||||
public TCheckUrl(ResourceGroup<TCheckUrl> limiter, IAPIEnvironment environment, String address, URI uri) {
|
||||
super(limiter);
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
if (isClosed()) return;
|
||||
environment.queueEvent(EVENT, address, true);
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import cc.tweaked.web.Main;
|
||||
import cc.tweaked.web.js.JavascriptConv;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.HandleGeneric;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.teavm.jso.ajax.XMLHttpRequest;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.StringReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Replaces {@link HttpRequest} with a version which uses AJAX/{@link XMLHttpRequest}.
|
||||
*/
|
||||
public class THttpRequest extends Resource<THttpRequest> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(THttpRequest.class);
|
||||
private static final String SUCCESS_EVENT = "http_success";
|
||||
private static final String FAILURE_EVENT = "http_failure";
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
|
||||
private final String address;
|
||||
private final @Nullable String postBuffer;
|
||||
private final HttpHeaders headers;
|
||||
private final boolean binary;
|
||||
|
||||
public THttpRequest(
|
||||
ResourceGroup<THttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable String postText,
|
||||
HttpHeaders headers, boolean binary, boolean followRedirects, int timeout
|
||||
) {
|
||||
super(limiter);
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
postBuffer = postText;
|
||||
this.headers = headers;
|
||||
this.binary = binary;
|
||||
|
||||
if (postText != null) {
|
||||
if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) {
|
||||
headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static URI checkUri(String address) throws HTTPRequestException {
|
||||
URI url;
|
||||
try {
|
||||
url = new URI(address);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new HTTPRequestException("URL malformed");
|
||||
}
|
||||
|
||||
checkUri(url);
|
||||
return url;
|
||||
}
|
||||
|
||||
public static void checkUri(URI url) throws HTTPRequestException {
|
||||
// Validate the URL
|
||||
if (url.getScheme() == null) throw new HTTPRequestException("Must specify http or https");
|
||||
if (url.getHost() == null) throw new HTTPRequestException("URL malformed");
|
||||
|
||||
var scheme = url.getScheme().toLowerCase(Locale.ROOT);
|
||||
if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
|
||||
throw new HTTPRequestException("Invalid protocol '" + scheme + "'");
|
||||
}
|
||||
}
|
||||
|
||||
public void request(URI uri, HttpMethod method) {
|
||||
if (isClosed()) return;
|
||||
|
||||
try {
|
||||
var request = XMLHttpRequest.create();
|
||||
request.setOnReadyStateChange(() -> onResponseStateChange(request));
|
||||
request.setResponseType(binary ? "arraybuffer" : "text");
|
||||
var address = uri.toASCIIString();
|
||||
request.open(method.toString(), Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address));
|
||||
for (var iterator = headers.iteratorAsString(); iterator.hasNext(); ) {
|
||||
var header = iterator.next();
|
||||
request.setRequestHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
request.send(postBuffer);
|
||||
checkClosed();
|
||||
} catch (Exception e) {
|
||||
failure("Could not connect");
|
||||
LOG.error(Logging.HTTP_ERROR, "Error in HTTP request", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onResponseStateChange(XMLHttpRequest request) {
|
||||
if (request.getReadyState() != XMLHttpRequest.DONE) return;
|
||||
if (request.getStatus() == 0) {
|
||||
this.failure("Could not connect");
|
||||
return;
|
||||
}
|
||||
|
||||
HandleGeneric reader;
|
||||
if (binary) {
|
||||
ArrayBuffer buffer = request.getResponse().cast();
|
||||
SeekableByteChannel contents = new ArrayByteChannel(JavascriptConv.asByteArray(buffer));
|
||||
reader = BinaryReadableHandle.of(contents);
|
||||
} else {
|
||||
reader = new EncodedReadableHandle(new BufferedReader(new StringReader(request.getResponseText())));
|
||||
}
|
||||
|
||||
Map<String, String> responseHeaders = new HashMap<>();
|
||||
for (var header : request.getAllResponseHeaders().split("\r\n")) {
|
||||
var index = header.indexOf(':');
|
||||
if (index < 0) continue;
|
||||
|
||||
// Normalise the header (so "content-type" becomes "Content-Type")
|
||||
var upcase = true;
|
||||
var headerBuilder = new StringBuilder(index);
|
||||
for (var i = 0; i < index; i++) {
|
||||
var c = header.charAt(i);
|
||||
headerBuilder.append(upcase ? Character.toUpperCase(c) : c);
|
||||
upcase = c == '-';
|
||||
}
|
||||
responseHeaders.put(headerBuilder.toString(), header.substring(index + 1).trim());
|
||||
}
|
||||
var stream = new HttpResponseHandle(reader, request.getStatus(), request.getStatusText(), responseHeaders);
|
||||
|
||||
if (request.getStatus() >= 200 && request.getStatus() < 400) {
|
||||
if (tryClose()) environment.queueEvent(SUCCESS_EVENT, address, stream);
|
||||
} else {
|
||||
if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, request.getStatusText(), stream);
|
||||
}
|
||||
}
|
||||
|
||||
void failure(String message) {
|
||||
if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message);
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import cc.tweaked.web.js.Console;
|
||||
import cc.tweaked.web.js.JavascriptConv;
|
||||
import com.google.common.base.Strings;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
import dan200.computercraft.core.apis.http.options.Action;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import org.teavm.jso.typedarrays.Int8Array;
|
||||
import org.teavm.jso.websocket.WebSocket;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Replaces {@link Websocket} with a version which uses Javascript's built-in {@link WebSocket} client.
|
||||
*/
|
||||
public class TWebsocket extends Resource<TWebsocket> implements WebsocketClient {
|
||||
private final IAPIEnvironment environment;
|
||||
private final URI uri;
|
||||
private final String address;
|
||||
|
||||
private @Nullable WebSocket websocket;
|
||||
|
||||
public TWebsocket(ResourceGroup<TWebsocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) {
|
||||
super(limiter);
|
||||
this.environment = environment;
|
||||
this.uri = uri;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
if (isClosed()) return;
|
||||
|
||||
var client = this.websocket = WebSocket.create(uri.toASCIIString());
|
||||
client.setBinaryType("arraybuffer");
|
||||
client.onOpen(e -> success(Action.ALLOW.toPartial().toOptions()));
|
||||
client.onError(e -> {
|
||||
Console.error(e);
|
||||
failure("Could not connect");
|
||||
});
|
||||
client.onMessage(e -> {
|
||||
if (isClosed()) return;
|
||||
if (JavascriptConv.isArrayBuffer(e.getData())) {
|
||||
var array = Int8Array.create(e.getDataAsArray());
|
||||
var contents = new byte[array.getLength()];
|
||||
for (var i = 0; i < contents.length; i++) contents[i] = array.get(i);
|
||||
environment.queueEvent("websocket_message", address, contents, true);
|
||||
} else {
|
||||
environment.queueEvent("websocket_message", address, e.getDataAsString(), false);
|
||||
}
|
||||
});
|
||||
client.onClose(e -> close(e.getCode(), e.getReason()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendText(String message) {
|
||||
if (websocket == null) return;
|
||||
websocket.send(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendBinary(ByteBuffer message) {
|
||||
if (websocket == null) return;
|
||||
|
||||
var array = Int8Array.create(message.remaining());
|
||||
for (var i = 0; i < array.getLength(); i++) array.set(i, message.get(i));
|
||||
websocket.send(array);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
super.dispose();
|
||||
if (websocket != null) {
|
||||
websocket.close();
|
||||
websocket = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void success(Options options) {
|
||||
if (isClosed()) return;
|
||||
|
||||
var handle = new WebsocketHandle(environment, address, this, options);
|
||||
environment.queueEvent(SUCCESS_EVENT, address, handle);
|
||||
createOwnerReference(handle);
|
||||
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
void failure(String message) {
|
||||
if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message);
|
||||
}
|
||||
|
||||
void close(int status, String reason) {
|
||||
if (!tryClose()) return;
|
||||
|
||||
environment.queueEvent(CLOSE_EVENT, address, Strings.isNullOrEmpty(reason) ? null : reason, status < 0 ? null : status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.api.peripheral.PeripheralType;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import dan200.computercraft.core.methods.NamedMethod;
|
||||
import org.teavm.metaprogramming.*;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Compile-time generation of {@link LuaMethod} methods.
|
||||
*
|
||||
* @see TLuaMethodSupplier
|
||||
* @see StaticGenerator
|
||||
*/
|
||||
@CompileTime
|
||||
public class MethodReflection {
|
||||
@Meta
|
||||
public static native boolean getMethods(Class<?> type, Consumer<NamedMethod<LuaMethod>> make);
|
||||
|
||||
private static void getMethods(ReflectClass<?> klass, Value<Consumer<NamedMethod<LuaMethod>>> make) {
|
||||
var result = getMethodsImpl(klass, make);
|
||||
// Using "unsupportedCase" here causes us to skip generating any code and just return null. While null isn't
|
||||
// a boolean, it's still false-y and thus has the same effect in the generated JS!
|
||||
if (!result) Metaprogramming.unsupportedCase();
|
||||
Metaprogramming.exit(() -> result);
|
||||
}
|
||||
|
||||
private static boolean getMethodsImpl(ReflectClass<?> klass, Value<Consumer<NamedMethod<LuaMethod>>> make) {
|
||||
if (!klass.getName().startsWith("dan200.computercraft.") && !klass.getName().startsWith("cc.tweaked.web.peripheral")) {
|
||||
return false;
|
||||
}
|
||||
if (klass.getName().contains("lambda")) return false;
|
||||
|
||||
Class<?> actualClass;
|
||||
try {
|
||||
actualClass = Metaprogramming.getClassLoader().loadClass(klass.getName());
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
var methods = Internal.getMethods(actualClass);
|
||||
for (var method : methods) {
|
||||
var name = method.name();
|
||||
var nonYielding = method.nonYielding();
|
||||
var actualField = method.method().getField("INSTANCE");
|
||||
|
||||
Metaprogramming.emit(() -> make.get().accept(new NamedMethod<>(name, (LuaMethod) actualField.get(null), nonYielding, null)));
|
||||
}
|
||||
|
||||
return !methods.isEmpty();
|
||||
}
|
||||
|
||||
private static final class Internal {
|
||||
private static final LoadingCache<Class<?>, List<NamedMethod<ReflectClass<LuaMethod>>>> CLASS_CACHE = CacheBuilder
|
||||
.newBuilder()
|
||||
.build(CacheLoader.from(Internal::getMethodsImpl));
|
||||
|
||||
private static final StaticGenerator<LuaMethod> GENERATOR = new StaticGenerator<>(
|
||||
LuaMethod.class, Collections.singletonList(ILuaContext.class), Internal::createClass
|
||||
);
|
||||
|
||||
static List<NamedMethod<ReflectClass<LuaMethod>>> getMethods(Class<?> klass) {
|
||||
try {
|
||||
return CLASS_CACHE.get(klass);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ReflectClass<?> createClass(byte[] bytes) {
|
||||
/*
|
||||
StaticGenerator is not declared to be @CompileTime, to ensure it loads in the same module/classloader as
|
||||
other files in this package. This means it can't call Metaprogramming.createClass directly, as that's
|
||||
only available to @CompileTime classes.
|
||||
|
||||
We need to use an explicit call (rather than a MethodReference), as TeaVM doesn't correctly rewrite the
|
||||
latter.
|
||||
*/
|
||||
return Metaprogramming.createClass(bytes);
|
||||
}
|
||||
|
||||
private static List<NamedMethod<ReflectClass<LuaMethod>>> getMethodsImpl(Class<?> klass) {
|
||||
ArrayList<NamedMethod<ReflectClass<LuaMethod>>> methods = null;
|
||||
|
||||
// Find all methods on the current class
|
||||
for (var method : klass.getMethods()) {
|
||||
var annotation = method.getAnnotation(LuaFunction.class);
|
||||
if (annotation == null) continue;
|
||||
|
||||
if (Modifier.isStatic(method.getModifiers())) {
|
||||
System.err.printf("LuaFunction method %s.%s should be an instance method.\n", method.getDeclaringClass(), method.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
var instance = GENERATOR.getMethod(method).orElse(null);
|
||||
if (instance == null) continue;
|
||||
|
||||
if (methods == null) methods = new ArrayList<>();
|
||||
addMethod(methods, method, annotation, null, instance);
|
||||
}
|
||||
|
||||
if (methods == null) return List.of();
|
||||
methods.trimToSize();
|
||||
return Collections.unmodifiableList(methods);
|
||||
}
|
||||
|
||||
private static void addMethod(List<NamedMethod<ReflectClass<LuaMethod>>> methods, Method method, LuaFunction annotation, @Nullable PeripheralType genericType, ReflectClass<LuaMethod> instance) {
|
||||
var names = annotation.value();
|
||||
var isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread();
|
||||
if (names.length == 0) {
|
||||
methods.add(new NamedMethod<>(method.getName(), instance, isSimple, genericType));
|
||||
} else {
|
||||
for (var name : names) {
|
||||
methods.add(new NamedMethod<>(name, instance, isSimple, genericType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.teavm.metaprogramming.ReflectClass;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
/**
|
||||
* The underlying generator for {@link LuaFunction}-annotated methods.
|
||||
* <p>
|
||||
* The constructor {@link StaticGenerator#StaticGenerator(Class, List, Function)} takes in the type of interface to generate (i.e.
|
||||
* {@link LuaMethod}) and the context arguments for this function (in the case of {@link LuaMethod}, this will just be
|
||||
* {@link ILuaContext}).
|
||||
* <p>
|
||||
* The generated class then implements this interface - the {@code apply} method calls the appropriate methods on
|
||||
* {@link IArguments} to extract the arguments, and then calls the original method.
|
||||
*
|
||||
* @param <T> The type of the interface the generated classes implement.
|
||||
*/
|
||||
public final class StaticGenerator<T> {
|
||||
private static final String METHOD_NAME = "apply";
|
||||
private static final String[] EXCEPTIONS = new String[]{Type.getInternalName(LuaException.class)};
|
||||
|
||||
private static final String INTERNAL_METHOD_RESULT = Type.getInternalName(MethodResult.class);
|
||||
private static final String DESC_METHOD_RESULT = Type.getDescriptor(MethodResult.class);
|
||||
|
||||
private static final String INTERNAL_ARGUMENTS = Type.getInternalName(IArguments.class);
|
||||
private static final String DESC_ARGUMENTS = Type.getDescriptor(IArguments.class);
|
||||
|
||||
private static final String INTERNAL_COERCED = Type.getInternalName(Coerced.class);
|
||||
|
||||
private final Class<T> base;
|
||||
private final List<Class<?>> context;
|
||||
|
||||
private final String[] interfaces;
|
||||
private final String methodDesc;
|
||||
private final String classPrefix;
|
||||
|
||||
private final Function<byte[], ReflectClass<?>> createClass;
|
||||
|
||||
private final LoadingCache<Method, Optional<ReflectClass<T>>> methodCache = CacheBuilder
|
||||
.newBuilder()
|
||||
.build(CacheLoader.from(catching(this::build, Optional.empty())));
|
||||
|
||||
public StaticGenerator(Class<T> base, List<Class<?>> context, Function<byte[], ReflectClass<?>> createClass) {
|
||||
this.base = base;
|
||||
this.context = context;
|
||||
this.createClass = createClass;
|
||||
|
||||
interfaces = new String[]{Type.getInternalName(base)};
|
||||
|
||||
var methodDesc = new StringBuilder().append("(Ljava/lang/Object;");
|
||||
for (var klass : context) methodDesc.append(Type.getDescriptor(klass));
|
||||
methodDesc.append(DESC_ARGUMENTS).append(")").append(DESC_METHOD_RESULT);
|
||||
this.methodDesc = methodDesc.toString();
|
||||
|
||||
classPrefix = StaticGenerator.class.getPackageName() + "." + base.getSimpleName() + "$";
|
||||
}
|
||||
|
||||
public Optional<ReflectClass<T>> getMethod(Method method) {
|
||||
return methodCache.getUnchecked(method);
|
||||
}
|
||||
|
||||
private Optional<ReflectClass<T>> build(Method method) {
|
||||
var name = method.getDeclaringClass().getName() + "." + method.getName();
|
||||
var modifiers = method.getModifiers();
|
||||
|
||||
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
|
||||
if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) {
|
||||
System.err.printf("Lua Method %s should be final.\n", name);
|
||||
}
|
||||
|
||||
if (!Modifier.isPublic(modifiers)) {
|
||||
System.err.printf("Lua Method %s should be a public method.\n", name);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
|
||||
System.err.printf("Lua Method %s should be on a public class.\n", name);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var exceptions = method.getExceptionTypes();
|
||||
for (var exception : exceptions) {
|
||||
if (exception != LuaException.class) {
|
||||
System.err.printf("Lua Method %s cannot throw %s.\n", name, exception.getName());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
var annotation = method.getAnnotation(LuaFunction.class);
|
||||
if (annotation.unsafe() && annotation.mainThread()) {
|
||||
System.err.printf("Lua Method %s cannot use unsafe and mainThread.\n", name);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// We have some rather ugly handling of static methods in both here and the main generate function. Static methods
|
||||
// only come from generic sources, so this should be safe.
|
||||
var target = Modifier.isStatic(modifiers) ? method.getParameterTypes()[0] : method.getDeclaringClass();
|
||||
|
||||
try {
|
||||
var bytes = generate(classPrefix + method.getDeclaringClass().getSimpleName() + "$" + method.getName(), target, method, annotation.unsafe());
|
||||
if (bytes == null) return Optional.empty();
|
||||
|
||||
return Optional.of(createClass.apply(bytes).asSubclass(base));
|
||||
} catch (ClassFormatError | RuntimeException e) {
|
||||
System.err.printf("Error generating %s\n", name);
|
||||
e.printStackTrace();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private byte[] generate(String className, Class<?> target, Method targetMethod, boolean unsafe) {
|
||||
var internalName = className.replace(".", "/");
|
||||
|
||||
// Construct a public final class which extends Object and implements MethodInstance.Delegate
|
||||
var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
cw.visit(V17, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces);
|
||||
cw.visitSource("CC generated method", null);
|
||||
|
||||
cw.visitField(ACC_PUBLIC | ACC_STATIC | ACC_FINAL, "INSTANCE", "L" + internalName + ";", null, null).visitEnd();
|
||||
|
||||
{ // Constructor just invokes super.
|
||||
var mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
mw.visitCode();
|
||||
mw.visitVarInsn(ALOAD, 0);
|
||||
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
mw.visitInsn(RETURN);
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
}
|
||||
|
||||
// Static initialiser sets the INSTANCE field.
|
||||
{
|
||||
var mw = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
|
||||
mw.visitCode();
|
||||
mw.visitTypeInsn(NEW, internalName);
|
||||
mw.visitInsn(DUP);
|
||||
mw.visitMethodInsn(INVOKESPECIAL, internalName, "<init>", "()V", false);
|
||||
mw.visitFieldInsn(PUTSTATIC, internalName, "INSTANCE", "L" + internalName + ";");
|
||||
mw.visitInsn(RETURN);
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
}
|
||||
|
||||
{
|
||||
var mw = cw.visitMethod(ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS);
|
||||
mw.visitCode();
|
||||
|
||||
// If we're an instance method, load the target as the first argument.
|
||||
if (!Modifier.isStatic(targetMethod.getModifiers())) {
|
||||
mw.visitVarInsn(ALOAD, 1);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target));
|
||||
}
|
||||
|
||||
var argIndex = 0;
|
||||
for (var genericArg : targetMethod.getGenericParameterTypes()) {
|
||||
var loadedArg = loadArg(mw, target, targetMethod, unsafe, genericArg, argIndex);
|
||||
if (loadedArg == null) return null;
|
||||
if (loadedArg) argIndex++;
|
||||
}
|
||||
|
||||
mw.visitMethodInsn(
|
||||
Modifier.isStatic(targetMethod.getModifiers()) ? INVOKESTATIC : INVOKEVIRTUAL,
|
||||
Type.getInternalName(targetMethod.getDeclaringClass()), targetMethod.getName(),
|
||||
Type.getMethodDescriptor(targetMethod), false
|
||||
);
|
||||
|
||||
// We allow a reasonable amount of flexibility on the return value's type. Alongside the obvious MethodResult,
|
||||
// we convert basic types into an immediate result.
|
||||
var ret = targetMethod.getReturnType();
|
||||
if (ret != MethodResult.class) {
|
||||
if (ret == void.class) {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "()" + DESC_METHOD_RESULT, false);
|
||||
} else if (ret.isPrimitive()) {
|
||||
var boxed = Primitives.wrap(ret);
|
||||
mw.visitMethodInsn(INVOKESTATIC, Type.getInternalName(boxed), "valueOf", "(" + Type.getDescriptor(ret) + ")" + Type.getDescriptor(boxed), false);
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
} else if (ret == Object[].class) {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "([Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
} else {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
}
|
||||
}
|
||||
|
||||
mw.visitInsn(ARETURN);
|
||||
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
}
|
||||
|
||||
cw.visitEnd();
|
||||
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Boolean loadArg(MethodVisitor mw, Class<?> target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex) {
|
||||
if (genericArg == target) {
|
||||
mw.visitVarInsn(ALOAD, 1);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target));
|
||||
return false;
|
||||
}
|
||||
|
||||
var arg = Reflect.getRawType(method, genericArg, true);
|
||||
if (arg == null) return null;
|
||||
|
||||
if (arg == IArguments.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
var idx = context.indexOf(arg);
|
||||
if (idx >= 0) {
|
||||
mw.visitVarInsn(ALOAD, 2 + idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arg == Coerced.class) {
|
||||
var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.COERCED_IN).getType(), false);
|
||||
if (klass == null) return null;
|
||||
|
||||
if (klass == String.class) {
|
||||
mw.visitTypeInsn(NEW, INTERNAL_COERCED);
|
||||
mw.visitInsn(DUP);
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getStringCoerced", "(I)Ljava/lang/String;", true);
|
||||
mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "<init>", "(Ljava/lang/Object;)V", false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (arg == Optional.class) {
|
||||
var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false);
|
||||
if (klass == null) return null;
|
||||
|
||||
if (Enum.class.isAssignableFrom(klass) && klass != Enum.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitLdcInsn(Type.getType(klass));
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
var name = Reflect.getLuaName(Primitives.unwrap(klass), unsafe);
|
||||
if (name != null) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Enum.class.isAssignableFrom(arg) && arg != Enum.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitLdcInsn(Type.getType(arg));
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;", true);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(arg));
|
||||
return true;
|
||||
}
|
||||
|
||||
var name = arg == Object.class ? "" : Reflect.getLuaName(arg, unsafe);
|
||||
if (name != null) {
|
||||
if (Reflect.getRawType(method, genericArg, false) == null) return null;
|
||||
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor(arg), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
System.err.printf("Unknown parameter type %s for method %s.%s.\n",
|
||||
arg.getName(), method.getDeclaringClass().getName(), method.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("Guava")
|
||||
static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
|
||||
return x -> {
|
||||
try {
|
||||
return function.apply(x);
|
||||
} catch (Exception | LinkageError e) {
|
||||
// LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching
|
||||
// methods on a class which references non-existent (i.e. client-only) types.
|
||||
e.printStackTrace();
|
||||
return def;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.methods.ObjectSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Replaces {@link LuaMethodSupplier} with a version which uses {@link MethodReflection} to fabricate the classes.
|
||||
*/
|
||||
public final class TLuaMethodSupplier implements MethodSupplier<LuaMethod> {
|
||||
static final TLuaMethodSupplier INSTANCE = new TLuaMethodSupplier();
|
||||
|
||||
private TLuaMethodSupplier() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forEachSelfMethod(Object object, UntargetedConsumer<LuaMethod> consumer) {
|
||||
return MethodReflection.getMethods(object.getClass(), method -> consumer.accept(method.name(), method.method(), method));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forEachMethod(Object object, TargetedConsumer<LuaMethod> consumer) {
|
||||
var hasMethods = MethodReflection.getMethods(object.getClass(), method -> consumer.accept(object, method.name(), method.method(), method));
|
||||
|
||||
if (object instanceof ObjectSource source) {
|
||||
for (var extra : source.getExtra()) {
|
||||
hasMethods |= MethodReflection.getMethods(extra.getClass(), method -> consumer.accept(extra, method.name(), method.method(), method));
|
||||
}
|
||||
}
|
||||
|
||||
return hasMethods;
|
||||
}
|
||||
|
||||
public static MethodSupplier<LuaMethod> create(List<GenericMethod> genericMethods) {
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.core.methods.LuaMethod;
|
||||
import dan200.computercraft.core.methods.MethodSupplier;
|
||||
import dan200.computercraft.core.methods.PeripheralMethod;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Replaces {@link PeripheralMethodSupplier} with a version which lifts {@link LuaMethod}s to {@link PeripheralMethod}.
|
||||
* As none of our peripherals need {@link IComputerAccess}, this is entirely safe.
|
||||
*/
|
||||
public final class TPeripheralMethodSupplier implements MethodSupplier<PeripheralMethod> {
|
||||
static final TPeripheralMethodSupplier INSTANCE = new TPeripheralMethodSupplier();
|
||||
|
||||
private TPeripheralMethodSupplier() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forEachSelfMethod(Object object, UntargetedConsumer<PeripheralMethod> consumer) {
|
||||
return TLuaMethodSupplier.INSTANCE.forEachSelfMethod(object, (name, method, info) -> consumer.accept(name, cast(method), null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forEachMethod(Object object, TargetedConsumer<PeripheralMethod> consumer) {
|
||||
return TLuaMethodSupplier.INSTANCE.forEachMethod(object, (target, name, method, info) -> consumer.accept(target, name, cast(method), null));
|
||||
}
|
||||
|
||||
private static PeripheralMethod cast(LuaMethod method) {
|
||||
return (target, context, computer, args) -> method.apply(target, context, args);
|
||||
}
|
||||
|
||||
public static MethodSupplier<PeripheralMethod> create(List<GenericMethod> genericMethods) {
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import cc.tweaked.web.js.Callbacks;
|
||||
import org.teavm.jso.browser.TimerHandler;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A reimplementation of {@link ComputerThread} which, well, avoids any threading!
|
||||
* <p>
|
||||
* This instead just exucutes work as soon as possible via {@link Callbacks#setImmediate(TimerHandler)}. Timeouts are
|
||||
* instead handled via polling, see {@link cc.tweaked.web.builder.PatchCobalt}.
|
||||
*/
|
||||
public class TComputerThread {
|
||||
private static final ArrayDeque<ComputerExecutor> executors = new ArrayDeque<>();
|
||||
private final TimerHandler callback = this::workOnce;
|
||||
|
||||
public TComputerThread(int threads) {
|
||||
}
|
||||
|
||||
public void queue(ComputerExecutor executor) {
|
||||
if (executor.onComputerQueue) throw new IllegalStateException("Cannot queue already queued executor");
|
||||
executor.onComputerQueue = true;
|
||||
|
||||
if (executors.isEmpty()) Callbacks.setImmediate(callback);
|
||||
executors.add(executor);
|
||||
}
|
||||
|
||||
private void workOnce() {
|
||||
var executor = executors.poll();
|
||||
if (executor == null) throw new IllegalStateException("Working, but executor is null");
|
||||
if (!executor.onComputerQueue) throw new IllegalArgumentException("Working but not on queue");
|
||||
|
||||
executor.beforeWork();
|
||||
try {
|
||||
executor.work();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (executor.afterWork()) executors.push(executor);
|
||||
if (!executors.isEmpty()) Callbacks.setImmediate(callback);
|
||||
}
|
||||
|
||||
public boolean hasPendingWork() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public long scaledPeriod() {
|
||||
return 50 * 1_000_000L;
|
||||
}
|
||||
|
||||
public boolean stop(long timeout, TimeUnit unit) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package io.netty.handler.codec.http;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* A replacement for {@link DefaultHttpHeaders}.
|
||||
* <p>
|
||||
* The default implementation does additional conversion for dates, which ends up pulling in a lot of code we can't
|
||||
* compile.
|
||||
*/
|
||||
public class TDefaultHttpHeaders extends HttpHeaders {
|
||||
private final Map<String, String> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public String get(String name) {
|
||||
return map.get(normalise(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAll(String name) {
|
||||
var value = get(name);
|
||||
return value == null ? List.of() : List.of(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<String, String>> entries() {
|
||||
return List.copyOf(map.entrySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String name) {
|
||||
return get(name) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public Iterator<Map.Entry<String, String>> iterator() {
|
||||
return map.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Iterator<Map.Entry<CharSequence, CharSequence>> iteratorCharSequence() {
|
||||
return (Iterator<Map.Entry<CharSequence, CharSequence>>) (Iterator<?>) map.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> names() {
|
||||
return map.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders add(String name, Object value) {
|
||||
return set(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders set(String name, Object value) {
|
||||
map.put(normalise(name), (String) value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders remove(String name) {
|
||||
map.remove(normalise(name));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders clear() {
|
||||
map.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
//region Uncalled/unsupported methods
|
||||
@Override
|
||||
public Integer getInt(CharSequence name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(CharSequence name, int defaultValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Short getShort(CharSequence name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getShort(CharSequence name, short defaultValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTimeMillis(CharSequence name) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeMillis(CharSequence name, long defaultValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders add(String name, Iterable<?> values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders addInt(CharSequence name, int value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders addShort(CharSequence name, short value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders set(String name, Iterable<?> values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders setInt(CharSequence name, int value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders setShort(CharSequence name, short value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
//endregion
|
||||
|
||||
private static String normalise(String string) {
|
||||
return string.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package io.netty.util;
|
||||
|
||||
import org.teavm.interop.NoSideEffects;
|
||||
|
||||
/**
|
||||
* A replacement for {@link AsciiString} which just wraps a normal string.
|
||||
* <p>
|
||||
* {@link AsciiString} relies heavily on Netty's low-level (and often unsafe!) code, which doesn't run on Javascript.
|
||||
*/
|
||||
public final class TAsciiString implements CharSequence {
|
||||
private final String value;
|
||||
|
||||
private TAsciiString(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@NoSideEffects
|
||||
public static TAsciiString cached(String value) {
|
||||
return new TAsciiString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return value.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
return value.charAt(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
return value.subSequence(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return this == o || (o instanceof TAsciiString other && value.equals(other.value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return value.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package org.slf4j;
|
||||
|
||||
import cc.tweaked.web.js.Console;
|
||||
import org.slf4j.event.Level;
|
||||
import org.slf4j.helpers.AbstractLogger;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A replacement for SLF4J's {@link LoggerFactory}, which skips service loading, returning a logger which prints to the
|
||||
* JS console.
|
||||
*/
|
||||
public final class TLoggerFactory {
|
||||
private static final Logger INSTANCE = new LoggerImpl();
|
||||
|
||||
private TLoggerFactory() {
|
||||
}
|
||||
|
||||
public static Logger getLogger(Class<?> klass) {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static final class LoggerImpl extends AbstractLogger {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 3442920913507872371L;
|
||||
|
||||
@Override
|
||||
protected String getFullyQualifiedCallerName() {
|
||||
return "logger";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleNormalizedLoggingCall(Level level, Marker marker, String msg, Object[] arguments, Throwable throwable) {
|
||||
if (arguments != null) msg += " " + Arrays.toString(arguments);
|
||||
switch (level) {
|
||||
case TRACE, DEBUG, INFO -> Console.log(msg);
|
||||
case WARN -> Console.warn(msg);
|
||||
case ERROR -> Console.error(msg);
|
||||
}
|
||||
|
||||
if (throwable != null) throwable.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTraceEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTraceEnabled(Marker marker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugEnabled(Marker marker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInfoEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInfoEnabled(Marker marker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWarnEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWarnEnabled(Marker marker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isErrorEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isErrorEnabled(Marker marker) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package org.slf4j;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* A replacement for SLF4J's {@link MarkerFactory}, which skips service loading and always uses a constant
|
||||
* {@link Marker}.
|
||||
*/
|
||||
public final class TMarkerFactory {
|
||||
private static final Marker INSTANCE = new MarkerImpl();
|
||||
|
||||
private TMarkerFactory() {
|
||||
}
|
||||
|
||||
public static Marker getMarker(String name) {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static final class MarkerImpl implements Marker {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 6353565105632304410L;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "unnamed";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(Marker reference) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Marker reference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public boolean hasChildren() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasReferences() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Marker> iterator() {
|
||||
return Collections.emptyIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Marker other) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user