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))