1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2026-06-08 05:42:06 +00:00

Replace getMethodNames/callMethod with annotations (#447)

When creating a peripheral or custom Lua object, one must implement two
methods:

 - getMethodNames(): String[] - Returns the name of the methods
 - callMethod(int, ...): Object[] - Invokes the method using an index in
   the above array.

This has a couple of problems:
 - It's somewhat unwieldy to use - you need to keep track of array
   indices, which leads to ugly code.
 - Functions which yield (for instance, those which run on the main
   thread) are blocking. This means we need to spawn new threads for
   each CC-side yield.

We replace this system with a few changes:

 - @LuaFunction annotation: One may annotate a public instance method
   with this annotation. This then exposes a peripheral/lua object
   method.

   Furthermore, this method can accept and return a variety of types,
   which often makes functions cleaner (e.g. can return an int rather
   than an Object[], and specify and int argument rather than
   Object[]).

 - MethodResult: Instead of returning an Object[] and having blocking
   yields, functions return a MethodResult. This either contains an
   immediate return, or an instruction to yield with some continuation
   to resume with.

   MethodResult is then interpreted by the Lua runtime (i.e. Cobalt),
   rather than our weird bodgey hacks before. This means we no longer
   spawn new threads when yielding within CC.

 - Methods accept IArguments instead of a raw Object array. This has a
   few benefits:
   - Consistent argument handling - people no longer need to use
     ArgumentHelper (as it doesn't exist!), or even be aware of its
     existence - you're rather forced into using it.
   - More efficient code in some cases. We provide a Cobalt-specific
     implementation of IArguments, which avoids the boxing/unboxing when
     handling numbers and binary strings.
This commit is contained in:
Jonathan Coates
2020-05-15 13:21:16 +01:00
committed by GitHub
parent d0deab3519
commit d5f82fa458
91 changed files with 5288 additions and 4133 deletions
+1
View File
@@ -109,6 +109,7 @@ dependencies {
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"
}
@@ -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.
*
* <h2>Example usage:</h2>
* <pre>
* {@code
* int slot = getInt( args, 0 );
* int amount = optInt( args, 1, 64 );
* }
* </pre>
*/
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<Object, Object> 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";
}
}
@@ -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:
*
* <ul>
* <li>Lua values of type "string" will be represented by a {@link String}.</li>
* <li>Lua values of type "number" will be represented by a {@link Number}.</li>
* <li>Lua values of type "boolean" will be represented by a {@link Boolean}.</li>
* <li>Lua values of type "table" will be represented by a {@link Map}.</li>
* <li>Lua values of any other type will be represented by a {@code null} value.</li>
* </ul>
*
* @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 <em>read only</em> 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 <T> 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 extends Enum<T>> T getEnum( int index, Class<T> 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<Double> 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<Integer> 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<Long> 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<Double> optFiniteDouble( int index ) throws LuaException
{
Optional<Double> 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<Boolean> 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<String> 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 <em>read only</em> buffer.
* @throws LuaException If the value is not a string.
*/
default Optional<ByteBuffer> 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 <T> 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 extends Enum<T>> Optional<T> optEnum( int index, Class<T> klass ) throws LuaException
{
Optional<String> 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<Map<?, ?>> 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<Object, Object> def ) throws LuaException
{
return optTable( index ).orElse( def );
}
}
@@ -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;
}
@@ -8,7 +8,8 @@ package dan200.computercraft.api.lua;
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 @@ import dan200.computercraft.api.ComputerCraftAPI;
* @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
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -8,11 +8,10 @@ package dan200.computercraft.api.lua;
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.
@@ -13,24 +13,33 @@ import javax.annotation.Nullable;
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.
@@ -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:
*
* <ul>
* <li>{@link ILuaContext} (and {@link IComputerAccess} if on a {@link IPeripheral})</li>
* <li>{@link IArguments}: The arguments supplied to this function.</li>
* <li>
* 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}.
* </li>
* </ul>
*
* 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;
}
@@ -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 <T> 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 extends Enum<T>> T checkEnum( int index, Class<T> 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 + ")" );
}
}
@@ -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 );
}
}
@@ -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<Object> args;
@Deprecated
@SuppressWarnings( "unused" )
public ObjectArguments( IArguments arguments )
{
throw new IllegalStateException();
}
public ObjectArguments( Object... args )
{
this.args = Arrays.asList( args );
}
public ObjectArguments( List<Object> 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();
}
}
@@ -8,8 +8,10 @@ package dan200.computercraft.api.peripheral;
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 @@ public interface IComputerAccess
*
* 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.
@@ -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;
}
@@ -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()}.<br>
* Lua values of type "string" will be represented by Object type String.<br>
* Lua values of type "number" will be represented by Object type Double.<br>
* Lua values of type "boolean" will be represented by Object type Boolean.<br>
* Lua values of type "table" will be represented by Object type Map.<br>
* Lua values of any other type will be represented by a null object.<br>
* 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.
*
@@ -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 )
@@ -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
@@ -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
@@ -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 abstract class TurtleBlockEvent extends TurtlePlayerEvent
* 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<String, ?> newData )
{
@@ -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 class TurtleInspectItemEvent extends TurtleActionEvent
* 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<String, ?> newData )
{
@@ -116,7 +116,7 @@ public abstract class ComputerAccess implements IComputerAccess
}
@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 );
@@ -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.FileSystemException;
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.Map;
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 class FSAPI implements ILuaAPI
@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<BufferedReader> 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<BufferedWriter> 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<BufferedWriter> 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<ReadableByteChannel> 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<WritableByteChannel> 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<WritableByteChannel> 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<BufferedReader> 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<BufferedWriter> 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<BufferedWriter> 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<ReadableByteChannel> 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<WritableByteChannel> 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<WritableByteChannel> 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<String, Object> 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<String, Object> attributes( String path ) throws LuaException
{
try
{
BasicFileAttributes attributes = fileSystem.getAttributes( path );
Map<String, Object> 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() );
}
}
@@ -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.net.URI;
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 class HTTPAPI implements ILuaAPI
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<Map<?, ?>> 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() };
}
}
@@ -43,7 +43,7 @@ public interface IAPIEnvironment
void reboot();
void queueEvent( String event, Object[] args );
void queueEvent( String event, Object... args );
void setOutput( ComputerSide side, int output );
@@ -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.ZonedDateTime;
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<Alarm> m_alarms = new Int2ObjectOpenHashMap<>();
private int m_clock;
@@ -55,11 +56,9 @@ public class OSAPI implements ILuaAPI
public OSAPI( IAPIEnvironment environment )
{
m_apiEnvironment = environment;
apiEnvironment = environment;
}
// ILuaAPI implementation
@Override
public String[] getNames()
{
@@ -69,8 +68,8 @@ public class OSAPI implements ILuaAPI
@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 class OSAPI implements ILuaAPI
{
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 class OSAPI implements ILuaAPI
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 class OSAPI implements ILuaAPI
}
}
@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 @@ public class OSAPI implements ILuaAPI
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<String> 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<String> 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<String> 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<String> formatA, Optional<Long> 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 );
}
}
@@ -7,88 +7,74 @@ package dan200.computercraft.core.apis;
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<String, Integer> m_methodMap;
private boolean m_attached;
private final String type;
private final Map<String, PeripheralMethod> 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<String> 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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
@Override
public String getAttachmentName()
{
if( !m_attached ) throw new NotAttachedException();
return m_side;
if( !attached ) throw new NotAttachedException();
return side;
}
@Nonnull
@Override
public Map<String, IPeripheral> getAvailablePeripherals()
{
if( !m_attached ) throw new NotAttachedException();
if( !attached ) throw new NotAttachedException();
Map<String, IPeripheral> peripherals = new HashMap<>();
for( PeripheralWrapper wrapper : m_peripherals )
for( PeripheralWrapper wrapper : PeripheralAPI.this.peripherals )
{
if( wrapper != null && wrapper.isAttached() )
{
@@ -186,9 +165,9 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
@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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
@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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
@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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
@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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
@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 class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
}
}
@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<String, PeripheralMethod> getMethods( IPeripheral peripheral )
{
String[] dynamicMethods = peripheral instanceof IDynamicPeripheral
? Objects.requireNonNull( ((IDynamicPeripheral) peripheral).getMethodNames(), "Peripheral methods cannot be null" )
: LuaMethod.EMPTY_METHODS;
List<NamedMethod<PeripheralMethod>> methods = PeripheralMethod.GENERATOR.getMethods( peripheral.getClass() );
Map<String, PeripheralMethod> 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<PeripheralMethod> method : methods )
{
methodMap.put( method.getName(), method.getMethod() );
}
return methodMap;
}
}
@@ -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 class RedstoneAPI implements ILuaAPI
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;
}
}
@@ -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 @@ public final class 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
@@ -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 class TermAPI implements ILuaAPI
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();
}
}
@@ -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();
}
}
}
@@ -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.ByteBuffer;
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<Integer> 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<ByteBuffer> 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<ByteBuffer> 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<Boolean> 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 );
}
}
}
@@ -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 @@ import java.nio.channels.WritableByteChannel;
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 );
}
}
}
@@ -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.Charset;
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 class EncodedReadableHandle extends HandleGeneric
this( reader, reader );
}
@Nonnull
@Override
public String[] getMethodNames()
@LuaFunction
public final Object[] readLine( Optional<Boolean> 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<Integer> 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;
}
}
@@ -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 @@ import java.nio.charset.StandardCharsets;
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 )
{
@@ -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.io.IOException;
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 @@ public abstract class HandleGeneric implements ILuaObject
* @throws LuaException If the arguments were invalid
* @see <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
*/
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":
@@ -47,11 +47,11 @@ public class CheckUrl extends Resource<CheckUrl>
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() );
}
}
@@ -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 @@ public class HttpRequest extends Resource<HttpRequest>
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 @@ public class HttpRequest extends Resource<HttpRequest>
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
@@ -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 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
// 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 )
{
@@ -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<String, String> responseHeaders;
public HttpResponseHandle( @Nonnull ILuaObject reader, int responseCode, String responseStatus, @Nonnull Map<String, String> responseHeaders )
public HttpResponseHandle( @Nonnull HandleGeneric reader, int responseCode, String responseStatus, @Nonnull Map<String, String> 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<String, String> getResponseHeaders()
{
return responseHeaders;
}
@Override
public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException
public Iterable<Object> 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 );
}
}
@@ -187,7 +187,7 @@ public class Websocket extends Resource<Websocket>
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 @@ public class Websocket extends Resource<Websocket>
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 );
}
}
@@ -7,10 +7,7 @@ package dan200.computercraft.core.apis.http.websocket;
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.BinaryWebSocketFrame;
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 class WebsocketHandle implements ILuaObject, Closeable
this.channel = channel;
}
@Nonnull
@Override
public String[] getMethodNames()
@LuaFunction
public final MethodResult result( Optional<Double> 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 class WebsocketHandle implements ILuaObject, Closeable
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;
}
}
}
@@ -68,14 +68,14 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object>
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 )
{
@@ -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 );
}
}
@@ -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<T>
{
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<T> base;
private final List<Class<?>> context;
private final String[] interfaces;
private final String methodDesc;
private final Function<T, T> wrap;
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder
.newBuilder()
.build( CacheLoader.from( this::build ) );
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
.newBuilder()
.build( CacheLoader.from( this::build ) );
Generator( Class<T> base, List<Class<?>> context, Function<T, T> 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<NamedMethod<T>> 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<NamedMethod<T>> build( Class<?> klass )
{
ArrayList<NamedMethod<T>> 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<T> 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, "<init>", "()V", null, null );
mw.visitCode();
mw.visitVarInsn( ALOAD, 0 );
mw.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false );
mw.visitInsn( RETURN );
mw.visitMaxs( 0, 0 );
mw.visitEnd();
}
{
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;
}
}
@@ -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<T>
{
private final IntFunction<T> factory;
private volatile Object[] cache = new Object[16];
IntCache( IntFunction<T> 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;
}
}
}
@@ -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<LuaMethod> 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<LuaMethod> 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;
}
@@ -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<T>
{
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;
}
}
@@ -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<Object> getExtra();
static <T> void allMethods( Generator<T> generator, Object object, BiConsumer<Object, NamedMethod<T>> accept )
{
for( NamedMethod<T> method : generator.getMethods( object.getClass() ) ) accept.accept( object, method );
if( object instanceof ObjectSource )
{
for( Object extra : ((ObjectSource) object).getExtra() )
{
for( NamedMethod<T> method : generator.getMethods( extra.getClass() ) ) accept.accept( extra, method );
}
}
}
}
@@ -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<PeripheralMethod> 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<PeripheralMethod> 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;
}
@@ -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
*/