From d5f82fa458fd5ed50292629e554f38650df1d588 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 15 May 2020 13:21:16 +0100 Subject: [PATCH] Replace getMethodNames/callMethod with annotations (#447) When creating a peripheral or custom Lua object, one must implement two methods: - getMethodNames(): String[] - Returns the name of the methods - callMethod(int, ...): Object[] - Invokes the method using an index in the above array. This has a couple of problems: - It's somewhat unwieldy to use - you need to keep track of array indices, which leads to ugly code. - Functions which yield (for instance, those which run on the main thread) are blocking. This means we need to spawn new threads for each CC-side yield. We replace this system with a few changes: - @LuaFunction annotation: One may annotate a public instance method with this annotation. This then exposes a peripheral/lua object method. Furthermore, this method can accept and return a variety of types, which often makes functions cleaner (e.g. can return an int rather than an Object[], and specify and int argument rather than Object[]). - MethodResult: Instead of returning an Object[] and having blocking yields, functions return a MethodResult. This either contains an immediate return, or an instruction to yield with some continuation to resume with. MethodResult is then interpreted by the Lua runtime (i.e. Cobalt), rather than our weird bodgey hacks before. This means we no longer spawn new threads when yielding within CC. - Methods accept IArguments instead of a raw Object array. This has a few benefits: - Consistent argument handling - people no longer need to use ArgumentHelper (as it doesn't exist!), or even be aware of its existence - you're rather forced into using it. - More efficient code in some cases. We provide a Cobalt-specific implementation of IArguments, which avoids the boxing/unboxing when handling numbers and binary strings. --- build.gradle | 1 + .../computercraft/api/lua/ArgumentHelper.java | 334 ---------- .../computercraft/api/lua/IArguments.java | 407 ++++++++++++ .../api/lua/IDynamicLuaObject.java | 45 ++ .../dan200/computercraft/api/lua/ILuaAPI.java | 5 +- .../computercraft/api/lua/ILuaCallback.java | 27 + .../computercraft/api/lua/ILuaContext.java | 82 +-- .../computercraft/api/lua/ILuaObject.java | 55 -- .../computercraft/api/lua/ILuaTask.java | 8 +- .../computercraft/api/lua/LuaException.java | 21 +- .../computercraft/api/lua/LuaFunction.java | 58 ++ .../computercraft/api/lua/LuaValues.java | 152 +++++ .../computercraft/api/lua/MethodResult.java | 170 +++++ .../api/lua/ObjectArguments.java | 66 ++ .../api/peripheral/IComputerAccess.java | 6 +- .../api/peripheral/IDynamicPeripheral.java | 53 ++ .../api/peripheral/IPeripheral.java | 61 +- .../api/peripheral/NotAttachedException.java | 2 +- .../api/turtle/ITurtleAccess.java | 14 +- .../api/turtle/ITurtleCommand.java | 8 +- .../api/turtle/event/TurtleBlockEvent.java | 5 +- .../turtle/event/TurtleInspectItemEvent.java | 5 +- .../core/apis/ComputerAccess.java | 2 +- .../dan200/computercraft/core/apis/FSAPI.java | 579 ++++++++--------- .../computercraft/core/apis/HTTPAPI.java | 224 +++---- .../core/apis/IAPIEnvironment.java | 2 +- .../dan200/computercraft/core/apis/OSAPI.java | 400 +++++------- .../core/apis/PeripheralAPI.java | 327 +++++----- .../computercraft/core/apis/RedstoneAPI.java | 148 ++--- .../computercraft/core/apis/TableHelper.java | 6 +- .../computercraft/core/apis/TermAPI.java | 283 +------- .../computercraft/core/apis/TermMethods.java | 222 +++++++ .../apis/handles/BinaryReadableHandle.java | 354 +++++----- .../apis/handles/BinaryWritableHandle.java | 136 ++-- .../apis/handles/EncodedReadableHandle.java | 204 +++--- .../apis/handles/EncodedWritableHandle.java | 112 ++-- .../core/apis/handles/HandleGeneric.java | 36 +- .../core/apis/http/CheckUrl.java | 4 +- .../core/apis/http/request/HttpRequest.java | 11 +- .../apis/http/request/HttpRequestHandler.java | 8 +- .../apis/http/request/HttpResponseHandle.java | 50 +- .../core/apis/http/websocket/Websocket.java | 10 +- .../apis/http/websocket/WebsocketHandle.java | 153 ++--- .../apis/http/websocket/WebsocketHandler.java | 4 +- .../core/asm/DeclaringClassLoader.java | 24 + .../computercraft/core/asm/Generator.java | 320 ++++++++++ .../computercraft/core/asm/IntCache.java | 41 ++ .../computercraft/core/asm/LuaMethod.java | 30 + .../computercraft/core/asm/NamedMethod.java | 40 ++ .../computercraft/core/asm/ObjectSource.java | 33 + .../core/asm/PeripheralMethod.java | 33 + .../computercraft/core/asm/Reflect.java | 95 +++ .../computercraft/core/asm/TaskCallback.java | 58 ++ .../core/computer/ApiWrapper.java | 18 +- .../computercraft/core/computer/Computer.java | 2 +- .../core/computer/ComputerExecutor.java | 4 +- .../core/computer/Environment.java | 6 +- .../computercraft/core/lua/BasicFunction.java | 75 +++ .../core/lua/CobaltLuaMachine.java | 186 ++---- .../computercraft/core/lua/ILuaMachine.java | 6 +- .../core/lua/ResultInterpreterFunction.java | 121 ++++ .../core/lua/VarargArguments.java | 102 +++ .../shared/computer/apis/CommandAPI.java | 231 +++---- .../computer/blocks/ComputerPeripheral.java | 80 ++- .../commandblock/CommandBlockPeripheral.java | 71 +- .../diskdrive/DiskDrivePeripheral.java | 171 +++-- .../peripheral/diskdrive/TileDiskDrive.java | 4 +- .../peripheral/modem/ModemPeripheral.java | 149 ++--- .../modem/wired/WiredModemPeripheral.java | 218 +++---- .../wireless/WirelessModemPeripheral.java | 2 +- .../peripheral/monitor/MonitorPeripheral.java | 226 ++----- .../peripheral/monitor/TileMonitor.java | 8 +- .../peripheral/printer/PrinterPeripheral.java | 159 ++--- .../peripheral/speaker/SpeakerPeripheral.java | 72 +-- .../shared/pocket/apis/PocketAPI.java | 142 ++-- .../shared/turtle/apis/TurtleAPI.java | 604 +++++++++--------- .../shared/turtle/core/TurtleBrain.java | 47 +- .../upgrades/CraftingTablePeripheral.java | 43 +- .../computercraft/shared/util/StringUtil.java | 14 +- .../computercraft/ContramapMatcher.java | 54 ++ .../core/ComputerTestDelegate.java | 383 +++++------ .../core/apis/ObjectWrapper.java | 83 +-- .../handles/BinaryReadableHandleTest.java | 14 +- .../computercraft/core/asm/GeneratorTest.java | 253 ++++++++ .../computercraft/core/asm/MethodTest.java | 208 ++++++ .../core/computer/ComputerBootstrap.java | 66 +- .../core/computer/ComputerTest.java | 18 +- .../core/filesystem/FileSystemTest.java | 4 +- .../shared/wired/NetworkTest.java | 17 - src/test/resources/benchmark.lua | 24 + .../test-rom/spec/apis/redstone_spec.lua | 2 +- 91 files changed, 5288 insertions(+), 4133 deletions(-) delete mode 100644 src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java create mode 100644 src/main/java/dan200/computercraft/api/lua/IArguments.java create mode 100644 src/main/java/dan200/computercraft/api/lua/IDynamicLuaObject.java create mode 100644 src/main/java/dan200/computercraft/api/lua/ILuaCallback.java delete mode 100644 src/main/java/dan200/computercraft/api/lua/ILuaObject.java create mode 100644 src/main/java/dan200/computercraft/api/lua/LuaFunction.java create mode 100644 src/main/java/dan200/computercraft/api/lua/LuaValues.java create mode 100644 src/main/java/dan200/computercraft/api/lua/MethodResult.java create mode 100644 src/main/java/dan200/computercraft/api/lua/ObjectArguments.java create mode 100644 src/main/java/dan200/computercraft/api/peripheral/IDynamicPeripheral.java create mode 100644 src/main/java/dan200/computercraft/core/apis/TermMethods.java create mode 100644 src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java create mode 100644 src/main/java/dan200/computercraft/core/asm/Generator.java create mode 100644 src/main/java/dan200/computercraft/core/asm/IntCache.java create mode 100644 src/main/java/dan200/computercraft/core/asm/LuaMethod.java create mode 100644 src/main/java/dan200/computercraft/core/asm/NamedMethod.java create mode 100644 src/main/java/dan200/computercraft/core/asm/ObjectSource.java create mode 100644 src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java create mode 100644 src/main/java/dan200/computercraft/core/asm/Reflect.java create mode 100644 src/main/java/dan200/computercraft/core/asm/TaskCallback.java create mode 100644 src/main/java/dan200/computercraft/core/lua/BasicFunction.java create mode 100644 src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java create mode 100644 src/main/java/dan200/computercraft/core/lua/VarargArguments.java create mode 100644 src/test/java/dan200/computercraft/ContramapMatcher.java create mode 100644 src/test/java/dan200/computercraft/core/asm/GeneratorTest.java create mode 100644 src/test/java/dan200/computercraft/core/asm/MethodTest.java create mode 100644 src/test/resources/benchmark.lua diff --git a/build.gradle b/build.gradle index 73446fdd6..5e75050db 100644 --- a/build.gradle +++ b/build.gradle @@ -109,6 +109,7 @@ accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg') testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' + testImplementation 'org.hamcrest:hamcrest:2.2' deployerJars "org.apache.maven.wagon:wagon-ssh:3.0.0" } diff --git a/src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java b/src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java deleted file mode 100644 index 97500fa97..000000000 --- a/src/main/java/dan200/computercraft/api/lua/ArgumentHelper.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * This file is part of the public ComputerCraft API - http://www.computercraft.info - * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. - * For help using the API, and posting your mods, visit the forums at computercraft.info. - */ -package dan200.computercraft.api.lua; - -import dan200.computercraft.api.peripheral.IComputerAccess; -import dan200.computercraft.api.peripheral.IPeripheral; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Map; - -/** - * Provides methods for extracting values and validating Lua arguments, such as those provided to - * {@link ILuaObject#callMethod(ILuaContext, int, Object[])} or - * {@link IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[])}. - * - * This provides two sets of functions: the {@code get*} methods, which require an argument to be valid, and - * {@code opt*}, which accept a default value and return that if the argument was not present or was {@code null}. - * If the argument is of the wrong type, a suitable error message will be thrown, with a similar format to Lua's own - * error messages. - * - *

Example usage:

- *
- * {@code
- * int slot = getInt( args, 0 );
- * int amount = optInt( args, 1, 64 );
- * }
- * 
- */ -public final class ArgumentHelper -{ - private ArgumentHelper() - { - } - - /** - * Get a string representation of the given value's type. - * - * @param value The value whose type we are trying to compute. - * @return A string representation of the given value's type, in a similar format to that provided by Lua's - * {@code type} function. - */ - @Nonnull - public static String getType( @Nullable Object value ) - { - if( value == null ) return "nil"; - if( value instanceof String ) return "string"; - if( value instanceof Boolean ) return "boolean"; - if( value instanceof Number ) return "number"; - if( value instanceof Map ) return "table"; - return "userdata"; - } - - /** - * Construct a "bad argument" exception, from an expected type and the actual value provided. - * - * @param index The argument number, starting from 0. - * @param expected The expected type for this argument. - * @param actual The actual value provided for this argument. - * @return The constructed exception, which should be thrown immediately. - */ - @Nonnull - public static LuaException badArgumentOf( int index, @Nonnull String expected, @Nullable Object actual ) - { - return badArgument( index, expected, getType( actual ) ); - } - - /** - * Construct a "bad argument" exception, from an expected and actual type. - * - * @param index The argument number, starting from 0. - * @param expected The expected type for this argument. - * @param actual The provided type for this argument. - * @return The constructed exception, which should be thrown immediately. - */ - @Nonnull - public static LuaException badArgument( int index, @Nonnull String expected, @Nonnull String actual ) - { - return new LuaException( "bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")" ); - } - - /** - * Get an argument as a double. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @return The argument's value. - * @throws LuaException If the value is not a number. - * @see #getFiniteDouble(Object[], int) if you require this to be finite (i.e. not infinite or NaN). - */ - public static double getDouble( @Nonnull Object[] args, int index ) throws LuaException - { - if( index >= args.length ) throw badArgument( index, "number", "nil" ); - Object value = args[index]; - if( !(value instanceof Number) ) throw badArgumentOf( index, "number", value ); - return ((Number) value).doubleValue(); - } - - /** - * Get an argument as an integer. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @return The argument's value. - * @throws LuaException If the value is not an integer. - */ - public static int getInt( @Nonnull Object[] args, int index ) throws LuaException - { - return (int) getLong( args, index ); - } - - /** - * Get an argument as a long. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @return The argument's value. - * @throws LuaException If the value is not a long. - */ - public static long getLong( @Nonnull Object[] args, int index ) throws LuaException - { - if( index >= args.length ) throw badArgument( index, "number", "nil" ); - Object value = args[index]; - if( !(value instanceof Number) ) throw badArgumentOf( index, "number", value ); - return checkFinite( index, (Number) value ).longValue(); - } - - /** - * Get an argument as a finite number (not infinite or NaN). - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @return The argument's value. - * @throws LuaException If the value is not finite. - */ - public static double getFiniteDouble( @Nonnull Object[] args, int index ) throws LuaException - { - return checkFinite( index, getDouble( args, index ) ); - } - - /** - * Get an argument as a boolean. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @return The argument's value. - * @throws LuaException If the value is not a boolean. - */ - public static boolean getBoolean( @Nonnull Object[] args, int index ) throws LuaException - { - if( index >= args.length ) throw badArgument( index, "boolean", "nil" ); - Object value = args[index]; - if( !(value instanceof Boolean) ) throw badArgumentOf( index, "boolean", value ); - return (Boolean) value; - } - - /** - * Get an argument as a string. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @return The argument's value. - * @throws LuaException If the value is not a string. - */ - @Nonnull - public static String getString( @Nonnull Object[] args, int index ) throws LuaException - { - if( index >= args.length ) throw badArgument( index, "string", "nil" ); - Object value = args[index]; - if( !(value instanceof String) ) throw badArgumentOf( index, "string", value ); - return (String) value; - } - - /** - * Get an argument as a table. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @return The argument's value. - * @throws LuaException If the value is not a table. - */ - @Nonnull - public static Map getTable( @Nonnull Object[] args, int index ) throws LuaException - { - if( index >= args.length ) throw badArgument( index, "table", "nil" ); - Object value = args[index]; - if( !(value instanceof Map) ) throw badArgumentOf( index, "table", value ); - return (Map) value; - } - - /** - * Get an argument as a double. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @param def The default value, if this argument is not given. - * @return The argument's value, or {@code def} if none was provided. - * @throws LuaException If the value is not a number. - */ - public static double optDouble( @Nonnull Object[] args, int index, double def ) throws LuaException - { - Object value = index < args.length ? args[index] : null; - if( value == null ) return def; - if( !(value instanceof Number) ) throw badArgumentOf( index, "number", value ); - return ((Number) value).doubleValue(); - } - - /** - * Get an argument as an int. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @param def The default value, if this argument is not given. - * @return The argument's value, or {@code def} if none was provided. - * @throws LuaException If the value is not a number. - */ - public static int optInt( @Nonnull Object[] args, int index, int def ) throws LuaException - { - return (int) optLong( args, index, def ); - } - - /** - * Get an argument as a long. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @param def The default value, if this argument is not given. - * @return The argument's value, or {@code def} if none was provided. - * @throws LuaException If the value is not a number. - */ - public static long optLong( @Nonnull Object[] args, int index, long def ) throws LuaException - { - Object value = index < args.length ? args[index] : null; - if( value == null ) return def; - if( !(value instanceof Number) ) throw badArgumentOf( index, "number", value ); - return checkFinite( index, (Number) value ).longValue(); - } - - /** - * Get an argument as a finite number (not infinite or NaN). - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @param def The default value, if this argument is not given. - * @return The argument's value, or {@code def} if none was provided. - * @throws LuaException If the value is not finite. - */ - public static double optFiniteDouble( @Nonnull Object[] args, int index, double def ) throws LuaException - { - return checkFinite( index, optDouble( args, index, def ) ); - } - - /** - * Get an argument as a boolean. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @param def The default value, if this argument is not given. - * @return The argument's value, or {@code def} if none was provided. - * @throws LuaException If the value is not a boolean. - */ - public static boolean optBoolean( @Nonnull Object[] args, int index, boolean def ) throws LuaException - { - Object value = index < args.length ? args[index] : null; - if( value == null ) return def; - if( !(value instanceof Boolean) ) throw badArgumentOf( index, "boolean", value ); - return (Boolean) value; - } - - /** - * Get an argument as a string. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @param def The default value, if this argument is not given. - * @return The argument's value, or {@code def} if none was provided. - * @throws LuaException If the value is not a string. - */ - public static String optString( @Nonnull Object[] args, int index, String def ) throws LuaException - { - Object value = index < args.length ? args[index] : null; - if( value == null ) return def; - if( !(value instanceof String) ) throw badArgumentOf( index, "string", value ); - return (String) value; - } - - /** - * Get an argument as a table. - * - * @param args The arguments to extract from. - * @param index The index into the argument array to read from. - * @param def The default value, if this argument is not given. - * @return The argument's value, or {@code def} if none was provided. - * @throws LuaException If the value is not a table. - */ - public static Map optTable( @Nonnull Object[] args, int index, Map def ) throws LuaException - { - Object value = index < args.length ? args[index] : null; - if( value == null ) return def; - if( !(value instanceof Map) ) throw badArgumentOf( index, "table", value ); - return (Map) value; - } - - private static Number checkFinite( int index, Number value ) throws LuaException - { - checkFinite( index, value.doubleValue() ); - return value; - } - - private static double checkFinite( int index, double value ) throws LuaException - { - if( !Double.isFinite( value ) ) throw badArgument( index, "number", getNumericType( value ) ); - return value; - } - - /** - * Returns a more detailed representation of this number's type. If this is finite, it will just return "number", - * otherwise it returns whether it is infinite or NaN. - * - * @param value The value to extract the type for. - * @return This value's numeric type. - */ - @Nonnull - public static String getNumericType( double value ) - { - if( Double.isNaN( value ) ) return "nan"; - if( value == Double.POSITIVE_INFINITY ) return "inf"; - if( value == Double.NEGATIVE_INFINITY ) return "-inf"; - return "number"; - } -} diff --git a/src/main/java/dan200/computercraft/api/lua/IArguments.java b/src/main/java/dan200/computercraft/api/lua/IArguments.java new file mode 100644 index 000000000..c19ed4526 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -0,0 +1,407 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.api.lua.LuaValues.checkFinite; + +/** + * The arguments passed to a function. + */ +public interface IArguments +{ + /** + * Get the number of arguments passed to this function. + * + * @return The number of passed arguments. + */ + int count(); + + /** + * Get the argument at the specific index. The returned value must obey the following conversion rules: + * + * + * + * @param index The argument number. + * @return The argument's value, or {@code null} if not present. + */ + @Nullable + Object get( int index ); + + /** + * Drop a number of arguments. The returned arguments instance will access arguments at position {@code i + count}, + * rather than {@code i}. However, errors will still use the given argument index. + * + * @param count The number of arguments to drop. + * @return The new {@link IArguments} instance. + */ + IArguments drop( int count ); + + default Object[] getAll() + { + Object[] result = new Object[count()]; + for( int i = 0; i < result.length; i++ ) result[i] = get( i ); + return result; + } + + /** + * Get an argument as a double. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a number. + * @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN). + */ + default double getDouble( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return ((Number) value).doubleValue(); + } + + /** + * Get an argument as an integer. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not an integer. + */ + default int getInt( int index ) throws LuaException + { + return (int) getLong( index ); + } + + /** + * Get an argument as a long. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a long. + */ + default long getLong( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return LuaValues.checkFiniteNum( index, (Number) value ).longValue(); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not finite. + */ + default double getFiniteDouble( int index ) throws LuaException + { + return checkFinite( index, getDouble( index ) ); + } + + /** + * Get an argument as a boolean. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a boolean. + */ + default boolean getBoolean( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Boolean) ) throw LuaValues.badArgumentOf( index, "boolean", value ); + return (Boolean) value; + } + + /** + * Get an argument as a string. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a string. + */ + @Nonnull + default String getString( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof String) ) throw LuaValues.badArgumentOf( index, "string", value ); + return (String) value; + } + + /** + * Get a string argument as a byte array. + * + * @param index The argument number. + * @return The argument's value. This is a read only buffer. + * @throws LuaException If the value is not a string. + */ + @Nonnull + default ByteBuffer getBytes( int index ) throws LuaException + { + return LuaValues.encode( getString( index ) ); + } + + /** + * Get a string argument as an enum value. + * + * @param index The argument number. + * @param klass The type of enum to parse. + * @param The type of enum to parse. + * @return The argument's value. + * @throws LuaException If the value is not a string or not a valid option for this enum. + */ + @Nonnull + default > T getEnum( int index, Class klass ) throws LuaException + { + return LuaValues.checkEnum( index, klass, getString( index ) ); + } + + /** + * Get an argument as a table. + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a table. + */ + @Nonnull + default Map getTable( int index ) throws LuaException + { + Object value = get( index ); + if( !(value instanceof Map) ) throw LuaValues.badArgumentOf( index, "table", value ); + return (Map) value; + } + + /** + * Get an argument as a double. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + */ + @Nonnull + default Optional optDouble( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return Optional.of( ((Number) value).doubleValue() ); + } + + /** + * Get an argument as an int. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + */ + @Nonnull + default Optional optInt( int index ) throws LuaException + { + return optLong( index ).map( Long::intValue ); + } + + /** + * Get an argument as a long. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a number. + */ + default Optional optLong( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Number) ) throw LuaValues.badArgumentOf( index, "number", value ); + return Optional.of( LuaValues.checkFiniteNum( index, (Number) value ).longValue() ); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not finite. + */ + default Optional optFiniteDouble( int index ) throws LuaException + { + Optional value = optDouble( index ); + if( value.isPresent() ) LuaValues.checkFiniteNum( index, value.get() ); + return value; + } + + /** + * Get an argument as a boolean. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a boolean. + */ + default Optional optBoolean( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Boolean) ) throw LuaValues.badArgumentOf( index, "boolean", value ); + return Optional.of( (Boolean) value ); + } + + /** + * Get an argument as a string. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a string. + */ + default Optional optString( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof String) ) throw LuaValues.badArgumentOf( index, "string", value ); + return Optional.of( (String) value ); + } + + /** + * Get a string argument as a byte array. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. This is a read only buffer. + * @throws LuaException If the value is not a string. + */ + default Optional optBytes( int index ) throws LuaException + { + return optString( index ).map( LuaValues::encode ); + } + + /** + * Get a string argument as an enum value. + * + * @param index The argument number. + * @param klass The type of enum to parse. + * @param The type of enum to parse. + * @return The argument's value. + * @throws LuaException If the value is not a string or not a valid option for this enum. + */ + @Nonnull + default > Optional optEnum( int index, Class klass ) throws LuaException + { + Optional str = optString( index ); + return str.isPresent() ? Optional.of( LuaValues.checkEnum( index, klass, str.get() ) ) : Optional.empty(); + } + + /** + * Get an argument as a table. + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a table. + */ + default Optional> optTable( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Map) ) throw LuaValues.badArgumentOf( index, "map", value ); + return Optional.of( (Map) value ); + } + + /** + * Get an argument as a double. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a number. + */ + default double optDouble( int index, double def ) throws LuaException + { + return optDouble( index ).orElse( def ); + } + + /** + * Get an argument as an int. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a number. + */ + default int optInt( int index, int def ) throws LuaException + { + return optInt( index ).orElse( def ); + } + + /** + * Get an argument as a long. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a number. + */ + default long optLong( int index, long def ) throws LuaException + { + return optLong( index ).orElse( def ); + } + + /** + * Get an argument as a finite number (not infinite or NaN). + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not finite. + */ + default double optFiniteDouble( int index, double def ) throws LuaException + { + return optFiniteDouble( index ).orElse( def ); + } + + /** + * Get an argument as a boolean. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a boolean. + */ + default boolean optBoolean( int index, boolean def ) throws LuaException + { + return optBoolean( index ).orElse( def ); + } + + /** + * Get an argument as a string. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a string. + */ + default String optString( int index, String def ) throws LuaException + { + return optString( index ).orElse( def ); + } + + /** + * Get an argument as a table. + * + * @param index The argument number. + * @param def The default value, if this argument is not given. + * @return The argument's value, or {@code def} if none was provided. + * @throws LuaException If the value is not a table. + */ + default Map optTable( int index, Map def ) throws LuaException + { + return optTable( index ).orElse( def ); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/IDynamicLuaObject.java b/src/main/java/dan200/computercraft/api/lua/IDynamicLuaObject.java new file mode 100644 index 000000000..13b2c7a8f --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/IDynamicLuaObject.java @@ -0,0 +1,45 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import dan200.computercraft.api.peripheral.IDynamicPeripheral; + +import javax.annotation.Nonnull; + +/** + * An interface for representing custom objects returned by peripherals or other Lua objects. + * + * Generally, one does not need to implement this type - it is sufficient to return an object with some methods + * annotated with {@link LuaFunction}. {@link IDynamicLuaObject} is useful when you wish your available methods to + * change at runtime. + */ +public interface IDynamicLuaObject +{ + /** + * Get the names of the methods that this object implements. This should not change over the course of the object's + * lifetime. + * + * @return The method names this object provides. + * @see IDynamicPeripheral#getMethodNames() + */ + @Nonnull + String[] getMethodNames(); + + /** + * Called when a user calls one of the methods that this object implements. + * + * @param context The context of the currently running lua thread. This can be used to wait for events + * or otherwise yield. + * @param method An integer identifying which method index from {@link #getMethodNames()} the computer wishes + * to call. + * @param arguments The arguments for this method. + * @return The result of this function. Either an immediate value ({@link MethodResult#of(Object...)} or an + * instruction to yield. + * @throws LuaException If the function threw an exception. + */ + @Nonnull + MethodResult callMethod( @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaAPI.java b/src/main/java/dan200/computercraft/api/lua/ILuaAPI.java index a37f0404a..ae405e3aa 100644 --- a/src/main/java/dan200/computercraft/api/lua/ILuaAPI.java +++ b/src/main/java/dan200/computercraft/api/lua/ILuaAPI.java @@ -8,7 +8,8 @@ import dan200.computercraft.api.ComputerCraftAPI; /** - * Represents a {@link ILuaObject} which is stored as a global variable on computer startup. + * Represents a Lua object which is stored as a global variable on computer startup. This must either provide + * {@link LuaFunction} annotated functions or implement {@link IDynamicLuaObject}. * * Before implementing this interface, consider alternative methods of providing methods. It is generally preferred * to use peripherals to provide functionality to users. @@ -16,7 +17,7 @@ * @see ILuaAPIFactory * @see ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory) */ -public interface ILuaAPI extends ILuaObject +public interface ILuaAPI { /** * Get the globals this API will be assigned to. This will override any other global, so you should diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaCallback.java b/src/main/java/dan200/computercraft/api/lua/ILuaCallback.java new file mode 100644 index 000000000..f05d82a73 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ILuaCallback.java @@ -0,0 +1,27 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import javax.annotation.Nonnull; + +/** + * A continuation which is called when this coroutine is resumed. + * + * @see MethodResult#yield(Object[], ILuaCallback) + */ +public interface ILuaCallback +{ + /** + * Resume this coroutine. + * + * @param args The result of resuming this coroutine. These will have the same form as described in + * {@link LuaFunction}. + * @return The result of this continuation. Either the result to return to the callee, or another yield. + * @throws LuaException On an error. + */ + @Nonnull + MethodResult resume( Object[] args ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaContext.java b/src/main/java/dan200/computercraft/api/lua/ILuaContext.java index 623f19a74..569b61e5b 100644 --- a/src/main/java/dan200/computercraft/api/lua/ILuaContext.java +++ b/src/main/java/dan200/computercraft/api/lua/ILuaContext.java @@ -6,99 +6,25 @@ package dan200.computercraft.api.lua; import javax.annotation.Nonnull; -import javax.annotation.Nullable; /** - * An interface passed to peripherals and {@link ILuaObject}s by computers or turtles, providing methods - * that allow the peripheral call to wait for events before returning, just like in lua. This is very useful if you need - * to signal work to be performed on the main thread, and don't want to return until the work has been completed. + * An interface passed to peripherals and {@link IDynamicLuaObject}s by computers or turtles, providing methods + * that allow the peripheral call to interface with the computer. */ public interface ILuaContext { - /** - * Wait for an event to occur on the computer, suspending the thread until it arises. This method is exactly - * equivalent to {@code os.pullEvent()} in lua. - * - * @param filter A specific event to wait for, or null to wait for any event. - * @return An object array containing the name of the event that occurred, and any event parameters. - * @throws LuaException If the user presses CTRL+T to terminate the current program while pullEvent() is - * waiting for an event, a "Terminated" exception will be thrown here. - * - * Do not attempt to catch this exception. You should use {@link #pullEventRaw(String)} - * should you wish to disable termination. - * @throws InterruptedException If the user shuts down or reboots the computer while pullEvent() is waiting for an - * event, InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state. - */ - @Nonnull - default Object[] pullEvent( @Nullable String filter ) throws LuaException, InterruptedException - { - Object[] results = pullEventRaw( filter ); - if( results.length >= 1 && results[0].equals( "terminate" ) ) throw new LuaException( "Terminated", 0 ); - return results; - } - - /** - * The same as {@link #pullEvent(String)}, except "terminated" events are ignored. Only use this if you want to - * prevent program termination, which is not recommended. This method is exactly equivalent to - * {@code os.pullEventRaw()} in lua. - * - * @param filter A specific event to wait for, or null to wait for any event. - * @return An object array containing the name of the event that occurred, and any event parameters. - * @throws InterruptedException If the user shuts down or reboots the computer while pullEventRaw() is waiting for - * an event, InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state. - * @see #pullEvent(String) - */ - @Nonnull - default Object[] pullEventRaw( @Nullable String filter ) throws InterruptedException - { - return yield( new Object[] { filter } ); - } - - /** - * Yield the current coroutine with some arguments until it is resumed. This method is exactly equivalent to - * {@code coroutine.yield()} in lua. Use {@code pullEvent()} if you wish to wait for events. - * - * @param arguments An object array containing the arguments to pass to coroutine.yield() - * @return An object array containing the return values from coroutine.yield() - * @throws InterruptedException If the user shuts down or reboots the computer the coroutine is suspended, - * InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state. - * @see #pullEvent(String) - */ - @Nonnull - Object[] yield( @Nullable Object[] arguments ) throws InterruptedException; - - /** - * Queue a task to be executed on the main server thread at the beginning of next tick, waiting for it to complete. - * This should be used when you need to interact with the world in a thread-safe manner. - * - * Note that the return values of your task are handled as events, meaning more complex objects such as maps or - * {@link ILuaObject} will not preserve their identities. - * - * @param task The task to execute on the main thread. - * @return The objects returned by {@code task}. - * @throws LuaException If the task could not be queued, or if the task threw an exception. - * @throws InterruptedException If the user shuts down or reboots the computer the coroutine is suspended, - * InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state. - */ - @Nullable - Object[] executeMainThreadTask( @Nonnull ILuaTask task ) throws LuaException, InterruptedException; - /** * Queue a task to be executed on the main server thread at the beginning of next tick, but do not wait for it to * complete. This should be used when you need to interact with the world in a thread-safe manner but do not care * about the result or you wish to run asynchronously. * * When the task has finished, it will enqueue a {@code task_completed} event, which takes the task id, a success - * value and the return values, or an error message if it failed. If you need to wait on this event, it may be - * better to use {@link #executeMainThreadTask(ILuaTask)}. + * value and the return values, or an error message if it failed. * * @param task The task to execute on the main thread. * @return The "id" of the task. This will be the first argument to the {@code task_completed} event. * @throws LuaException If the task could not be queued. + * @see LuaFunction#mainThread() To run functions on the main thread and return their results synchronously. */ long issueMainThreadTask( @Nonnull ILuaTask task ) throws LuaException; } diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaObject.java b/src/main/java/dan200/computercraft/api/lua/ILuaObject.java deleted file mode 100644 index 4d0f37f89..000000000 --- a/src/main/java/dan200/computercraft/api/lua/ILuaObject.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of the public ComputerCraft API - http://www.computercraft.info - * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. - * For help using the API, and posting your mods, visit the forums at computercraft.info. - */ -package dan200.computercraft.api.lua; - -import dan200.computercraft.api.peripheral.IComputerAccess; -import dan200.computercraft.api.peripheral.IPeripheral; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * An interface for representing custom objects returned by {@link IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[])} - * calls. - * - * Return objects implementing this interface to expose objects with methods to lua. - */ -public interface ILuaObject -{ - /** - * Get the names of the methods that this object implements. This works the same as {@link IPeripheral#getMethodNames()}. - * See that method for detailed documentation. - * - * @return The method names this object provides. - * @see IPeripheral#getMethodNames() - */ - @Nonnull - String[] getMethodNames(); - - /** - * Called when a user calls one of the methods that this object implements. This works the same as - * {@link IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[])}}. See that method for detailed - * documentation. - * - * @param context The context of the currently running lua thread. This can be used to wait for events - * or otherwise yield. - * @param method An integer identifying which of the methods from getMethodNames() the computercraft - * wishes to call. The integer indicates the index into the getMethodNames() table - * that corresponds to the string passed into peripheral.call() - * @param arguments The arguments for this method. See {@link IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[])} - * the possible values and conversion rules. - * @return An array of objects, representing the values you wish to return to the Lua program. - * See {@link IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[])} for the valid values and - * conversion rules. - * @throws LuaException If the task could not be queued, or if the task threw an exception. - * @throws InterruptedException If the user shuts down or reboots the computer the coroutine is suspended, - * InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state.w - * @see IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[]) - */ - @Nullable - Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException; -} diff --git a/src/main/java/dan200/computercraft/api/lua/ILuaTask.java b/src/main/java/dan200/computercraft/api/lua/ILuaTask.java index f99ce283b..4ea060358 100644 --- a/src/main/java/dan200/computercraft/api/lua/ILuaTask.java +++ b/src/main/java/dan200/computercraft/api/lua/ILuaTask.java @@ -8,11 +8,10 @@ import javax.annotation.Nullable; /** - * A task which can be executed via {@link ILuaContext#executeMainThreadTask(ILuaTask)} or - * {@link ILuaContext#issueMainThreadTask(ILuaTask)}. This will be run on the main thread, at the beginning of the + * A task which can be executed via {@link ILuaContext#issueMainThreadTask(ILuaTask)} This will be run on the main + * thread, at the beginning of the * next tick. * - * @see ILuaContext#executeMainThreadTask(ILuaTask) * @see ILuaContext#issueMainThreadTask(ILuaTask) */ @FunctionalInterface @@ -21,8 +20,7 @@ public interface ILuaTask /** * Execute this task. * - * @return The arguments to add to the {@code task_completed} event. These will be returned by - * {@link ILuaContext#executeMainThreadTask(ILuaTask)}. + * @return The arguments to add to the {@code task_completed} event. * @throws LuaException If you throw any exception from this function, a lua error will be raised with the * same message as your exception. Use this to throw appropriate errors if the wrong * arguments are supplied to your method. diff --git a/src/main/java/dan200/computercraft/api/lua/LuaException.java b/src/main/java/dan200/computercraft/api/lua/LuaException.java index 65d8d0284..949a8eeba 100644 --- a/src/main/java/dan200/computercraft/api/lua/LuaException.java +++ b/src/main/java/dan200/computercraft/api/lua/LuaException.java @@ -13,24 +13,33 @@ public class LuaException extends Exception { private static final long serialVersionUID = -6136063076818512651L; + private final boolean hasLevel; private final int level; - public LuaException() - { - this( "error", 1 ); - } - public LuaException( @Nullable String message ) { - this( message, 1 ); + super( message ); + this.hasLevel = false; + this.level = 1; } public LuaException( @Nullable String message, int level ) { super( message ); + this.hasLevel = true; this.level = level; } + /** + * Whether a level was explicitly specified when constructing. This is used to determine + * + * @return Whether this has an explicit level. + */ + public boolean hasLevel() + { + return hasLevel; + } + /** * The level this error is raised at. Level 1 is the function's caller, level 2 is that function's caller, and so * on. diff --git a/src/main/java/dan200/computercraft/api/lua/LuaFunction.java b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java new file mode 100644 index 000000000..e17e30ac4 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java @@ -0,0 +1,58 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; + +import java.lang.annotation.*; +import java.util.Map; +import java.util.Optional; + +/** + * Used to mark a Java function which is callable from Lua. + * + * Methods annotated with {@link LuaFunction} must be public final instance methods. They can have any number of + * parameters, but they must be of the following types: + * + *
    + *
  • {@link ILuaContext} (and {@link IComputerAccess} if on a {@link IPeripheral})
  • + *
  • {@link IArguments}: The arguments supplied to this function.
  • + *
  • + * Alternatively, one may specify the desired arguments as normal parameters and the argument parsing code will + * be generated automatically. + * + * Each parameter must be one of the given types supported by {@link IArguments} (for instance, {@link int} or + * {@link Map}). Optional values are supported by accepting a parameter of type {@link Optional}. + *
  • + *
+ * + * This function may return {@link MethodResult}. However, if you simply return a value (rather than having to yield), + * you may return {@code void}, a single value (either an object or a primitive like {@code int}) or array of objects. + * These will be treated the same as {@link MethodResult#of()}, {@link MethodResult#of(Object)} and + * {@link MethodResult#of(Object...)}. + */ +@Documented +@Retention( RetentionPolicy.RUNTIME ) +@Target( ElementType.METHOD ) +public @interface LuaFunction +{ + /** + * Explicitly specify the method names of this function. If not given, it uses the name of the annotated method. + * + * @return This function's name(s). + */ + String[] value() default {}; + + /** + * Run this function on the main server thread. This should be specified for any method which interacts with + * Minecraft in a thread-unsafe manner. + * + * @return Whether this functi + * @see ILuaContext#issueMainThreadTask(ILuaTask) + */ + boolean mainThread() default false; +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaValues.java b/src/main/java/dan200/computercraft/api/lua/LuaValues.java new file mode 100644 index 000000000..f89b24a17 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaValues.java @@ -0,0 +1,152 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * Various utility functions for operating with Lua values. + * + * @see IArguments + */ +public final class LuaValues +{ + private LuaValues() + { + } + + /** + * Encode a Lua string into a read-only {@link ByteBuffer}. + * + * @param string The string to encode. + * @return The encoded string. + */ + @Nonnull + public static ByteBuffer encode( @Nonnull String string ) + { + byte[] chars = new byte[string.length()]; + for( int i = 0; i < chars.length; i++ ) + { + char c = string.charAt( i ); + chars[i] = c < 256 ? (byte) c : 63; + } + + return ByteBuffer.wrap( chars ).asReadOnlyBuffer(); + } + + /** + * Returns a more detailed representation of this number's type. If this is finite, it will just return "number", + * otherwise it returns whether it is infinite or NaN. + * + * @param value The value to extract the type for. + * @return This value's numeric type. + */ + @Nonnull + public static String getNumericType( double value ) + { + if( Double.isNaN( value ) ) return "nan"; + if( value == Double.POSITIVE_INFINITY ) return "inf"; + if( value == Double.NEGATIVE_INFINITY ) return "-inf"; + return "number"; + } + + /** + * Get a string representation of the given value's type. + * + * @param value The value whose type we are trying to compute. + * @return A string representation of the given value's type, in a similar format to that provided by Lua's + * {@code type} function. + */ + @Nonnull + public static String getType( @Nullable Object value ) + { + if( value == null ) return "nil"; + if( value instanceof String ) return "string"; + if( value instanceof Boolean ) return "boolean"; + if( value instanceof Number ) return "number"; + if( value instanceof Map ) return "table"; + return "userdata"; + } + + /** + * Construct a "bad argument" exception, from an expected type and the actual value provided. + * + * @param index The argument number, starting from 0. + * @param expected The expected type for this argument. + * @param actual The actual value provided for this argument. + * @return The constructed exception, which should be thrown immediately. + */ + @Nonnull + public static LuaException badArgumentOf( int index, @Nonnull String expected, @Nullable Object actual ) + { + return badArgument( index, expected, getType( actual ) ); + } + + /** + * Construct a "bad argument" exception, from an expected and actual type. + * + * @param index The argument number, starting from 0. + * @param expected The expected type for this argument. + * @param actual The provided type for this argument. + * @return The constructed exception, which should be thrown immediately. + */ + @Nonnull + public static LuaException badArgument( int index, @Nonnull String expected, @Nonnull String actual ) + { + return new LuaException( "bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")" ); + } + + /** + * Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}. + * + * @param index The argument index to check. + * @param value The value to check. + * @return The input {@code value}. + * @throws LuaException If this is not a finite number. + */ + public static Number checkFiniteNum( int index, Number value ) throws LuaException + { + checkFinite( index, value.doubleValue() ); + return value; + } + + /** + * Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}. + * + * @param index The argument index to check. + * @param value The value to check. + * @return The input {@code value}. + * @throws LuaException If this is not a finite number. + */ + public static double checkFinite( int index, double value ) throws LuaException + { + if( !Double.isFinite( value ) ) throw badArgument( index, "number", getNumericType( value ) ); + return value; + } + + /** + * Ensure a string is a valid enum value. + * + * @param index The argument index to check. + * @param klass The class of the enum instance. + * @param value The value to extract. + * @param The type of enum we are extracting. + * @return The parsed enum value. + * @throws LuaException If this is not a known enum value. + */ + public static > T checkEnum( int index, Class klass, String value ) throws LuaException + { + for( T possibility : klass.getEnumConstants() ) + { + if( possibility.name().equalsIgnoreCase( value ) ) return possibility; + } + + throw new LuaException( "bad argument #" + (index + 1) + " (unknown option " + value + ")" ); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/MethodResult.java b/src/main/java/dan200/computercraft/api/lua/MethodResult.java new file mode 100644 index 000000000..2a7f80155 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/MethodResult.java @@ -0,0 +1,170 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import dan200.computercraft.api.peripheral.IComputerAccess; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * The result of invoking a Lua method. + * + * Method results either return a value immediately ({@link #of(Object...)} or yield control to the parent coroutine. + * When the current coroutine is resumed, we invoke the provided {@link ILuaCallback#resume(Object[])} callback. + */ +public final class MethodResult +{ + private static final MethodResult empty = new MethodResult( null, null ); + + private final Object[] result; + private final ILuaCallback callback; + private final int adjust; + + private MethodResult( Object[] arguments, ILuaCallback callback ) + { + this.result = arguments; + this.callback = callback; + this.adjust = 0; + } + + private MethodResult( Object[] arguments, ILuaCallback callback, int adjust ) + { + this.result = arguments; + this.callback = callback; + this.adjust = adjust; + } + + /** + * Return no values immediately. + * + * @return A method result which returns immediately with no values. + */ + @Nonnull + public static MethodResult of() + { + return empty; + } + + /** + * Return a single value immediately. + * + * Integers, doubles, floats, strings, booleans, {@link Map}, {@link Collection}s, arrays and {@code null} will be + * converted to their corresponding Lua type. {@code byte[]} and {@link ByteBuffer} will be treated as binary + * strings. + * + * In order to provide a custom object with methods, one may return a {@link IDynamicLuaObject}, or an arbitrary + * class with {@link LuaFunction} annotations. Anything else will be converted to {@code nil}. + * + * @param value The value to return to the calling Lua function. + * @return A method result which returns immediately with the given value. + */ + @Nonnull + public static MethodResult of( @Nullable Object value ) + { + return new MethodResult( new Object[] { value }, null ); + } + + /** + * Return any number of values immediately. + * + * @param values The values to return. See {@link #of(Object)} for acceptable values. + * @return A method result which returns immediately with the given values. + */ + @Nonnull + public static MethodResult of( @Nullable Object... values ) + { + return values == null || values.length == 0 ? empty : new MethodResult( values, null ); + } + + /** + * Wait for an event to occur on the computer, suspending the thread until it arises. This method is exactly + * equivalent to {@code os.pullEvent()} in lua. + * + * @param filter A specific event to wait for, or null to wait for any event. + * @param callback The callback to resume with the name of the event that occurred, and any event parameters. + * @return The method result which represents this yield. + * @see IComputerAccess#queueEvent(String, Object[]) + */ + @Nonnull + public static MethodResult pullEvent( @Nullable String filter, @Nonnull ILuaCallback callback ) + { + Objects.requireNonNull( callback, "callback cannot be null" ); + return new MethodResult( new Object[] { filter }, results -> { + if( results.length >= 1 && results[0].equals( "terminate" ) ) throw new LuaException( "Terminated", 0 ); + return callback.resume( results ); + } ); + } + + /** + * The same as {@link #pullEvent(String, ILuaCallback)}, except "terminated" events are ignored. Only use this if + * you want to prevent program termination, which is not recommended. This method is exactly equivalent to + * {@code os.pullEventRaw()} in Lua. + * + * @param filter A specific event to wait for, or null to wait for any event. + * @param callback The callback to resume with the name of the event that occurred, and any event parameters. + * @return The method result which represents this yield. + * @see #pullEvent(String, ILuaCallback) + */ + @Nonnull + public static MethodResult pullEventRaw( @Nullable String filter, @Nonnull ILuaCallback callback ) + { + Objects.requireNonNull( callback, "callback cannot be null" ); + return new MethodResult( new Object[] { filter }, callback ); + } + + /** + * Yield the current coroutine with some arguments until it is resumed. This method is exactly equivalent to + * {@code coroutine.yield()} in lua. Use {@code pullEvent()} if you wish to wait for events. + * + * @param arguments An object array containing the arguments to pass to coroutine.yield() + * @param callback The callback to resume with an array containing the return values from coroutine.yield() + * @return The method result which represents this yield. + * @see #pullEvent(String, ILuaCallback) + */ + @Nonnull + public static MethodResult yield( @Nullable Object[] arguments, @Nonnull ILuaCallback callback ) + { + Objects.requireNonNull( callback, "callback cannot be null" ); + return new MethodResult( arguments, callback ); + } + + @Nullable + public Object[] getResult() + { + return result; + } + + @Nullable + public ILuaCallback getCallback() + { + return callback; + } + + public int getErrorAdjust() + { + return adjust; + } + + /** + * Increase the Lua error by a specific amount. One should never need to use this function - it largely exists for + * some CC internal code. + * + * @param adjust The amount to increase the level by. + * @return The new {@link MethodResult} with an adjusted error. This has no effect on immediate results. + */ + @Nonnull + public MethodResult adjustError( int adjust ) + { + if( adjust < 0 ) throw new IllegalArgumentException( "cannot adjust by a negative amount" ); + if( adjust == 0 || callback == null ) return this; + return new MethodResult( result, callback, this.adjust + adjust ); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java new file mode 100644 index 000000000..4ccc50b89 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java @@ -0,0 +1,66 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * An implementation of {@link IArguments} which wraps an array of {@link Object}. + */ +public final class ObjectArguments implements IArguments +{ + private static final IArguments EMPTY = new ObjectArguments(); + private final List args; + + @Deprecated + @SuppressWarnings( "unused" ) + public ObjectArguments( IArguments arguments ) + { + throw new IllegalStateException(); + } + + public ObjectArguments( Object... args ) + { + this.args = Arrays.asList( args ); + } + + public ObjectArguments( List args ) + { + this.args = Objects.requireNonNull( args ); + } + + @Override + public int count() + { + return args.size(); + } + + @Override + public IArguments drop( int count ) + { + if( count < 0 ) throw new IllegalStateException( "count cannot be negative" ); + if( count == 0 ) return this; + if( count >= args.size() ) return EMPTY; + + return new ObjectArguments( args.subList( count, args.size() ) ); + } + + @Nullable + @Override + public Object get( int index ) + { + return index >= args.size() ? null : args.get( index ); + } + + @Override + public Object[] getAll() + { + return args.toArray(); + } +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java b/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java index 9a6f243e9..fd46dd056 100644 --- a/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java +++ b/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java @@ -8,8 +8,10 @@ import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.filesystem.IMount; import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaCallback; import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.ILuaTask; +import dan200.computercraft.api.lua.MethodResult; import net.minecraft.world.World; import javax.annotation.Nonnull; @@ -146,9 +148,9 @@ default String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritabl * * You may supply {@code null} to indicate that no arguments are to be supplied. * @throws NotAttachedException If the peripheral has been detached. - * @see IPeripheral#callMethod + * @see MethodResult#pullEvent(String, ILuaCallback) */ - void queueEvent( @Nonnull String event, @Nullable Object[] arguments ); + void queueEvent( @Nonnull String event, @Nullable Object... arguments ); /** * Get a string, unique to the computer, by which the computer refers to this peripheral. diff --git a/src/main/java/dan200/computercraft/api/peripheral/IDynamicPeripheral.java b/src/main/java/dan200/computercraft/api/peripheral/IDynamicPeripheral.java new file mode 100644 index 000000000..5e7e8bfcc --- /dev/null +++ b/src/main/java/dan200/computercraft/api/peripheral/IDynamicPeripheral.java @@ -0,0 +1,53 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.peripheral; + +import dan200.computercraft.api.lua.*; + +import javax.annotation.Nonnull; + +/** + * A peripheral whose methods are not known at runtime. + * + * This behaves similarly to {@link IDynamicLuaObject}, though also accepting the current {@link IComputerAccess}. + * Generally one may use {@link LuaFunction} instead of implementing this interface. + */ +public interface IDynamicPeripheral extends IPeripheral +{ + /** + * Should return an array of strings that identify the methods that this peripheral exposes to Lua. This will be + * called once before each attachment, and should not change when called multiple times. + * + * @return An array of strings representing method names. + * @see #callMethod + */ + @Nonnull + String[] getMethodNames(); + + /** + * This is called when a lua program on an attached computer calls {@code peripheral.call()} with + * one of the methods exposed by {@link #getMethodNames()}. + * + * Be aware that this will be called from the ComputerCraft Lua thread, and must be thread-safe when interacting + * with Minecraft objects. + * + * @param computer The interface to the computer that is making the call. Remember that multiple + * computers can be attached to a peripheral at once. + * @param context The context of the currently running lua thread. This can be used to wait for events + * or otherwise yield. + * @param method An integer identifying which of the methods from getMethodNames() the computercraft + * wishes to call. The integer indicates the index into the getMethodNames() table + * that corresponds to the string passed into peripheral.call() + * @param arguments The arguments for this method. + * @return A {@link MethodResult} containing the values to return or the action to perform. + * @throws LuaException If you throw any exception from this function, a lua error will be raised with the + * same message as your exception. Use this to throw appropriate errors if the wrong + * arguments are supplied to your method. + * @see #getMethodNames() + */ + @Nonnull + MethodResult callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java b/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java index 310ff24c5..a361848c3 100644 --- a/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java +++ b/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java @@ -5,15 +5,17 @@ */ package dan200.computercraft.api.peripheral; -import dan200.computercraft.api.lua.ArgumentHelper; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** - * The interface that defines a peripheral. See {@link IPeripheralProvider} for how to associate blocks with peripherals. + * The interface that defines a peripheral. See {@link IPeripheralProvider} for how to associate blocks with + * peripherals. + * + * Peripherals should provide a series of methods to the user, either using {@link LuaFunction} or by implementing + * {@link IDynamicPeripheral}. */ public interface IPeripheral { @@ -26,57 +28,6 @@ public interface IPeripheral @Nonnull String getType(); - /** - * Should return an array of strings that identify the methods that this - * peripheral exposes to Lua. This will be called once before each attachment, - * and should not change when called multiple times. - * - * @return An array of strings representing method names. - * @see #callMethod - */ - @Nonnull - String[] getMethodNames(); - - /** - * This is called when a lua program on an attached computer calls {@code peripheral.call()} with - * one of the methods exposed by {@link #getMethodNames()}. - * - * Be aware that this will be called from the ComputerCraft Lua thread, and must be thread-safe when interacting - * with Minecraft objects. - * - * @param computer The interface to the computer that is making the call. Remember that multiple - * computers can be attached to a peripheral at once. - * @param context The context of the currently running lua thread. This can be used to wait for events - * or otherwise yield. - * @param method An integer identifying which of the methods from getMethodNames() the computercraft - * wishes to call. The integer indicates the index into the getMethodNames() table - * that corresponds to the string passed into peripheral.call() - * @param arguments An array of objects, representing the arguments passed into {@code peripheral.call()}.
- * Lua values of type "string" will be represented by Object type String.
- * Lua values of type "number" will be represented by Object type Double.
- * Lua values of type "boolean" will be represented by Object type Boolean.
- * Lua values of type "table" will be represented by Object type Map.
- * Lua values of any other type will be represented by a null object.
- * This array will be empty if no arguments are passed. - * - * It is recommended you use {@link ArgumentHelper} in order to validate and process arguments. - * @return An array of objects, representing values you wish to return to the lua program. Integers, Doubles, Floats, - * Strings, Booleans, Maps, ILuaObject and null be converted to their corresponding lua type. All other types will - * be converted to nil. - * - * You may return null to indicate no values should be returned. - * @throws LuaException If you throw any exception from this function, a lua error will be raised with the - * same message as your exception. Use this to throw appropriate errors if the wrong - * arguments are supplied to your method. - * @throws InterruptedException If the user shuts down or reboots the computer the coroutine is suspended, - * InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state. - * @see #getMethodNames - * @see ArgumentHelper - */ - @Nullable - Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException; - /** * Is called when when a computer is attaching to the peripheral. * diff --git a/src/main/java/dan200/computercraft/api/peripheral/NotAttachedException.java b/src/main/java/dan200/computercraft/api/peripheral/NotAttachedException.java index 08c343303..a01efa5a6 100644 --- a/src/main/java/dan200/computercraft/api/peripheral/NotAttachedException.java +++ b/src/main/java/dan200/computercraft/api/peripheral/NotAttachedException.java @@ -15,7 +15,7 @@ public class NotAttachedException extends IllegalStateException public NotAttachedException() { - super( "You are not attached to this Computer" ); + super( "You are not attached to this computer" ); } public NotAttachedException( String s ) diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java index 6e33e2871..53b176dbf 100644 --- a/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java +++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java @@ -6,8 +6,8 @@ package dan200.computercraft.api.turtle; import com.mojang.authlib.GameProfile; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.ILuaCallback; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IPeripheral; import net.minecraft.inventory.IInventory; import net.minecraft.nbt.CompoundNBT; @@ -228,21 +228,15 @@ public interface ITurtleAccess * be supplied as a parameter to a "turtle_response" event issued to the turtle after the command has completed. Look at the * lua source code for "rom/apis/turtle" for how to build a lua wrapper around this functionality. * - * @param context The Lua context to pull events from. * @param command An object which will execute the custom command when its point in the queue is reached * @return The objects the command returned when executed. you should probably return these to the player * unchanged if called from a peripheral method. * @throws UnsupportedOperationException When attempting to execute a command on the client side. - * @throws LuaException If the user presses CTRL+T to terminate the current program while {@code executeCommand()} is - * waiting for an event, a "Terminated" exception will be thrown here. - * @throws InterruptedException If the user shuts down or reboots the computer while pullEvent() is waiting for an - * event, InterruptedException will be thrown. This exception must not be caught or - * intercepted, or the computer will leak memory and end up in a broken state. * @see ITurtleCommand - * @see ILuaContext#pullEvent(String) + * @see MethodResult#pullEvent(String, ILuaCallback) */ @Nonnull - Object[] executeCommand( @Nonnull ILuaContext context, @Nonnull ITurtleCommand command ) throws LuaException, InterruptedException; + MethodResult executeCommand( @Nonnull ITurtleCommand command ); /** * Start playing a specific animation. This will prevent other turtle commands from executing until diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleCommand.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleCommand.java index 88b4b7ab5..00c17ab54 100644 --- a/src/main/java/dan200/computercraft/api/turtle/ITurtleCommand.java +++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleCommand.java @@ -5,14 +5,12 @@ */ package dan200.computercraft.api.turtle; -import dan200.computercraft.api.lua.ILuaContext; - import javax.annotation.Nonnull; /** - * An interface for objects executing custom turtle commands, used with {@link ITurtleAccess#executeCommand(ILuaContext, ITurtleCommand)}. + * An interface for objects executing custom turtle commands, used with {@link ITurtleAccess#executeCommand(ITurtleCommand)}. * - * @see ITurtleAccess#executeCommand(ILuaContext, ITurtleCommand) + * @see ITurtleAccess#executeCommand(ITurtleCommand) */ @FunctionalInterface public interface ITurtleCommand @@ -25,7 +23,7 @@ public interface ITurtleCommand * * @param turtle Access to the turtle for whom the command was issued. * @return A result, indicating whether this action succeeded or not. - * @see ITurtleAccess#executeCommand(ILuaContext, ITurtleCommand) + * @see ITurtleAccess#executeCommand(ITurtleCommand) * @see TurtleCommandResult#success() * @see TurtleCommandResult#failure(String) * @see TurtleCommandResult diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java index e20ab37b1..efd20fd36 100644 --- a/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java @@ -5,8 +5,7 @@ */ package dan200.computercraft.api.turtle.event; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.TurtleSide; @@ -223,7 +222,7 @@ public Map getData() * Add new information to the inspection result. Note this will override fields with the same name. * * @param newData The data to add. Note all values should be convertible to Lua (see - * {@link dan200.computercraft.api.peripheral.IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[])}). + * {@link MethodResult#of(Object)}). */ public void addData( @Nonnull Map newData ) { diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java index 12a1b751b..f21756a1a 100644 --- a/src/main/java/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java +++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInspectItemEvent.java @@ -5,8 +5,7 @@ */ package dan200.computercraft.api.turtle.event; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.turtle.ITurtleAccess; import net.minecraft.item.ItemStack; @@ -63,7 +62,7 @@ public Map getData() * Add new information to the inspection result. Note this will override fields with the same name. * * @param newData The data to add. Note all values should be convertible to Lua (see - * {@link dan200.computercraft.api.peripheral.IPeripheral#callMethod(IComputerAccess, ILuaContext, int, Object[])}). + * {@link MethodResult#of(Object)}). */ public void addData( @Nonnull Map newData ) { diff --git a/src/main/java/dan200/computercraft/core/apis/ComputerAccess.java b/src/main/java/dan200/computercraft/core/apis/ComputerAccess.java index 019333500..1eebc5e01 100644 --- a/src/main/java/dan200/computercraft/core/apis/ComputerAccess.java +++ b/src/main/java/dan200/computercraft/core/apis/ComputerAccess.java @@ -116,7 +116,7 @@ public int getID() } @Override - public void queueEvent( @Nonnull final String event, final Object[] arguments ) + public void queueEvent( @Nonnull String event, Object... arguments ) { Objects.requireNonNull( event, "event cannot be null" ); m_environment.queueEvent( event, arguments ); diff --git a/src/main/java/dan200/computercraft/core/apis/FSAPI.java b/src/main/java/dan200/computercraft/core/apis/FSAPI.java index 90a866a53..f9f45a243 100644 --- a/src/main/java/dan200/computercraft/core/apis/FSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/FSAPI.java @@ -6,8 +6,8 @@ package dan200.computercraft.core.apis; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.apis.handles.BinaryReadableHandle; import dan200.computercraft.core.apis.handles.BinaryWritableHandle; import dan200.computercraft.core.apis.handles.EncodedReadableHandle; @@ -17,7 +17,6 @@ import dan200.computercraft.core.filesystem.FileSystemWrapper; import dan200.computercraft.core.tracking.TrackingField; -import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.BufferedWriter; import java.nio.channels.ReadableByteChannel; @@ -29,17 +28,14 @@ import java.util.OptionalLong; import java.util.function.Function; -import static dan200.computercraft.api.lua.ArgumentHelper.getString; - public class FSAPI implements ILuaAPI { - private IAPIEnvironment m_env; - private FileSystem m_fileSystem; + private final IAPIEnvironment environment; + private FileSystem fileSystem = null; public FSAPI( IAPIEnvironment env ) { - m_env = env; - m_fileSystem = null; + environment = env; } @Override @@ -51,329 +47,280 @@ public String[] getNames() @Override public void startup() { - m_fileSystem = m_env.getFileSystem(); + fileSystem = environment.getFileSystem(); } @Override public void shutdown() { - m_fileSystem = null; + fileSystem = null; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final String[] list( String path ) throws LuaException { - return new String[] { - "list", - "combine", - "getName", - "getSize", - "exists", - "isDir", - "isReadOnly", - "makeDir", - "move", - "copy", - "delete", - "open", - "getDrive", - "getFreeSpace", - "find", - "getDir", - "getCapacity", - "attributes", - }; - } - - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException - { - switch( method ) + environment.addTrackingChange( TrackingField.FS_OPS ); + try { - case 0: + return fileSystem.list( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final String combine( String pathA, String pathB ) + { + return fileSystem.combine( pathA, pathB ); + } + + @LuaFunction + public final String getName( String path ) + { + return FileSystem.getName( path ); + } + + @LuaFunction + public final String getDir( String path ) + { + return FileSystem.getDirectory( path ); + } + + @LuaFunction + public final long getSize( String path ) throws LuaException + { + try + { + return fileSystem.getSize( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final boolean exists( String path ) + { + try + { + return fileSystem.exists( path ); + } + catch( FileSystemException e ) + { + return false; + } + } + + @LuaFunction + public final boolean isDir( String path ) + { + try + { + return fileSystem.isDir( path ); + } + catch( FileSystemException e ) + { + return false; + } + } + + @LuaFunction + public final boolean isReadOnly( String path ) + { + try + { + return fileSystem.isReadOnly( path ); + } + catch( FileSystemException e ) + { + return false; + } + } + + @LuaFunction + public final void makeDir( String path ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.makeDir( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final void move( String path, String dest ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.move( path, dest ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final void copy( String path, String dest ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.copy( path, dest ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final void delete( String path ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + fileSystem.delete( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final Object[] open( String path, String mode ) throws LuaException + { + environment.addTrackingChange( TrackingField.FS_OPS ); + try + { + switch( mode ) { - // list - String path = getString( args, 0 ); - m_env.addTrackingChange( TrackingField.FS_OPS ); - try + case "r": { - return new Object[] { m_fileSystem.list( path ) }; + // Open the file for reading, then create a wrapper around the reader + FileSystemWrapper reader = fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 ); + return new Object[] { new EncodedReadableHandle( reader.get(), reader ) }; } - catch( FileSystemException e ) + case "w": { - throw new LuaException( e.getMessage() ); + // Open the file for writing, then create a wrapper around the writer + FileSystemWrapper writer = fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 ); + return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; } + case "a": + { + // Open the file for appending, then create a wrapper around the writer + FileSystemWrapper writer = fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 ); + return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; + } + case "rb": + { + // Open the file for binary reading, then create a wrapper around the reader + FileSystemWrapper reader = fileSystem.openForRead( path, Function.identity() ); + return new Object[] { BinaryReadableHandle.of( reader.get(), reader ) }; + } + case "wb": + { + // Open the file for binary writing, then create a wrapper around the writer + FileSystemWrapper writer = fileSystem.openForWrite( path, false, Function.identity() ); + return new Object[] { BinaryWritableHandle.of( writer.get(), writer ) }; + } + case "ab": + { + // Open the file for binary appending, then create a wrapper around the reader + FileSystemWrapper writer = fileSystem.openForWrite( path, true, Function.identity() ); + return new Object[] { BinaryWritableHandle.of( writer.get(), writer ) }; + } + default: + throw new LuaException( "Unsupported mode" ); } - case 1: - { - // combine - String pathA = getString( args, 0 ); - String pathB = getString( args, 1 ); - return new Object[] { m_fileSystem.combine( pathA, pathB ) }; - } - case 2: - { - // getName - String path = getString( args, 0 ); - return new Object[] { FileSystem.getName( path ) }; - } - case 3: - { - // getSize - String path = getString( args, 0 ); - try - { - return new Object[] { m_fileSystem.getSize( path ) }; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 4: - { - // exists - String path = getString( args, 0 ); - try - { - return new Object[] { m_fileSystem.exists( path ) }; - } - catch( FileSystemException e ) - { - return new Object[] { false }; - } - } - case 5: - { - // isDir - String path = getString( args, 0 ); - try - { - return new Object[] { m_fileSystem.isDir( path ) }; - } - catch( FileSystemException e ) - { - return new Object[] { false }; - } - } - case 6: - { - // isReadOnly - String path = getString( args, 0 ); - try - { - return new Object[] { m_fileSystem.isReadOnly( path ) }; - } - catch( FileSystemException e ) - { - return new Object[] { false }; - } - } - case 7: - { - // makeDir - String path = getString( args, 0 ); - try - { - m_env.addTrackingChange( TrackingField.FS_OPS ); - m_fileSystem.makeDir( path ); - return null; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 8: - { - // move - String path = getString( args, 0 ); - String dest = getString( args, 1 ); - try - { - m_env.addTrackingChange( TrackingField.FS_OPS ); - m_fileSystem.move( path, dest ); - return null; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 9: - { - // copy - String path = getString( args, 0 ); - String dest = getString( args, 1 ); - try - { - m_env.addTrackingChange( TrackingField.FS_OPS ); - m_fileSystem.copy( path, dest ); - return null; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 10: - { - // delete - String path = getString( args, 0 ); - try - { - m_env.addTrackingChange( TrackingField.FS_OPS ); - m_fileSystem.delete( path ); - return null; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 11: - { - // open - String path = getString( args, 0 ); - String mode = getString( args, 1 ); - m_env.addTrackingChange( TrackingField.FS_OPS ); - try - { - switch( mode ) - { - case "r": - { - // Open the file for reading, then create a wrapper around the reader - FileSystemWrapper reader = m_fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 ); - return new Object[] { new EncodedReadableHandle( reader.get(), reader ) }; - } - case "w": - { - // Open the file for writing, then create a wrapper around the writer - FileSystemWrapper writer = m_fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 ); - return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; - } - case "a": - { - // Open the file for appending, then create a wrapper around the writer - FileSystemWrapper writer = m_fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 ); - return new Object[] { new EncodedWritableHandle( writer.get(), writer ) }; - } - case "rb": - { - // Open the file for binary reading, then create a wrapper around the reader - FileSystemWrapper reader = m_fileSystem.openForRead( path, Function.identity() ); - return new Object[] { new BinaryReadableHandle( reader.get(), reader ) }; - } - case "wb": - { - // Open the file for binary writing, then create a wrapper around the writer - FileSystemWrapper writer = m_fileSystem.openForWrite( path, false, Function.identity() ); - return new Object[] { new BinaryWritableHandle( writer.get(), writer ) }; - } - case "ab": - { - // Open the file for binary appending, then create a wrapper around the reader - FileSystemWrapper writer = m_fileSystem.openForWrite( path, true, Function.identity() ); - return new Object[] { new BinaryWritableHandle( writer.get(), writer ) }; - } - default: - throw new LuaException( "Unsupported mode" ); - } - } - catch( FileSystemException e ) - { - return new Object[] { null, e.getMessage() }; - } - } - case 12: - { - // getDrive - String path = getString( args, 0 ); - try - { - if( !m_fileSystem.exists( path ) ) - { - return null; - } - return new Object[] { m_fileSystem.getMountLabel( path ) }; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 13: - { - // getFreeSpace - String path = getString( args, 0 ); - try - { - long freeSpace = m_fileSystem.getFreeSpace( path ); - if( freeSpace >= 0 ) - { - return new Object[] { freeSpace }; - } - return new Object[] { "unlimited" }; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 14: // find - { - String path = getString( args, 0 ); - try - { - m_env.addTrackingChange( TrackingField.FS_OPS ); - return new Object[] { m_fileSystem.find( path ) }; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 15: // getDir - { - String path = getString( args, 0 ); - return new Object[] { FileSystem.getDirectory( path ) }; - } - case 16: // getCapacity - { - String path = getString( args, 0 ); - try - { - OptionalLong capacity = m_fileSystem.getCapacity( path ); - return new Object[] { capacity.isPresent() ? capacity.getAsLong() : null }; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 17: // attributes - { - String path = getString( args, 0 ); - try - { - BasicFileAttributes attributes = m_fileSystem.getAttributes( path ); - Map result = new HashMap<>(); - result.put( "modification", getFileTime( attributes.lastModifiedTime() ) ); - result.put( "created", getFileTime( attributes.creationTime() ) ); - result.put( "size", attributes.isDirectory() ? 0 : attributes.size() ); - result.put( "isDir", attributes.isDirectory() ); - return new Object[] { result }; - } - catch( FileSystemException e ) - { - throw new LuaException( e.getMessage() ); - } - } - default: - assert false; - return null; + } + catch( FileSystemException e ) + { + return new Object[] { null, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] getDrive( String path ) throws LuaException + { + try + { + return fileSystem.exists( path ) ? new Object[] { fileSystem.getMountLabel( path ) } : null; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final Object getFreeSpace( String path ) throws LuaException + { + try + { + long freeSpace = fileSystem.getFreeSpace( path ); + return freeSpace >= 0 ? freeSpace : "unlimited"; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final String[] find( String path ) throws LuaException + { + try + { + environment.addTrackingChange( TrackingField.FS_OPS ); + return fileSystem.find( path ); + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final Object getCapacity( String path ) throws LuaException + { + try + { + OptionalLong capacity = fileSystem.getCapacity( path ); + return capacity.isPresent() ? capacity.getAsLong() : null; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final Map attributes( String path ) throws LuaException + { + try + { + BasicFileAttributes attributes = fileSystem.getAttributes( path ); + Map result = new HashMap<>(); + result.put( "modification", getFileTime( attributes.lastModifiedTime() ) ); + result.put( "created", getFileTime( attributes.creationTime() ) ); + result.put( "size", attributes.isDirectory() ? 0 : attributes.size() ); + result.put( "isDir", attributes.isDirectory() ); + return result; + } + catch( FileSystemException e ) + { + throw new LuaException( e.getMessage() ); } } diff --git a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java index 375135ffd..eac31b9f8 100644 --- a/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/HTTPAPI.java @@ -6,9 +6,10 @@ package dan200.computercraft.core.apis; import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.apis.http.*; import dan200.computercraft.core.apis.http.request.HttpRequest; import dan200.computercraft.core.apis.http.websocket.Websocket; @@ -21,8 +22,8 @@ import java.util.Collections; import java.util.Locale; import java.util.Map; +import java.util.Optional; -import static dan200.computercraft.api.lua.ArgumentHelper.*; import static dan200.computercraft.core.apis.TableHelper.*; public class HTTPAPI implements ILuaAPI @@ -68,135 +69,112 @@ public void update() Resource.cleanup(); } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final Object[] request( IArguments args ) throws LuaException { - return new String[] { - "request", - "checkURL", - "websocket", - }; + String address, postString, requestMethod; + Map headerTable; + boolean binary, redirect; + + if( args.get( 0 ) instanceof Map ) + { + Map options = args.getTable( 0 ); + address = getStringField( options, "url" ); + postString = optStringField( options, "body", null ); + headerTable = optTableField( options, "headers", Collections.emptyMap() ); + binary = optBooleanField( options, "binary", false ); + requestMethod = optStringField( options, "method", null ); + redirect = optBooleanField( options, "redirect", true ); + + } + else + { + // Get URL and post information + address = args.getString( 0 ); + postString = args.optString( 1, null ); + headerTable = args.optTable( 2, Collections.emptyMap() ); + binary = args.optBoolean( 3, false ); + requestMethod = null; + redirect = true; + } + + HttpHeaders headers = getHeaders( headerTable ); + + HttpMethod httpMethod; + if( requestMethod == null ) + { + httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; + } + else + { + httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) ); + if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) ) + { + throw new LuaException( "Unsupported HTTP method" ); + } + } + + try + { + URI uri = HttpRequest.checkUri( address ); + HttpRequest request = new HttpRequest( requests, m_apiEnvironment, address, postString, headers, binary, redirect ); + + long requestBody = request.body().readableBytes() + HttpRequest.getHeaderSize( headers ); + if( ComputerCraft.httpMaxUpload != 0 && requestBody > ComputerCraft.httpMaxUpload ) + { + throw new HTTPRequestException( "Request body is too large" ); + } + + // Make the request + request.queue( r -> r.request( uri, httpMethod ) ); + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } } - @Override - @SuppressWarnings( "resource" ) - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + @LuaFunction + public final Object[] checkURL( String address ) { - switch( method ) + try { - case 0: // request + URI uri = HttpRequest.checkUri( address ); + new CheckUrl( checkUrls, m_apiEnvironment, address, uri ).queue( CheckUrl::run ); + + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; + } + } + + @LuaFunction + public final Object[] websocket( String address, Optional> headerTbl ) throws LuaException + { + if( !ComputerCraft.httpWebsocketEnabled ) + { + throw new LuaException( "Websocket connections are disabled" ); + } + + HttpHeaders headers = getHeaders( headerTbl.orElse( Collections.emptyMap() ) ); + + try + { + URI uri = Websocket.checkUri( address ); + if( !new Websocket( websockets, m_apiEnvironment, uri, address, headers ).queue( Websocket::connect ) ) { - String address, postString, requestMethod; - Map headerTable; - boolean binary, redirect; - - if( args.length >= 1 && args[0] instanceof Map ) - { - Map options = (Map) args[0]; - address = getStringField( options, "url" ); - postString = optStringField( options, "body", null ); - headerTable = optTableField( options, "headers", Collections.emptyMap() ); - binary = optBooleanField( options, "binary", false ); - requestMethod = optStringField( options, "method", null ); - redirect = optBooleanField( options, "redirect", true ); - - } - else - { - // Get URL and post information - address = getString( args, 0 ); - postString = optString( args, 1, null ); - headerTable = optTable( args, 2, Collections.emptyMap() ); - binary = optBoolean( args, 3, false ); - requestMethod = null; - redirect = true; - } - - HttpHeaders headers = getHeaders( headerTable ); - - - HttpMethod httpMethod; - if( requestMethod == null ) - { - httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST; - } - else - { - httpMethod = HttpMethod.valueOf( requestMethod.toUpperCase( Locale.ROOT ) ); - if( httpMethod == null || requestMethod.equalsIgnoreCase( "CONNECT" ) ) - { - throw new LuaException( "Unsupported HTTP method" ); - } - } - - try - { - URI uri = HttpRequest.checkUri( address ); - HttpRequest request = new HttpRequest( requests, m_apiEnvironment, address, postString, headers, binary, redirect ); - - long requestBody = request.body().readableBytes() + HttpRequest.getHeaderSize( headers ); - if( ComputerCraft.httpMaxUpload != 0 && requestBody > ComputerCraft.httpMaxUpload ) - { - throw new HTTPRequestException( "Request body is too large" ); - } - - // Make the request - request.queue( r -> r.request( uri, httpMethod ) ); - - return new Object[] { true }; - } - catch( HTTPRequestException e ) - { - return new Object[] { false, e.getMessage() }; - } + throw new LuaException( "Too many websockets already open" ); } - case 1: // checkURL - { - String address = getString( args, 0 ); - // Check URL - try - { - URI uri = HttpRequest.checkUri( address ); - new CheckUrl( checkUrls, m_apiEnvironment, address, uri ).queue( CheckUrl::run ); - - return new Object[] { true }; - } - catch( HTTPRequestException e ) - { - return new Object[] { false, e.getMessage() }; - } - } - case 2: // websocket - { - String address = getString( args, 0 ); - Map headerTbl = optTable( args, 1, Collections.emptyMap() ); - - if( !ComputerCraft.httpWebsocketEnabled ) - { - throw new LuaException( "Websocket connections are disabled" ); - } - - HttpHeaders headers = getHeaders( headerTbl ); - - try - { - URI uri = Websocket.checkUri( address ); - if( !new Websocket( websockets, m_apiEnvironment, uri, address, headers ).queue( Websocket::connect ) ) - { - throw new LuaException( "Too many websockets already open" ); - } - - return new Object[] { true }; - } - catch( HTTPRequestException e ) - { - return new Object[] { false, e.getMessage() }; - } - } - default: - return null; + return new Object[] { true }; + } + catch( HTTPRequestException e ) + { + return new Object[] { false, e.getMessage() }; } } diff --git a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java index 0a4315960..19e2d2f14 100644 --- a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java +++ b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -43,7 +43,7 @@ interface IPeripheralChangeListener void reboot(); - void queueEvent( String event, Object[] args ); + void queueEvent( String event, Object... args ); void setOutput( ComputerSide side, int output ); diff --git a/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/src/main/java/dan200/computercraft/core/apis/OSAPI.java index 825ad05fa..1b63323ea 100644 --- a/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -5,9 +5,10 @@ */ package dan200.computercraft.core.apis; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.shared.util.StringUtil; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -20,11 +21,11 @@ import java.time.format.DateTimeFormatterBuilder; import java.util.*; -import static dan200.computercraft.api.lua.ArgumentHelper.*; +import static dan200.computercraft.api.lua.LuaValues.checkFinite; public class OSAPI implements ILuaAPI { - private IAPIEnvironment m_apiEnvironment; + private final IAPIEnvironment apiEnvironment; private final Int2ObjectMap m_alarms = new Int2ObjectOpenHashMap<>(); private int m_clock; @@ -55,11 +56,9 @@ public int compareTo( @Nonnull Alarm o ) public OSAPI( IAPIEnvironment environment ) { - m_apiEnvironment = environment; + apiEnvironment = environment; } - // ILuaAPI implementation - @Override public String[] getNames() { @@ -69,8 +68,8 @@ public String[] getNames() @Override public void startup() { - m_time = m_apiEnvironment.getComputerEnvironment().getTimeOfDay(); - m_day = m_apiEnvironment.getComputerEnvironment().getDay(); + m_time = apiEnvironment.getComputerEnvironment().getTimeOfDay(); + m_day = apiEnvironment.getComputerEnvironment().getDay(); m_clock = 0; synchronized( m_alarms ) @@ -89,8 +88,8 @@ public void update() { double previousTime = m_time; int previousDay = m_day; - double time = m_apiEnvironment.getComputerEnvironment().getTimeOfDay(); - int day = m_apiEnvironment.getComputerEnvironment().getDay(); + double time = apiEnvironment.getComputerEnvironment().getTimeOfDay(); + int day = apiEnvironment.getComputerEnvironment().getDay(); if( time > previousTime || day > previousDay ) { @@ -103,7 +102,7 @@ public void update() double t = alarm.m_day * 24.0 + alarm.m_time; if( now >= t ) { - queueLuaEvent( "alarm", new Object[] { entry.getIntKey() } ); + apiEnvironment.queueEvent( "alarm", entry.getIntKey() ); it.remove(); } } @@ -123,31 +122,6 @@ public void shutdown() } } - @Nonnull - @Override - public String[] getMethodNames() - { - return new String[] { - "queueEvent", - "startTimer", - "setAlarm", - "shutdown", - "reboot", - "computerID", - "getComputerID", - "setComputerLabel", - "computerLabel", - "getComputerLabel", - "clock", - "time", - "day", - "cancelTimer", - "cancelAlarm", - "epoch", - "date", - }; - } - private static float getTimeForCalendar( Calendar c ) { float time = c.get( Calendar.HOUR_OF_DAY ); @@ -174,214 +148,174 @@ private static long getEpochForCalendar( Calendar c ) return c.getTime().getTime(); } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + @LuaFunction + public final void queueEvent( String name, IArguments args ) { - switch( method ) + apiEnvironment.queueEvent( name, args.drop( 1 ).getAll() ); + } + + @LuaFunction + public final int startTimer( double timer ) throws LuaException + { + return apiEnvironment.startTimer( Math.round( checkFinite( 0, timer ) / 0.05 ) ); + } + + @LuaFunction + public final void cancelTimer( int token ) + { + apiEnvironment.cancelTimer( token ); + } + + @LuaFunction + public final int setAlarm( double time ) throws LuaException + { + checkFinite( 0, time ); + if( time < 0.0 || time >= 24.0 ) throw new LuaException( "Number out of range" ); + synchronized( m_alarms ) { - case 0: // queueEvent - queueLuaEvent( getString( args, 0 ), trimArray( args, 1 ) ); - return null; - case 1: - { - // startTimer - double timer = getFiniteDouble( args, 0 ); - int id = m_apiEnvironment.startTimer( Math.round( timer / 0.05 ) ); - return new Object[] { id }; - } - case 2: - { - // setAlarm - double time = getFiniteDouble( args, 0 ); - if( time < 0.0 || time >= 24.0 ) - { - throw new LuaException( "Number out of range" ); - } - synchronized( m_alarms ) - { - int day = time > m_time ? m_day : m_day + 1; - m_alarms.put( m_nextAlarmToken, new Alarm( time, day ) ); - return new Object[] { m_nextAlarmToken++ }; - } - } - case 3: // shutdown - m_apiEnvironment.shutdown(); - return null; - case 4: // reboot - m_apiEnvironment.reboot(); - return null; - case 5: - case 6: // computerID/getComputerID - return new Object[] { getComputerID() }; - case 7: - { - // setComputerLabel - String label = optString( args, 0, null ); - m_apiEnvironment.setLabel( StringUtil.normaliseLabel( label ) ); - return null; - } - case 8: - case 9: - { - // computerLabel/getComputerLabel - String label = m_apiEnvironment.getLabel(); - if( label != null ) - { - return new Object[] { label }; - } - return null; - } - case 10: // clock - return new Object[] { m_clock * 0.05 }; - case 11: - { - // time - Object value = args.length > 0 ? args[0] : null; - if( value instanceof Map ) return new Object[] { LuaDateTime.fromTable( (Map) value ) }; - - String param = optString( args, 0, "ingame" ); - switch( param.toLowerCase( Locale.ROOT ) ) - { - case "utc": - { - // Get Hour of day (UTC) - Calendar c = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); - return new Object[] { getTimeForCalendar( c ) }; - } - case "local": - { - // Get Hour of day (local time) - Calendar c = Calendar.getInstance(); - return new Object[] { getTimeForCalendar( c ) }; - } - case "ingame": - // Get ingame hour - synchronized( m_alarms ) - { - return new Object[] { m_time }; - } - default: - throw new LuaException( "Unsupported operation" ); - } - } - case 12: - { - // day - String param = optString( args, 0, "ingame" ); - switch( param.toLowerCase( Locale.ROOT ) ) - { - case "utc": - { - // Get numbers of days since 1970-01-01 (utc) - Calendar c = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); - return new Object[] { getDayForCalendar( c ) }; - } - case "local": - { - // Get numbers of days since 1970-01-01 (local time) - Calendar c = Calendar.getInstance(); - return new Object[] { getDayForCalendar( c ) }; - } - case "ingame": - // Get game day - synchronized( m_alarms ) - { - return new Object[] { m_day }; - } - default: - throw new LuaException( "Unsupported operation" ); - } - } - case 13: - { - // cancelTimer - int token = getInt( args, 0 ); - m_apiEnvironment.cancelTimer( token ); - return null; - } - case 14: - { - // cancelAlarm - int token = getInt( args, 0 ); - synchronized( m_alarms ) - { - m_alarms.remove( token ); - } - return null; - } - case 15: // epoch - { - String param = optString( args, 0, "ingame" ); - switch( param.toLowerCase( Locale.ROOT ) ) - { - case "utc": - { - // Get utc epoch - Calendar c = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); - return new Object[] { getEpochForCalendar( c ) }; - } - case "local": - { - // Get local epoch - Calendar c = Calendar.getInstance(); - return new Object[] { getEpochForCalendar( c ) }; - } - case "ingame": - // Get in-game epoch - synchronized( m_alarms ) - { - return new Object[] { m_day * 86400000 + (int) (m_time * 3600000.0f) }; - } - default: - throw new LuaException( "Unsupported operation" ); - } - } - case 16: // date - { - String format = optString( args, 0, "%c" ); - long time = optLong( args, 1, Instant.now().getEpochSecond() ); - - Instant instant = Instant.ofEpochSecond( time ); - ZonedDateTime date; - ZoneOffset offset; - if( format.startsWith( "!" ) ) - { - offset = ZoneOffset.UTC; - date = ZonedDateTime.ofInstant( instant, offset ); - format = format.substring( 1 ); - } - else - { - ZoneId id = ZoneId.systemDefault(); - offset = id.getRules().getOffset( instant ); - date = ZonedDateTime.ofInstant( instant, id ); - } - - if( format.equals( "*t" ) ) return new Object[] { LuaDateTime.toTable( date, offset, instant ) }; - - DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder(); - LuaDateTime.format( formatter, format, offset ); - return new Object[] { formatter.toFormatter( Locale.ROOT ).format( date ) }; - } - default: - return null; + int day = time > m_time ? m_day : m_day + 1; + m_alarms.put( m_nextAlarmToken, new Alarm( time, day ) ); + return m_nextAlarmToken++; } } - // Private methods - - private void queueLuaEvent( String event, Object[] args ) + @LuaFunction + public final void cancelAlarm( int token ) { - m_apiEnvironment.queueEvent( event, args ); + synchronized( m_alarms ) + { + m_alarms.remove( token ); + } } - private Object[] trimArray( Object[] array, int skip ) + @LuaFunction( "shutdown" ) + public final void doShutdown() { - return Arrays.copyOfRange( array, skip, array.length ); + apiEnvironment.shutdown(); } - private int getComputerID() + @LuaFunction( "reboot" ) + public final void doReboot() { - return m_apiEnvironment.getComputerID(); + apiEnvironment.reboot(); } + + @LuaFunction( { "getComputerID", "computerID" } ) + public final int getComputerID() + { + return apiEnvironment.getComputerID(); + } + + @LuaFunction( { "getComputerLabel", "computerLabel" } ) + public final Object[] getComputerLabel() + { + String label = apiEnvironment.getLabel(); + return label == null ? null : new Object[] { label }; + } + + @LuaFunction + public final void setComputerLabel( Optional label ) + { + apiEnvironment.setLabel( StringUtil.normaliseLabel( label.orElse( null ) ) ); + } + + @LuaFunction + public final double clock() + { + return m_clock * 0.05; + } + + @LuaFunction + public final Object time( IArguments args ) throws LuaException + { + Object value = args.get( 0 ); + if( value instanceof Map ) return LuaDateTime.fromTable( (Map) value ); + + String param = args.optString( 0, "ingame" ); + switch( param.toLowerCase( Locale.ROOT ) ) + { + case "utc": // Get Hour of day (UTC) + return getTimeForCalendar( Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ) ); + case "local": // Get Hour of day (local time) + return getTimeForCalendar( Calendar.getInstance() ); + case "ingame": // Get in-game hour + return m_time; + default: + throw new LuaException( "Unsupported operation" ); + } + } + + @LuaFunction + public final int day( Optional args ) throws LuaException + { + switch( args.orElse( "ingame" ).toLowerCase( Locale.ROOT ) ) + { + case "utc": // Get numbers of days since 1970-01-01 (utc) + return getDayForCalendar( Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ) ); + case "local": // Get numbers of days since 1970-01-01 (local time) + return getDayForCalendar( Calendar.getInstance() ); + case "ingame":// Get game day + return m_day; + default: + throw new LuaException( "Unsupported operation" ); + } + } + + @LuaFunction + public final long epoch( Optional args ) throws LuaException + { + switch( args.orElse( "ingame" ).toLowerCase( Locale.ROOT ) ) + { + case "utc": + { + // Get utc epoch + Calendar c = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) ); + return getEpochForCalendar( c ); + } + case "local": + { + // Get local epoch + Calendar c = Calendar.getInstance(); + return getEpochForCalendar( c ); + } + case "ingame": + // Get in-game epoch + synchronized( m_alarms ) + { + return m_day * 86400000 + (int) (m_time * 3600000.0f); + } + default: + throw new LuaException( "Unsupported operation" ); + } + } + + @LuaFunction + public final Object date( Optional formatA, Optional timeA ) throws LuaException + { + String format = formatA.orElse( "%c" ); + long time = timeA.orElseGet( () -> Instant.now().getEpochSecond() ); + + Instant instant = Instant.ofEpochSecond( time ); + ZonedDateTime date; + ZoneOffset offset; + if( format.startsWith( "!" ) ) + { + offset = ZoneOffset.UTC; + date = ZonedDateTime.ofInstant( instant, offset ); + format = format.substring( 1 ); + } + else + { + ZoneId id = ZoneId.systemDefault(); + offset = id.getRules().getOffset( instant ); + date = ZonedDateTime.ofInstant( instant, id ); + } + + if( format.equals( "*t" ) ) return LuaDateTime.toTable( date, offset, instant ); + + DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder(); + LuaDateTime.format( formatter, format, offset ); + return formatter.toFormatter( Locale.ROOT ).format( date ); + } + } diff --git a/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java index 2d07d58bc..ee37d3ba4 100644 --- a/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java @@ -7,88 +7,74 @@ import dan200.computercraft.api.filesystem.IMount; import dan200.computercraft.api.filesystem.IWritableMount; -import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IWorkMonitor; import dan200.computercraft.api.peripheral.NotAttachedException; +import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.asm.NamedMethod; +import dan200.computercraft.core.asm.PeripheralMethod; import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.tracking.TrackingField; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import static dan200.computercraft.api.lua.ArgumentHelper.getString; +import java.util.*; public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener { private class PeripheralWrapper extends ComputerAccess { - private final String m_side; - private final IPeripheral m_peripheral; + private final String side; + private final IPeripheral peripheral; - private String m_type; - private String[] m_methods; - private Map m_methodMap; - private boolean m_attached; + private final String type; + private final Map methodMap; + private boolean attached; PeripheralWrapper( IPeripheral peripheral, String side ) { - super( m_environment ); - m_side = side; - m_peripheral = peripheral; - m_attached = false; + super( environment ); + this.side = side; + this.peripheral = peripheral; + attached = false; - m_type = peripheral.getType(); - m_methods = peripheral.getMethodNames(); - assert m_type != null; - assert m_methods != null; + type = Objects.requireNonNull( peripheral.getType(), "Peripheral type cannot be null" ); - m_methodMap = new HashMap<>(); - for( int i = 0; i < m_methods.length; i++ ) - { - if( m_methods[i] != null ) - { - m_methodMap.put( m_methods[i], i ); - } - } + methodMap = PeripheralAPI.getMethods( peripheral ); } public IPeripheral getPeripheral() { - return m_peripheral; + return peripheral; } public String getType() { - return m_type; + return type; } - public String[] getMethods() + public Collection getMethods() { - return m_methods; + return methodMap.keySet(); } public synchronized boolean isAttached() { - return m_attached; + return attached; } public synchronized void attach() { - m_attached = true; - m_peripheral.attach( this ); + attached = true; + peripheral.attach( this ); } public void detach() { // Call detach - m_peripheral.detach( this ); + peripheral.detach( this ); synchronized( this ) { @@ -96,63 +82,56 @@ public void detach() unmountAll(); } - m_attached = false; + attached = false; } - public Object[] call( ILuaContext context, String methodName, Object[] arguments ) throws LuaException, InterruptedException + public MethodResult call( ILuaContext context, String methodName, IArguments arguments ) throws LuaException { - int method = -1; + PeripheralMethod method; synchronized( this ) { - if( m_methodMap.containsKey( methodName ) ) - { - method = m_methodMap.get( methodName ); - } - } - if( method >= 0 ) - { - m_environment.addTrackingChange( TrackingField.PERIPHERAL_OPS ); - return m_peripheral.callMethod( this, context, method, arguments ); - } - else - { - throw new LuaException( "No such method " + methodName ); + method = methodMap.get( methodName ); } + + if( method == null ) throw new LuaException( "No such method " + methodName ); + + environment.addTrackingChange( TrackingField.PERIPHERAL_OPS ); + return method.apply( peripheral, context, this, arguments ); } // IComputerAccess implementation @Override public synchronized String mount( @Nonnull String desiredLoc, @Nonnull IMount mount, @Nonnull String driveName ) { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); return super.mount( desiredLoc, mount, driveName ); } @Override public synchronized String mountWritable( @Nonnull String desiredLoc, @Nonnull IWritableMount mount, @Nonnull String driveName ) { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); return super.mountWritable( desiredLoc, mount, driveName ); } @Override public synchronized void unmount( String location ) { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); super.unmount( location ); } @Override public int getID() { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); return super.getID(); } @Override - public void queueEvent( @Nonnull final String event, final Object[] arguments ) + public void queueEvent( @Nonnull String event, Object... arguments ) { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); super.queueEvent( event, arguments ); } @@ -160,18 +139,18 @@ public void queueEvent( @Nonnull final String event, final Object[] arguments ) @Override public String getAttachmentName() { - if( !m_attached ) throw new NotAttachedException(); - return m_side; + if( !attached ) throw new NotAttachedException(); + return side; } @Nonnull @Override public Map getAvailablePeripherals() { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); Map peripherals = new HashMap<>(); - for( PeripheralWrapper wrapper : m_peripherals ) + for( PeripheralWrapper wrapper : PeripheralAPI.this.peripherals ) { if( wrapper != null && wrapper.isAttached() ) { @@ -186,9 +165,9 @@ public Map getAvailablePeripherals() @Override public IPeripheral getAvailablePeripheral( @Nonnull String name ) { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); - for( PeripheralWrapper wrapper : m_peripherals ) + for( PeripheralWrapper wrapper : peripherals ) { if( wrapper != null && wrapper.isAttached() && wrapper.getAttachmentName().equals( name ) ) { @@ -202,27 +181,20 @@ public IPeripheral getAvailablePeripheral( @Nonnull String name ) @Override public IWorkMonitor getMainThreadMonitor() { - if( !m_attached ) throw new NotAttachedException(); + if( !attached ) throw new NotAttachedException(); return super.getMainThreadMonitor(); } } - private final IAPIEnvironment m_environment; - private final PeripheralWrapper[] m_peripherals; - private boolean m_running; + private final IAPIEnvironment environment; + private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6]; + private boolean running; public PeripheralAPI( IAPIEnvironment environment ) { - m_environment = environment; - m_environment.setPeripheralChangeListener( this ); - - m_peripherals = new PeripheralWrapper[6]; - for( int i = 0; i < 6; i++ ) - { - m_peripherals[i] = null; - } - - m_running = false; + this.environment = environment; + this.environment.setPeripheralChangeListener( this ); + running = false; } // IPeripheralChangeListener @@ -230,37 +202,35 @@ public PeripheralAPI( IAPIEnvironment environment ) @Override public void onPeripheralChanged( ComputerSide side, IPeripheral newPeripheral ) { - synchronized( m_peripherals ) + synchronized( peripherals ) { int index = side.ordinal(); - if( m_peripherals[index] != null ) + if( peripherals[index] != null ) { // Queue a detachment - final PeripheralWrapper wrapper = m_peripherals[index]; + final PeripheralWrapper wrapper = peripherals[index]; if( wrapper.isAttached() ) wrapper.detach(); // Queue a detachment event - m_environment.queueEvent( "peripheral_detach", new Object[] { side.getName() } ); + environment.queueEvent( "peripheral_detach", side.getName() ); } // Assign the new peripheral - m_peripherals[index] = newPeripheral == null ? null + peripherals[index] = newPeripheral == null ? null : new PeripheralWrapper( newPeripheral, side.getName() ); - if( m_peripherals[index] != null ) + if( peripherals[index] != null ) { // Queue an attachment - final PeripheralWrapper wrapper = m_peripherals[index]; - if( m_running && !wrapper.isAttached() ) wrapper.attach(); + final PeripheralWrapper wrapper = peripherals[index]; + if( running && !wrapper.isAttached() ) wrapper.attach(); // Queue an attachment event - m_environment.queueEvent( "peripheral", new Object[] { side.getName() } ); + environment.queueEvent( "peripheral", side.getName() ); } } } - // ILuaAPI implementation - @Override public String[] getNames() { @@ -270,12 +240,12 @@ public String[] getNames() @Override public void startup() { - synchronized( m_peripherals ) + synchronized( peripherals ) { - m_running = true; + running = true; for( int i = 0; i < 6; i++ ) { - PeripheralWrapper wrapper = m_peripherals[i]; + PeripheralWrapper wrapper = peripherals[i]; if( wrapper != null && !wrapper.isAttached() ) wrapper.attach(); } } @@ -284,12 +254,12 @@ public void startup() @Override public void shutdown() { - synchronized( m_peripherals ) + synchronized( peripherals ) { - m_running = false; + running = false; for( int i = 0; i < 6; i++ ) { - PeripheralWrapper wrapper = m_peripherals[i]; + PeripheralWrapper wrapper = peripherals[i]; if( wrapper != null && wrapper.isAttached() ) { wrapper.detach(); @@ -298,98 +268,95 @@ public void shutdown() } } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final boolean isPresent( String sideName ) { - return new String[] { - "isPresent", - "getType", - "getMethods", - "call", - }; + ComputerSide side = ComputerSide.valueOfInsensitive( sideName ); + if( side != null ) + { + synchronized( peripherals ) + { + PeripheralWrapper p = peripherals[side.ordinal()]; + if( p != null ) return true; + } + } + return false; } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException + @LuaFunction + public final Object[] getType( String sideName ) { - switch( method ) + ComputerSide side = ComputerSide.valueOfInsensitive( sideName ); + if( side == null ) return null; + + synchronized( peripherals ) { - case 0: - { - // isPresent - boolean present = false; - ComputerSide side = ComputerSide.valueOfInsensitive( getString( args, 0 ) ); - if( side != null ) - { - synchronized( m_peripherals ) - { - PeripheralWrapper p = m_peripherals[side.ordinal()]; - if( p != null ) present = true; - } - } - return new Object[] { present }; - } - case 1: - { - // getType - ComputerSide side = ComputerSide.valueOfInsensitive( getString( args, 0 ) ); - if( side != null ) - { - synchronized( m_peripherals ) - { - PeripheralWrapper p = m_peripherals[side.ordinal()]; - if( p != null ) return new Object[] { p.getType() }; - } - } - return null; - } - case 2: - { - // getMethods - ComputerSide side = ComputerSide.valueOfInsensitive( getString( args, 0 ) ); - if( side != null ) - { - synchronized( m_peripherals ) - { - PeripheralWrapper p = m_peripherals[side.ordinal()]; - if( p != null ) return new Object[] { p.getMethods() }; - } - } + PeripheralWrapper p = peripherals[side.ordinal()]; + if( p != null ) return new Object[] { p.getType() }; + } + return null; + } - return null; - } - case 3: - { - // call - ComputerSide side = ComputerSide.valueOfInsensitive( getString( args, 0 ) ); - String methodName = getString( args, 1 ); - Object[] methodArgs = Arrays.copyOfRange( args, 2, args.length ); + @LuaFunction + public final Object[] getMethods( String sideName ) + { + ComputerSide side = ComputerSide.valueOfInsensitive( sideName ); + if( side == null ) return null; - if( side == null ) throw new LuaException( "No peripheral attached" ); + synchronized( peripherals ) + { + PeripheralWrapper p = peripherals[side.ordinal()]; + if( p != null ) return new Object[] { p.getMethods() }; + } + return null; + } - PeripheralWrapper p; - synchronized( m_peripherals ) - { - p = m_peripherals[side.ordinal()]; - } - if( p == null ) throw new LuaException( "No peripheral attached" ); + @LuaFunction + public final MethodResult call( ILuaContext context, IArguments args ) throws LuaException + { + ComputerSide side = ComputerSide.valueOfInsensitive( args.getString( 0 ) ); + String methodName = args.getString( 1 ); + IArguments methodArgs = args.drop( 2 ); - try - { - return p.call( context, methodName, methodArgs ); - } - 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; - } - } - default: - return null; + 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; } } + public static Map getMethods( IPeripheral peripheral ) + { + String[] dynamicMethods = peripheral instanceof IDynamicPeripheral + ? Objects.requireNonNull( ((IDynamicPeripheral) peripheral).getMethodNames(), "Peripheral methods cannot be null" ) + : LuaMethod.EMPTY_METHODS; + + List> methods = PeripheralMethod.GENERATOR.getMethods( peripheral.getClass() ); + + Map methodMap = new HashMap<>( methods.size() + dynamicMethods.length ); + for( int i = 0; i < dynamicMethods.length; i++ ) + { + methodMap.put( dynamicMethods[i], PeripheralMethod.DYNAMIC.get( i ) ); + } + for( NamedMethod method : methods ) + { + methodMap.put( method.getName(), method.getMethod() ); + } + return methodMap; + } } diff --git a/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java index fbb8feffa..ce70fc5d2 100644 --- a/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/RedstoneAPI.java @@ -6,14 +6,10 @@ package dan200.computercraft.core.apis; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.computer.ComputerSide; -import javax.annotation.Nonnull; - -import static dan200.computercraft.api.lua.ArgumentHelper.*; - public class RedstoneAPI implements ILuaAPI { private final IAPIEnvironment environment; @@ -29,95 +25,71 @@ public String[] getNames() return new String[] { "rs", "redstone" }; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final String[] getSides() { - return new String[] { - "getSides", - "setOutput", - "getOutput", - "getInput", - "setBundledOutput", - "getBundledOutput", - "getBundledInput", - "testBundledInput", - "setAnalogOutput", - "setAnalogueOutput", - "getAnalogOutput", - "getAnalogueOutput", - "getAnalogInput", - "getAnalogueInput", - }; + return ComputerSide.NAMES; } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + @LuaFunction + public final void setOutput( ComputerSide side, boolean output ) { - switch( method ) - { - case 0: // getSides - return new Object[] { ComputerSide.NAMES }; - case 1: - { - // setOutput - ComputerSide side = parseSide( args ); - boolean output = getBoolean( args, 1 ); - environment.setOutput( side, output ? 15 : 0 ); - return null; - } - case 2: // getOutput - return new Object[] { environment.getOutput( parseSide( args ) ) > 0 }; - case 3: // getInput - return new Object[] { environment.getInput( parseSide( args ) ) > 0 }; - case 4: - { - // setBundledOutput - ComputerSide side = parseSide( args ); - int output = getInt( args, 1 ); - environment.setBundledOutput( side, output ); - return null; - } - case 5: // getBundledOutput - return new Object[] { environment.getBundledOutput( parseSide( args ) ) }; - case 6: // getBundledInput - return new Object[] { environment.getBundledInput( parseSide( args ) ) }; - case 7: - { - // testBundledInput - ComputerSide side = parseSide( args ); - int mask = getInt( args, 1 ); - int input = environment.getBundledInput( side ); - return new Object[] { (input & mask) == mask }; - } - case 8: - case 9: - { - // setAnalogOutput/setAnalogueOutput - ComputerSide side = parseSide( args ); - int output = getInt( args, 1 ); - if( output < 0 || output > 15 ) - { - throw new LuaException( "Expected number in range 0-15" ); - } - environment.setOutput( side, output ); - return null; - } - case 10: - case 11: // getAnalogOutput/getAnalogueOutput - return new Object[] { environment.getOutput( parseSide( args ) ) }; - case 12: - case 13: // getAnalogInput/getAnalogueInput - return new Object[] { environment.getInput( parseSide( args ) ) }; - default: - return null; - } + environment.setOutput( side, output ? 15 : 0 ); } - private static ComputerSide parseSide( Object[] args ) throws LuaException + @LuaFunction + public final boolean getOutput( ComputerSide side ) { - ComputerSide side = ComputerSide.valueOfInsensitive( getString( args, 0 ) ); - if( side == null ) throw new LuaException( "Invalid side." ); - return side; + return environment.getOutput( side ) > 0; + } + + @LuaFunction + public final boolean getInput( ComputerSide side ) + { + return environment.getInput( side ) > 0; + } + + @LuaFunction( { "setAnalogOutput", "setAnalogueOutput" } ) + public final void setAnalogOutput( ComputerSide side, int output ) throws LuaException + { + if( output < 0 || output > 15 ) throw new LuaException( "Expected number in range 0-15" ); + environment.setOutput( side, output ); + } + + @LuaFunction( { "getAnalogOutput", "getAnalogueOutput" } ) + public final int getAnalogOutput( ComputerSide side ) + { + return environment.getOutput( side ); + } + + @LuaFunction( { "getAnalogInput", "getAnalogueInput" } ) + public final int getAnalogInput( ComputerSide side ) + { + return environment.getInput( side ); + } + + @LuaFunction + public final void setBundledOutput( ComputerSide side, int output ) + { + environment.setBundledOutput( side, output ); + } + + @LuaFunction + public final int getBundledOutput( ComputerSide side ) + { + return environment.getBundledOutput( side ); + } + + @LuaFunction + public final int getBundledInput( ComputerSide side ) + { + return environment.getBundledOutput( side ); + } + + @LuaFunction + public final boolean testBundledInput( ComputerSide side, int mask ) + { + int input = environment.getBundledInput( side ); + return (input & mask) == mask; } } diff --git a/src/main/java/dan200/computercraft/core/apis/TableHelper.java b/src/main/java/dan200/computercraft/core/apis/TableHelper.java index 4ceae1659..296bfb0a1 100644 --- a/src/main/java/dan200/computercraft/core/apis/TableHelper.java +++ b/src/main/java/dan200/computercraft/core/apis/TableHelper.java @@ -5,14 +5,14 @@ */ package dan200.computercraft.core.apis; -import dan200.computercraft.api.lua.ArgumentHelper; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaValues; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Map; -import static dan200.computercraft.api.lua.ArgumentHelper.getNumericType; +import static dan200.computercraft.api.lua.LuaValues.getNumericType; /** * Various helpers for tables. @@ -27,7 +27,7 @@ private TableHelper() @Nonnull public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nullable Object actual ) { - return badKey( key, expected, ArgumentHelper.getType( actual ) ); + return badKey( key, expected, LuaValues.getType( actual ) ); } @Nonnull diff --git a/src/main/java/dan200/computercraft/core/apis/TermAPI.java b/src/main/java/dan200/computercraft/core/apis/TermAPI.java index 6bb290f67..195085331 100644 --- a/src/main/java/dan200/computercraft/core/apis/TermAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/TermAPI.java @@ -6,27 +6,23 @@ package dan200.computercraft.core.apis; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.computer.IComputerEnvironment; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.shared.util.Colour; -import dan200.computercraft.shared.util.Palette; -import org.apache.commons.lang3.ArrayUtils; import javax.annotation.Nonnull; -import static dan200.computercraft.api.lua.ArgumentHelper.*; - -public class TermAPI implements ILuaAPI +public class TermAPI extends TermMethods implements ILuaAPI { - private final Terminal m_terminal; - private final IComputerEnvironment m_environment; + private final Terminal terminal; + private final IComputerEnvironment environment; public TermAPI( IAPIEnvironment environment ) { - m_terminal = environment.getTerminal(); - m_environment = environment.getComputerEnvironment(); + terminal = environment.getTerminal(); + this.environment = environment.getComputerEnvironment(); } @Override @@ -35,262 +31,29 @@ public String[] getNames() return new String[] { "term" }; } + @LuaFunction( { "nativePaletteColour", "nativePaletteColor" } ) + public final Object[] nativePaletteColour( int colourArg ) throws LuaException + { + int colour = 15 - parseColour( colourArg ); + Colour c = Colour.fromInt( colour ); + + float[] rgb = c.getRGB(); + + Object[] rgbObj = new Object[rgb.length]; + for( int i = 0; i < rgbObj.length; ++i ) rgbObj[i] = rgb[i]; + return rgbObj; + } + @Nonnull @Override - public String[] getMethodNames() + public Terminal getTerminal() { - return new String[] { - "write", - "scroll", - "setCursorPos", - "setCursorBlink", - "getCursorPos", - "getSize", - "clear", - "clearLine", - "setTextColour", - "setTextColor", - "setBackgroundColour", - "setBackgroundColor", - "isColour", - "isColor", - "getTextColour", - "getTextColor", - "getBackgroundColour", - "getBackgroundColor", - "blit", - "setPaletteColour", - "setPaletteColor", - "getPaletteColour", - "getPaletteColor", - "nativePaletteColour", - "nativePaletteColor", - "getCursorBlink", - }; - } - - public static int parseColour( Object[] args ) throws LuaException - { - int colour = getInt( args, 0 ); - if( colour <= 0 ) - { - throw new LuaException( "Colour out of range" ); - } - colour = getHighestBit( colour ) - 1; - if( colour < 0 || colour > 15 ) - { - throw new LuaException( "Colour out of range" ); - } - return colour; - } - - public static Object[] encodeColour( int colour ) throws LuaException - { - return new Object[] { 1 << colour }; - } - - public static void setColour( Terminal terminal, int colour, double r, double g, double b ) - { - if( terminal.getPalette() != null ) - { - terminal.getPalette().setColour( colour, r, g, b ); - terminal.setChanged(); - } + return terminal; } @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + public boolean isColour() { - switch( method ) - { - case 0: - { - // write - String text = args.length > 0 && args[0] != null ? args[0].toString() : ""; - synchronized( m_terminal ) - { - m_terminal.write( text ); - m_terminal.setCursorPos( m_terminal.getCursorX() + text.length(), m_terminal.getCursorY() ); - } - return null; - } - case 1: - { - // scroll - int y = getInt( args, 0 ); - synchronized( m_terminal ) - { - m_terminal.scroll( y ); - } - return null; - } - case 2: - { - // setCursorPos - int x = getInt( args, 0 ) - 1; - int y = getInt( args, 1 ) - 1; - synchronized( m_terminal ) - { - m_terminal.setCursorPos( x, y ); - } - return null; - } - case 3: - { - // setCursorBlink - boolean b = getBoolean( args, 0 ); - synchronized( m_terminal ) - { - m_terminal.setCursorBlink( b ); - } - return null; - } - case 4: - { - // getCursorPos - int x, y; - synchronized( m_terminal ) - { - x = m_terminal.getCursorX(); - y = m_terminal.getCursorY(); - } - return new Object[] { x + 1, y + 1 }; - } - case 5: - { - // getSize - int width, height; - synchronized( m_terminal ) - { - width = m_terminal.getWidth(); - height = m_terminal.getHeight(); - } - return new Object[] { width, height }; - } - case 6: // clear - synchronized( m_terminal ) - { - m_terminal.clear(); - } - return null; - case 7: // clearLine - synchronized( m_terminal ) - { - m_terminal.clearLine(); - } - return null; - case 8: - case 9: - { - // setTextColour/setTextColor - int colour = parseColour( args ); - synchronized( m_terminal ) - { - m_terminal.setTextColour( colour ); - } - return null; - } - case 10: - case 11: - { - // setBackgroundColour/setBackgroundColor - int colour = parseColour( args ); - synchronized( m_terminal ) - { - m_terminal.setBackgroundColour( colour ); - } - return null; - } - case 12: - case 13: // isColour/isColor - return new Object[] { m_environment.isColour() }; - case 14: - case 15: // getTextColour/getTextColor - return encodeColour( m_terminal.getTextColour() ); - case 16: - case 17: // getBackgroundColour/getBackgroundColor - return encodeColour( m_terminal.getBackgroundColour() ); - case 18: - { - // blit - String text = getString( args, 0 ); - String textColour = getString( args, 1 ); - String backgroundColour = getString( args, 2 ); - if( textColour.length() != text.length() || backgroundColour.length() != text.length() ) - { - throw new LuaException( "Arguments must be the same length" ); - } - - synchronized( m_terminal ) - { - m_terminal.blit( text, textColour, backgroundColour ); - m_terminal.setCursorPos( m_terminal.getCursorX() + text.length(), m_terminal.getCursorY() ); - } - return null; - } - case 19: - case 20: - { - // setPaletteColour/setPaletteColor - int colour = 15 - parseColour( args ); - if( args.length == 2 ) - { - int hex = getInt( args, 1 ); - double[] rgb = Palette.decodeRGB8( hex ); - setColour( m_terminal, colour, rgb[0], rgb[1], rgb[2] ); - } - else - { - double r = getFiniteDouble( args, 1 ); - double g = getFiniteDouble( args, 2 ); - double b = getFiniteDouble( args, 3 ); - setColour( m_terminal, colour, r, g, b ); - } - return null; - } - case 21: - case 22: - { - // getPaletteColour/getPaletteColor - int colour = 15 - parseColour( args ); - synchronized( m_terminal ) - { - if( m_terminal.getPalette() != null ) - { - return ArrayUtils.toObject( m_terminal.getPalette().getColour( colour ) ); - } - } - return null; - } - case 23: - case 24: - { - // nativePaletteColour/nativePaletteColor - int colour = 15 - parseColour( args ); - Colour c = Colour.fromInt( colour ); - - float[] rgb = c.getRGB(); - - Object[] rgbObj = new Object[rgb.length]; - for( int i = 0; i < rgbObj.length; ++i ) rgbObj[i] = rgb[i]; - return rgbObj; - } - case 25: - // getCursorBlink - return new Object[] { m_terminal.getCursorBlink() }; - default: - return null; - } - } - - private static int getHighestBit( int group ) - { - int bit = 0; - while( group > 0 ) - { - group >>= 1; - bit++; - } - return bit; + return environment.isColour(); } } diff --git a/src/main/java/dan200/computercraft/core/apis/TermMethods.java b/src/main/java/dan200/computercraft/core/apis/TermMethods.java new file mode 100644 index 000000000..d7674ef3f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/apis/TermMethods.java @@ -0,0 +1,222 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.apis; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.shared.util.Palette; +import dan200.computercraft.shared.util.StringUtil; +import org.apache.commons.lang3.ArrayUtils; + +import javax.annotation.Nonnull; + +/** + * A base class for all objects which interact with a terminal. Namely the {@link TermAPI} and monitors. + */ +public abstract class TermMethods +{ + private static int getHighestBit( int group ) + { + int bit = 0; + while( group > 0 ) + { + group >>= 1; + bit++; + } + return bit; + } + + @Nonnull + public abstract Terminal getTerminal() throws LuaException; + + public abstract boolean isColour() throws LuaException; + + @LuaFunction + public final void write( IArguments arguments ) throws LuaException + { + String text = StringUtil.toString( arguments.get( 0 ) ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.write( text ); + terminal.setCursorPos( terminal.getCursorX() + text.length(), terminal.getCursorY() ); + } + } + + @LuaFunction + public final void scroll( int y ) throws LuaException + { + getTerminal().scroll( y ); + } + + @LuaFunction + public final Object[] getCursorPos() throws LuaException + { + Terminal terminal = getTerminal(); + return new Object[] { terminal.getCursorX() + 1, terminal.getCursorY() + 1 }; + } + + @LuaFunction + public final void setCursorPos( int x, int y ) throws LuaException + { + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setCursorPos( x - 1, y - 1 ); + } + } + + @LuaFunction + public final boolean getCursorBlink() throws LuaException + { + return getTerminal().getCursorBlink(); + } + + @LuaFunction + public final void setCursorBlink( boolean blink ) throws LuaException + { + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setCursorBlink( blink ); + } + } + + @LuaFunction + public final Object[] getSize() throws LuaException + { + Terminal terminal = getTerminal(); + return new Object[] { terminal.getWidth(), terminal.getHeight() }; + } + + @LuaFunction + public final void clear() throws LuaException + { + getTerminal().clear(); + } + + @LuaFunction + public final void clearLine() throws LuaException + { + getTerminal().clearLine(); + } + + @LuaFunction( { "getTextColour", "getTextColor" } ) + public final int getTextColour() throws LuaException + { + return encodeColour( getTerminal().getTextColour() ); + } + + @LuaFunction( { "setTextColour", "setTextColor" } ) + public final void setTextColour( int colourArg ) throws LuaException + { + int colour = parseColour( colourArg ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setTextColour( colour ); + } + } + + @LuaFunction( { "getBackgroundColour", "getBackgroundColor" } ) + public final int getBackgroundColour() throws LuaException + { + return encodeColour( getTerminal().getBackgroundColour() ); + } + + @LuaFunction( { "setBackgroundColour", "setBackgroundColor" } ) + public final void setBackgroundColour( int colourArg ) throws LuaException + { + int colour = parseColour( colourArg ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.setBackgroundColour( colour ); + } + } + + @LuaFunction( { "isColour", "isColor" } ) + public final boolean getIsColour() throws LuaException + { + return isColour(); + } + + @LuaFunction + public final void blit( String text, String textColour, String backgroundColour ) throws LuaException + { + if( textColour.length() != text.length() || backgroundColour.length() != text.length() ) + { + throw new LuaException( "Arguments must be the same length" ); + } + + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + terminal.blit( text, textColour, backgroundColour ); + terminal.setCursorPos( terminal.getCursorX() + text.length(), terminal.getCursorY() ); + } + } + + @LuaFunction( { "setPaletteColour", "setPaletteColor" } ) + public final void setPaletteColour( IArguments args ) throws LuaException + { + int colour = 15 - parseColour( args.getInt( 0 ) ); + if( args.count() == 2 ) + { + int hex = args.getInt( 1 ); + double[] rgb = Palette.decodeRGB8( hex ); + setColour( getTerminal(), colour, rgb[0], rgb[1], rgb[2] ); + } + else + { + double r = args.getFiniteDouble( 1 ); + double g = args.getFiniteDouble( 2 ); + double b = args.getFiniteDouble( 3 ); + setColour( getTerminal(), colour, r, g, b ); + } + } + + @LuaFunction( { "getPaletteColour", "getPaletteColor" } ) + public final Object[] getPaletteColour( int colourArg ) throws LuaException + { + int colour = 15 - parseColour( colourArg ); + Terminal terminal = getTerminal(); + synchronized( terminal ) + { + if( terminal.getPalette() != null ) + { + return ArrayUtils.toObject( terminal.getPalette().getColour( colour ) ); + } + } + return null; + } + + public static int parseColour( int colour ) throws LuaException + { + if( colour <= 0 ) throw new LuaException( "Colour out of range" ); + colour = getHighestBit( colour ) - 1; + if( colour < 0 || colour > 15 ) throw new LuaException( "Colour out of range" ); + return colour; + } + + + public static int encodeColour( int colour ) + { + return 1 << colour; + } + + public static void setColour( Terminal terminal, int colour, double r, double g, double b ) + { + if( terminal.getPalette() != null ) + { + terminal.getPalette().setColour( colour, r, g, b ); + terminal.setChanged(); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java index df0ff48d5..ce9fda477 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryReadableHandle.java @@ -5,11 +5,10 @@ */ package dan200.computercraft.core.apis.handles; -import com.google.common.collect.ObjectArrays; -import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; -import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; @@ -17,208 +16,205 @@ import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; - -import static dan200.computercraft.api.lua.ArgumentHelper.getInt; -import static dan200.computercraft.api.lua.ArgumentHelper.optBoolean; +import java.util.Optional; public class BinaryReadableHandle extends HandleGeneric { private static final int BUFFER_SIZE = 8192; - private static final String[] METHOD_NAMES = new String[] { "read", "readAll", "readLine", "close" }; - private static final String[] METHOD_SEEK_NAMES = ObjectArrays.concat( METHOD_NAMES, new String[] { "seek" }, String.class ); - - private final ReadableByteChannel m_reader; - private final SeekableByteChannel m_seekable; + private final ReadableByteChannel reader; + final SeekableByteChannel seekable; private final ByteBuffer single = ByteBuffer.allocate( 1 ); - public BinaryReadableHandle( ReadableByteChannel channel, Closeable closeable ) + protected BinaryReadableHandle( ReadableByteChannel reader, SeekableByteChannel seekable, Closeable closeable ) { super( closeable ); - m_reader = channel; - m_seekable = asSeekable( channel ); + this.reader = reader; + this.seekable = seekable; } - public BinaryReadableHandle( ReadableByteChannel channel ) + public static BinaryReadableHandle of( ReadableByteChannel channel, Closeable closeable ) { - this( channel, channel ); + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryReadableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); } - @Nonnull - @Override - public String[] getMethodNames() + public static BinaryReadableHandle of( ReadableByteChannel channel ) { - return m_seekable == null ? METHOD_NAMES : METHOD_SEEK_NAMES; + return of( channel, channel ); } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + @LuaFunction + public final Object[] read( Optional countArg ) throws LuaException { - switch( method ) + checkOpen(); + try { - case 0: // read - checkOpen(); - try - { - if( args.length > 0 && args[0] != null ) - { - int count = getInt( args, 0 ); - if( count < 0 ) - { - throw new LuaException( "Cannot read a negative number of bytes" ); - } - else if( count == 0 && m_seekable != null ) - { - return m_seekable.position() >= m_seekable.size() ? null : new Object[] { "" }; - } - - if( count <= BUFFER_SIZE ) - { - ByteBuffer buffer = ByteBuffer.allocate( count ); - - int read = m_reader.read( buffer ); - if( read < 0 ) return null; - return new Object[] { read < count ? Arrays.copyOf( buffer.array(), read ) : buffer.array() }; - } - else - { - // Read the initial set of characters, failing if none are read. - ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE ); - int read = m_reader.read( buffer ); - if( read < 0 ) return null; - - // If we failed to read "enough" here, let's just abort - if( read >= count || read < BUFFER_SIZE ) - { - return new Object[] { Arrays.copyOf( buffer.array(), read ) }; - } - - // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation - // than doubling up the buffer each time. - int totalRead = read; - List parts = new ArrayList<>( 4 ); - parts.add( buffer ); - while( read >= BUFFER_SIZE && totalRead < count ) - { - buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) ); - read = m_reader.read( buffer ); - if( read < 0 ) break; - - totalRead += read; - parts.add( buffer ); - } - - // Now just copy all the bytes across! - byte[] bytes = new byte[totalRead]; - int pos = 0; - for( ByteBuffer part : parts ) - { - System.arraycopy( part.array(), 0, bytes, pos, part.position() ); - pos += part.position(); - } - return new Object[] { bytes }; - } - } - else - { - single.clear(); - int b = m_reader.read( single ); - return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF }; - } - } - catch( IOException e ) - { - return null; - } - case 1: // readAll - checkOpen(); - try - { - int expected = 32; - if( m_seekable != null ) - { - expected = Math.max( expected, (int) (m_seekable.size() - m_seekable.position()) ); - } - ByteArrayOutputStream stream = new ByteArrayOutputStream( expected ); - - ByteBuffer buf = ByteBuffer.allocate( 8192 ); - boolean readAnything = false; - while( true ) - { - buf.clear(); - int r = m_reader.read( buf ); - if( r == -1 ) break; - - readAnything = true; - stream.write( buf.array(), 0, r ); - } - return readAnything ? new Object[] { stream.toByteArray() } : null; - } - catch( IOException e ) - { - return null; - } - case 2: // readLine + if( countArg.isPresent() ) { - checkOpen(); - boolean withTrailing = optBoolean( args, 0, false ); - try + int count = countArg.get(); + if( count < 0 ) throw new LuaException( "Cannot read a negative number of bytes" ); + if( count == 0 && seekable != null ) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - - boolean readAnything = false, readRc = false; - while( true ) - { - single.clear(); - int read = m_reader.read( single ); - if( read <= 0 ) - { - // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it - // back. - if( readRc ) stream.write( '\r' ); - return readAnything ? new Object[] { stream.toByteArray() } : null; - } - - readAnything = true; - - byte chr = single.get( 0 ); - if( chr == '\n' ) - { - if( withTrailing ) - { - if( readRc ) stream.write( '\r' ); - stream.write( chr ); - } - return new Object[] { stream.toByteArray() }; - } - else - { - // We want to skip \r\n, but obviously need to include cases where \r is not followed by \n. - // Note, this behaviour is non-standard compliant (strictly speaking we should have no - // special logic for \r), but we preserve compatibility with EncodedReadableHandle and - // previous behaviour of the io library. - if( readRc ) stream.write( '\r' ); - readRc = chr == '\r'; - if( !readRc ) stream.write( chr ); - } - } + return seekable.position() >= seekable.size() ? null : new Object[] { "" }; } - catch( IOException e ) + + if( count <= BUFFER_SIZE ) { - return null; + ByteBuffer buffer = ByteBuffer.allocate( count ); + + int read = reader.read( buffer ); + if( read < 0 ) return null; + buffer.flip(); + return new Object[] { buffer }; + } + else + { + // Read the initial set of characters, failing if none are read. + ByteBuffer buffer = ByteBuffer.allocate( BUFFER_SIZE ); + int read = reader.read( buffer ); + if( read < 0 ) return null; + + // If we failed to read "enough" here, let's just abort + if( read >= count || read < BUFFER_SIZE ) + { + buffer.flip(); + return new Object[] { buffer }; + } + + // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation + // than doubling up the buffer each time. + int totalRead = read; + List parts = new ArrayList<>( 4 ); + parts.add( buffer ); + while( read >= BUFFER_SIZE && totalRead < count ) + { + buffer = ByteBuffer.allocate( Math.min( BUFFER_SIZE, count - totalRead ) ); + read = reader.read( buffer ); + if( read < 0 ) break; + + totalRead += read; + parts.add( buffer ); + } + + // Now just copy all the bytes across! + byte[] bytes = new byte[totalRead]; + int pos = 0; + for( ByteBuffer part : parts ) + { + System.arraycopy( part.array(), 0, bytes, pos, part.position() ); + pos += part.position(); + } + return new Object[] { bytes }; } } - case 3: // close - checkOpen(); - close(); - return null; - case 4: // seek - checkOpen(); - return handleSeek( m_seekable, args ); - default: - return null; + else + { + single.clear(); + int b = reader.read( single ); + return b == -1 ? null : new Object[] { single.get( 0 ) & 0xFF }; + } + } + catch( IOException e ) + { + return null; + } + } + + @LuaFunction + public final Object[] readAll() throws LuaException + { + checkOpen(); + try + { + int expected = 32; + if( seekable != null ) expected = Math.max( expected, (int) (seekable.size() - seekable.position()) ); + ByteArrayOutputStream stream = new ByteArrayOutputStream( expected ); + + ByteBuffer buf = ByteBuffer.allocate( 8192 ); + boolean readAnything = false; + while( true ) + { + buf.clear(); + int r = reader.read( buf ); + if( r == -1 ) break; + + readAnything = true; + stream.write( buf.array(), 0, r ); + } + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + catch( IOException e ) + { + return null; + } + } + + @LuaFunction + public final Object[] readLine( Optional withTrailingArg ) throws LuaException + { + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + boolean readAnything = false, readRc = false; + while( true ) + { + single.clear(); + int read = reader.read( single ); + if( read <= 0 ) + { + // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it + // back. + if( readRc ) stream.write( '\r' ); + return readAnything ? new Object[] { stream.toByteArray() } : null; + } + + readAnything = true; + + byte chr = single.get( 0 ); + if( chr == '\n' ) + { + if( withTrailing ) + { + if( readRc ) stream.write( '\r' ); + stream.write( chr ); + } + return new Object[] { stream.toByteArray() }; + } + else + { + // We want to skip \r\n, but obviously need to include cases where \r is not followed by \n. + // Note, this behaviour is non-standard compliant (strictly speaking we should have no + // special logic for \r), but we preserve compatibility with EncodedReadableHandle and + // previous behaviour of the io library. + if( readRc ) stream.write( '\r' ); + readRc = chr == '\r'; + if( !readRc ) stream.write( chr ); + } + } + } + catch( IOException e ) + { + return null; + } + } + + public static class Seekable extends BinaryReadableHandle + { + public Seekable( SeekableByteChannel seekable, Closeable closeable ) + { + super( seekable, seekable, closeable ); + } + + @LuaFunction + public final Object[] seek( IArguments arguments ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, arguments ); } } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java index 2ab94bd4b..b5b3647bb 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/BinaryWritableHandle.java @@ -5,13 +5,11 @@ */ package dan200.computercraft.core.apis.handles; -import com.google.common.collect.ObjectArrays; -import dan200.computercraft.api.lua.ArgumentHelper; -import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.shared.util.StringUtil; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.LuaValues; -import javax.annotation.Nonnull; import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; @@ -21,87 +19,85 @@ public class BinaryWritableHandle extends HandleGeneric { - private static final String[] METHOD_NAMES = new String[] { "write", "flush", "close" }; - private static final String[] METHOD_SEEK_NAMES = ObjectArrays.concat( METHOD_NAMES, new String[] { "seek" }, String.class ); - - private final WritableByteChannel m_writer; - private final SeekableByteChannel m_seekable; + private final WritableByteChannel writer; + final SeekableByteChannel seekable; private final ByteBuffer single = ByteBuffer.allocate( 1 ); - public BinaryWritableHandle( WritableByteChannel channel, Closeable closeable ) + protected BinaryWritableHandle( WritableByteChannel writer, SeekableByteChannel seekable, Closeable closeable ) { super( closeable ); - m_writer = channel; - m_seekable = asSeekable( channel ); + this.writer = writer; + this.seekable = seekable; } - public BinaryWritableHandle( WritableByteChannel channel ) + public static BinaryWritableHandle of( WritableByteChannel channel, Closeable closeable ) { - this( channel, channel ); + SeekableByteChannel seekable = asSeekable( channel ); + return seekable == null ? new BinaryWritableHandle( channel, null, closeable ) : new Seekable( seekable, closeable ); } - @Nonnull - @Override - public String[] getMethodNames() + public static BinaryWritableHandle of( WritableByteChannel channel ) { - return m_seekable == null ? METHOD_NAMES : METHOD_SEEK_NAMES; + return of( channel, channel ); } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + @LuaFunction + public final void write( IArguments arguments ) throws LuaException { - switch( method ) + checkOpen(); + try { - case 0: // write - checkOpen(); - try - { - if( args.length > 0 && args[0] instanceof Number ) - { - int number = ((Number) args[0]).intValue(); - single.clear(); - single.put( (byte) number ); - single.flip(); + Object arg = arguments.get( 0 ); + if( arg instanceof Number ) + { + int number = ((Number) arg).intValue(); + single.clear(); + single.put( (byte) number ); + single.flip(); - m_writer.write( single ); - } - else if( args.length > 0 && args[0] instanceof String ) - { - String value = (String) args[0]; - m_writer.write( ByteBuffer.wrap( StringUtil.encodeString( value ) ) ); - } - else - { - throw ArgumentHelper.badArgumentOf( 0, "string or number", args.length > 0 ? args[0] : null ); - } - return null; - } - catch( IOException e ) - { - throw new LuaException( e.getMessage() ); - } - case 1: // flush - checkOpen(); - try - { - // Technically this is not needed - if( m_writer instanceof FileChannel ) ((FileChannel) m_writer).force( false ); + writer.write( single ); + } + else if( arg instanceof String ) + { + writer.write( arguments.getBytes( 0 ) ); + } + else + { + throw LuaValues.badArgumentOf( 0, "string or number", arg ); + } + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); + } + } - return null; - } - catch( IOException e ) - { - return null; - } - case 2: // close - checkOpen(); - close(); - return null; - case 3: // seek - checkOpen(); - return handleSeek( m_seekable, args ); - default: - return null; + @LuaFunction + public final void flush() throws LuaException + { + checkOpen(); + try + { + // Technically this is not needed + if( writer instanceof FileChannel ) ((FileChannel) writer).force( false ); + } + catch( IOException ignored ) + { + } + } + + public static class Seekable extends BinaryWritableHandle + { + public Seekable( SeekableByteChannel seekable, Closeable closeable ) + { + super( seekable, seekable, closeable ); + } + + @LuaFunction + public final Object[] seek( IArguments args ) throws LuaException + { + checkOpen(); + return handleSeek( seekable, args ); } } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java index af714dbdc..0a935fc22 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedReadableHandle.java @@ -5,8 +5,8 @@ */ package dan200.computercraft.core.apis.handles; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import javax.annotation.Nonnull; import java.io.BufferedReader; @@ -18,20 +18,18 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; - -import static dan200.computercraft.api.lua.ArgumentHelper.optBoolean; -import static dan200.computercraft.api.lua.ArgumentHelper.optInt; +import java.util.Optional; public class EncodedReadableHandle extends HandleGeneric { private static final int BUFFER_SIZE = 8192; - private BufferedReader m_reader; + private final BufferedReader reader; public EncodedReadableHandle( @Nonnull BufferedReader reader, @Nonnull Closeable closable ) { super( closable ); - m_reader = reader; + this.reader = reader; } public EncodedReadableHandle( @Nonnull BufferedReader reader ) @@ -39,123 +37,107 @@ public EncodedReadableHandle( @Nonnull BufferedReader reader ) this( reader, reader ); } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final Object[] readLine( Optional withTrailingArg ) throws LuaException { - return new String[] { - "readLine", - "readAll", - "read", - "close", - }; + checkOpen(); + boolean withTrailing = withTrailingArg.orElse( false ); + try + { + String line = reader.readLine(); + if( line != null ) + { + // While this is technically inaccurate, it's better than nothing + if( withTrailing ) line += "\n"; + return new Object[] { line }; + } + else + { + return null; + } + } + catch( IOException e ) + { + return null; + } } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + @LuaFunction + public final Object[] readAll() throws LuaException { - switch( method ) + checkOpen(); + try { - case 0: // readLine + StringBuilder result = new StringBuilder(); + String line = reader.readLine(); + while( line != null ) { - checkOpen(); - boolean withTrailing = optBoolean( args, 0, false ); - try + result.append( line ); + line = reader.readLine(); + if( line != null ) { - String line = m_reader.readLine(); - if( line != null ) - { - // While this is technically inaccurate, it's better than nothing - if( withTrailing ) line += "\n"; - return new Object[] { line }; - } - else - { - return null; - } - } - catch( IOException e ) - { - return null; + result.append( "\n" ); } } - case 1: // readAll - checkOpen(); - try + return new Object[] { result.toString() }; + } + catch( IOException e ) + { + return null; + } + } + + @LuaFunction + public final Object[] read( Optional countA ) throws LuaException + { + checkOpen(); + try + { + int count = countA.orElse( 1 ); + if( count < 0 ) + { + // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so + // it seems best to remain somewhat consistent. + throw new LuaException( "Cannot read a negative number of characters" ); + } + else if( count <= BUFFER_SIZE ) + { + // If we've got a small count, then allocate that and read it. + char[] chars = new char[count]; + int read = reader.read( chars ); + + return read < 0 ? null : new Object[] { new String( chars, 0, read ) }; + } + else + { + // If we've got a large count, read in bunches of 8192. + char[] buffer = new char[BUFFER_SIZE]; + + // Read the initial set of characters, failing if none are read. + int read = reader.read( buffer, 0, Math.min( buffer.length, count ) ); + if( read < 0 ) return null; + + StringBuilder out = new StringBuilder( read ); + int totalRead = read; + out.append( buffer, 0, read ); + + // Otherwise read until we either reach the limit or we no longer consume + // the full buffer. + while( read >= BUFFER_SIZE && totalRead < count ) { - StringBuilder result = new StringBuilder(); - String line = m_reader.readLine(); - while( line != null ) - { - result.append( line ); - line = m_reader.readLine(); - if( line != null ) - { - result.append( "\n" ); - } - } - return new Object[] { result.toString() }; + read = reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) ); + if( read < 0 ) break; + + totalRead += read; + out.append( buffer, 0, read ); } - catch( IOException e ) - { - return null; - } - case 2: // read - checkOpen(); - try - { - int count = optInt( args, 0, 1 ); - if( count < 0 ) - { - // Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so - // it seems best to remain somewhat consistent. - throw new LuaException( "Cannot read a negative number of characters" ); - } - else if( count <= BUFFER_SIZE ) - { - // If we've got a small count, then allocate that and read it. - char[] chars = new char[count]; - int read = m_reader.read( chars ); - return read < 0 ? null : new Object[] { new String( chars, 0, read ) }; - } - else - { - // If we've got a large count, read in bunches of 8192. - char[] buffer = new char[BUFFER_SIZE]; - - // Read the initial set of characters, failing if none are read. - int read = m_reader.read( buffer, 0, Math.min( buffer.length, count ) ); - if( read < 0 ) return null; - - StringBuilder out = new StringBuilder( read ); - int totalRead = read; - out.append( buffer, 0, read ); - - // Otherwise read until we either reach the limit or we no longer consume - // the full buffer. - while( read >= BUFFER_SIZE && totalRead < count ) - { - read = m_reader.read( buffer, 0, Math.min( BUFFER_SIZE, count - totalRead ) ); - if( read < 0 ) break; - - totalRead += read; - out.append( buffer, 0, read ); - } - - return new Object[] { out.toString() }; - } - } - catch( IOException e ) - { - return null; - } - case 3: // close - checkOpen(); - close(); - return null; - default: - return null; + return new Object[] { out.toString() }; + } + } + catch( IOException e ) + { + return null; } } diff --git a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java index c9cfb5ba7..61bfe152b 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/EncodedWritableHandle.java @@ -5,8 +5,10 @@ */ package dan200.computercraft.core.apis.handles; -import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.shared.util.StringUtil; import javax.annotation.Nonnull; import java.io.BufferedWriter; @@ -21,85 +23,57 @@ public class EncodedWritableHandle extends HandleGeneric { - private BufferedWriter m_writer; + private final BufferedWriter writer; public EncodedWritableHandle( @Nonnull BufferedWriter writer, @Nonnull Closeable closable ) { super( closable ); - m_writer = writer; + this.writer = writer; } - public EncodedWritableHandle( @Nonnull BufferedWriter writer ) + @LuaFunction + public final void write( IArguments args ) throws LuaException { - this( writer, writer ); - } - - @Nonnull - @Override - public String[] getMethodNames() - { - return new String[] { - "write", - "writeLine", - "flush", - "close", - }; - } - - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException - { - switch( method ) + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try { - case 0: // write - { - checkOpen(); - String text = args.length > 0 && args[0] != null ? args[0].toString() : ""; - try - { - m_writer.write( text, 0, text.length() ); - return null; - } - catch( IOException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 1: // writeLine - { - checkOpen(); - String text = args.length > 0 && args[0] != null ? args[0].toString() : ""; - try - { - m_writer.write( text, 0, text.length() ); - m_writer.newLine(); - return null; - } - catch( IOException e ) - { - throw new LuaException( e.getMessage() ); - } - } - case 2: // flush - checkOpen(); - try - { - m_writer.flush(); - return null; - } - catch( IOException e ) - { - return null; - } - case 3: // close - checkOpen(); - close(); - return null; - default: - return null; + writer.write( text, 0, text.length() ); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); } } + @LuaFunction + public final void writeLine( IArguments args ) throws LuaException + { + checkOpen(); + String text = StringUtil.toString( args.get( 0 ) ); + try + { + writer.write( text, 0, text.length() ); + writer.newLine(); + } + catch( IOException e ) + { + throw new LuaException( e.getMessage() ); + } + } + + @LuaFunction + public final void flush() throws LuaException + { + checkOpen(); + try + { + writer.flush(); + } + catch( IOException ignored ) + { + } + } public static BufferedWriter openUtf8( WritableByteChannel channel ) { diff --git a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java index 2ebad4202..35b5fd883 100644 --- a/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java +++ b/src/main/java/dan200/computercraft/core/apis/handles/HandleGeneric.java @@ -5,8 +5,9 @@ */ package dan200.computercraft.core.apis.handles; -import dan200.computercraft.api.lua.ILuaObject; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.shared.util.IoUtil; import javax.annotation.Nonnull; @@ -15,36 +16,41 @@ import java.nio.channels.Channel; import java.nio.channels.SeekableByteChannel; -import static dan200.computercraft.api.lua.ArgumentHelper.optLong; -import static dan200.computercraft.api.lua.ArgumentHelper.optString; - -public abstract class HandleGeneric implements ILuaObject +public abstract class HandleGeneric { - private Closeable m_closable; - private boolean m_open = true; + private Closeable closable; + private boolean open = true; protected HandleGeneric( @Nonnull Closeable closable ) { - m_closable = closable; + this.closable = closable; } protected void checkOpen() throws LuaException { - if( !m_open ) throw new LuaException( "attempt to use a closed file" ); + if( !open ) throw new LuaException( "attempt to use a closed file" ); } protected final void close() { - m_open = false; + open = false; - Closeable closeable = m_closable; + Closeable closeable = closable; if( closeable != null ) { IoUtil.closeQuietly( closeable ); - m_closable = null; + closable = null; } } + @LuaFunction( "close" ) + public final void doClose() throws LuaException + { + checkOpen(); + close(); + } + + /** * Shared implementation for various file handle types. * @@ -54,12 +60,12 @@ protected final void close() * @throws LuaException If the arguments were invalid * @see {@code file:seek} in the Lua manual. */ - protected static Object[] handleSeek( SeekableByteChannel channel, Object[] args ) throws LuaException + protected static Object[] handleSeek( SeekableByteChannel channel, IArguments args ) throws LuaException { + String whence = args.optString( 0, "cur" ); + long offset = args.optLong( 1, 0 ); try { - String whence = optString( args, 0, "cur" ); - long offset = optLong( args, 1, 0 ); switch( whence ) { case "set": diff --git a/src/main/java/dan200/computercraft/core/apis/http/CheckUrl.java b/src/main/java/dan200/computercraft/core/apis/http/CheckUrl.java index ab4b56de2..f92d5926e 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/CheckUrl.java +++ b/src/main/java/dan200/computercraft/core/apis/http/CheckUrl.java @@ -47,11 +47,11 @@ private void doRun() try { NetworkUtils.getAddress( host, 80, false ); - if( tryClose() ) environment.queueEvent( EVENT, new Object[] { address, true } ); + if( tryClose() ) environment.queueEvent( EVENT, address, true ); } catch( HTTPRequestException e ) { - if( tryClose() ) environment.queueEvent( EVENT, new Object[] { address, false, e.getMessage() } ); + if( tryClose() ) environment.queueEvent( EVENT, address, false, e.getMessage() ); } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java index b15ce77d5..3af3bacc3 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequest.java @@ -6,7 +6,6 @@ package dan200.computercraft.core.apis.http.request; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.lua.ILuaObject; import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.apis.http.HTTPRequestException; import dan200.computercraft.core.apis.http.NetworkUtils; @@ -201,7 +200,7 @@ protected void initChannel( SocketChannel ch ) void failure( String message ) { - if( tryClose() ) environment.queueEvent( FAILURE_EVENT, new Object[] { address, message } ); + if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message ); } void failure( Throwable cause ) @@ -227,14 +226,14 @@ else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeou failure( message ); } - void failure( String message, ILuaObject object ) + void failure( String message, HttpResponseHandle object ) { - if( tryClose() ) environment.queueEvent( FAILURE_EVENT, new Object[] { address, message, object } ); + if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message, object ); } - void success( ILuaObject object ) + void success( HttpResponseHandle object ) { - if( tryClose() ) environment.queueEvent( SUCCESS_EVENT, new Object[] { address, object } ); + if( tryClose() ) environment.queueEvent( SUCCESS_EVENT, address, object ); } @Override diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java index 866d0d138..d742ec538 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpRequestHandler.java @@ -6,10 +6,10 @@ package dan200.computercraft.core.apis.http.request; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.lua.ILuaObject; 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.NetworkUtils; import dan200.computercraft.core.tracking.TrackingField; @@ -209,10 +209,10 @@ private void sendResponse() // Prepare to queue an event ArrayByteChannel contents = new ArrayByteChannel( bytes ); - final ILuaObject reader = request.isBinary() - ? new BinaryReadableHandle( contents ) + HandleGeneric reader = request.isBinary() + ? BinaryReadableHandle.of( contents ) : new EncodedReadableHandle( EncodedReadableHandle.open( contents, responseCharset ) ); - ILuaObject stream = new HttpResponseHandle( reader, status.code(), status.reasonPhrase(), headers ); + HttpResponseHandle stream = new HttpResponseHandle( reader, status.code(), status.reasonPhrase(), headers ); if( status.code() >= 200 && status.code() < 400 ) { diff --git a/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java b/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java index bfface4eb..8000a6c1e 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/http/request/HttpResponseHandle.java @@ -5,62 +5,48 @@ */ package dan200.computercraft.core.apis.http.request; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.ILuaObject; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.core.apis.handles.HandleGeneric; +import dan200.computercraft.core.asm.ObjectSource; import javax.annotation.Nonnull; -import java.util.Arrays; +import java.util.Collections; import java.util.Map; /** * Wraps a {@link dan200.computercraft.core.apis.handles.HandleGeneric} and provides additional methods for * getting the response code and headers. */ -public class HttpResponseHandle implements ILuaObject +public class HttpResponseHandle implements ObjectSource { - private final String[] newMethods; - private final int methodOffset; - private final ILuaObject reader; + private final Object reader; private final int responseCode; private final String responseStatus; private final Map responseHeaders; - public HttpResponseHandle( @Nonnull ILuaObject reader, int responseCode, String responseStatus, @Nonnull Map responseHeaders ) + public HttpResponseHandle( @Nonnull HandleGeneric reader, int responseCode, String responseStatus, @Nonnull Map responseHeaders ) { this.reader = reader; this.responseCode = responseCode; this.responseStatus = responseStatus; this.responseHeaders = responseHeaders; - - String[] oldMethods = reader.getMethodNames(); - final int methodOffset = this.methodOffset = oldMethods.length; - - final String[] newMethods = this.newMethods = Arrays.copyOf( oldMethods, oldMethods.length + 2 ); - newMethods[methodOffset + 0] = "getResponseCode"; - newMethods[methodOffset + 1] = "getResponseHeaders"; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final Object[] getResponseCode() { - return newMethods; + return new Object[] { responseCode, responseStatus }; + } + + @LuaFunction + public final Map getResponseHeaders() + { + return responseHeaders; } @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException + public Iterable getExtra() { - if( method < methodOffset ) return reader.callMethod( context, method, args ); - - switch( method - methodOffset ) - { - case 0: // getResponseCode - return new Object[] { responseCode, responseStatus }; - case 1: // getResponseHeaders - return new Object[] { responseHeaders }; - default: - return null; - } + return Collections.singletonList( reader ); } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java index 5e1562d04..fe591f592 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/Websocket.java @@ -187,7 +187,7 @@ void success( Channel channel ) if( isClosed() ) return; WebsocketHandle handle = new WebsocketHandle( this, channel ); - environment().queueEvent( SUCCESS_EVENT, new Object[] { address, handle } ); + environment().queueEvent( SUCCESS_EVENT, address, handle ); websocketHandle = createOwnerReference( handle ); checkClosed(); @@ -195,18 +195,16 @@ void success( Channel channel ) void failure( String message ) { - if( tryClose() ) environment.queueEvent( FAILURE_EVENT, new Object[] { address, message } ); + if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message ); } void close( int status, String reason ) { if( tryClose() ) { - environment.queueEvent( CLOSE_EVENT, new Object[] { - address, + environment.queueEvent( CLOSE_EVENT, address, Strings.isNullOrEmpty( reason ) ? null : reason, - status < 0 ? null : status, - } ); + status < 0 ? null : status ); } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java index 8433d30c3..78bb17bdf 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandle.java @@ -7,10 +7,7 @@ import com.google.common.base.Objects; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.lua.ArgumentHelper; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.ILuaObject; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.*; import dan200.computercraft.core.tracking.TrackingField; import dan200.computercraft.shared.util.StringUtil; import io.netty.buffer.Unpooled; @@ -19,16 +16,16 @@ import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.io.Closeable; import java.util.Arrays; +import java.util.Optional; -import static dan200.computercraft.api.lua.ArgumentHelper.optBoolean; +import static dan200.computercraft.api.lua.LuaValues.checkFinite; import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT; import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT; import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT; -public class WebsocketHandle implements ILuaObject, Closeable +public class WebsocketHandle implements Closeable { private final Websocket websocket; private boolean closed = false; @@ -41,87 +38,45 @@ public WebsocketHandle( Websocket websocket, Channel channel ) this.channel = channel; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final MethodResult result( Optional timeout ) throws LuaException { - return new String[] { "receive", "send", "close" }; + checkOpen(); + int timeoutId = timeout.isPresent() + ? websocket.environment().startTimer( Math.round( checkFinite( 0, timeout.get() ) / 0.05 ) ) + : -1; + + return new ReceiveCallback( timeoutId ).pull; } - @Nullable - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction + public final void send( IArguments args ) throws LuaException { - switch( method ) + checkOpen(); + + String text = StringUtil.toString( args.get( 0 ) ); + if( ComputerCraft.httpMaxWebsocketMessage != 0 && text.length() > ComputerCraft.httpMaxWebsocketMessage ) { - case 0: // receive - { - checkOpen(); - int timeoutId; - if( arguments.length <= 0 || arguments[0] == null ) - { - // We do this rather odd argument validation to ensure we can tell the difference between a - // negative timeout and an absent one. - timeoutId = -1; - } - else - { - double timeout = ArgumentHelper.getFiniteDouble( arguments, 0 ); - timeoutId = websocket.environment().startTimer( Math.round( timeout / 0.05 ) ); - } - - while( true ) - { - Object[] event = context.pullEvent( null ); - if( event.length >= 3 && Objects.equal( event[0], MESSAGE_EVENT ) && Objects.equal( event[1], websocket.address() ) ) - { - return Arrays.copyOfRange( event, 2, event.length ); - } - else if( event.length >= 2 && Objects.equal( event[0], CLOSE_EVENT ) && Objects.equal( event[1], websocket.address() ) && closed ) - { - // If the socket is closed abort. - return null; - } - else if( event.length >= 2 && timeoutId != -1 && Objects.equal( event[0], TIMER_EVENT ) - && event[1] instanceof Number && ((Number) event[1]).intValue() == timeoutId ) - { - // If we received a matching timer event then abort. - return null; - } - } - } - - case 1: // send - { - checkOpen(); - - String text = arguments.length > 0 && arguments[0] != null ? arguments[0].toString() : ""; - if( ComputerCraft.httpMaxWebsocketMessage != 0 && text.length() > ComputerCraft.httpMaxWebsocketMessage ) - { - throw new LuaException( "Message is too large" ); - } - - boolean binary = optBoolean( arguments, 1, false ); - websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() ); - - Channel channel = this.channel; - if( channel != null ) - { - channel.writeAndFlush( binary - ? new BinaryWebSocketFrame( Unpooled.wrappedBuffer( StringUtil.encodeString( text ) ) ) - : new TextWebSocketFrame( text ) ); - } - - return null; - } - - case 2: // close - close(); - websocket.close(); - return null; - default: - return null; + throw new LuaException( "Message is too large" ); } + + boolean binary = args.optBoolean( 1, false ); + websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() ); + + Channel channel = this.channel; + if( channel != null ) + { + channel.writeAndFlush( binary + ? new BinaryWebSocketFrame( Unpooled.wrappedBuffer( LuaValues.encode( text ) ) ) + : new TextWebSocketFrame( text ) ); + } + } + + @LuaFunction( "close" ) + public final void doClose() + { + close(); + websocket.close(); } private void checkOpen() throws LuaException @@ -141,4 +96,38 @@ public void close() this.channel = null; } } + + private final class ReceiveCallback implements ILuaCallback + { + final MethodResult pull = MethodResult.pullEvent( null, this ); + private final int timeoutId; + + ReceiveCallback( int timeoutId ) + { + this.timeoutId = timeoutId; + } + + @Nonnull + @Override + public MethodResult resume( Object[] event ) + { + if( event.length >= 3 && Objects.equal( event[0], MESSAGE_EVENT ) && Objects.equal( event[1], websocket.address() ) ) + { + return MethodResult.of( Arrays.copyOfRange( event, 2, event.length ) ); + } + else if( event.length >= 2 && Objects.equal( event[0], CLOSE_EVENT ) && Objects.equal( event[1], websocket.address() ) && closed ) + { + // If the socket is closed abort. + return MethodResult.of(); + } + else if( event.length >= 2 && timeoutId != -1 && Objects.equal( event[0], TIMER_EVENT ) + && event[1] instanceof Number && ((Number) event[1]).intValue() == timeoutId ) + { + // If we received a matching timer event then abort. + return MethodResult.of(); + } + + return pull; + } + } } diff --git a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java index 70a2e77a6..c3e69d7b9 100644 --- a/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java +++ b/src/main/java/dan200/computercraft/core/apis/http/websocket/WebsocketHandler.java @@ -68,14 +68,14 @@ public void channelRead0( ChannelHandlerContext ctx, Object msg ) String data = ((TextWebSocketFrame) frame).text(); websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, data.length() ); - websocket.environment().queueEvent( MESSAGE_EVENT, new Object[] { websocket.address(), data, false } ); + websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), data, false ); } else if( frame instanceof BinaryWebSocketFrame ) { byte[] converted = NetworkUtils.toBytes( frame.content() ); websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, converted.length ); - websocket.environment().queueEvent( MESSAGE_EVENT, new Object[] { websocket.address(), converted, true } ); + websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), converted, true ); } else if( frame instanceof CloseWebSocketFrame ) { diff --git a/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java b/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java new file mode 100644 index 000000000..009a8840e --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/DeclaringClassLoader.java @@ -0,0 +1,24 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import java.security.ProtectionDomain; + +final class DeclaringClassLoader extends ClassLoader +{ + static final DeclaringClassLoader INSTANCE = new DeclaringClassLoader(); + + private DeclaringClassLoader() + { + super( DeclaringClassLoader.class.getClassLoader() ); + } + + Class define( String name, byte[] bytes, ProtectionDomain protectionDomain ) throws ClassFormatError + { + return defineClass( name, bytes, 0, bytes.length, protectionDomain ); + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java new file mode 100644 index 000000000..801969d0c --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -0,0 +1,320 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +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.ComputerCraft; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.MethodResult; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; + +import javax.annotation.Nonnull; +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.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + +import static org.objectweb.asm.Opcodes.*; + +public class Generator +{ + private static final AtomicInteger METHOD_ID = new AtomicInteger(); + + 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 final Class base; + private final List> context; + + private final String[] interfaces; + private final String methodDesc; + + private final Function wrap; + + private final LoadingCache, List>> classCache = CacheBuilder + .newBuilder() + .build( CacheLoader.from( this::build ) ); + + private final LoadingCache> methodCache = CacheBuilder + .newBuilder() + .build( CacheLoader.from( this::build ) ); + + Generator( Class base, List> context, Function wrap ) + { + this.base = base; + this.context = context; + this.interfaces = new String[] { Type.getInternalName( base ) }; + this.wrap = wrap; + + StringBuilder methodDesc = new StringBuilder().append( "(Ljava/lang/Object;" ); + for( Class klass : context ) methodDesc.append( Type.getDescriptor( klass ) ); + methodDesc.append( DESC_ARGUMENTS ).append( ")" ).append( DESC_METHOD_RESULT ); + this.methodDesc = methodDesc.toString(); + } + + @Nonnull + public List> getMethods( @Nonnull Class klass ) + { + try + { + return classCache.get( klass ); + } + catch( ExecutionException e ) + { + ComputerCraft.log.error( "Error getting methods for {}.", klass.getName(), e.getCause() ); + return Collections.emptyList(); + } + } + + @Nonnull + private List> build( Class klass ) + { + ArrayList> methods = null; + for( Method method : klass.getMethods() ) + { + LuaFunction annotation = method.getAnnotation( LuaFunction.class ); + if( annotation == null ) continue; + + T instance = methodCache.getUnchecked( method ).orElse( null ); + if( instance == null ) continue; + + if( methods == null ) methods = new ArrayList<>(); + + if( annotation.mainThread() ) instance = wrap.apply( instance ); + + String[] names = annotation.value(); + boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); + if( names.length == 0 ) + { + methods.add( new NamedMethod<>( method.getName(), instance, isSimple ) ); + } + else + { + for( String name : names ) + { + methods.add( new NamedMethod<>( name, instance, isSimple ) ); + } + } + } + + if( methods == null ) return Collections.emptyList(); + methods.trimToSize(); + return Collections.unmodifiableList( methods ); + } + + @Nonnull + private Optional build( Method method ) + { + String name = method.getDeclaringClass().getName() + "." + method.getName(); + int modifiers = method.getModifiers(); + if( !Modifier.isFinal( modifiers ) ) + { + ComputerCraft.log.warn( "Lua Method {} should be final.", name ); + } + + if( Modifier.isStatic( modifiers ) || !Modifier.isPublic( modifiers ) ) + { + ComputerCraft.log.error( "Lua Method {} should be a public instance method.", name ); + return Optional.empty(); + } + + if( !Modifier.isPublic( method.getDeclaringClass().getModifiers() ) ) + { + ComputerCraft.log.error( "Lua Method {} should be on a public class.", name ); + return Optional.empty(); + } + + ComputerCraft.log.debug( "Generating method wrapper for {}.", name ); + + Class[] exceptions = method.getExceptionTypes(); + for( Class exception : exceptions ) + { + if( exception != LuaException.class ) + { + ComputerCraft.log.error( "Lua Method {} cannot throw {}.", name, exception.getName() ); + return Optional.empty(); + } + } + + try + { + String className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement(); + byte[] bytes = generate( className, method ); + if( bytes == null ) return Optional.empty(); + + Class klass = DeclaringClassLoader.INSTANCE.define( className, bytes, method.getDeclaringClass().getProtectionDomain() ); + return Optional.of( klass.asSubclass( base ).newInstance() ); + } + catch( InstantiationException | IllegalAccessException | ClassFormatError | RuntimeException e ) + { + ComputerCraft.log.error( "Error generating wrapper for {}.", name, e ); + return Optional.empty(); + } + + } + + @Nullable + private byte[] generate( String className, Method method ) + { + String internalName = className.replace( ".", "/" ); + + // Construct a public final class which extends Object and implements MethodInstance.Delegate + ClassWriter cw = new ClassWriter( ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS ); + cw.visit( V1_8, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces ); + cw.visitSource( "CC generated method", null ); + + { // Constructor just invokes super. + MethodVisitor mw = cw.visitMethod( ACC_PUBLIC, "", "()V", null, null ); + mw.visitCode(); + mw.visitVarInsn( ALOAD, 0 ); + mw.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "", "()V", false ); + mw.visitInsn( RETURN ); + mw.visitMaxs( 0, 0 ); + mw.visitEnd(); + } + + { + MethodVisitor mw = cw.visitMethod( ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS ); + mw.visitCode(); + mw.visitVarInsn( ALOAD, 1 ); + mw.visitTypeInsn( CHECKCAST, Type.getInternalName( method.getDeclaringClass() ) ); + + int argIndex = 0; + for( java.lang.reflect.Type genericArg : method.getGenericParameterTypes() ) + { + Boolean loadedArg = loadArg( mw, method, genericArg, argIndex ); + if( loadedArg == null ) return null; + if( loadedArg ) argIndex++; + } + + mw.visitMethodInsn( INVOKEVIRTUAL, Type.getInternalName( method.getDeclaringClass() ), method.getName(), + Type.getMethodDescriptor( method ), 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. + Class ret = method.getReturnType(); + if( ret != MethodResult.class ) + { + if( ret == void.class ) + { + mw.visitMethodInsn( INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "()" + DESC_METHOD_RESULT, false ); + } + else if( ret.isPrimitive() ) + { + Class 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(); + } + + private Boolean loadArg( MethodVisitor mw, Method method, java.lang.reflect.Type genericArg, int argIndex ) + { + Class arg = Reflect.getRawType( method, genericArg, true ); + if( arg == null ) return null; + + if( arg == IArguments.class ) + { + mw.visitVarInsn( ALOAD, 2 + context.size() ); + return false; + } + + int idx = context.indexOf( arg ); + if( idx >= 0 ) + { + mw.visitVarInsn( ALOAD, 2 + idx ); + return false; + } + + if( arg == Optional.class ) + { + Class 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; + } + + String name = Reflect.getLuaName( Primitives.unwrap( klass ) ); + 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; + } + + String name = arg == Object.class ? "" : Reflect.getLuaName( arg ); + 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; + } + + ComputerCraft.log.error( "Unknown parameter type {} for method {}.{}.", + arg.getName(), method.getDeclaringClass().getName(), method.getName() ); + return null; + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/IntCache.java b/src/main/java/dan200/computercraft/core/asm/IntCache.java new file mode 100644 index 000000000..d50f54b2a --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/IntCache.java @@ -0,0 +1,41 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import java.util.Arrays; +import java.util.function.IntFunction; + +public final class IntCache +{ + private final IntFunction factory; + private volatile Object[] cache = new Object[16]; + + IntCache( IntFunction factory ) + { + this.factory = factory; + } + + @SuppressWarnings( "unchecked" ) + public T get( int index ) + { + if( index < 0 ) throw new IllegalArgumentException( "index < 0" ); + + if( index <= cache.length ) + { + T current = (T) cache[index]; + if( current != null ) return current; + } + + synchronized( this ) + { + if( index > cache.length ) cache = Arrays.copyOf( cache, Math.max( cache.length * 2, index ) ); + T current = (T) cache[index]; + if( current == null ) cache[index] = current = factory.apply( index ); + return current; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/LuaMethod.java b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java new file mode 100644 index 000000000..35ad33b76 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/LuaMethod.java @@ -0,0 +1,30 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.lua.*; + +import javax.annotation.Nonnull; +import java.util.Collections; + +public interface LuaMethod +{ + Generator GENERATOR = new Generator<>( LuaMethod.class, Collections.singletonList( ILuaContext.class ), + m -> ( target, context, args ) -> { + long id = context.issueMainThreadTask( () -> TaskCallback.checkUnwrap( m.apply( target, context, args ) ) ); + return new TaskCallback( id ).pull; + } ); + + IntCache DYNAMIC = new IntCache<>( + method -> ( instance, context, args ) -> ((IDynamicLuaObject) instance).callMethod( context, method, args ) + ); + + String[] EMPTY_METHODS = new String[0]; + + @Nonnull + MethodResult apply( @Nonnull Object target, @Nonnull ILuaContext context, @Nonnull IArguments args ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/core/asm/NamedMethod.java b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java new file mode 100644 index 000000000..d7525fc7f --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/NamedMethod.java @@ -0,0 +1,40 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import javax.annotation.Nonnull; + +public class NamedMethod +{ + private final String name; + private final T method; + private final boolean nonYielding; + + NamedMethod( String name, T method, boolean nonYielding ) + { + this.name = name; + this.method = method; + this.nonYielding = nonYielding; + } + + @Nonnull + public String getName() + { + return name; + } + + @Nonnull + public T getMethod() + { + return method; + } + + public boolean nonYielding() + { + return nonYielding; + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/ObjectSource.java b/src/main/java/dan200/computercraft/core/asm/ObjectSource.java new file mode 100644 index 000000000..f05ac7070 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/ObjectSource.java @@ -0,0 +1,33 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import java.util.function.BiConsumer; + +/** + * A Lua object which exposes additional methods. + * + * This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely + * happy with the interface - something I'd like to think about first. + */ +public interface ObjectSource +{ + Iterable getExtra(); + + static void allMethods( Generator generator, Object object, BiConsumer> accept ) + { + for( NamedMethod method : generator.getMethods( object.getClass() ) ) accept.accept( object, method ); + + if( object instanceof ObjectSource ) + { + for( Object extra : ((ObjectSource) object).getExtra() ) + { + for( NamedMethod method : generator.getMethods( extra.getClass() ) ) accept.accept( extra, method ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java b/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java new file mode 100644 index 000000000..637cfb806 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/PeripheralMethod.java @@ -0,0 +1,33 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; + +import javax.annotation.Nonnull; +import java.util.Arrays; + +public interface PeripheralMethod +{ + Generator GENERATOR = new Generator<>( PeripheralMethod.class, Arrays.asList( ILuaContext.class, IComputerAccess.class ), + m -> ( target, context, computer, args ) -> { + long id = context.issueMainThreadTask( () -> TaskCallback.checkUnwrap( m.apply( target, context, computer, args ) ) ); + return new TaskCallback( id ).pull; + } ); + + IntCache DYNAMIC = new IntCache<>( + method -> ( instance, context, computer, args ) -> ((IDynamicPeripheral) instance).callMethod( computer, context, method, args ) + ); + + @Nonnull + MethodResult apply( @Nonnull Object target, @Nonnull ILuaContext context, @Nonnull IComputerAccess computer, @Nonnull IArguments args ) throws LuaException; +} diff --git a/src/main/java/dan200/computercraft/core/asm/Reflect.java b/src/main/java/dan200/computercraft/core/asm/Reflect.java new file mode 100644 index 000000000..429402206 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/Reflect.java @@ -0,0 +1,95 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import dan200.computercraft.ComputerCraft; +import org.objectweb.asm.MethodVisitor; + +import javax.annotation.Nullable; +import java.lang.reflect.*; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Optional; + +import static org.objectweb.asm.Opcodes.ICONST_0; + +final class Reflect +{ + static final java.lang.reflect.Type OPTIONAL_IN = Optional.class.getTypeParameters()[0]; + + private Reflect() + { + } + + @Nullable + static String getLuaName( Class klass ) + { + if( klass.isPrimitive() ) + { + if( klass == int.class ) return "Int"; + if( klass == boolean.class ) return "Boolean"; + if( klass == double.class ) return "Double"; + if( klass == long.class ) return "Long"; + } + else + { + if( klass == Map.class ) return "Table"; + if( klass == String.class ) return "String"; + if( klass == ByteBuffer.class ) return "Bytes"; + } + + return null; + } + + @Nullable + static Class getRawType( Method method, Type root, boolean allowParameter ) + { + Type underlying = root; + while( true ) + { + if( underlying instanceof Class ) return (Class) underlying; + + if( underlying instanceof ParameterizedType ) + { + ParameterizedType type = (ParameterizedType) underlying; + if( !allowParameter ) + { + for( java.lang.reflect.Type arg : type.getActualTypeArguments() ) + { + if( arg instanceof WildcardType ) continue; + if( arg instanceof TypeVariable && ((TypeVariable) arg).getName().startsWith( "capture#" ) ) + { + continue; + } + + ComputerCraft.log.error( "Method {}.{} has generic type {} with non-wildcard argument {}.", method.getDeclaringClass(), method.getName(), root, arg ); + return null; + } + } + + // Continue to extract from this child + underlying = type.getRawType(); + continue; + } + + ComputerCraft.log.error( "Method {}.{} has unknown generic type {}.", method.getDeclaringClass(), method.getName(), root ); + return null; + } + } + + static void loadInt( MethodVisitor visitor, int value ) + { + if( value >= -1 && value <= 5 ) + { + visitor.visitInsn( ICONST_0 + value ); + } + else + { + visitor.visitLdcInsn( value ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/TaskCallback.java b/src/main/java/dan200/computercraft/core/asm/TaskCallback.java new file mode 100644 index 000000000..9ea4e67ad --- /dev/null +++ b/src/main/java/dan200/computercraft/core/asm/TaskCallback.java @@ -0,0 +1,58 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.lua.ILuaCallback; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.MethodResult; + +import javax.annotation.Nonnull; +import java.util.Arrays; + +class TaskCallback implements ILuaCallback +{ + final MethodResult pull = MethodResult.pullEvent( "task_complete", this ); + private final long task; + + TaskCallback( long task ) + { + this.task = task; + } + + @Nonnull + @Override + public MethodResult resume( Object[] response ) throws LuaException + { + if( response.length < 3 || !(response[1] instanceof Number) || !(response[2] instanceof Boolean) ) + { + return pull; + } + + if( ((Number) response[1]).longValue() != task ) return pull; + + if( (Boolean) response[2] ) + { + // Extract the return values from the event and return them + return MethodResult.of( Arrays.copyOfRange( response, 3, response.length ) ); + } + else if( response.length >= 4 && response[3] instanceof String ) + { + // Extract the error message from the event and raise it + throw new LuaException( (String) response[3] ); + } + else + { + throw new LuaException( "error" ); + } + } + + public static Object[] checkUnwrap( MethodResult result ) + { + if( result.getCallback() != null ) throw new IllegalStateException( "Cannot return MethodResult currently" ); + return result.getResult(); + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/ApiWrapper.java b/src/main/java/dan200/computercraft/core/computer/ApiWrapper.java index 96be03c3e..693045dfe 100644 --- a/src/main/java/dan200/computercraft/core/computer/ApiWrapper.java +++ b/src/main/java/dan200/computercraft/core/computer/ApiWrapper.java @@ -6,11 +6,6 @@ package dan200.computercraft.core.computer; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; /** * A wrapper for {@link ILuaAPI}s which cleans up after a {@link ComputerSystem} when the computer is shutdown. @@ -51,17 +46,8 @@ public void shutdown() system.unmountAll(); } - @Nonnull - @Override - public String[] getMethodNames() + public ILuaAPI getDelegate() { - return delegate.getMethodNames(); - } - - @Nullable - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException - { - return delegate.callMethod( context, method, arguments ); + return delegate; } } diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java index 55f02086d..8affb0f01 100644 --- a/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -44,7 +44,7 @@ public class Computer // Additional state about the computer and its environment. private boolean m_blinking = false; private final Environment internalEnvironment = new Environment( this ); - private AtomicBoolean externalOutputChanged = new AtomicBoolean(); + private final AtomicBoolean externalOutputChanged = new AtomicBoolean(); private boolean startRequested; private int m_ticksSinceStart = -1; diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java index 811168b80..021c4b473 100644 --- a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java +++ b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java @@ -388,8 +388,8 @@ private ILuaMachine createLuaMachine() // Create the lua machine ILuaMachine machine = new CobaltLuaMachine( computer, timeout ); - // Add the APIs - for( ILuaAPI api : apis ) machine.addAPI( api ); + // Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object. + for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api ); // Start the machine running the bios resource MachineResult result = machine.loadBios( biosStream ); diff --git a/src/main/java/dan200/computercraft/core/computer/Environment.java b/src/main/java/dan200/computercraft/core/computer/Environment.java index 819997c92..041973fef 100644 --- a/src/main/java/dan200/computercraft/core/computer/Environment.java +++ b/src/main/java/dan200/computercraft/core/computer/Environment.java @@ -111,7 +111,7 @@ public void reboot() } @Override - public void queueEvent( String event, Object[] args ) + public void queueEvent( String event, Object... args ) { computer.queueEvent( event, args ); } @@ -226,7 +226,7 @@ void tick() if( inputChanged ) { inputChanged = false; - queueEvent( "redstone", null ); + queueEvent( "redstone" ); } synchronized( timers ) @@ -241,7 +241,7 @@ void tick() if( timer.ticksLeft <= 0 ) { // Queue the "timer" event - queueEvent( TIMER_EVENT, new Object[] { entry.getIntKey() } ); + queueEvent( TIMER_EVENT, entry.getIntKey() ); it.remove(); } } diff --git a/src/main/java/dan200/computercraft/core/lua/BasicFunction.java b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java new file mode 100644 index 000000000..3c988f025 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java @@ -0,0 +1,75 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.lua; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.MethodResult; +import dan200.computercraft.core.asm.LuaMethod; +import org.squiddev.cobalt.LuaError; +import org.squiddev.cobalt.LuaState; +import org.squiddev.cobalt.Varargs; +import org.squiddev.cobalt.function.VarArgFunction; + +/** + * An "optimised" version of {@link ResultInterpreterFunction} which is guaranteed to never yield. + * + * As we never yield, we do not need to push a function to the stack, which removes a small amount of overhead. + */ +class BasicFunction extends VarArgFunction +{ + private final CobaltLuaMachine machine; + private final LuaMethod method; + private final Object instance; + private final ILuaContext context; + private final String name; + + BasicFunction( CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name ) + { + this.machine = machine; + this.method = method; + this.instance = instance; + this.context = context; + this.name = name; + } + + @Override + public Varargs invoke( LuaState luaState, Varargs args ) throws LuaError + { + IArguments arguments = CobaltLuaMachine.toArguments( args ); + MethodResult results; + try + { + results = method.apply( instance, context, arguments ); + } + catch( LuaException e ) + { + throw wrap( e ); + } + catch( Throwable t ) + { + if( ComputerCraft.logPeripheralErrors ) + { + ComputerCraft.log.error( "Error calling " + name + " on " + instance, t ); + } + throw new LuaError( "Java Exception Thrown: " + t, 0 ); + } + + if( results.getCallback() != null ) + { + throw new IllegalStateException( "Cannot have a yielding non-yielding function" ); + } + return machine.toValues( results.getResult() ); + } + + public static LuaError wrap( LuaException exception ) + { + return exception.hasLevel() ? new LuaError( exception.getMessage() ) : new LuaError( exception.getMessage(), exception.getLevel() ); + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java index 8e5e88bb0..dc8291b21 100644 --- a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -7,6 +7,8 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.asm.ObjectSource; import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.MainThread; import dan200.computercraft.core.computer.TimeoutState; @@ -20,13 +22,13 @@ import org.squiddev.cobalt.debug.DebugHandler; import org.squiddev.cobalt.debug.DebugState; import org.squiddev.cobalt.function.LuaFunction; -import org.squiddev.cobalt.function.VarArgFunction; import org.squiddev.cobalt.lib.*; import org.squiddev.cobalt.lib.platform.VoidResourceManipulator; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.InputStream; +import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -116,11 +118,14 @@ public void addAPI( @Nonnull ILuaAPI api ) { // Add the methods of an API to the global table LuaTable table = wrapLuaObject( api ); - String[] names = api.getNames(); - for( String name : names ) + if( table == null ) { - m_globals.rawset( name, table ); + ComputerCraft.log.warn( "API {} does not provide any methods", api ); + table = new LuaTable(); } + + String[] names = api.getNames(); + for( String name : names ) m_globals.rawset( name, table ); } @Override @@ -216,54 +221,38 @@ public void close() m_globals = null; } - private LuaTable wrapLuaObject( ILuaObject object ) + @Nullable + private LuaTable wrapLuaObject( Object object ) { + String[] dynamicMethods = object instanceof IDynamicLuaObject + ? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(), "Methods cannot be null" ) + : LuaMethod.EMPTY_METHODS; + LuaTable table = new LuaTable(); - String[] methods = object.getMethodNames(); - for( int i = 0; i < methods.length; i++ ) + for( int i = 0; i < dynamicMethods.length; i++ ) { - if( methods[i] != null ) - { - final int method = i; - final ILuaObject apiObject = object; - final String methodName = methods[i]; - table.rawset( methodName, new VarArgFunction() - { - @Override - public Varargs invoke( final LuaState state, Varargs args ) throws LuaError - { - Object[] arguments = toObjects( args, 1 ); - Object[] results; - try - { - results = apiObject.callMethod( context, method, arguments ); - } - catch( InterruptedException e ) - { - throw new InterruptedError( e ); - } - catch( LuaException e ) - { - throw new LuaError( e.getMessage(), e.getLevel() ); - } - catch( Throwable t ) - { - if( ComputerCraft.logPeripheralErrors ) - { - ComputerCraft.log.error( "Error calling " + methodName + " on " + apiObject, t ); - } - throw new LuaError( "Java Exception Thrown: " + t, 0 ); - } - return toValues( results ); - } - } ); - } + String method = dynamicMethods[i]; + table.rawset( method, new ResultInterpreterFunction( this, LuaMethod.DYNAMIC.get( i ), object, context, method ) ); } + + ObjectSource.allMethods( LuaMethod.GENERATOR, object, ( instance, method ) -> + table.rawset( method.getName(), method.nonYielding() + ? new BasicFunction( this, method.getMethod(), instance, context, method.getName() ) + : new ResultInterpreterFunction( this, method.getMethod(), instance, context, method.getName() ) ) ); + + try + { + if( table.keyCount() == 0 ) return null; + } + catch( LuaError ignored ) + { + } + return table; } @Nonnull - private LuaValue toValue( @Nullable Object object, @Nonnull Map values ) + private LuaValue toValue( @Nullable Object object, @Nullable Map values ) { if( object == null ) return Constants.NIL; if( object instanceof Number ) return valueOf( ((Number) object).doubleValue() ); @@ -274,13 +263,22 @@ private LuaValue toValue( @Nullable Object object, @Nonnull Map( 1 ); LuaValue result = values.get( object ); if( result != null ) return result; - if( object instanceof ILuaObject ) + if( object instanceof IDynamicLuaObject ) { - LuaValue wrapped = wrapLuaObject( (ILuaObject) object ); + LuaValue wrapped = wrapLuaObject( object ); + if( wrapped == null ) wrapped = new LuaTable(); values.put( object, wrapped ); return wrapped; } @@ -318,6 +316,13 @@ private LuaValue toValue( @Nullable Object object, @Nonnull Map result = new IdentityHashMap<>( 0 ); LuaValue[] values = new LuaValue[objects.length]; @@ -339,7 +345,7 @@ private Varargs toValues( Object[] objects ) return varargsOf( values ); } - private static Object toObject( LuaValue value, Map objects ) + static Object toObject( LuaValue value, Map objects ) { switch( value.type() ) { @@ -359,11 +365,12 @@ private static Object toObject( LuaValue value, Map objects ) // Start remembering stuff if( objects == null ) { - objects = new IdentityHashMap<>(); + objects = new IdentityHashMap<>( 1 ); } - else if( objects.containsKey( value ) ) + else { - return objects.get( value ); + Object existing = objects.get( value ); + if( existing != null ) return existing; } Map table = new HashMap<>(); objects.put( value, table ); @@ -384,10 +391,7 @@ else if( objects.containsKey( value ) ) break; } k = keyValue.first(); - if( k.isNil() ) - { - break; - } + if( k.isNil() ) break; LuaValue v = keyValue.arg( 2 ); Object keyObject = toObject( k, objects ); @@ -404,19 +408,19 @@ else if( objects.containsKey( value ) ) } } - private static Object[] toObjects( Varargs values, int startIdx ) + static Object[] toObjects( Varargs values ) { int count = values.count(); - Object[] objects = new Object[count - startIdx + 1]; - for( int n = startIdx; n <= count; n++ ) - { - int i = n - startIdx; - LuaValue value = values.arg( n ); - objects[i] = toObject( value, null ); - } + Object[] objects = new Object[count]; + for( int i = 0; i < count; i++ ) objects[i] = toObject( values.arg( i + 1 ), null ); return objects; } + static IArguments toArguments( Varargs values ) + { + return values == Constants.NONE ? VarargArguments.EMPTY : new VarargArguments( values ); + } + /** * A {@link DebugHandler} which observes the {@link TimeoutState} and responds accordingly. */ @@ -500,23 +504,6 @@ private void handleSoftAbort() throws LuaError private class CobaltLuaContext implements ILuaContext { - @Nonnull - @Override - public Object[] yield( Object[] yieldArgs ) throws InterruptedException - { - try - { - LuaState state = m_state; - if( state == null ) throw new InterruptedException(); - Varargs results = LuaThread.yieldBlocking( state, toValues( yieldArgs ) ); - return toObjects( results, 1 ); - } - catch( LuaError e ) - { - throw new IllegalStateException( e.getMessage() ); - } - } - @Override public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException { @@ -560,45 +547,6 @@ public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaExcept throw new LuaException( "Task limit exceeded" ); } } - - @Override - public Object[] executeMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException, InterruptedException - { - // Issue task - final long taskID = issueMainThreadTask( task ); - - // Wait for response - while( true ) - { - Object[] response = pullEvent( "task_complete" ); - if( response.length >= 3 && response[1] instanceof Number && response[2] instanceof Boolean ) - { - if( ((Number) response[1]).intValue() == taskID ) - { - Object[] returnValues = new Object[response.length - 3]; - if( (Boolean) response[2] ) - { - // Extract the return values from the event and return them - System.arraycopy( response, 3, returnValues, 0, returnValues.length ); - return returnValues; - } - else - { - // Extract the error message from the event and raise it - if( response.length >= 4 && response[3] instanceof String ) - { - throw new LuaException( (String) response[3] ); - } - else - { - throw new LuaException(); - } - } - } - } - } - - } } private static final class HardAbortError extends Error diff --git a/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java index 4ca5d4b07..5c9de9cb3 100644 --- a/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/ILuaMachine.java @@ -5,8 +5,8 @@ */ package dan200.computercraft.core.lua; +import dan200.computercraft.api.lua.IDynamicLuaObject; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaObject; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -21,13 +21,13 @@ * mechanism for registering these. * * This should provide implementations of {@link dan200.computercraft.api.lua.ILuaContext}, and the ability to convert - * {@link ILuaObject}s into something the VM understands, as well as handling method calls. + * {@link IDynamicLuaObject}s into something the VM understands, as well as handling method calls. */ public interface ILuaMachine { /** * Inject an API into the global environment of this machine. This should construct an object, as it would for any - * {@link ILuaObject} and set it to all names in {@link ILuaAPI#getNames()}. + * {@link IDynamicLuaObject} and set it to all names in {@link ILuaAPI#getNames()}. * * Called before {@link #loadBios(InputStream)}. * diff --git a/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java b/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java new file mode 100644 index 000000000..09e6ecd0b --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java @@ -0,0 +1,121 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.lua; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.asm.LuaMethod; +import org.squiddev.cobalt.*; +import org.squiddev.cobalt.debug.DebugFrame; +import org.squiddev.cobalt.function.ResumableVarArgFunction; + +import javax.annotation.Nonnull; + +/** + * Calls a {@link LuaMethod}, and interprets the resulting {@link MethodResult}, either returning the result or yielding + * and resuming the supplied continuation. + */ +class ResultInterpreterFunction extends ResumableVarArgFunction +{ + @Nonnull + static class Container + { + ILuaCallback callback; + int errorAdjust; + + Container( ILuaCallback callback, int errorAdjust ) + { + this.callback = callback; + this.errorAdjust = errorAdjust; + } + } + + private final CobaltLuaMachine machine; + private final LuaMethod method; + private final Object instance; + private final ILuaContext context; + private final String name; + + ResultInterpreterFunction( CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name ) + { + this.machine = machine; + this.method = method; + this.instance = instance; + this.context = context; + this.name = name; + } + + @Override + protected Varargs invoke( LuaState state, DebugFrame debugFrame, Varargs args ) throws LuaError, UnwindThrowable + { + IArguments arguments = CobaltLuaMachine.toArguments( args ); + MethodResult results; + try + { + results = method.apply( instance, context, arguments ); + } + catch( LuaException e ) + { + throw wrap( e, 0 ); + } + catch( Throwable t ) + { + if( ComputerCraft.logPeripheralErrors ) + { + ComputerCraft.log.error( "Error calling " + name + " on " + instance, t ); + } + throw new LuaError( "Java Exception Thrown: " + t, 0 ); + } + + ILuaCallback callback = results.getCallback(); + Varargs ret = machine.toValues( results.getResult() ); + + if( callback == null ) return ret; + + debugFrame.state = new Container( callback, results.getErrorAdjust() ); + return LuaThread.yield( state, ret ); + } + + @Override + protected Varargs resumeThis( LuaState state, Container container, Varargs args ) throws LuaError, UnwindThrowable + { + MethodResult results; + Object[] arguments = CobaltLuaMachine.toObjects( args ); + try + { + results = container.callback.resume( arguments ); + } + catch( LuaException e ) + { + throw wrap( e, container.errorAdjust ); + } + catch( Throwable t ) + { + if( ComputerCraft.logPeripheralErrors ) + { + ComputerCraft.log.error( "Error calling " + name + " on " + container.callback, t ); + } + throw new LuaError( "Java Exception Thrown: " + t, 0 ); + } + + Varargs ret = machine.toValues( results.getResult() ); + + ILuaCallback callback = results.getCallback(); + if( callback == null ) return ret; + + container.callback = callback; + return LuaThread.yield( state, ret ); + } + + public static LuaError wrap( LuaException exception, int adjust ) + { + if( !exception.hasLevel() && adjust == 0 ) return new LuaError( exception.getMessage() ); + + int level = exception.getLevel(); + return new LuaError( exception.getMessage(), level <= 0 ? level : level + adjust + 1 ); + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/VarargArguments.java b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java new file mode 100644 index 000000000..4ad8d58a4 --- /dev/null +++ b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java @@ -0,0 +1,102 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.lua; + +import dan200.computercraft.api.lua.IArguments; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaValues; +import org.squiddev.cobalt.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.ByteBuffer; +import java.util.Optional; + +class VarargArguments implements IArguments +{ + static final IArguments EMPTY = new VarargArguments( Constants.NONE ); + + private final Varargs varargs; + private Object[] cache; + + VarargArguments( Varargs varargs ) + { + this.varargs = varargs; + } + + @Override + public int count() + { + return varargs.count(); + } + + @Nullable + @Override + public Object get( int index ) + { + if( index < 0 || index >= varargs.count() ) return null; + + Object[] cache = this.cache; + if( cache == null ) + { + cache = this.cache = new Object[varargs.count()]; + } + else + { + Object existing = cache[index]; + if( existing != null ) return existing; + } + + return cache[index] = CobaltLuaMachine.toObject( varargs.arg( index + 1 ), null ); + } + + @Override + public IArguments drop( int count ) + { + if( count < 0 ) throw new IllegalStateException( "count cannot be negative" ); + if( count == 0 ) return this; + return new VarargArguments( varargs.subargs( count + 1 ) ); + } + + @Override + public double getDouble( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( !(value instanceof LuaNumber) ) throw LuaValues.badArgument( index, "number", value.typeName() ); + return value.toDouble(); + } + + @Override + public long getLong( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( !(value instanceof LuaNumber) ) throw LuaValues.badArgument( index, "number", value.typeName() ); + return value instanceof LuaInteger ? value.toInteger() : (long) LuaValues.checkFinite( index, value.toDouble() ); + } + + @Nonnull + @Override + public ByteBuffer getBytes( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( !(value instanceof LuaString) ) throw LuaValues.badArgument( index, "string", value.typeName() ); + + LuaString str = (LuaString) value; + return ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer(); + } + + @Override + public Optional optBytes( int index ) throws LuaException + { + LuaValue value = varargs.arg( index + 1 ); + if( value.isNil() ) return Optional.empty(); + if( !(value instanceof LuaString) ) throw LuaValues.badArgument( index, "string", value.typeName() ); + + LuaString str = (LuaString) value; + return Optional.of( ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer() ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java index b9c3e7b65..c7ef8986a 100644 --- a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java +++ b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java @@ -9,9 +9,7 @@ import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.*; import dan200.computercraft.shared.computer.blocks.TileCommandComputer; import dan200.computercraft.shared.util.NBTUtil; import net.minecraft.block.Block; @@ -26,43 +24,23 @@ import net.minecraft.world.World; import net.minecraftforge.registries.ForgeRegistries; -import javax.annotation.Nonnull; import java.util.*; -import static dan200.computercraft.api.lua.ArgumentHelper.getInt; -import static dan200.computercraft.api.lua.ArgumentHelper.getString; - public class CommandAPI implements ILuaAPI { - private TileCommandComputer m_computer; + private final TileCommandComputer computer; public CommandAPI( TileCommandComputer computer ) { - m_computer = computer; + this.computer = computer; } - // ILuaAPI implementation - @Override public String[] getNames() { return new String[] { "commands" }; } - @Nonnull - @Override - public String[] getMethodNames() - { - return new String[] { - "exec", - "execAsync", - "list", - "getBlockPosition", - "getBlockInfos", - "getBlockInfo", - }; - } - private static Object createOutput( String output ) { return new Object[] { output }; @@ -70,18 +48,18 @@ private static Object createOutput( String output ) private Object[] doCommand( String command ) { - MinecraftServer server = m_computer.getWorld().getServer(); + MinecraftServer server = computer.getWorld().getServer(); if( server == null || !server.isCommandBlockEnabled() ) { return new Object[] { false, createOutput( "Command blocks disabled by server" ) }; } Commands commandManager = server.getCommandManager(); - TileCommandComputer.CommandReceiver receiver = m_computer.getReceiver(); + TileCommandComputer.CommandReceiver receiver = computer.getReceiver(); try { receiver.clearOutput(); - int result = commandManager.handleCommand( m_computer.getSource(), command ); + int result = commandManager.handleCommand( computer.getSource(), command ); return new Object[] { result > 0, receiver.copyOutput(), result }; } catch( Throwable t ) @@ -91,7 +69,7 @@ private Object[] doCommand( String command ) } } - private static Object getBlockInfo( World world, BlockPos pos ) + private static Map getBlockInfo( World world, BlockPos pos ) { // Get the details of the block BlockState state = world.getBlockState( pos ); @@ -121,121 +99,100 @@ private static Object getPropertyValue( IProperty property, Comparable value ) return property.getName( value ); } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction( mainThread = true ) + public final Object[] exec( String command ) { - switch( method ) + return doCommand( command ); + } + + @LuaFunction + public final long execAsync( ILuaContext context, String command ) throws LuaException + { + return context.issueMainThreadTask( () -> doCommand( command ) ); + } + + @LuaFunction( mainThread = true ) + public final List list( IArguments args ) throws LuaException + { + MinecraftServer server = computer.getWorld().getServer(); + + if( server == null ) return Collections.emptyList(); + CommandNode node = server.getCommandManager().getDispatcher().getRoot(); + for( int j = 0; j < args.count(); j++ ) { - case 0: // exec + String name = args.getString( j ); + node = node.getChild( name ); + if( !(node instanceof LiteralCommandNode) ) return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for( CommandNode child : node.getChildren() ) + { + if( child instanceof LiteralCommandNode ) result.add( child.getName() ); + } + return result; + } + + @LuaFunction + public final Object[] getBlockPosition() + { + // This is probably safe to do on the Lua thread. Probably. + BlockPos pos = computer.getPos(); + return new Object[] { pos.getX(), pos.getY(), pos.getZ() }; + } + + @LuaFunction( mainThread = true ) + public final List> getBlockInfos( int minX, int minY, int minZ, int maxX, int maxY, int maxZ ) throws LuaException + { + // Get the details of the block + World world = computer.getWorld(); + BlockPos min = new BlockPos( + Math.min( minX, maxX ), + Math.min( minY, maxY ), + Math.min( minZ, maxZ ) + ); + BlockPos max = new BlockPos( + Math.max( minX, maxX ), + Math.max( minY, maxY ), + Math.max( minZ, maxZ ) + ); + if( !World.isValid( min ) || !World.isValid( max ) ) + { + throw new LuaException( "Co-ordinates out of range" ); + } + + int blocks = (max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1) * (max.getZ() - min.getZ() + 1); + if( blocks > 4096 ) throw new LuaException( "Too many blocks" ); + + List> results = new ArrayList<>( blocks ); + for( int y = min.getY(); y <= max.getY(); y++ ) + { + for( int z = min.getZ(); z <= max.getZ(); z++ ) { - final String command = getString( arguments, 0 ); - return context.executeMainThreadTask( () -> doCommand( command ) ); - } - case 1: // execAsync - { - final String command = getString( arguments, 0 ); - long taskID = context.issueMainThreadTask( () -> doCommand( command ) ); - return new Object[] { taskID }; - } - case 2: - // list - return context.executeMainThreadTask( () -> + for( int x = min.getX(); x <= max.getX(); x++ ) { - MinecraftServer server = m_computer.getWorld().getServer(); - - if( server == null ) return new Object[] { Collections.emptyMap() }; - CommandNode node = server.getCommandManager().getDispatcher().getRoot(); - for( int j = 0; j < arguments.length; j++ ) - { - String name = getString( arguments, j ); - node = node.getChild( name ); - if( !(node instanceof LiteralCommandNode) ) return new Object[] { Collections.emptyMap() }; - } - - List result = new ArrayList<>(); - for( CommandNode child : node.getChildren() ) - { - if( child instanceof LiteralCommandNode ) result.add( child.getName() ); - } - return new Object[] { result }; - } ); - case 3: // getBlockPosition - { - // This is probably safe to do on the Lua thread. Probably. - BlockPos pos = m_computer.getPos(); - return new Object[] { pos.getX(), pos.getY(), pos.getZ() }; + BlockPos pos = new BlockPos( x, y, z ); + results.add( getBlockInfo( world, pos ) ); + } } - case 4: - { - // getBlockInfos - final int minX = getInt( arguments, 0 ); - final int minY = getInt( arguments, 1 ); - final int minZ = getInt( arguments, 2 ); - final int maxX = getInt( arguments, 3 ); - final int maxY = getInt( arguments, 4 ); - final int maxZ = getInt( arguments, 5 ); - return context.executeMainThreadTask( () -> - { - // Get the details of the block - World world = m_computer.getWorld(); - BlockPos min = new BlockPos( - Math.min( minX, maxX ), - Math.min( minY, maxY ), - Math.min( minZ, maxZ ) - ); - BlockPos max = new BlockPos( - Math.max( minX, maxX ), - Math.max( minY, maxY ), - Math.max( minZ, maxZ ) - ); - if( !World.isValid( min ) || !World.isValid( max ) ) - { - throw new LuaException( "Co-ordinates out of range" ); - } + } - int blocks = (max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1) * (max.getZ() - min.getZ() + 1); - if( blocks > 4096 ) throw new LuaException( "Too many blocks" ); + return results; + } - List results = new ArrayList<>( blocks ); - for( int y = min.getY(); y <= max.getY(); y++ ) - { - for( int z = min.getZ(); z <= max.getZ(); z++ ) - { - for( int x = min.getX(); x <= max.getX(); x++ ) - { - BlockPos pos = new BlockPos( x, y, z ); - results.add( getBlockInfo( world, pos ) ); - } - } - } - return new Object[] { results }; - } ); - } - case 5: - { - // getBlockInfo - final int x = getInt( arguments, 0 ); - final int y = getInt( arguments, 1 ); - final int z = getInt( arguments, 2 ); - return context.executeMainThreadTask( () -> - { - // Get the details of the block - World world = m_computer.getWorld(); - BlockPos position = new BlockPos( x, y, z ); - if( World.isValid( position ) ) - { - return new Object[] { getBlockInfo( world, position ) }; - } - else - { - throw new LuaException( "Co-ordinates out of range" ); - } - } ); - } - default: - { - return null; - } + @LuaFunction( mainThread = true ) + public final Map getBlockInfo( int x, int y, int z ) throws LuaException + { + // Get the details of the block + World world = computer.getWorld(); + BlockPos position = new BlockPos( x, y, z ); + if( World.isValid( position ) ) + { + return getBlockInfo( world, position ); + } + else + { + throw new LuaException( "Co-ordinates out of range" ); } } } diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java index a536a9e29..71b394256 100644 --- a/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java @@ -5,69 +5,63 @@ */ package dan200.computercraft.shared.computer.blocks; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; import javax.annotation.Nonnull; public class ComputerPeripheral implements IPeripheral { - private final String m_type; - private final ComputerProxy m_computer; + private final String type; + private final ComputerProxy computer; public ComputerPeripheral( String type, ComputerProxy computer ) { - m_type = type; - m_computer = computer; + this.type = type; + this.computer = computer; } - // IPeripheral implementation - @Nonnull @Override public String getType() { - return m_type; + return type; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final void turnOn() { - return new String[] { - "turnOn", - "shutdown", - "reboot", - "getID", - "isOn", - "getLabel", - }; + computer.turnOn(); } - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) + @LuaFunction + public final void shutdown() { - switch( method ) - { - case 0: // turnOn - m_computer.turnOn(); - return null; - case 1: // shutdown - m_computer.shutdown(); - return null; - case 2: // reboot - m_computer.reboot(); - return null; - case 3: // getID - return new Object[] { m_computer.assignID() }; - case 4: // isOn - return new Object[] { m_computer.isOn() }; - case 5: // getLabel - return new Object[] { m_computer.getLabel() }; - default: - return null; - } + computer.shutdown(); + } + + @LuaFunction + public final void reboot() + { + computer.reboot(); + } + + @LuaFunction + public final int getID() + { + return computer.assignID(); + } + + @LuaFunction + public final boolean isOn() + { + return computer.isOn(); + } + + @LuaFunction + public final String getLabel() + { + return computer.getLabel(); } @Override @@ -80,6 +74,6 @@ public boolean equals( IPeripheral other ) @Override public Object getTarget() { - return m_computer.getTile(); + return computer.getTile(); } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java index 2ed7dd1ea..383f65f62 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java @@ -5,27 +5,21 @@ */ package dan200.computercraft.shared.peripheral.commandblock; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; import net.minecraft.tileentity.CommandBlockTileEntity; import javax.annotation.Nonnull; -import static dan200.computercraft.api.lua.ArgumentHelper.getString; - public class CommandBlockPeripheral implements IPeripheral { - private final CommandBlockTileEntity m_commandBlock; + private final CommandBlockTileEntity commandBlock; public CommandBlockPeripheral( CommandBlockTileEntity commandBlock ) { - m_commandBlock = commandBlock; + this.commandBlock = commandBlock; } - // IPeripheral methods - @Nonnull @Override public String getType() @@ -33,54 +27,25 @@ public String getType() return "command"; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction( mainThread = true ) + public final String getCommand() { - return new String[] { - "getCommand", - "setCommand", - "runCommand", - }; + return commandBlock.getCommandBlockLogic().getCommand(); } - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull final Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction( mainThread = true ) + public final void setCommand( String command ) { - switch( method ) - { - case 0: // getCommand - return context.executeMainThreadTask( () -> new Object[] { - m_commandBlock.getCommandBlockLogic().getCommand(), - } ); - case 1: - { - // setCommand - final String command = getString( arguments, 0 ); - context.issueMainThreadTask( () -> - { - m_commandBlock.getCommandBlockLogic().setCommand( command ); - m_commandBlock.getCommandBlockLogic().updateCommand(); - return null; - } ); - return null; - } - case 2: // runCommand - return context.executeMainThreadTask( () -> - { - m_commandBlock.getCommandBlockLogic().trigger( m_commandBlock.getWorld() ); - int result = m_commandBlock.getCommandBlockLogic().getSuccessCount(); - if( result > 0 ) - { - return new Object[] { true }; - } - else - { - return new Object[] { false, "Command failed" }; - } - } ); - } - return null; + commandBlock.getCommandBlockLogic().setCommand( command ); + commandBlock.getCommandBlockLogic().updateCommand(); + } + + @LuaFunction( mainThread = true ) + public final Object runCommand() + { + commandBlock.getCommandBlockLogic().trigger( commandBlock.getWorld() ); + int result = commandBlock.getCommandBlockLogic().getSuccessCount(); + return result > 0 ? new Object[] { true } : new Object[] { false, "Command failed" }; } @Override diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java index 97d7ef2cf..1a56cd236 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java @@ -5,8 +5,8 @@ */ package dan200.computercraft.shared.peripheral.diskdrive; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; @@ -16,16 +16,15 @@ import net.minecraft.item.ItemStack; import javax.annotation.Nonnull; +import java.util.Optional; -import static dan200.computercraft.api.lua.ArgumentHelper.optString; - -class DiskDrivePeripheral implements IPeripheral +public class DiskDrivePeripheral implements IPeripheral { - private final TileDiskDrive m_diskDrive; + private final TileDiskDrive diskDrive; DiskDrivePeripheral( TileDiskDrive diskDrive ) { - m_diskDrive = diskDrive; + this.diskDrive = diskDrive; } @Nonnull @@ -35,114 +34,110 @@ public String getType() return "drive"; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final boolean isDiskPresent() { - return new String[] { - "isDiskPresent", - "getDiskLabel", - "setDiskLabel", - "hasData", - "getMountPath", - "hasAudio", - "getAudioTitle", - "playAudio", - "stopAudio", - "ejectDisk", - "getDiskID", - }; + return !diskDrive.getDiskStack().isEmpty(); } - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction + public final Object[] getDiskLabel() { - switch( method ) + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return media == null ? null : new Object[] { media.getLabel( stack ) }; + } + + @LuaFunction( mainThread = true ) + public final void setDiskLabel( Optional labelA ) throws LuaException + { + String label = labelA.orElse( null ); + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + if( media == null ) return; + + if( !media.setLabel( stack, StringUtil.normaliseLabel( label ) ) ) { - case 0: // isDiskPresent - return new Object[] { !m_diskDrive.getDiskStack().isEmpty() }; - case 1: // getDiskLabel - { - ItemStack stack = m_diskDrive.getDiskStack(); - IMedia media = MediaProviders.get( stack ); - return media == null ? null : new Object[] { media.getLabel( stack ) }; - } - case 2: // setDiskLabel - { - String label = optString( arguments, 0, null ); - - return context.executeMainThreadTask( () -> { - ItemStack stack = m_diskDrive.getDiskStack(); - IMedia media = MediaProviders.get( stack ); - if( media == null ) return null; - - if( !media.setLabel( stack, StringUtil.normaliseLabel( label ) ) ) - { - throw new LuaException( "Disk label cannot be changed" ); - } - m_diskDrive.setDiskStack( stack ); - return null; - } ); - } - case 3: // hasData - return new Object[] { m_diskDrive.getDiskMountPath( computer ) != null }; - case 4: // getMountPath - return new Object[] { m_diskDrive.getDiskMountPath( computer ) }; - case 5: - { - // hasAudio - ItemStack stack = m_diskDrive.getDiskStack(); - IMedia media = MediaProviders.get( stack ); - return new Object[] { media != null && media.getAudio( stack ) != null }; - } - case 6: - { - // getAudioTitle - ItemStack stack = m_diskDrive.getDiskStack(); - IMedia media = MediaProviders.get( stack ); - return new Object[] { media != null ? media.getAudioTitle( stack ) : false }; - } - case 7: // playAudio - m_diskDrive.playDiskAudio(); - return null; - case 8: // stopAudio - m_diskDrive.stopDiskAudio(); - return null; - case 9: // eject - m_diskDrive.ejectDisk(); - return null; - case 10: // getDiskID - { - ItemStack disk = m_diskDrive.getDiskStack(); - return disk.getItem() instanceof ItemDisk ? new Object[] { ItemDisk.getDiskID( disk ) } : null; - } - default: - return null; + throw new LuaException( "Disk label cannot be changed" ); } + diskDrive.setDiskStack( stack ); + } + + @LuaFunction + public final boolean hasData( IComputerAccess computer ) + { + return diskDrive.getDiskMountPath( computer ) != null; + } + + @LuaFunction + public final String getMountPath( IComputerAccess computer ) + { + return diskDrive.getDiskMountPath( computer ); + } + + @LuaFunction + public final boolean hasAudio() + { + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return media != null && media.getAudio( stack ) != null; + } + + @LuaFunction + public final Object getAudioTitle() + { + ItemStack stack = diskDrive.getDiskStack(); + IMedia media = MediaProviders.get( stack ); + return media != null ? media.getAudioTitle( stack ) : false; + } + + @LuaFunction + public final void playAudio() + { + diskDrive.playDiskAudio(); + } + + @LuaFunction + public final void stopAudio() + { + diskDrive.stopDiskAudio(); + } + + @LuaFunction + public final void eject() + { + diskDrive.ejectDisk(); + } + + @LuaFunction + public final Object[] getDiskID() + { + ItemStack disk = diskDrive.getDiskStack(); + return disk.getItem() instanceof ItemDisk ? new Object[] { ItemDisk.getDiskID( disk ) } : null; } @Override public void attach( @Nonnull IComputerAccess computer ) { - m_diskDrive.mount( computer ); + diskDrive.mount( computer ); } @Override public void detach( @Nonnull IComputerAccess computer ) { - m_diskDrive.unmount( computer ); + diskDrive.unmount( computer ); } @Override public boolean equals( IPeripheral other ) { - return this == other || other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).m_diskDrive == m_diskDrive; + return this == other || other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).diskDrive == diskDrive; } @Nonnull @Override public Object getTarget() { - return m_diskDrive; + return diskDrive; } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java index 79a8422a4..a6337d1dc 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java @@ -434,7 +434,7 @@ private synchronized void mountDisk( IComputerAccess computer ) info.mountPath = null; } } - computer.queueEvent( "disk", new Object[] { computer.getAttachmentName() } ); + computer.queueEvent( "disk", computer.getAttachmentName() ); } } @@ -449,7 +449,7 @@ private synchronized void unmountDisk( IComputerAccess computer ) computer.unmount( info.mountPath ); info.mountPath = null; } - computer.queueEvent( "disk_eject", new Object[] { computer.getAttachmentName() } ); + computer.queueEvent( "disk_eject", computer.getAttachmentName() ); } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java index 7a29744a7..32df37b98 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemPeripheral.java @@ -5,8 +5,8 @@ */ package dan200.computercraft.shared.peripheral.modem; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.network.IPacketNetwork; import dan200.computercraft.api.network.IPacketReceiver; import dan200.computercraft.api.network.IPacketSender; @@ -20,8 +20,6 @@ import java.util.HashSet; import java.util.Set; -import static dan200.computercraft.api.lua.ArgumentHelper.getInt; - public abstract class ModemPeripheral implements IPeripheral, IPacketSender, IPacketReceiver { private IPacketNetwork m_network; @@ -52,11 +50,6 @@ private synchronized void setNetwork( IPacketNetwork network ) if( m_network != null ) m_network.addReceiver( this ); } - protected void switchNetwork() - { - setNetwork( getNetwork() ); - } - public void destroy() { setNetwork( null ); @@ -71,9 +64,8 @@ public void receiveSameDimension( @Nonnull Packet packet, double distance ) { for( IComputerAccess computer : m_computers ) { - computer.queueEvent( "modem_message", new Object[] { - computer.getAttachmentName(), packet.getChannel(), packet.getReplyChannel(), packet.getPayload(), distance, - } ); + computer.queueEvent( "modem_message", + computer.getAttachmentName(), packet.getChannel(), packet.getReplyChannel(), packet.getPayload(), distance ); } } } @@ -87,17 +79,14 @@ public void receiveDifferentDimension( @Nonnull Packet packet ) { for( IComputerAccess computer : m_computers ) { - computer.queueEvent( "modem_message", new Object[] { - computer.getAttachmentName(), packet.getChannel(), packet.getReplyChannel(), packet.getPayload(), - } ); + computer.queueEvent( "modem_message", + computer.getAttachmentName(), packet.getChannel(), packet.getReplyChannel(), packet.getPayload() ); } } } protected abstract IPacketNetwork getNetwork(); - // IPeripheral implementation - @Nonnull @Override public String getType() @@ -105,90 +94,64 @@ public String getType() return "modem"; } - @Nonnull - @Override - public String[] getMethodNames() + private static int parseChannel( int channel ) throws LuaException { - return new String[] { - "open", - "isOpen", - "close", - "closeAll", - "transmit", - "isWireless", - }; - } - - private static int parseChannel( Object[] arguments, int index ) throws LuaException - { - int channel = getInt( arguments, index ); - if( channel < 0 || channel > 65535 ) - { - throw new LuaException( "Expected number in range 0-65535" ); - } + if( channel < 0 || channel > 65535 ) throw new LuaException( "Expected number in range 0-65535" ); return channel; } - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction + public final void open( int channel ) throws LuaException { - switch( method ) + m_state.open( parseChannel( channel ) ); + } + + @LuaFunction + public final boolean isOpen( int channel ) throws LuaException + { + return m_state.isOpen( parseChannel( channel ) ); + } + + @LuaFunction + public final void close( int channel ) throws LuaException + { + m_state.close( parseChannel( channel ) ); + } + + @LuaFunction + public final void closeAll() + { + m_state.closeAll(); + } + + @LuaFunction + public final void transmit( int channel, int replyChannel, Object payload ) throws LuaException + { + parseChannel( channel ); + parseChannel( replyChannel ); + + World world = getWorld(); + Vec3d position = getPosition(); + IPacketNetwork network = m_network; + + if( world == null || position == null || network == null ) return; + + Packet packet = new Packet( channel, replyChannel, payload, this ); + if( isInterdimensional() ) { - case 0: - { - // open - int channel = parseChannel( arguments, 0 ); - m_state.open( channel ); - return null; - } - case 1: - { - // isOpen - int channel = parseChannel( arguments, 0 ); - return new Object[] { m_state.isOpen( channel ) }; - } - case 2: - { - // close - int channel = parseChannel( arguments, 0 ); - m_state.close( channel ); - return null; - } - case 3: // closeAll - m_state.closeAll(); - return null; - case 4: - { - // transmit - int channel = parseChannel( arguments, 0 ); - int replyChannel = parseChannel( arguments, 1 ); - Object payload = arguments.length > 2 ? arguments[2] : null; - World world = getWorld(); - Vec3d position = getPosition(); - IPacketNetwork network = m_network; - if( world != null && position != null && network != null ) - { - Packet packet = new Packet( channel, replyChannel, payload, this ); - if( isInterdimensional() ) - { - network.transmitInterdimensional( packet ); - } - else - { - network.transmitSameDimension( packet, getRange() ); - } - } - return null; - } - case 5: - { - // isWireless - IPacketNetwork network = m_network; - return new Object[] { network != null && network.isWireless() }; - } - default: - return null; + network.transmitInterdimensional( packet ); } + else + { + network.transmitSameDimension( packet, getRange() ); + } + } + + @LuaFunction + public final boolean isWireless() + { + IPacketNetwork network = m_network; + return network != null && network.isWireless(); } @Override diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java index 88223b71d..620f02cbd 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemPeripheral.java @@ -8,27 +8,28 @@ import com.google.common.collect.ImmutableMap; import dan200.computercraft.api.filesystem.IMount; import dan200.computercraft.api.filesystem.IWritableMount; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.*; import dan200.computercraft.api.network.IPacketNetwork; import dan200.computercraft.api.network.wired.IWiredNode; import dan200.computercraft.api.network.wired.IWiredSender; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IWorkMonitor; +import dan200.computercraft.core.apis.PeripheralAPI; +import dan200.computercraft.core.asm.PeripheralMethod; import dan200.computercraft.shared.peripheral.modem.ModemPeripheral; import dan200.computercraft.shared.peripheral.modem.ModemState; import net.minecraft.world.World; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import static dan200.computercraft.api.lua.ArgumentHelper.getString; - public abstract class WiredModemPeripheral extends ModemPeripheral implements IWiredSender { private final WiredModemElement modem; @@ -71,93 +72,51 @@ public World getWorld() protected abstract WiredModemLocalPeripheral getLocalPeripheral(); //endregion - //region IPeripheral - @Nonnull - @Override - public String[] getMethodNames() + //region Peripheral methods + @LuaFunction + public final Collection getNamesRemote( IComputerAccess computer ) { - String[] methods = super.getMethodNames(); - String[] newMethods = new String[methods.length + 6]; - System.arraycopy( methods, 0, newMethods, 0, methods.length ); - newMethods[methods.length] = "getNamesRemote"; - newMethods[methods.length + 1] = "isPresentRemote"; - newMethods[methods.length + 2] = "getTypeRemote"; - newMethods[methods.length + 3] = "getMethodsRemote"; - newMethods[methods.length + 4] = "callRemote"; - newMethods[methods.length + 5] = "getNameLocal"; - return newMethods; + return getWrappers( computer ).keySet(); } - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction + public final boolean isPresentRemote( IComputerAccess computer, String name ) { - String[] methods = super.getMethodNames(); - switch( method - methods.length ) - { - case 0: - { - // getNamesRemote - Map wrappers = getWrappers( computer ); - Map table = new HashMap<>(); - if( wrappers != null ) - { - int idx = 1; - for( String name : wrappers.keySet() ) table.put( idx++, name ); - } - return new Object[] { table }; - } - case 1: - { - // isPresentRemote - String name = getString( arguments, 0 ); - return new Object[] { getWrapper( computer, name ) != null }; - } - case 2: - { - // getTypeRemote - String name = getString( arguments, 0 ); - RemotePeripheralWrapper wrapper = getWrapper( computer, name ); - return wrapper != null ? new Object[] { wrapper.getType() } : null; - } - case 3: - { - // getMethodsRemote - String name = getString( arguments, 0 ); - RemotePeripheralWrapper wrapper = getWrapper( computer, name ); - if( wrapper == null ) return null; + return getWrapper( computer, name ) != null; + } - String[] methodNames = wrapper.getMethodNames(); - Map table = new HashMap<>(); - for( int i = 0; i < methodNames.length; i++ ) - { - table.put( i + 1, methodNames[i] ); - } - return new Object[] { table }; - } - case 4: - { - // callRemote - String remoteName = getString( arguments, 0 ); - String methodName = getString( arguments, 1 ); - RemotePeripheralWrapper wrapper = getWrapper( computer, remoteName ); - if( wrapper == null ) throw new LuaException( "No peripheral: " + remoteName ); + @LuaFunction + public final Object[] getTypeRemote( IComputerAccess computer, String name ) + { + RemotePeripheralWrapper wrapper = getWrapper( computer, name ); + return wrapper != null ? new Object[] { wrapper.getType() } : null; + } - Object[] methodArgs = new Object[arguments.length - 2]; - System.arraycopy( arguments, 2, methodArgs, 0, arguments.length - 2 ); - return wrapper.callMethod( context, methodName, methodArgs ); - } - case 5: - { - // getNameLocal - String local = getLocalPeripheral().getConnectedName(); - return local == null ? null : new Object[] { local }; - } - default: - { - // The regular modem methods - return super.callMethod( computer, context, method, arguments ); - } - } + @LuaFunction + public final Object[] getMethodsRemote( IComputerAccess computer, String name ) + { + RemotePeripheralWrapper wrapper = getWrapper( computer, name ); + if( wrapper == null ) return null; + + return new Object[] { wrapper.getMethodNames() }; + } + + @LuaFunction + public final MethodResult callRemote( IComputerAccess computer, ILuaContext context, IArguments arguments ) throws LuaException + { + String remoteName = arguments.getString( 0 ); + String methodName = arguments.getString( 1 ); + RemotePeripheralWrapper wrapper = getWrapper( computer, remoteName ); + if( wrapper == null ) throw new LuaException( "No peripheral: " + remoteName ); + + return wrapper.callMethod( context, methodName, arguments.drop( 2 ) ); + } + + @LuaFunction + public final Object[] getNameLocal() + { + String local = getLocalPeripheral().getConnectedName(); + return local == null ? null : new Object[] { local }; } @Override @@ -267,67 +226,52 @@ private RemotePeripheralWrapper getWrapper( IComputerAccess computer, String rem private static class RemotePeripheralWrapper implements IComputerAccess { - private final WiredModemElement m_element; - private final IPeripheral m_peripheral; - private final IComputerAccess m_computer; - private final String m_name; + private final WiredModemElement element; + private final IPeripheral peripheral; + private final IComputerAccess computer; + private final String name; - private final String m_type; - private final String[] m_methods; - private final Map m_methodMap; + private final String type; + private final Map methodMap; RemotePeripheralWrapper( WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name ) { - m_element = element; - m_peripheral = peripheral; - m_computer = computer; - m_name = name; + this.element = element; + this.peripheral = peripheral; + this.computer = computer; + this.name = name; - m_type = peripheral.getType(); - m_methods = peripheral.getMethodNames(); - assert m_type != null; - assert m_methods != null; - - m_methodMap = new HashMap<>(); - for( int i = 0; i < m_methods.length; i++ ) - { - if( m_methods[i] != null ) - { - m_methodMap.put( m_methods[i], i ); - } - } + type = Objects.requireNonNull( peripheral.getType(), "Peripheral type cannot be null" ); + methodMap = PeripheralAPI.getMethods( peripheral ); } public void attach() { - m_peripheral.attach( this ); - m_computer.queueEvent( "peripheral", new Object[] { getAttachmentName() } ); + peripheral.attach( this ); + computer.queueEvent( "peripheral", getAttachmentName() ); } public void detach() { - m_peripheral.detach( this ); - m_computer.queueEvent( "peripheral_detach", new Object[] { getAttachmentName() } ); + peripheral.detach( this ); + computer.queueEvent( "peripheral_detach", getAttachmentName() ); } public String getType() { - return m_type; + return type; } - public String[] getMethodNames() + public Collection getMethodNames() { - return m_methods; + return methodMap.keySet(); } - public Object[] callMethod( ILuaContext context, String methodName, Object[] arguments ) throws LuaException, InterruptedException + public MethodResult callMethod( ILuaContext context, String methodName, IArguments arguments ) throws LuaException { - if( m_methodMap.containsKey( methodName ) ) - { - int method = m_methodMap.get( methodName ); - return m_peripheral.callMethod( this, context, method, arguments ); - } - throw new LuaException( "No such method " + methodName ); + PeripheralMethod method = methodMap.get( methodName ); + if( method == null ) throw new LuaException( "No such method " + methodName ); + return method.apply( peripheral, context, this, arguments ); } // IComputerAccess implementation @@ -335,66 +279,66 @@ public Object[] callMethod( ILuaContext context, String methodName, Object[] arg @Override public String mount( @Nonnull String desiredLocation, @Nonnull IMount mount ) { - return m_computer.mount( desiredLocation, mount, m_name ); + return computer.mount( desiredLocation, mount, name ); } @Override public String mount( @Nonnull String desiredLocation, @Nonnull IMount mount, @Nonnull String driveName ) { - return m_computer.mount( desiredLocation, mount, driveName ); + return computer.mount( desiredLocation, mount, driveName ); } @Override public String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount ) { - return m_computer.mountWritable( desiredLocation, mount, m_name ); + return computer.mountWritable( desiredLocation, mount, name ); } @Override public String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount, @Nonnull String driveName ) { - return m_computer.mountWritable( desiredLocation, mount, driveName ); + return computer.mountWritable( desiredLocation, mount, driveName ); } @Override public void unmount( String location ) { - m_computer.unmount( location ); + computer.unmount( location ); } @Override public int getID() { - return m_computer.getID(); + return computer.getID(); } @Override - public void queueEvent( @Nonnull String event, Object[] arguments ) + public void queueEvent( @Nonnull String event, Object... arguments ) { - m_computer.queueEvent( event, arguments ); + computer.queueEvent( event, arguments ); } @Nonnull @Override public IWorkMonitor getMainThreadMonitor() { - return m_computer.getMainThreadMonitor(); + return computer.getMainThreadMonitor(); } @Nonnull @Override public String getAttachmentName() { - return m_name; + return name; } @Nonnull @Override public Map getAvailablePeripherals() { - synchronized( m_element.getRemotePeripherals() ) + synchronized( element.getRemotePeripherals() ) { - return ImmutableMap.copyOf( m_element.getRemotePeripherals() ); + return ImmutableMap.copyOf( element.getRemotePeripherals() ); } } @@ -402,9 +346,9 @@ public Map getAvailablePeripherals() @Override public IPeripheral getAvailablePeripheral( @Nonnull String name ) { - synchronized( m_element.getRemotePeripherals() ) + synchronized( element.getRemotePeripherals() ) { - return m_element.getRemotePeripherals().get( name ); + return element.getRemotePeripherals().get( name ); } } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java index a6db98161..1bd677a8c 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessModemPeripheral.java @@ -14,7 +14,7 @@ public abstract class WirelessModemPeripheral extends ModemPeripheral { - private boolean m_advanced; + private final boolean m_advanced; public WirelessModemPeripheral( ModemState state, boolean advanced ) { diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java index 17246c424..67c88dc40 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorPeripheral.java @@ -5,26 +5,23 @@ */ package dan200.computercraft.shared.peripheral.monitor; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.LuaValues; import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.core.apis.TermAPI; +import dan200.computercraft.core.apis.TermMethods; import dan200.computercraft.core.terminal.Terminal; -import dan200.computercraft.shared.util.Palette; -import org.apache.commons.lang3.ArrayUtils; import javax.annotation.Nonnull; -import static dan200.computercraft.api.lua.ArgumentHelper.*; - -public class MonitorPeripheral implements IPeripheral +public class MonitorPeripheral extends TermMethods implements IPeripheral { - private final TileMonitor m_monitor; + private final TileMonitor monitor; public MonitorPeripheral( TileMonitor monitor ) { - m_monitor = monitor; + this.monitor = monitor; } @Nonnull @@ -34,201 +31,58 @@ public String getType() return "monitor"; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final void setTextScale( double scaleArg ) throws LuaException { - return new String[] { - "write", - "scroll", - "setCursorPos", - "setCursorBlink", - "getCursorPos", - "getSize", - "clear", - "clearLine", - "setTextScale", - "setTextColour", - "setTextColor", - "setBackgroundColour", - "setBackgroundColor", - "isColour", - "isColor", - "getTextColour", - "getTextColor", - "getBackgroundColour", - "getBackgroundColor", - "blit", - "setPaletteColour", - "setPaletteColor", - "getPaletteColour", - "getPaletteColor", - "getTextScale", - "getCursorBlink", - }; + int scale = (int) (LuaValues.checkFinite( 0, scaleArg ) * 2.0); + if( scale < 1 || scale > 10 ) throw new LuaException( "Expected number in range 0.5-5" ); + getMonitor().setTextScale( scale ); } - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException + @LuaFunction + public final double getTextScale() throws LuaException { - ServerMonitor monitor = m_monitor.getCachedServerMonitor(); - if( monitor == null ) throw new LuaException( "Monitor has been detached" ); - - Terminal terminal = monitor.getTerminal(); - if( terminal == null ) throw new LuaException( "Monitor has been detached" ); - - switch( method ) - { - case 0: - { - // write - String text = args.length > 0 && args[0] != null ? args[0].toString() : ""; - terminal.write( text ); - terminal.setCursorPos( terminal.getCursorX() + text.length(), terminal.getCursorY() ); - return null; - } - case 1: - { - // scroll - int value = getInt( args, 0 ); - terminal.scroll( value ); - return null; - } - case 2: - { - // setCursorPos - int x = getInt( args, 0 ) - 1; - int y = getInt( args, 1 ) - 1; - terminal.setCursorPos( x, y ); - return null; - } - case 3: - { - // setCursorBlink - boolean blink = getBoolean( args, 0 ); - terminal.setCursorBlink( blink ); - return null; - } - case 4: // getCursorPos - return new Object[] { terminal.getCursorX() + 1, terminal.getCursorY() + 1 }; - case 5: // getSize - return new Object[] { terminal.getWidth(), terminal.getHeight() }; - case 6: // clear - terminal.clear(); - return null; - case 7: // clearLine - terminal.clearLine(); - return null; - case 8: - { - // setTextScale - int scale = (int) (getFiniteDouble( args, 0 ) * 2.0); - if( scale < 1 || scale > 10 ) - { - throw new LuaException( "Expected number in range 0.5-5" ); - } - monitor.setTextScale( scale ); - return null; - } - case 9: - case 10: - { - // setTextColour/setTextColor - int colour = TermAPI.parseColour( args ); - terminal.setTextColour( colour ); - return null; - } - case 11: - case 12: - { - // setBackgroundColour/setBackgroundColor - int colour = TermAPI.parseColour( args ); - terminal.setBackgroundColour( colour ); - return null; - } - case 13: - case 14: // isColour/isColor - return new Object[] { monitor.isColour() }; - case 15: - case 16: // getTextColour/getTextColor - return TermAPI.encodeColour( terminal.getTextColour() ); - case 17: - case 18: // getBackgroundColour/getBackgroundColor - return TermAPI.encodeColour( terminal.getBackgroundColour() ); - case 19: - { - // blit - String text = getString( args, 0 ); - String textColour = getString( args, 1 ); - String backgroundColour = getString( args, 2 ); - if( textColour.length() != text.length() || backgroundColour.length() != text.length() ) - { - throw new LuaException( "Arguments must be the same length" ); - } - - terminal.blit( text, textColour, backgroundColour ); - terminal.setCursorPos( terminal.getCursorX() + text.length(), terminal.getCursorY() ); - return null; - } - case 20: - case 21: - { - // setPaletteColour/setPaletteColor - int colour = 15 - TermAPI.parseColour( args ); - if( args.length == 2 ) - { - int hex = getInt( args, 1 ); - double[] rgb = Palette.decodeRGB8( hex ); - TermAPI.setColour( terminal, colour, rgb[0], rgb[1], rgb[2] ); - } - else - { - double r = getFiniteDouble( args, 1 ); - double g = getFiniteDouble( args, 2 ); - double b = getFiniteDouble( args, 3 ); - TermAPI.setColour( terminal, colour, r, g, b ); - } - return null; - } - case 22: - case 23: - { - // getPaletteColour/getPaletteColor - Palette palette = terminal.getPalette(); - - int colour = 15 - TermAPI.parseColour( args ); - - if( palette != null ) - { - return ArrayUtils.toObject( palette.getColour( colour ) ); - } - return null; - } - case 24: // getTextScale - return new Object[] { monitor.getTextScale() / 2.0 }; - case 25: - // getCursorBlink - return new Object[] { terminal.getCursorBlink() }; - default: - return null; - } + return getMonitor().getTextScale() / 2.0; } @Override public void attach( @Nonnull IComputerAccess computer ) { - m_monitor.addComputer( computer ); + monitor.addComputer( computer ); } @Override public void detach( @Nonnull IComputerAccess computer ) { - m_monitor.removeComputer( computer ); + monitor.removeComputer( computer ); } @Override public boolean equals( IPeripheral other ) { - return other instanceof MonitorPeripheral && m_monitor == ((MonitorPeripheral) other).m_monitor; + return other instanceof MonitorPeripheral && monitor == ((MonitorPeripheral) other).monitor; + } + + @Nonnull + private ServerMonitor getMonitor() throws LuaException + { + ServerMonitor monitor = this.monitor.getCachedServerMonitor(); + if( monitor == null ) throw new LuaException( "Monitor has been detached" ); + return monitor; + } + + @Nonnull + @Override + public Terminal getTerminal() throws LuaException + { + Terminal terminal = getMonitor().getTerminal(); + if( terminal == null ) throw new LuaException( "Monitor has been detached" ); + return terminal; + } + + @Override + public boolean isColour() throws LuaException + { + return getMonitor().isColour(); } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java index fa692be17..ff185fffc 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java @@ -165,9 +165,7 @@ public void blockTick() for( IComputerAccess computer : monitor.m_computers ) { - computer.queueEvent( "monitor_resize", new Object[] { - computer.getAttachmentName(), - } ); + computer.queueEvent( "monitor_resize", computer.getAttachmentName() ); } } } @@ -635,9 +633,7 @@ private void monitorTouched( float xPos, float yPos, float zPos ) for( IComputerAccess computer : monitor.m_computers ) { - computer.queueEvent( "monitor_touch", new Object[] { - computer.getAttachmentName(), xCharPos, yCharPos, - } ); + computer.queueEvent( "monitor_touch", computer.getAttachmentName(), xCharPos, yCharPos ); } } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java index b699b8eb5..ee83959d4 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java @@ -5,25 +5,22 @@ */ package dan200.computercraft.shared.peripheral.printer; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.shared.util.StringUtil; import javax.annotation.Nonnull; - -import static dan200.computercraft.api.lua.ArgumentHelper.getInt; -import static dan200.computercraft.api.lua.ArgumentHelper.optString; +import java.util.Optional; public class PrinterPeripheral implements IPeripheral { - private final TilePrinter m_printer; + private final TilePrinter printer; public PrinterPeripheral( TilePrinter printer ) { - m_printer = printer; + this.printer = printer; } @Nonnull @@ -33,108 +30,94 @@ public String getType() return "printer"; } - @Nonnull - @Override - public String[] getMethodNames() + // FIXME: There's a theoretical race condition here between getCurrentPage and then using the page. Ideally + // we'd lock on the page, consume it, and unlock. + + // FIXME: None of our page modification functions actually mark the tile as dirty, so the page may not be + // persisted correctly. + + public final void write( Object[] args ) throws LuaException { - return new String[] { - "write", - "setCursorPos", - "getCursorPos", - "getPageSize", - "newPage", - "endPage", - "getInkLevel", - "setPageTitle", - "getPaperLevel", - }; + String text = args.length > 0 && args[0] != null ? args[0].toString() : ""; + Terminal page = getCurrentPage(); + page.write( text ); + page.setCursorPos( page.getCursorX() + text.length(), page.getCursorY() ); } - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException + @LuaFunction + public final Object[] getCursorPos() throws LuaException { - // FIXME: There's a theoretical race condition here between getCurrentPage and then using the page. Ideally - // we'd lock on the page, consume it, and unlock. + Terminal page = getCurrentPage(); + int x = page.getCursorX(); + int y = page.getCursorY(); + return new Object[] { x + 1, y + 1 }; + } - // FIXME: None of our page modification functions actually mark the tile as dirty, so the page may not be - // persisted correctly. - switch( method ) - { - case 0: // write - { - String text = args.length > 0 && args[0] != null ? args[0].toString() : ""; - Terminal page = getCurrentPage(); - page.write( text ); - page.setCursorPos( page.getCursorX() + text.length(), page.getCursorY() ); - return null; - } - case 1: - { - // setCursorPos - int x = getInt( args, 0 ) - 1; - int y = getInt( args, 1 ) - 1; - Terminal page = getCurrentPage(); - page.setCursorPos( x, y ); - return null; - } - case 2: - { - // getCursorPos - Terminal page = getCurrentPage(); - int x = page.getCursorX(); - int y = page.getCursorY(); - return new Object[] { x + 1, y + 1 }; - } - case 3: - { - // getPageSize - Terminal page = getCurrentPage(); - int width = page.getWidth(); - int height = page.getHeight(); - return new Object[] { width, height }; - } - case 4: // newPage - return context.executeMainThreadTask( () -> new Object[] { m_printer.startNewPage() } ); - case 5: // endPage - getCurrentPage(); - return context.executeMainThreadTask( () -> { - getCurrentPage(); - return new Object[] { m_printer.endCurrentPage() }; - } ); - case 6: // getInkLevel - return new Object[] { m_printer.getInkLevel() }; - case 7: - { - // setPageTitle - String title = optString( args, 0, "" ); - getCurrentPage(); - m_printer.setPageTitle( StringUtil.normaliseLabel( title ) ); - return null; - } - case 8: // getPaperLevel - return new Object[] { m_printer.getPaperLevel() }; - default: - return null; - } + @LuaFunction + public final void setCursorPos( int x, int y ) throws LuaException + { + Terminal page = getCurrentPage(); + page.setCursorPos( x - 1, y - 1 ); + } + + @LuaFunction + public final Object[] getPageSize() throws LuaException + { + Terminal page = getCurrentPage(); + int width = page.getWidth(); + int height = page.getHeight(); + return new Object[] { width, height }; + } + + @LuaFunction( mainThread = true ) + public final boolean newPage() + { + return printer.startNewPage(); + } + + @LuaFunction( mainThread = true ) + public final boolean endPage() throws LuaException + { + getCurrentPage(); + return printer.endCurrentPage(); + } + + @LuaFunction + public final void setPageTitle( Optional title ) throws LuaException + { + getCurrentPage(); + printer.setPageTitle( StringUtil.normaliseLabel( title.orElse( "" ) ) ); + } + + @LuaFunction + public final int getInkLevel() + { + return printer.getInkLevel(); + } + + @LuaFunction + public final int getPaperLevel() + { + return printer.getPaperLevel(); } @Override public boolean equals( IPeripheral other ) { - return other instanceof PrinterPeripheral && ((PrinterPeripheral) other).m_printer == m_printer; + return other instanceof PrinterPeripheral && ((PrinterPeripheral) other).printer == printer; } @Nonnull @Override public Object getTarget() { - return m_printer; + return printer; } @Nonnull private Terminal getCurrentPage() throws LuaException { - Terminal currentPage = m_printer.getCurrentPage(); + Terminal currentPage = printer.getCurrentPage(); if( currentPage == null ) throw new LuaException( "Page not started" ); return currentPage; } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java index 0352c8163..4920e7260 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -8,7 +8,7 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; import net.minecraft.network.play.server.SPlaySoundPacket; import net.minecraft.server.MinecraftServer; @@ -20,10 +20,10 @@ import net.minecraft.world.World; import javax.annotation.Nonnull; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; -import static dan200.computercraft.api.lua.ArgumentHelper.getString; -import static dan200.computercraft.api.lua.ArgumentHelper.optFiniteDouble; +import static dan200.computercraft.api.lua.LuaValues.checkFinite; public abstract class SpeakerPeripheral implements IPeripheral { @@ -53,54 +53,30 @@ public String getType() return "speaker"; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final boolean playSound( ILuaContext context, String name, Optional volumeA, Optional pitchA ) throws LuaException { - return new String[] { - "playSound", - "playNote", - }; - } + float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) ); + float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) ); - @Override - public Object[] callMethod( @Nonnull IComputerAccess computerAccess, @Nonnull ILuaContext context, int methodIndex, @Nonnull Object[] args ) throws LuaException - { - switch( methodIndex ) + ResourceLocation identifier; + try { - case 0: // playSound - { - String name = getString( args, 0 ); - float volume = (float) optFiniteDouble( args, 1, 1.0 ); - float pitch = (float) optFiniteDouble( args, 2, 1.0 ); - - ResourceLocation identifier; - try - { - identifier = new ResourceLocation( name ); - } - catch( ResourceLocationException e ) - { - throw new LuaException( "Malformed sound name '" + name + "' " ); - } - - return new Object[] { playSound( context, identifier, volume, pitch, false ) }; - } - - case 1: // playNote - return playNote( args, context ); - - default: - throw new IllegalStateException( "Method index out of range!" ); + identifier = new ResourceLocation( name ); } + catch( ResourceLocationException e ) + { + throw new LuaException( "Malformed sound name '" + name + "' " ); + } + + return playSound( context, identifier, volume, pitch, false ); } - @Nonnull - private synchronized Object[] playNote( Object[] arguments, ILuaContext context ) throws LuaException + @LuaFunction + public final synchronized boolean playNote( ILuaContext context, String name, Optional volumeA, Optional pitchA ) throws LuaException { - String name = getString( arguments, 0 ); - float volume = (float) optFiniteDouble( arguments, 1, 1.0 ); - float pitch = (float) optFiniteDouble( arguments, 2, 1.0 ); + float volume = (float) checkFinite( 1, volumeA.orElse( 1.0 ) ); + float pitch = (float) checkFinite( 2, pitchA.orElse( 1.0 ) ); NoteBlockInstrument instrument = null; for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() ) @@ -113,16 +89,12 @@ private synchronized Object[] playNote( Object[] arguments, ILuaContext context } // Check if the note exists - if( instrument == null ) - { - throw new LuaException( "Invalid instrument, \"" + name + "\"!" ); - } + if( instrument == null ) throw new LuaException( "Invalid instrument, \"" + name + "\"!" ); // If the resource location for note block notes changes, this method call will need to be updated boolean success = playSound( context, instrument.getSound().getRegistryName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true ); - if( success ) m_notesThisTick.incrementAndGet(); - return new Object[] { success }; + return success; } private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException diff --git a/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java b/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java index 9d481758b..15f8df2c0 100644 --- a/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java +++ b/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java @@ -6,8 +6,7 @@ package dan200.computercraft.shared.pocket.apis; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.pocket.IPocketUpgrade; import dan200.computercraft.shared.PocketUpgrades; import dan200.computercraft.shared.pocket.core.PocketServerComputer; @@ -20,15 +19,13 @@ import net.minecraft.util.NonNullList; import net.minecraftforge.items.wrapper.PlayerMainInvWrapper; -import javax.annotation.Nonnull; - public class PocketAPI implements ILuaAPI { - private final PocketServerComputer m_computer; + private final PocketServerComputer computer; public PocketAPI( PocketServerComputer computer ) { - m_computer = computer; + this.computer = computer; } @Override @@ -37,89 +34,68 @@ public String[] getNames() return new String[] { "pocket" }; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction( mainThread = true ) + public final Object[] equipBack() { - return new String[] { - "equipBack", - "unequipBack", - }; + Entity entity = computer.getEntity(); + if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" }; + PlayerEntity player = (PlayerEntity) entity; + PlayerInventory inventory = player.inventory; + IPocketUpgrade previousUpgrade = computer.getUpgrade(); + + // Attempt to find the upgrade, starting in the main segment, and then looking in the opposite + // one. We start from the position the item is currently in and loop round to the start. + IPocketUpgrade newUpgrade = findUpgrade( inventory.mainInventory, inventory.currentItem, previousUpgrade ); + if( newUpgrade == null ) + { + newUpgrade = findUpgrade( inventory.offHandInventory, 0, previousUpgrade ); + } + if( newUpgrade == null ) return new Object[] { false, "Cannot find a valid upgrade" }; + + // Remove the current upgrade + if( previousUpgrade != null ) + { + ItemStack stack = previousUpgrade.getCraftingItem(); + if( !stack.isEmpty() ) + { + stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.currentItem ); + if( !stack.isEmpty() ) + { + WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPositionVec() ); + } + } + } + + // Set the new upgrade + computer.setUpgrade( newUpgrade ); + + return new Object[] { true }; } - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction( mainThread = true ) + public final Object[] unequipBack() { - switch( method ) + Entity entity = computer.getEntity(); + if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" }; + PlayerEntity player = (PlayerEntity) entity; + PlayerInventory inventory = player.inventory; + IPocketUpgrade previousUpgrade = computer.getUpgrade(); + + if( previousUpgrade == null ) return new Object[] { false, "Nothing to unequip" }; + + computer.setUpgrade( null ); + + ItemStack stack = previousUpgrade.getCraftingItem(); + if( !stack.isEmpty() ) { - case 0: - // equipBack - return context.executeMainThreadTask( () -> - { - Entity entity = m_computer.getEntity(); - if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" }; - PlayerEntity player = (PlayerEntity) entity; - PlayerInventory inventory = player.inventory; - IPocketUpgrade previousUpgrade = m_computer.getUpgrade(); - - // Attempt to find the upgrade, starting in the main segment, and then looking in the opposite - // one. We start from the position the item is currently in and loop round to the start. - IPocketUpgrade newUpgrade = findUpgrade( inventory.mainInventory, inventory.currentItem, previousUpgrade ); - if( newUpgrade == null ) - { - newUpgrade = findUpgrade( inventory.offHandInventory, 0, previousUpgrade ); - } - if( newUpgrade == null ) return new Object[] { false, "Cannot find a valid upgrade" }; - - // Remove the current upgrade - if( previousUpgrade != null ) - { - ItemStack stack = previousUpgrade.getCraftingItem(); - if( !stack.isEmpty() ) - { - stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.currentItem ); - if( !stack.isEmpty() ) - { - WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPositionVec() ); - } - } - } - - // Set the new upgrade - m_computer.setUpgrade( newUpgrade ); - - return new Object[] { true }; - } ); - - case 1: - // unequipBack - return context.executeMainThreadTask( () -> - { - Entity entity = m_computer.getEntity(); - if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" }; - PlayerEntity player = (PlayerEntity) entity; - PlayerInventory inventory = player.inventory; - IPocketUpgrade previousUpgrade = m_computer.getUpgrade(); - - if( previousUpgrade == null ) return new Object[] { false, "Nothing to unequip" }; - - m_computer.setUpgrade( null ); - - ItemStack stack = previousUpgrade.getCraftingItem(); - if( !stack.isEmpty() ) - { - stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.currentItem ); - if( stack.isEmpty() ) - { - WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPositionVec() ); - } - } - - return new Object[] { true }; - } ); - default: - return null; + stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.currentItem ); + if( stack.isEmpty() ) + { + WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.getPositionVec() ); + } } + + return new Object[] { true }; } private static IPocketUpgrade findUpgrade( NonNullList inv, int start, IPocketUpgrade previous ) diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java index a124d8e76..ddd19fcea 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java +++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java @@ -5,9 +5,7 @@ */ package dan200.computercraft.shared.turtle.apis; -import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.*; import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.api.turtle.ITurtleCommand; import dan200.computercraft.api.turtle.TurtleCommandResult; @@ -22,343 +20,333 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.registries.ForgeRegistries; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.HashMap; import java.util.Map; - -import static dan200.computercraft.api.lua.ArgumentHelper.*; +import java.util.Optional; public class TurtleAPI implements ILuaAPI { - private IAPIEnvironment m_environment; - private ITurtleAccess m_turtle; + private final IAPIEnvironment environment; + private final ITurtleAccess turtle; public TurtleAPI( IAPIEnvironment environment, ITurtleAccess turtle ) { - m_environment = environment; - m_turtle = turtle; + this.environment = environment; + this.turtle = turtle; } - // ILuaAPI implementation - @Override public String[] getNames() { return new String[] { "turtle" }; } - @Nonnull - @Override - public String[] getMethodNames() + private MethodResult trackCommand( ITurtleCommand command ) { - return new String[] { - "forward", - "back", - "up", - "down", - "turnLeft", - "turnRight", - "dig", - "digUp", - "digDown", - "place", - "placeUp", - "placeDown", - "drop", - "select", - "getItemCount", - "getItemSpace", - "detect", - "detectUp", - "detectDown", - "compare", - "compareUp", - "compareDown", - "attack", - "attackUp", - "attackDown", - "dropUp", - "dropDown", - "suck", - "suckUp", - "suckDown", - "getFuelLevel", - "refuel", - "compareTo", - "transferTo", - "getSelectedSlot", - "getFuelLimit", - "equipLeft", - "equipRight", - "inspect", - "inspectUp", - "inspectDown", - "getItemDetail", - }; + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return turtle.executeCommand( command ); } - private Object[] tryCommand( ILuaContext context, ITurtleCommand command ) throws LuaException, InterruptedException + @LuaFunction + public final MethodResult forward() { - return m_turtle.executeCommand( context, command ); + return trackCommand( new TurtleMoveCommand( MoveDirection.FORWARD ) ); } - private int parseSlotNumber( Object[] arguments, int index ) throws LuaException + @LuaFunction + public final MethodResult back() + { + return trackCommand( new TurtleMoveCommand( MoveDirection.BACK ) ); + } + + @LuaFunction + public final MethodResult up() + { + return trackCommand( new TurtleMoveCommand( MoveDirection.UP ) ); + } + + @LuaFunction + public final MethodResult down() + { + return trackCommand( new TurtleMoveCommand( MoveDirection.DOWN ) ); + } + + @LuaFunction + public final MethodResult turnLeft() + { + return trackCommand( new TurtleTurnCommand( TurnDirection.LEFT ) ); + } + + @LuaFunction + public final MethodResult turnRight() + { + return trackCommand( new TurtleTurnCommand( TurnDirection.RIGHT ) ); + } + + @LuaFunction + public final MethodResult dig( Optional side ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return trackCommand( TurtleToolCommand.dig( InteractDirection.FORWARD, side.orElse( null ) ) ); + } + + @LuaFunction + public final MethodResult digUp( Optional side ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return trackCommand( TurtleToolCommand.dig( InteractDirection.UP, side.orElse( null ) ) ); + } + + @LuaFunction + public final MethodResult digDown( Optional side ) + { + environment.addTrackingChange( TrackingField.TURTLE_OPS ); + return trackCommand( TurtleToolCommand.dig( InteractDirection.DOWN, side.orElse( null ) ) ); + } + + @LuaFunction + public final MethodResult place( IArguments args ) + { + return trackCommand( new TurtlePlaceCommand( InteractDirection.FORWARD, args.getAll() ) ); + } + + @LuaFunction + public final MethodResult placeUp( IArguments args ) + { + return trackCommand( new TurtlePlaceCommand( InteractDirection.UP, args.getAll() ) ); + } + + @LuaFunction + public final MethodResult placeDown( IArguments args ) + { + return trackCommand( new TurtlePlaceCommand( InteractDirection.DOWN, args.getAll() ) ); + } + + @LuaFunction + public final MethodResult drop( Optional count ) throws LuaException + { + return trackCommand( new TurtleDropCommand( InteractDirection.FORWARD, checkCount( count ) ) ); + } + + @LuaFunction + public final MethodResult dropUp( Optional count ) throws LuaException + { + return trackCommand( new TurtleDropCommand( InteractDirection.UP, checkCount( count ) ) ); + } + + @LuaFunction + public final MethodResult dropDown( Optional count ) throws LuaException + { + return trackCommand( new TurtleDropCommand( InteractDirection.DOWN, checkCount( count ) ) ); + } + + @LuaFunction + public final MethodResult select( int slot ) throws LuaException + { + int actualSlot = checkSlot( slot ); + return turtle.executeCommand( turtle -> { + turtle.setSelectedSlot( actualSlot ); + return TurtleCommandResult.success(); + } ); + } + + @LuaFunction + public final int getItemCount( Optional slot ) throws LuaException + { + int actualSlot = checkSlot( slot ).orElse( turtle.getSelectedSlot() ); + return turtle.getInventory().getStackInSlot( actualSlot ).getCount(); + } + + @LuaFunction + public final int getItemSpace( Optional slot ) throws LuaException + { + int actualSlot = checkSlot( slot ).orElse( turtle.getSelectedSlot() ); + ItemStack stack = turtle.getInventory().getStackInSlot( actualSlot ); + return stack.isEmpty() ? 64 : Math.min( stack.getMaxStackSize(), 64 ) - stack.getCount(); + } + + @LuaFunction + public final MethodResult detect() + { + return trackCommand( new TurtleDetectCommand( InteractDirection.FORWARD ) ); + } + + @LuaFunction + public final MethodResult detectUp() + { + return trackCommand( new TurtleDetectCommand( InteractDirection.UP ) ); + } + + @LuaFunction + public final MethodResult detectDown() + { + return trackCommand( new TurtleDetectCommand( InteractDirection.DOWN ) ); + } + + @LuaFunction + public final MethodResult compare() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.FORWARD ) ); + } + + @LuaFunction + public final MethodResult compareUp() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.UP ) ); + } + + @LuaFunction + public final MethodResult compareDown() + { + return trackCommand( new TurtleCompareCommand( InteractDirection.DOWN ) ); + } + + @LuaFunction + public final MethodResult attack( Optional side ) + { + return trackCommand( TurtleToolCommand.attack( InteractDirection.FORWARD, side.orElse( null ) ) ); + } + + @LuaFunction + public final MethodResult attackUp( Optional side ) + { + return trackCommand( TurtleToolCommand.attack( InteractDirection.UP, side.orElse( null ) ) ); + } + + @LuaFunction + public final MethodResult attackDown( Optional side ) + { + return trackCommand( TurtleToolCommand.attack( InteractDirection.DOWN, side.orElse( null ) ) ); + } + + @LuaFunction + public final MethodResult suck( Optional count ) throws LuaException + { + return trackCommand( new TurtleSuckCommand( InteractDirection.FORWARD, checkCount( count ) ) ); + } + + @LuaFunction + public final MethodResult suckUp( Optional count ) throws LuaException + { + return trackCommand( new TurtleSuckCommand( InteractDirection.UP, checkCount( count ) ) ); + } + + @LuaFunction + public final MethodResult suckDown( Optional count ) throws LuaException + { + return trackCommand( new TurtleSuckCommand( InteractDirection.DOWN, checkCount( count ) ) ); + } + + @LuaFunction + public final Object getFuelLevel() + { + return turtle.isFuelNeeded() ? turtle.getFuelLevel() : "unlimited"; + } + + @LuaFunction + public final MethodResult refuel( Optional countA ) throws LuaException + { + int count = countA.orElse( Integer.MAX_VALUE ); + if( count < 0 ) throw new LuaException( "Refuel count " + count + " out of range" ); + return trackCommand( new TurtleRefuelCommand( count ) ); + } + + @LuaFunction + public final MethodResult compareTo( int slot ) throws LuaException + { + return trackCommand( new TurtleCompareToCommand( checkSlot( slot ) ) ); + } + + @LuaFunction + public final MethodResult transferTo( int slotArg, Optional countArg ) throws LuaException + { + int slot = checkSlot( slotArg ); + int count = checkCount( countArg ); + return trackCommand( new TurtleTransferToCommand( slot, count ) ); + } + + @LuaFunction + public final int getSelectedSlot() + { + return turtle.getSelectedSlot() + 1; + } + + @LuaFunction + public final Object getFuelLimit() + { + return turtle.isFuelNeeded() ? turtle.getFuelLimit() : "unlimited"; + } + + @LuaFunction + public final MethodResult equipLeft() + { + return trackCommand( new TurtleEquipCommand( TurtleSide.LEFT ) ); + } + + @LuaFunction + public final MethodResult equipRight() + { + return trackCommand( new TurtleEquipCommand( TurtleSide.RIGHT ) ); + } + + @LuaFunction + public final MethodResult inspect() + { + return trackCommand( new TurtleInspectCommand( InteractDirection.FORWARD ) ); + } + + @LuaFunction + public final MethodResult inspectUp() + { + return trackCommand( new TurtleInspectCommand( InteractDirection.UP ) ); + } + + @LuaFunction + public final MethodResult inspectDown() + { + return trackCommand( new TurtleInspectCommand( InteractDirection.DOWN ) ); + } + + @LuaFunction + public final Object[] getItemDetail( Optional slotArg ) throws LuaException + { + // FIXME: There's a race condition here if the stack is being modified (mutating NBT, etc...) + // on another thread. The obvious solution is to move this into a command, but some programs rely + // on this having a 0-tick delay. + int slot = checkSlot( slotArg ).orElse( turtle.getSelectedSlot() ); + ItemStack stack = turtle.getInventory().getStackInSlot( slot ); + if( stack.isEmpty() ) return new Object[] { null }; + + Item item = stack.getItem(); + String name = ForgeRegistries.ITEMS.getKey( item ).toString(); + int count = stack.getCount(); + + Map table = new HashMap<>(); + table.put( "name", name ); + table.put( "count", count ); + + TurtleActionEvent event = new TurtleInspectItemEvent( turtle, stack, table ); + if( MinecraftForge.EVENT_BUS.post( event ) ) return new Object[] { false, event.getFailureMessage() }; + + return new Object[] { table }; + } + + + private static int checkSlot( int slot ) throws LuaException { - int slot = getInt( arguments, index ); if( slot < 1 || slot > 16 ) throw new LuaException( "Slot number " + slot + " out of range" ); return slot - 1; } - private int parseOptionalSlotNumber( Object[] arguments, int index, int fallback ) throws LuaException + private static Optional checkSlot( Optional slot ) throws LuaException { - if( index >= arguments.length || arguments[index] == null ) return fallback; - return parseSlotNumber( arguments, index ); + return slot.isPresent() ? Optional.of( checkSlot( slot.get() ) ) : Optional.empty(); } - private static int parseCount( Object[] arguments, int index ) throws LuaException + private static int checkCount( Optional countArg ) throws LuaException { - int count = optInt( arguments, index, 64 ); + int count = countArg.orElse( 64 ); if( count < 0 || count > 64 ) throw new LuaException( "Item count " + count + " out of range" ); return count; } - - @Nullable - private static TurtleSide parseSide( Object[] arguments, int index ) throws LuaException - { - String side = optString( arguments, index, null ); - if( side == null ) - { - return null; - } - else if( side.equalsIgnoreCase( "left" ) ) - { - return TurtleSide.LEFT; - } - else if( side.equalsIgnoreCase( "right" ) ) - { - return TurtleSide.RIGHT; - } - else - { - throw new LuaException( "Invalid side" ); - } - } - - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException - { - switch( method ) - { - case 0: // forward - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleMoveCommand( MoveDirection.FORWARD ) ); - case 1: // back - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleMoveCommand( MoveDirection.BACK ) ); - case 2: // up - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleMoveCommand( MoveDirection.UP ) ); - case 3: // down - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleMoveCommand( MoveDirection.DOWN ) ); - case 4: // turnLeft - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleTurnCommand( TurnDirection.LEFT ) ); - case 5: // turnRight - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleTurnCommand( TurnDirection.RIGHT ) ); - case 6: - { - // dig - TurtleSide side = parseSide( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, TurtleToolCommand.dig( InteractDirection.FORWARD, side ) ); - } - case 7: - { - // digUp - TurtleSide side = parseSide( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, TurtleToolCommand.dig( InteractDirection.UP, side ) ); - } - case 8: - { - // digDown - TurtleSide side = parseSide( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, TurtleToolCommand.dig( InteractDirection.DOWN, side ) ); - } - case 9: // place - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtlePlaceCommand( InteractDirection.FORWARD, args ) ); - case 10: // placeUp - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtlePlaceCommand( InteractDirection.UP, args ) ); - case 11: // placeDown - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtlePlaceCommand( InteractDirection.DOWN, args ) ); - case 12: - { - // drop - int count = parseCount( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleDropCommand( InteractDirection.FORWARD, count ) ); - } - case 13: - { - // select - int slot = parseSlotNumber( args, 0 ); - return tryCommand( context, turtle -> { - turtle.setSelectedSlot( slot ); - return TurtleCommandResult.success(); - } ); - } - case 14: - { - // getItemCount - int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() ); - ItemStack stack = m_turtle.getInventory().getStackInSlot( slot ); - return new Object[] { stack.getCount() }; - } - case 15: - { - // getItemSpace - int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() ); - ItemStack stack = m_turtle.getInventory().getStackInSlot( slot ); - return new Object[] { stack.isEmpty() ? 64 : Math.min( stack.getMaxStackSize(), 64 ) - stack.getCount() }; - } - case 16: // detect - return tryCommand( context, new TurtleDetectCommand( InteractDirection.FORWARD ) ); - case 17: // detectUp - return tryCommand( context, new TurtleDetectCommand( InteractDirection.UP ) ); - case 18: // detectDown - return tryCommand( context, new TurtleDetectCommand( InteractDirection.DOWN ) ); - case 19: // compare - return tryCommand( context, new TurtleCompareCommand( InteractDirection.FORWARD ) ); - case 20: // compareUp - return tryCommand( context, new TurtleCompareCommand( InteractDirection.UP ) ); - case 21: // compareDown - return tryCommand( context, new TurtleCompareCommand( InteractDirection.DOWN ) ); - case 22: - { - // attack - TurtleSide side = parseSide( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, TurtleToolCommand.attack( InteractDirection.FORWARD, side ) ); - } - case 23: - { - // attackUp - TurtleSide side = parseSide( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, TurtleToolCommand.attack( InteractDirection.UP, side ) ); - } - case 24: - { - // attackDown - TurtleSide side = parseSide( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, TurtleToolCommand.attack( InteractDirection.DOWN, side ) ); - } - case 25: - { - // dropUp - int count = parseCount( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleDropCommand( InteractDirection.UP, count ) ); - } - case 26: - { - // dropDown - int count = parseCount( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleDropCommand( InteractDirection.DOWN, count ) ); - } - case 27: - { - // suck - int count = parseCount( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleSuckCommand( InteractDirection.FORWARD, count ) ); - } - case 28: - { - // suckUp - int count = parseCount( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleSuckCommand( InteractDirection.UP, count ) ); - } - case 29: - { - // suckDown - int count = parseCount( args, 0 ); - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleSuckCommand( InteractDirection.DOWN, count ) ); - } - case 30: // getFuelLevel - return new Object[] { m_turtle.isFuelNeeded() ? m_turtle.getFuelLevel() : "unlimited" }; - case 31: - { - // refuel - int count = optInt( args, 0, Integer.MAX_VALUE ); - if( count < 0 ) throw new LuaException( "Refuel count " + count + " out of range" ); - return tryCommand( context, new TurtleRefuelCommand( count ) ); - } - case 32: - { - // compareTo - int slot = parseSlotNumber( args, 0 ); - return tryCommand( context, new TurtleCompareToCommand( slot ) ); - } - case 33: - { - // transferTo - int slot = parseSlotNumber( args, 0 ); - int count = parseCount( args, 1 ); - return tryCommand( context, new TurtleTransferToCommand( slot, count ) ); - } - case 34: // getSelectedSlot - return new Object[] { m_turtle.getSelectedSlot() + 1 }; - case 35: // getFuelLimit - return new Object[] { m_turtle.isFuelNeeded() ? m_turtle.getFuelLimit() : "unlimited" }; - case 36: // equipLeft - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleEquipCommand( TurtleSide.LEFT ) ); - case 37: // equipRight - m_environment.addTrackingChange( TrackingField.TURTLE_OPS ); - return tryCommand( context, new TurtleEquipCommand( TurtleSide.RIGHT ) ); - case 38: // inspect - return tryCommand( context, new TurtleInspectCommand( InteractDirection.FORWARD ) ); - case 39: // inspectUp - return tryCommand( context, new TurtleInspectCommand( InteractDirection.UP ) ); - case 40: // inspectDown - return tryCommand( context, new TurtleInspectCommand( InteractDirection.DOWN ) ); - case 41: // getItemDetail - { - // FIXME: There's a race condition here if the stack is being modified (mutating NBT, etc...) - // on another thread. The obvious solution is to move this into a command, but some programs rely - // on this having a 0-tick delay. - int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() ); - ItemStack stack = m_turtle.getInventory().getStackInSlot( slot ); - if( stack.isEmpty() ) return new Object[] { null }; - - Item item = stack.getItem(); - String name = ForgeRegistries.ITEMS.getKey( item ).toString(); - int count = stack.getCount(); - - Map table = new HashMap<>(); - table.put( "name", name ); - table.put( "count", count ); - - TurtleActionEvent event = new TurtleInspectItemEvent( m_turtle, stack, table ); - if( MinecraftForge.EVENT_BUS.post( event ) ) return new Object[] { false, event.getFailureMessage() }; - - return new Object[] { table }; - } - - default: - return null; - } - } } diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java index 44ab1e5b8..28eaded9b 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java @@ -8,8 +8,8 @@ import com.google.common.base.Objects; import com.mojang.authlib.GameProfile; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.ILuaCallback; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.turtle.*; import dan200.computercraft.core.computer.ComputerSide; @@ -517,27 +517,13 @@ private int issueCommand( ITurtleCommand command ) @Nonnull @Override - public Object[] executeCommand( @Nonnull ILuaContext context, @Nonnull ITurtleCommand command ) throws LuaException, InterruptedException + public MethodResult executeCommand( @Nonnull ITurtleCommand command ) { if( getWorld().isRemote ) throw new UnsupportedOperationException( "Cannot run commands on the client" ); // Issue command int commandID = issueCommand( command ); - - // Wait for response - while( true ) - { - Object[] response = context.pullEvent( "turtle_response" ); - if( response.length >= 3 && response[1] instanceof Number && response[2] instanceof Boolean ) - { - if( ((Number) response[1]).intValue() == commandID ) - { - Object[] returnValues = new Object[response.length - 2]; - System.arraycopy( response, 2, returnValues, 0, returnValues.length ); - return returnValues; - } - } - } + return new CommandCallback( commandID ).pull; } @Override @@ -951,4 +937,29 @@ private float getAnimationFraction( float f ) float previous = (float) m_lastAnimationProgress / ANIM_DURATION; return previous + (next - previous) * f; } + + private static final class CommandCallback implements ILuaCallback + { + final MethodResult pull = MethodResult.pullEvent( "turtle_response", this ); + private final int command; + + CommandCallback( int command ) + { + this.command = command; + } + + @Nonnull + @Override + public MethodResult resume( Object[] response ) + { + if( response.length < 3 || !(response[1] instanceof Number) || !(response[2] instanceof Boolean) ) + { + return pull; + } + + if( ((Number) response[1]).intValue() != command ) return pull; + + return MethodResult.of( Arrays.copyOfRange( response, 2, response.length ) ); + } + } } diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java index f33762694..9b5238f85 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java +++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/CraftingTablePeripheral.java @@ -5,16 +5,15 @@ */ package dan200.computercraft.shared.turtle.upgrades; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.lua.LuaFunction; +import dan200.computercraft.api.lua.MethodResult; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.turtle.ITurtleAccess; import dan200.computercraft.shared.turtle.core.TurtleCraftCommand; import javax.annotation.Nonnull; - -import static dan200.computercraft.api.lua.ArgumentHelper.optInt; +import java.util.Optional; public class CraftingTablePeripheral implements IPeripheral { @@ -32,41 +31,17 @@ public String getType() return "workbench"; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final MethodResult craft( Optional count ) throws LuaException { - return new String[] { - "craft", - }; - } - - private static int parseCount( Object[] arguments ) throws LuaException - { - int count = optInt( arguments, 0, 64 ); - if( count < 0 || count > 64 ) throw new LuaException( "Crafting count " + count + " out of range" ); - return count; - } - - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException - { - switch( method ) - { - case 0: - { - // craft - final int limit = parseCount( arguments ); - return turtle.executeCommand( context, new TurtleCraftCommand( limit ) ); - } - default: - return null; - } + int limit = count.orElse( 65 ); + if( limit < 0 || limit > 64 ) throw new LuaException( "Crafting count " + limit + " out of range" ); + return turtle.executeCommand( new TurtleCraftCommand( limit ) ); } @Override public boolean equals( IPeripheral other ) { - return this == other || other instanceof CraftingTablePeripheral; + return other instanceof CraftingTablePeripheral; } } diff --git a/src/main/java/dan200/computercraft/shared/util/StringUtil.java b/src/main/java/dan200/computercraft/shared/util/StringUtil.java index 57ea7d488..64a7f1e2d 100644 --- a/src/main/java/dan200/computercraft/shared/util/StringUtil.java +++ b/src/main/java/dan200/computercraft/shared/util/StringUtil.java @@ -5,6 +5,8 @@ */ package dan200.computercraft.shared.util; +import javax.annotation.Nullable; + public final class StringUtil { private StringUtil() {} @@ -31,16 +33,8 @@ public static String normaliseLabel( String label ) return builder.toString(); } - public static byte[] encodeString( String string ) + public static String toString( @Nullable Object value ) { - byte[] chars = new byte[string.length()]; - - for( int i = 0; i < chars.length; i++ ) - { - char c = string.charAt( i ); - chars[i] = c < 256 ? (byte) c : 63; - } - - return chars; + return value == null ? "" : value.toString(); } } diff --git a/src/test/java/dan200/computercraft/ContramapMatcher.java b/src/test/java/dan200/computercraft/ContramapMatcher.java new file mode 100644 index 000000000..35e7117fa --- /dev/null +++ b/src/test/java/dan200/computercraft/ContramapMatcher.java @@ -0,0 +1,54 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import java.util.function.Function; + +public class ContramapMatcher extends TypeSafeDiagnosingMatcher +{ + private final String desc; + private final Function convert; + private final Matcher matcher; + + public ContramapMatcher( String desc, Function convert, Matcher matcher ) + { + this.desc = desc; + this.convert = convert; + this.matcher = matcher; + } + + @Override + protected boolean matchesSafely( T item, Description mismatchDescription ) + { + U converted = convert.apply( item ); + if( matcher.matches( converted ) ) return true; + + mismatchDescription.appendText( desc ).appendText( " " ); + matcher.describeMismatch( converted, mismatchDescription ); + return false; + } + + @Override + public void describeTo( Description description ) + { + description.appendText( desc ).appendText( " " ).appendDescriptionOf( matcher ); + } + + public static Matcher contramap( Matcher matcher, String desc, Function convert ) + { + return new ContramapMatcher<>( desc, convert, matcher ); + } + + public static Matcher contramap( Matcher matcher, Function convert ) + { + return new ContramapMatcher<>( "-f(_)->", convert, matcher ); + } +} diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index f2d2453a7..91fdc4cad 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -8,8 +8,8 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.filesystem.IWritableMount; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.core.computer.BasicEnvironment; import dan200.computercraft.core.computer.Computer; @@ -44,8 +44,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; -import static dan200.computercraft.api.lua.ArgumentHelper.getTable; -import static dan200.computercraft.api.lua.ArgumentHelper.getType; +import static dan200.computercraft.api.lua.LuaValues.getType; /** * Loads tests from {@code test-rom/spec} and executes them. @@ -107,189 +106,7 @@ public void before() throws IOException computer = new Computer( new BasicEnvironment( mount ), term, 0 ); computer.getEnvironment().setPeripheral( ComputerSide.TOP, new FakeModem() ); - computer.addApi( new ILuaAPI() - { - @Override - public String[] getNames() - { - return new String[] { "cct_test" }; - } - - @Nonnull - @Override - public String[] getMethodNames() - { - return new String[] { "start", "submit", "finish" }; - } - - @Override - public void startup() - { - try - { - computer.getAPIEnvironment().getFileSystem().mount( - "test-rom", "test-rom", - BasicEnvironment.createMount( ComputerTestDelegate.class, "test-rom", "test" ) - ); - } - catch( FileSystemException e ) - { - throw new IllegalStateException( e ); - } - } - - @Nullable - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException - { - switch( method ) - { - case 0: // start: Submit several tests and signal for #get to run - { - LOG.info( "Received tests from computer" ); - DynamicNodeBuilder root = new DynamicNodeBuilder( "" ); - for( Object key : getTable( arguments, 0 ).keySet() ) - { - if( !(key instanceof String) ) throw new LuaException( "Non-key string " + getType( key ) ); - - String name = (String) key; - String[] parts = name.split( "\0" ); - DynamicNodeBuilder builder = root; - for( int i = 0; i < parts.length - 1; i++ ) builder = builder.get( parts[i] ); - builder.runs( parts[parts.length - 1], () -> { - // Run it - lock.lockInterruptibly(); - try - { - // Set the current test - runResult = null; - runFinished = false; - currentTest = name; - - // Tell the computer to run it - LOG.info( "Starting '{}'", formatName( name ) ); - computer.queueEvent( "cct_test_run", new Object[] { name } ); - - long remaining = TIMEOUT; - while( remaining > 0 && computer.isOn() && !runFinished ) - { - tick(); - - long waiting = hasRun.awaitNanos( TICK_TIME ); - if( waiting > 0 ) break; - remaining -= TICK_TIME; - } - - LOG.info( "Finished '{}'", formatName( name ) ); - - if( remaining <= 0 ) - { - throw new IllegalStateException( "Timed out waiting for test" ); - } - else if( !computer.isOn() ) - { - throw new IllegalStateException( "Computer turned off mid-execution" ); - } - - if( runResult != null ) throw runResult; - } - finally - { - lock.unlock(); - currentTest = null; - } - } ); - } - - lock.lockInterruptibly(); - try - { - tests = root; - hasTests.signal(); - } - finally - { - lock.unlock(); - } - - return null; - } - case 1: // submit: Submit the result of a test, allowing the test executor to continue - { - Map tbl = getTable( arguments, 0 ); - String name = (String) tbl.get( "name" ); - String status = (String) tbl.get( "status" ); - String message = (String) tbl.get( "message" ); - String trace = (String) tbl.get( "trace" ); - - StringBuilder wholeMessage = new StringBuilder(); - if( message != null ) wholeMessage.append( message ); - if( trace != null ) - { - if( wholeMessage.length() != 0 ) wholeMessage.append( '\n' ); - wholeMessage.append( trace ); - } - - lock.lockInterruptibly(); - try - { - LOG.info( "'{}' finished with {}", formatName( name ), status ); - - // Skip if a test mismatch - if( !name.equals( currentTest ) ) - { - LOG.warn( "Skipping test '{}', as we're currently executing '{}'", formatName( name ), formatName( currentTest ) ); - return null; - } - - switch( status ) - { - case "ok": - case "pending": - break; - case "fail": - runResult = new AssertionFailedError( wholeMessage.toString() ); - break; - case "error": - runResult = new IllegalStateException( wholeMessage.toString() ); - break; - } - - runFinished = true; - hasRun.signal(); - } - finally - { - lock.unlock(); - } - - return null; - } - case 2: // finish: Signal to after that execution has finished - LOG.info( "Finished" ); - lock.lockInterruptibly(); - try - { - finished = true; - if( arguments.length > 0 ) - { - @SuppressWarnings( "unchecked" ) - Map> finished = (Map>) arguments[0]; - finishedWith = finished; - } - - hasFinished.signal(); - } - finally - { - lock.unlock(); - } - return null; - default: - return null; - } - } - } ); + computer.addApi( new CctTestAPI() ); computer.turnOn(); } @@ -475,4 +292,198 @@ public boolean equals( @Nullable IPeripheral other ) return this == other; } } + + public class CctTestAPI implements ILuaAPI + { + @Override + public String[] getNames() + { + return new String[] { "cct_test" }; + } + + @Override + public void startup() + { + try + { + computer.getAPIEnvironment().getFileSystem().mount( + "test-rom", "test-rom", + BasicEnvironment.createMount( ComputerTestDelegate.class, "test-rom", "test" ) + ); + } + catch( FileSystemException e ) + { + throw new IllegalStateException( e ); + } + } + + @LuaFunction + public final void start( Map tests ) throws LuaException + { + // Submit several tests and signal for #get to run + LOG.info( "Received tests from computer" ); + DynamicNodeBuilder root = new DynamicNodeBuilder( "" ); + for( Object key : tests.keySet() ) + { + if( !(key instanceof String) ) throw new LuaException( "Non-key string " + getType( key ) ); + + String name = (String) key; + String[] parts = name.split( "\0" ); + DynamicNodeBuilder builder = root; + for( int i = 0; i < parts.length - 1; i++ ) builder = builder.get( parts[i] ); + builder.runs( parts[parts.length - 1], () -> { + // Run it + lock.lockInterruptibly(); + try + { + // Set the current test + runResult = null; + runFinished = false; + currentTest = name; + + // Tell the computer to run it + LOG.info( "Starting '{}'", formatName( name ) ); + computer.queueEvent( "cct_test_run", new Object[] { name } ); + + long remaining = TIMEOUT; + while( remaining > 0 && computer.isOn() && !runFinished ) + { + tick(); + + long waiting = hasRun.awaitNanos( TICK_TIME ); + if( waiting > 0 ) break; + remaining -= TICK_TIME; + } + + LOG.info( "Finished '{}'", formatName( name ) ); + + if( remaining <= 0 ) + { + throw new IllegalStateException( "Timed out waiting for test" ); + } + else if( !computer.isOn() ) + { + throw new IllegalStateException( "Computer turned off mid-execution" ); + } + + if( runResult != null ) throw runResult; + } + finally + { + lock.unlock(); + currentTest = null; + } + } ); + } + + try + { + lock.lockInterruptibly(); + } + catch( InterruptedException e ) + { + throw new RuntimeException( e ); + } + try + { + ComputerTestDelegate.this.tests = root; + hasTests.signal(); + } + finally + { + lock.unlock(); + } + } + + @LuaFunction + public final void submit( Map tbl ) + { + // Submit the result of a test, allowing the test executor to continue + String name = (String) tbl.get( "name" ); + if( name == null ) + { + ComputerCraft.log.error( "Oh no: {}", tbl ); + } + String status = (String) tbl.get( "status" ); + String message = (String) tbl.get( "message" ); + String trace = (String) tbl.get( "trace" ); + + StringBuilder wholeMessage = new StringBuilder(); + if( message != null ) wholeMessage.append( message ); + if( trace != null ) + { + if( wholeMessage.length() != 0 ) wholeMessage.append( '\n' ); + wholeMessage.append( trace ); + } + + try + { + lock.lockInterruptibly(); + } + catch( InterruptedException e ) + { + throw new RuntimeException( e ); + } + try + { + LOG.info( "'{}' finished with {}", formatName( name ), status ); + + // Skip if a test mismatch + if( !name.equals( currentTest ) ) + { + LOG.warn( "Skipping test '{}', as we're currently executing '{}'", formatName( name ), formatName( currentTest ) ); + return; + } + + switch( status ) + { + case "ok": + case "pending": + break; + case "fail": + runResult = new AssertionFailedError( wholeMessage.toString() ); + break; + case "error": + runResult = new IllegalStateException( wholeMessage.toString() ); + break; + } + + runFinished = true; + hasRun.signal(); + } + finally + { + lock.unlock(); + } + } + + @LuaFunction + public final void finish( Optional> result ) + { + @SuppressWarnings( "unchecked" ) + Map> finishedResult = (Map>) result.orElse( null ); + LOG.info( "Finished" ); + + // Signal to after that execution has finished + try + { + lock.lockInterruptibly(); + } + catch( InterruptedException e ) + { + throw new RuntimeException( e ); + } + try + { + finished = true; + if( finishedResult != null ) finishedWith = finishedResult; + + hasFinished.signal(); + } + finally + { + lock.unlock(); + } + } + } } diff --git a/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java b/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java index cd7684270..a01cc639a 100644 --- a/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java +++ b/src/test/java/dan200/computercraft/core/apis/ObjectWrapper.java @@ -5,52 +5,47 @@ */ package dan200.computercraft.core.apis; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.ILuaObject; -import dan200.computercraft.api.lua.ILuaTask; -import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.lua.*; +import dan200.computercraft.core.asm.LuaMethod; +import dan200.computercraft.core.asm.NamedMethod; import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; public class ObjectWrapper implements ILuaContext { - private final ILuaObject object; - private final String[] methods; + private final Object object; + private final Map methodMap; - public ObjectWrapper( ILuaObject object ) + public ObjectWrapper( Object object ) { this.object = object; - this.methods = object.getMethodNames(); - } + String[] dynamicMethods = object instanceof IDynamicLuaObject + ? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(), "Methods cannot be null" ) + : LuaMethod.EMPTY_METHODS; - private int findMethod( String method ) - { - for( int i = 0; i < methods.length; i++ ) + List> methods = LuaMethod.GENERATOR.getMethods( object.getClass() ); + + Map methodMap = this.methodMap = new HashMap<>( methods.size() + dynamicMethods.length ); + for( int i = 0; i < dynamicMethods.length; i++ ) { - if( method.equals( methods[i] ) ) return i; + methodMap.put( dynamicMethods[i], LuaMethod.DYNAMIC.get( i ) ); + } + for( NamedMethod method : methods ) + { + methodMap.put( method.getName(), method.getMethod() ); } - return -1; - } - - public boolean hasMethod( String method ) - { - return findMethod( method ) >= 0; } public Object[] call( String name, Object... args ) throws LuaException { - int method = findMethod( name ); - if( method < 0 ) throw new IllegalStateException( "No such method '" + name + "'" ); + LuaMethod method = methodMap.get( name ); + if( method == null ) throw new IllegalStateException( "No such method '" + name + "'" ); - try - { - return object.callMethod( this, method, args ); - } - catch( InterruptedException e ) - { - throw new IllegalStateException( "Should never be interrupted", e ); - } + return method.apply( object, this, new ObjectArguments( args ) ).getResult(); } @SuppressWarnings( "unchecked" ) @@ -64,34 +59,6 @@ public T callOf( Class klass, String name, Object... args ) throws LuaExc return klass.cast( call( name, args )[0] ); } - @Nonnull - @Override - public Object[] pullEvent( @Nullable String filter ) - { - throw new IllegalStateException( "Method should never yield" ); - } - - @Nonnull - @Override - public Object[] pullEventRaw( @Nullable String filter ) - { - throw new IllegalStateException( "Method should never yield" ); - } - - @Nonnull - @Override - public Object[] yield( @Nullable Object[] arguments ) - { - throw new IllegalStateException( "Method should never yield" ); - } - - @Nullable - @Override - public Object[] executeMainThreadTask( @Nonnull ILuaTask task ) - { - throw new IllegalStateException( "Method should never yield" ); - } - @Override public long issueMainThreadTask( @Nonnull ILuaTask task ) { diff --git a/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java b/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java index 3def2ce9f..ad272c9aa 100644 --- a/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java +++ b/src/test/java/dan200/computercraft/core/apis/handles/BinaryReadableHandleTest.java @@ -9,6 +9,7 @@ import dan200.computercraft.core.apis.ObjectWrapper; import org.junit.jupiter.api.Test; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -27,17 +28,16 @@ public void testReadChar() throws LuaException public void testReadShortComplete() throws LuaException { ObjectWrapper wrapper = fromLength( 10 ); - assertEquals( 5, wrapper.callOf( "read", 5 ).length ); + assertEquals( 5, wrapper.callOf( "read", 5 ).remaining() ); } @Test public void testReadShortPartial() throws LuaException { ObjectWrapper wrapper = fromLength( 5 ); - assertEquals( 5, wrapper.callOf( "read", 10 ).length ); + assertEquals( 5, wrapper.callOf( "read", 10 ).remaining() ); } - @Test public void testReadLongComplete() throws LuaException { @@ -56,13 +56,13 @@ public void testReadLongPartial() throws LuaException public void testReadLongPartialSmaller() throws LuaException { ObjectWrapper wrapper = fromLength( 1000 ); - assertEquals( 1000, wrapper.callOf( "read", 11000 ).length ); + assertEquals( 1000, wrapper.callOf( "read", 11000 ).remaining() ); } @Test public void testReadLine() throws LuaException { - ObjectWrapper wrapper = new ObjectWrapper( new BinaryReadableHandle( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) ); + ObjectWrapper wrapper = new ObjectWrapper( BinaryReadableHandle.of( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) ); assertArrayEquals( "hello".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine" ) ); assertArrayEquals( "world\r!".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine" ) ); assertNull( wrapper.call( "readLine" ) ); @@ -71,7 +71,7 @@ public void testReadLine() throws LuaException @Test public void testReadLineTrailing() throws LuaException { - ObjectWrapper wrapper = new ObjectWrapper( new BinaryReadableHandle( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) ); + ObjectWrapper wrapper = new ObjectWrapper( BinaryReadableHandle.of( new ArrayByteChannel( "hello\r\nworld\r!".getBytes( StandardCharsets.UTF_8 ) ) ) ); assertArrayEquals( "hello\r\n".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine", true ) ); assertArrayEquals( "world\r!".getBytes( StandardCharsets.UTF_8 ), wrapper.callOf( "readLine", true ) ); assertNull( wrapper.call( "readLine", true ) ); @@ -81,6 +81,6 @@ private static ObjectWrapper fromLength( int length ) { byte[] input = new byte[length]; Arrays.fill( input, (byte) 'A' ); - return new ObjectWrapper( new BinaryReadableHandle( new ArrayByteChannel( input ) ) ); + return new ObjectWrapper( BinaryReadableHandle.of( new ArrayByteChannel( input ) ) ); } } diff --git a/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java new file mode 100644 index 000000000..8104fa4a1 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -0,0 +1,253 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.core.computer.ComputerSide; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static dan200.computercraft.ContramapMatcher.contramap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class GeneratorTest +{ + @Test + public void testBasic() + { + List> methods = LuaMethod.GENERATOR.getMethods( Basic.class ); + assertThat( methods, contains( + allOf( + named( "go" ), + contramap( is( true ), "non-yielding", NamedMethod::nonYielding ) + ) + ) ); + } + + @Test + public void testIdentical() + { + List> methods = LuaMethod.GENERATOR.getMethods( Basic.class ); + List> methods2 = LuaMethod.GENERATOR.getMethods( Basic.class ); + assertThat( methods, sameInstance( methods2 ) ); + } + + @Test + public void testIdenticalMethods() + { + List> methods = LuaMethod.GENERATOR.getMethods( Basic.class ); + List> methods2 = LuaMethod.GENERATOR.getMethods( Basic2.class ); + assertThat( methods, contains( named( "go" ) ) ); + assertThat( methods.get( 0 ).getMethod(), sameInstance( methods2.get( 0 ).getMethod() ) ); + } + + @Test + public void testEmptyClass() + { + assertThat( LuaMethod.GENERATOR.getMethods( Empty.class ), is( empty() ) ); + } + + @Test + public void testNonPublicClass() + { + assertThat( LuaMethod.GENERATOR.getMethods( NonPublic.class ), is( empty() ) ); + } + + @Test + public void testNonInstance() + { + assertThat( LuaMethod.GENERATOR.getMethods( NonInstance.class ), is( empty() ) ); + } + + @Test + public void testIllegalThrows() + { + assertThat( LuaMethod.GENERATOR.getMethods( IllegalThrows.class ), is( empty() ) ); + } + + @Test + public void testCustomNames() + { + List> methods = LuaMethod.GENERATOR.getMethods( CustomNames.class ); + assertThat( methods, contains( named( "go1" ), named( "go2" ) ) ); + } + + @Test + public void testArgKinds() + { + List> methods = LuaMethod.GENERATOR.getMethods( ArgKinds.class ); + assertThat( methods, containsInAnyOrder( + named( "objectArg" ), named( "intArg" ), named( "optIntArg" ), + named( "context" ), named( "arguments" ) + ) ); + } + + @Test + public void testEnum() throws LuaException + { + List> methods = LuaMethod.GENERATOR.getMethods( EnumMethods.class ); + assertThat( methods, containsInAnyOrder( named( "getEnum" ), named( "optEnum" ) ) ); + + assertThat( apply( methods, new EnumMethods(), "getEnum", "front" ), one( is( "FRONT" ) ) ); + assertThat( apply( methods, new EnumMethods(), "optEnum", "front" ), one( is( "FRONT" ) ) ); + assertThat( apply( methods, new EnumMethods(), "optEnum" ), one( is( "?" ) ) ); + assertThrows( LuaException.class, () -> apply( methods, new EnumMethods(), "getEnum", "not as side" ) ); + } + + @Test + public void testMainThread() throws LuaException + { + List> methods = LuaMethod.GENERATOR.getMethods( MainThread.class ); + assertThat( methods, contains( allOf( + named( "go" ), + contramap( is( false ), "non-yielding", NamedMethod::nonYielding ) + ) ) ); + + assertThat( apply( methods, new MainThread(), "go" ), + contramap( notNullValue(), "callback", MethodResult::getCallback ) ); + } + + public static class Basic + { + @LuaFunction + public final void go() + { } + } + + public static class Basic2 extends Basic + { + } + + public static class Empty + { + } + + static class NonPublic + { + @LuaFunction + public final void go() + { } + } + + public static class NonInstance + { + @LuaFunction + public static void go() + { } + } + + public static class IllegalThrows + { + @LuaFunction + public final void go() throws IOException + { + throw new IOException(); + } + } + + public static class CustomNames + { + @LuaFunction( { "go1", "go2" } ) + public final void go() + { } + } + + public static class ArgKinds + { + @LuaFunction + public final void objectArg( Object arg ) + { } + + @LuaFunction + public final void intArg( int arg ) + { } + + @LuaFunction + public final void optIntArg( Optional arg ) + { } + + @LuaFunction + public final void context( ILuaContext arg ) + { } + + @LuaFunction + public final void arguments( IArguments arg ) + { } + + @LuaFunction + public final void unknown( IComputerAccess arg ) + { } + + @LuaFunction + public final void illegalMap( Map arg ) + { } + + @LuaFunction + public final void optIllegalMap( Optional> arg ) + { } + } + + public static class EnumMethods + { + @LuaFunction + public final String getEnum( ComputerSide side ) + { + return side.name(); + } + + @LuaFunction + public final String optEnum( Optional side ) + { + return side.map( ComputerSide::name ).orElse( "?" ); + } + } + + public static class MainThread + { + @LuaFunction( mainThread = true ) + public final void go() + { } + } + + private static T find( Collection> methods, String name ) + { + return methods.stream() + .filter( x -> x.getName().equals( name ) ) + .map( NamedMethod::getMethod ) + .findAny() + .orElseThrow( NullPointerException::new ); + } + + public static MethodResult apply( Collection> methods, Object instance, String name, Object... args ) throws LuaException + { + return find( methods, name ).apply( instance, CONTEXT, new ObjectArguments( args ) ); + } + + public static Matcher one( Matcher object ) + { + return allOf( + contramap( nullValue(), "callback", MethodResult::getCallback ), + contramap( array( object ), "result", MethodResult::getResult ) + ); + } + + public static Matcher> named( String method ) + { + return contramap( is( method ), "name", NamedMethod::getName ); + } + + private static final ILuaContext CONTEXT = task -> 0; +} diff --git a/src/test/java/dan200/computercraft/core/asm/MethodTest.java b/src/test/java/dan200/computercraft/core/asm/MethodTest.java new file mode 100644 index 000000000..58be91ecd --- /dev/null +++ b/src/test/java/dan200/computercraft/core/asm/MethodTest.java @@ -0,0 +1,208 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.asm; + +import dan200.computercraft.api.lua.*; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IDynamicPeripheral; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.core.computer.ComputerBootstrap; +import dan200.computercraft.core.computer.ComputerSide; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class MethodTest +{ + @Test + public void testMainThread() + { + ComputerBootstrap.run( "assert(main_thread.go() == 123)", x -> x.addApi( new MainThread() ), 50 ); + } + + @Test + public void testMainThreadPeripheral() + { + ComputerBootstrap.run( "assert(peripheral.call('top', 'go') == 123)", + x -> x.getEnvironment().setPeripheral( ComputerSide.TOP, new MainThread() ), + 50 ); + } + + @Test + public void testDynamic() + { + ComputerBootstrap.run( + "assert(dynamic.foo() == 123, 'foo: ' .. tostring(dynamic.foo()))\n" + + "assert(dynamic.bar() == 321, 'bar: ' .. tostring(dynamic.bar()))", + x -> x.addApi( new Dynamic() ), 50 ); + } + + @Test + public void testDynamicPeripheral() + { + ComputerBootstrap.run( + "local dynamic = peripheral.wrap('top')\n" + + "assert(dynamic.foo() == 123, 'foo: ' .. tostring(dynamic.foo()))\n" + + "assert(dynamic.bar() == 321, 'bar: ' .. tostring(dynamic.bar()))", + x -> x.getEnvironment().setPeripheral( ComputerSide.TOP, new Dynamic() ), + 50 + ); + } + + @Test + public void testExtra() + { + ComputerBootstrap.run( "assert(extra.go, 'go')\nassert(extra.go2, 'go2')", + x -> x.addApi( new ExtraObject() ), + 50 ); + } + + @Test + public void testPeripheralThrow() + { + ComputerBootstrap.run( + "local throw = peripheral.wrap('top')\n" + + "local _, err = pcall(throw.thisThread) assert(err == 'pcall: !', err)\n" + + "local _, err = pcall(throw.mainThread) assert(err == 'pcall: !', err)", + x -> x.getEnvironment().setPeripheral( ComputerSide.TOP, new PeripheralThrow() ), + 50 + ); + } + + public static class MainThread implements ILuaAPI, IPeripheral + { + public final String thread = Thread.currentThread().getName(); + + @Override + public String[] getNames() + { + return new String[] { "main_thread" }; + } + + @LuaFunction( mainThread = true ) + public final int go() + { + assertThat( Thread.currentThread().getName(), is( thread ) ); + return 123; + } + + @Nonnull + @Override + public String getType() + { + return "main_thread"; + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + return this == other; + } + } + + public static class Dynamic implements IDynamicLuaObject, ILuaAPI, IDynamicPeripheral + { + @Nonnull + @Override + public String[] getMethodNames() + { + return new String[] { "foo" }; + } + + @Nonnull + @Override + public MethodResult callMethod( @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) + { + return MethodResult.of( 123 ); + } + + @Nonnull + @Override + public MethodResult callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) + { + return callMethod( context, method, arguments ); + } + + @LuaFunction + public final int bar() + { + return 321; + } + + @Override + public String[] getNames() + { + return new String[] { "dynamic" }; + } + + @Nonnull + @Override + public String getType() + { + return "dynamic"; + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + return this == other; + } + } + + public static class ExtraObject implements ObjectSource, ILuaAPI + { + @Override + public String[] getNames() + { + return new String[] { "extra" }; + } + + @LuaFunction + public final void go2() + { + } + + @Override + public Iterable getExtra() + { + return Collections.singletonList( new GeneratorTest.Basic() ); + } + } + + public static class PeripheralThrow implements IPeripheral + { + @LuaFunction + public final void thisThread() throws LuaException + { + throw new LuaException( "!" ); + } + + @LuaFunction( mainThread = true ) + public final void mainThread() throws LuaException + { + throw new LuaException( "!" ); + } + + @Nonnull + @Override + public String getType() + { + return "throw"; + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + return this == other; + } + } +} diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java index 6ac8a7b55..e2deba3cb 100644 --- a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java +++ b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java @@ -7,16 +7,14 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.IArguments; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.lua.ArgumentHelper; +import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.core.filesystem.MemoryMount; import dan200.computercraft.core.terminal.Terminal; import org.junit.jupiter.api.Assertions; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.Arrays; import java.util.function.Consumer; @@ -26,20 +24,26 @@ public class ComputerBootstrap { private static final int TPS = 20; - private static final int MAX_TIME = 10; + public static final int MAX_TIME = 10; - public static void run( String program ) + public static void run( String program, Consumer setup, int maxTimes ) { MemoryMount mount = new MemoryMount() .addFile( "test.lua", program ) - .addFile( "startup", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()" ); + .addFile( "startup.lua", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()" ); - run( mount, x -> { } ); + run( mount, setup, maxTimes ); } - public static void run( IWritableMount mount, Consumer setup ) + public static void run( String program, int maxTimes ) + { + run( program, x -> { }, maxTimes ); + } + + public static void run( IWritableMount mount, Consumer setup, int maxTicks ) { ComputerCraft.logPeripheralErrors = true; + ComputerCraft.maxMainComputerTime = ComputerCraft.maxMainGlobalTime = Integer.MAX_VALUE; Terminal term = new Terminal( ComputerCraft.terminalWidth_computer, ComputerCraft.terminalHeight_computer ); final Computer computer = new Computer( new BasicEnvironment( mount ), term, 0 ); @@ -54,7 +58,7 @@ public static void run( IWritableMount mount, Consumer setup ) computer.turnOn(); boolean everOn = false; - for( int tick = 0; tick < TPS * MAX_TIME; tick++ ) + for( int tick = 0; tick < TPS * maxTicks; tick++ ) { long start = System.currentTimeMillis(); @@ -99,7 +103,7 @@ public static void run( IWritableMount mount, Consumer setup ) } } - private static class AssertApi implements ILuaAPI + public static class AssertApi implements ILuaAPI { boolean didAssert; String message; @@ -110,39 +114,25 @@ public String[] getNames() return new String[] { "assertion" }; } - @Nonnull - @Override - public String[] getMethodNames() + @LuaFunction + public final void log( IArguments arguments ) { - return new String[] { "assert", "log" }; + ComputerCraft.log.info( "[Computer] {}", Arrays.toString( arguments.getAll() ) ); } - @Nullable - @Override - public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + @LuaFunction( "assert" ) + public final Object[] doAssert( IArguments arguments ) throws LuaException { - switch( method ) + didAssert = true; + + Object arg = arguments.get( 0 ); + if( arg == null || arg == Boolean.FALSE ) { - case 0: // assert - { - didAssert = true; - - Object arg = arguments.length >= 1 ? arguments[0] : null; - if( arg == null || arg == Boolean.FALSE ) - { - message = ArgumentHelper.optString( arguments, 1, "Assertion failed" ); - throw new LuaException( message ); - } - - return arguments; - } - case 1: - ComputerCraft.log.info( "[Computer] {}", Arrays.toString( arguments ) ); - return null; - - default: - return null; + message = arguments.optString( 1, "Assertion failed" ); + throw new LuaException( message ); } + + return arguments.getAll(); } } } diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerTest.java b/src/test/java/dan200/computercraft/core/computer/ComputerTest.java index d2d6d744c..830e27305 100644 --- a/src/test/java/dan200/computercraft/core/computer/ComputerTest.java +++ b/src/test/java/dan200/computercraft/core/computer/ComputerTest.java @@ -5,9 +5,15 @@ */ package dan200.computercraft.core.computer; +import com.google.common.io.CharStreams; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + import static java.time.Duration.ofSeconds; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; @@ -19,7 +25,7 @@ public void testTimeout() assertTimeoutPreemptively( ofSeconds( 20 ), () -> { try { - ComputerBootstrap.run( "print('Hello') while true do end" ); + ComputerBootstrap.run( "print('Hello') while true do end", ComputerBootstrap.MAX_TIME ); } catch( AssertionError e ) { @@ -30,4 +36,14 @@ public void testTimeout() Assertions.fail( "Expected computer to timeout" ); } ); } + + public static void main( String[] args ) throws Exception + { + InputStream stream = ComputerTest.class.getClassLoader().getResourceAsStream( "benchmark.lua" ); + try( InputStreamReader reader = new InputStreamReader( Objects.requireNonNull( stream ), StandardCharsets.UTF_8 ) ) + { + String contents = CharStreams.toString( reader ); + ComputerBootstrap.run( contents, 1000 ); + } + } } diff --git a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java index deb403e5a..73aaaccda 100644 --- a/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java +++ b/src/test/java/dan200/computercraft/core/filesystem/FileSystemTest.java @@ -27,8 +27,8 @@ public class FileSystemTest * Ensures writing a file truncates it. * * @throws FileSystemException When the file system cannot be constructed. - * @throws LuaException When Lua functions fail. - * @throws IOException When reading and writing from strings + * @throws LuaException When Lua functions fail. + * @throws IOException When reading and writing from strings */ @Test public void testWriteTruncates() throws FileSystemException, LuaException, IOException diff --git a/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java index 829432c83..174558001 100644 --- a/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java +++ b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java @@ -8,13 +8,10 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import dan200.computercraft.api.ComputerCraftAPI; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.network.wired.IWiredElement; import dan200.computercraft.api.network.wired.IWiredNetwork; import dan200.computercraft.api.network.wired.IWiredNetworkChange; import dan200.computercraft.api.network.wired.IWiredNode; -import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.shared.util.DirectionUtil; import net.minecraft.util.Direction; @@ -397,20 +394,6 @@ public String getType() return "test"; } - @Nonnull - @Override - public String[] getMethodNames() - { - return new String[0]; - } - - @Nullable - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException - { - return new Object[0]; - } - @Override public boolean equals( @Nullable IPeripheral other ) { diff --git a/src/test/resources/benchmark.lua b/src/test/resources/benchmark.lua new file mode 100644 index 000000000..dbbef23a1 --- /dev/null +++ b/src/test/resources/benchmark.lua @@ -0,0 +1,24 @@ +local function log(msg) + print(msg) + if assertion then assertion.log(msg) end +end + +local function run(name, n, f, ...) + sleep(0) + local s = os.epoch("utc") + for _ = 1, n do f(...) end + local e = os.epoch("utc") - s + log(("%10s %.2fs %.fop/s"):format(name, e*1e-3, n/e)) +end + +local function run5(...) for _ = 1, 5 do run(...) end end + +local native = term.native() +local x, y = native.getCursorPos() + +log("Starting the benchmark") +run5("redstone.getAnalogInput", 1e7, redstone.getAnalogInput, "top") +run5("term.getCursorPos", 2e7, native.getCursorPos) +run5("term.setCursorPos", 2e7, native.setCursorPos, x, y) + +if assertion then assertion.assert(true) end diff --git a/src/test/resources/test-rom/spec/apis/redstone_spec.lua b/src/test/resources/test-rom/spec/apis/redstone_spec.lua index 17d8acab0..148d08700 100644 --- a/src/test/resources/test-rom/spec/apis/redstone_spec.lua +++ b/src/test/resources/test-rom/spec/apis/redstone_spec.lua @@ -2,7 +2,7 @@ local function it_side(func, ...) local arg = table.pack(...) it("requires a specific side", function() expect.error(func, 0):eq("bad argument #1 (string expected, got number)") - expect.error(func, "blah", table.unpack(arg)):eq("Invalid side.") + expect.error(func, "blah", table.unpack(arg)):eq("bad argument #1 (unknown option blah)") func("top", table.unpack(arg)) func("Top", table.unpack(arg))