Allow mounting folders in the standalone emulator

This theoretically allows you to use the emulator to run the test suite
(via  --mount-ro projects/core/src/test/resources/test-rom/:test-rom),
but not sure how useful this is in practice.
This commit is contained in:
Jonathan Coates 2024-04-03 21:27:18 +01:00
parent 6d14ce625f
commit bce099ef32
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
1 changed files with 98 additions and 26 deletions

View File

@ -5,11 +5,16 @@
package cc.tweaked.standalone;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.apis.http.options.Action;
import dan200.computercraft.core.apis.http.options.AddressRule;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.filesystem.FileMount;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.core.util.Colour;
@ -31,6 +36,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
@ -55,37 +61,58 @@ public class Main {
private static final Logger LOG = LoggerFactory.getLogger(Main.class);
private static final boolean DEBUG = Checks.DEBUG;
private record TermSize(int width, int height) {
public static final TermSize DEFAULT = new TermSize(51, 19);
public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$");
}
private static <T> T getParsedOptionValue(CommandLine cli, Option opt, Class<T> klass) throws ParseException {
var res = cli.getOptionValue(opt);
if (klass == Path.class) {
try {
return klass.cast(Path.of(res));
} catch (InvalidPathException e) {
throw new ParseException("'" + res + "' is not a valid path (" + e.getReason() + ")");
}
} else if (klass == TermSize.class) {
var matcher = TermSize.PATTERN.matcher(res);
if (!matcher.matches()) throw new ParseException("'" + res + "' is not a valid terminal size.");
return klass.cast(new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))));
} else {
return klass.cast(TypeHandler.createValue(res, klass));
private static Path parsePath(String path) throws ParseException {
try {
return Path.of(path);
} catch (InvalidPathException e) {
throw new ParseException("'" + path + "' is not a valid path (" + e.getReason() + ")");
}
}
private record TermSize(int width, int height) {
public static final TermSize DEFAULT = new TermSize(51, 19);
public static final Pattern PATTERN = Pattern.compile("^(\\d+)x(\\d+)$");
public static TermSize parse(String value) throws ParseException {
var matcher = TermSize.PATTERN.matcher(value);
if (!matcher.matches()) throw new ParseException("'" + value + "' is not a valid terminal size.");
return new TermSize(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
}
}
private record MountPaths(Path src, String dest) {
public static final Pattern PATTERN = Pattern.compile("^([^:]+):([^:]+)$");
public static MountPaths parse(String value) throws ParseException {
var matcher = MountPaths.PATTERN.matcher(value);
if (!matcher.matches()) throw new ParseException("'" + value + "' is not a mount spec.");
return new MountPaths(parsePath(matcher.group(1)), matcher.group(2));
}
}
private interface ValueParser<T> {
T parse(String path) throws ParseException;
}
@Contract("_, _, _, !null -> !null")
private static <T> @Nullable T getParsedOptionValue(CommandLine cli, Option opt, Class<T> klass, @Nullable T defaultValue) throws ParseException {
return cli.hasOption(opt) ? getParsedOptionValue(cli, opt, klass) : defaultValue;
private static <T> @Nullable T getParsedOptionValue(CommandLine cli, Option opt, ValueParser<T> parser, @Nullable T defaultValue) throws ParseException {
return cli.hasOption(opt) ? parser.parse(cli.getOptionValue(opt)) : defaultValue;
}
private static <T> List<T> getParsedOptionValues(CommandLine cli, Option opt, ValueParser<T> parser) throws ParseException {
var values = cli.getOptionValues(opt);
if (values == null) return List.of();
List<T> parsedValues = new ArrayList<>(values.length);
for (var value : values) parsedValues.add(parser.parse(value));
return List.copyOf(parsedValues);
}
public static void main(String[] args) throws InterruptedException {
var options = new Options();
Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt;
Option resourceOpt, computerOpt, termSizeOpt, allowLocalDomainsOpt, helpOpt, mountOpt, mountRoOpt;
options.addOption(resourceOpt = Option.builder("r").argName("PATH").longOpt("resources").hasArg()
.desc("The path to the resources directory")
.build());
@ -98,6 +125,12 @@ public static void main(String[] args) throws InterruptedException {
options.addOption(allowLocalDomainsOpt = Option.builder("L").longOpt("allow-local-domains")
.desc("Allow accessing local domains with the HTTP API.")
.build());
options.addOption(mountOpt = Option.builder().longOpt("mount").hasArg().argName("SRC:DEST")
.desc("Mount a folder SRC at directory DEST on the computer.")
.build());
options.addOption(mountRoOpt = Option.builder().longOpt("mount-ro").hasArg().argName("SRC:DEST")
.desc("Mount a read-only folder SRC at directory DEST on the computer.")
.build());
options.addOption(helpOpt = Option.builder("h").longOpt("help")
.desc("Print help message")
@ -107,6 +140,7 @@ public static void main(String[] args) throws InterruptedException {
Path computerDirectory;
TermSize termSize;
boolean allowLocalDomains;
List<MountPaths> mounts, readOnlyMounts;
try {
var cli = new DefaultParser().parse(options, args);
if (cli.hasOption(helpOpt)) {
@ -115,10 +149,12 @@ public static void main(String[] args) throws InterruptedException {
}
if (!cli.hasOption(resourceOpt)) throw new ParseException("--resources directory is required");
resourcesDirectory = getParsedOptionValue(cli, resourceOpt, Path.class);
computerDirectory = getParsedOptionValue(cli, computerOpt, Path.class, null);
termSize = getParsedOptionValue(cli, termSizeOpt, TermSize.class, TermSize.DEFAULT);
resourcesDirectory = parsePath(cli.getOptionValue(resourceOpt));
computerDirectory = getParsedOptionValue(cli, computerOpt, Main::parsePath, null);
termSize = getParsedOptionValue(cli, termSizeOpt, TermSize::parse, TermSize.DEFAULT);
allowLocalDomains = cli.hasOption(allowLocalDomainsOpt);
mounts = getParsedOptionValues(cli, mountOpt, MountPaths::parse);
readOnlyMounts = getParsedOptionValues(cli, mountRoOpt, MountPaths::parse);
} catch (ParseException e) {
System.err.println(e.getLocalizedMessage());
@ -143,6 +179,7 @@ public static void main(String[] args) throws InterruptedException {
new Terminal(termSize.width(), termSize.height(), true, () -> isDirty.set(true)),
0
);
computer.addApi(new FileMounter(computer.getAPIEnvironment(), readOnlyMounts, mounts));
computer.turnOn();
runAndInit(gl, computer, isDirty);
@ -154,6 +191,41 @@ public static void main(String[] args) throws InterruptedException {
}
}
/**
* An {@link ILuaAPI} which is used to mount additional files, but does not expose any new globals/methods.
*/
private static final class FileMounter implements ILuaAPI {
private final IAPIEnvironment environment;
private final List<MountPaths> readOnlyMounts;
private final List<MountPaths> mounts;
FileMounter(IAPIEnvironment environment, List<MountPaths> readOnlyMounts, List<MountPaths> mounts) {
this.environment = environment;
this.readOnlyMounts = readOnlyMounts;
this.mounts = mounts;
}
@Override
public String[] getNames() {
return new String[0];
}
@Override
public void startup() {
try {
var fs = environment.getFileSystem();
for (var mount : readOnlyMounts) {
fs.mount(mount.dest(), mount.dest(), new FileMount(mount.src()));
}
for (var mount : mounts) {
fs.mount(mount.dest(), mount.dest(), new WritableFileMount(mount.src().toFile(), 1_000_000));
}
} catch (FileSystemException e) {
throw new IllegalStateException(e);
}
}
}
private static final int SCALE = 2;
private static final int MARGIN = 2;
private static final int PIXEL_WIDTH = 6;