1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-08-29 08:42:17 +00:00

Use CC:T's version of dan200.computercraft.core

This commit is contained in:
Jonathan Coates
2021-06-09 07:55:17 +01:00
parent b429095f88
commit a198a5241d
72 changed files with 4106 additions and 5068 deletions

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.ILuaAPIFactory;
@@ -16,13 +15,13 @@ import java.util.Objects;
public final class ApiFactories
{
private static final Collection<ILuaAPIFactory> factories = new LinkedHashSet<>();
private static final Collection<ILuaAPIFactory> factoriesView = Collections.unmodifiableCollection( factories );
private ApiFactories()
{
}
private static final Collection<ILuaAPIFactory> factories = new LinkedHashSet<>();
private static final Collection<ILuaAPIFactory> factoriesView = Collections.unmodifiableCollection( factories );
public static synchronized void register( @Nonnull ILuaAPIFactory factory )
{
Objects.requireNonNull( factory, "provider cannot be null" );

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.filesystem.IMount;
@@ -20,22 +19,22 @@ import java.util.Set;
public abstract class ComputerAccess implements IComputerAccess
{
private final IAPIEnvironment m_environment;
private final Set<String> m_mounts = new HashSet<>();
private final IAPIEnvironment environment;
private final Set<String> mounts = new HashSet<>();
protected ComputerAccess( IAPIEnvironment environment )
{
this.m_environment = environment;
this.environment = environment;
}
public void unmountAll()
{
FileSystem fileSystem = this.m_environment.getFileSystem();
for( String mount : this.m_mounts )
FileSystem fileSystem = environment.getFileSystem();
for( String mount : mounts )
{
fileSystem.unmount( mount );
}
this.m_mounts.clear();
mounts.clear();
}
@Override
@@ -47,15 +46,12 @@ public abstract class ComputerAccess implements IComputerAccess
// Mount the location
String location;
FileSystem fileSystem = this.m_environment.getFileSystem();
if( fileSystem == null )
{
throw new IllegalStateException( "File system has not been created" );
}
FileSystem fileSystem = environment.getFileSystem();
if( fileSystem == null ) throw new IllegalStateException( "File system has not been created" );
synchronized( fileSystem )
{
location = this.findFreeLocation( desiredLoc );
location = findFreeLocation( desiredLoc );
if( location != null )
{
try
@@ -68,10 +64,7 @@ public abstract class ComputerAccess implements IComputerAccess
}
}
if( location != null )
{
this.m_mounts.add( location );
}
if( location != null ) mounts.add( location );
return location;
}
@@ -84,15 +77,12 @@ public abstract class ComputerAccess implements IComputerAccess
// Mount the location
String location;
FileSystem fileSystem = this.m_environment.getFileSystem();
if( fileSystem == null )
{
throw new IllegalStateException( "File system has not been created" );
}
FileSystem fileSystem = environment.getFileSystem();
if( fileSystem == null ) throw new IllegalStateException( "File system has not been created" );
synchronized( fileSystem )
{
location = this.findFreeLocation( desiredLoc );
location = findFreeLocation( desiredLoc );
if( location != null )
{
try
@@ -105,59 +95,46 @@ public abstract class ComputerAccess implements IComputerAccess
}
}
if( location != null )
{
this.m_mounts.add( location );
}
if( location != null ) mounts.add( location );
return location;
}
@Override
public void unmount( String location )
{
if( location == null )
{
return;
}
if( !this.m_mounts.contains( location ) )
{
throw new IllegalStateException( "You didn't mount this location" );
}
if( location == null ) return;
if( !mounts.contains( location ) ) throw new IllegalStateException( "You didn't mount this location" );
this.m_environment.getFileSystem()
.unmount( location );
this.m_mounts.remove( location );
environment.getFileSystem().unmount( location );
mounts.remove( location );
}
@Override
public int getID()
{
return this.m_environment.getComputerID();
return environment.getComputerID();
}
@Override
public void queueEvent( @Nonnull String event, Object... arguments )
{
Objects.requireNonNull( event, "event cannot be null" );
this.m_environment.queueEvent( event, arguments );
environment.queueEvent( event, arguments );
}
@Nonnull
@Override
public IWorkMonitor getMainThreadMonitor()
{
return this.m_environment.getMainThreadMonitor();
return environment.getMainThreadMonitor();
}
private String findFreeLocation( String desiredLoc )
{
try
{
FileSystem fileSystem = this.m_environment.getFileSystem();
if( !fileSystem.exists( desiredLoc ) )
{
return desiredLoc;
}
FileSystem fileSystem = environment.getFileSystem();
if( !fileSystem.exists( desiredLoc ) ) return desiredLoc;
// We used to check foo2, foo3, foo4, etc here but the disk drive does this itself now
return null;

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.IArguments;
@@ -42,7 +41,7 @@ public class FSAPI implements ILuaAPI
public FSAPI( IAPIEnvironment env )
{
this.environment = env;
environment = env;
}
@Override
@@ -54,13 +53,13 @@ public class FSAPI implements ILuaAPI
@Override
public void startup()
{
this.fileSystem = this.environment.getFileSystem();
fileSystem = environment.getFileSystem();
}
@Override
public void shutdown()
{
this.fileSystem = null;
fileSystem = null;
}
/**
@@ -73,10 +72,10 @@ public class FSAPI implements ILuaAPI
@LuaFunction
public final String[] list( String path ) throws LuaException
{
this.environment.addTrackingChange( TrackingField.FS_OPS );
environment.addTrackingChange( TrackingField.FS_OPS );
try
{
return this.fileSystem.list( path );
return fileSystem.list( path );
}
catch( FileSystemException e )
{
@@ -86,7 +85,7 @@ public class FSAPI implements ILuaAPI
/**
* Combines several parts of a path into one full path, adding separators as
* needed
* needed.
*
* @param arguments The paths to combine.
* @return The new path, with separators added between parts as needed.
@@ -146,7 +145,7 @@ public class FSAPI implements ILuaAPI
{
try
{
return this.fileSystem.getSize( path );
return fileSystem.getSize( path );
}
catch( FileSystemException e )
{
@@ -165,7 +164,7 @@ public class FSAPI implements ILuaAPI
{
try
{
return this.fileSystem.exists( path );
return fileSystem.exists( path );
}
catch( FileSystemException e )
{
@@ -184,7 +183,7 @@ public class FSAPI implements ILuaAPI
{
try
{
return this.fileSystem.isDir( path );
return fileSystem.isDir( path );
}
catch( FileSystemException e )
{
@@ -203,7 +202,7 @@ public class FSAPI implements ILuaAPI
{
try
{
return this.fileSystem.isReadOnly( path );
return fileSystem.isReadOnly( path );
}
catch( FileSystemException e )
{
@@ -222,8 +221,8 @@ public class FSAPI implements ILuaAPI
{
try
{
this.environment.addTrackingChange( TrackingField.FS_OPS );
this.fileSystem.makeDir( path );
environment.addTrackingChange( TrackingField.FS_OPS );
fileSystem.makeDir( path );
}
catch( FileSystemException e )
{
@@ -245,8 +244,8 @@ public class FSAPI implements ILuaAPI
{
try
{
this.environment.addTrackingChange( TrackingField.FS_OPS );
this.fileSystem.move( path, dest );
environment.addTrackingChange( TrackingField.FS_OPS );
fileSystem.move( path, dest );
}
catch( FileSystemException e )
{
@@ -268,8 +267,8 @@ public class FSAPI implements ILuaAPI
{
try
{
this.environment.addTrackingChange( TrackingField.FS_OPS );
this.fileSystem.copy( path, dest );
environment.addTrackingChange( TrackingField.FS_OPS );
fileSystem.copy( path, dest );
}
catch( FileSystemException e )
{
@@ -280,7 +279,8 @@ public class FSAPI implements ILuaAPI
/**
* Deletes a file or directory.
*
* If the path points to a directory, all of the enclosed files and subdirectories are also deleted.
* If the path points to a directory, all of the enclosed files and
* subdirectories are also deleted.
*
* @param path The path to the file or directory to delete.
* @throws LuaException If the file or directory couldn't be deleted.
@@ -290,8 +290,8 @@ public class FSAPI implements ILuaAPI
{
try
{
this.environment.addTrackingChange( TrackingField.FS_OPS );
this.fileSystem.delete( path );
environment.addTrackingChange( TrackingField.FS_OPS );
fileSystem.delete( path );
}
catch( FileSystemException e )
{
@@ -304,8 +304,10 @@ public class FSAPI implements ILuaAPI
/**
* Opens a file for reading or writing at a path.
*
* The mode parameter can be {@code r} to read, {@code w} to write (deleting all contents), or {@code a} to append (keeping contents). If {@code b} is
* added to the end, the file will be opened in binary mode; otherwise, it's opened in text mode.
* The mode parameter can be {@code r} to read, {@code w} to write (deleting
* all contents), or {@code a} to append (keeping contents). If {@code b} is
* added to the end, the file will be opened in binary mode; otherwise, it's
* opened in text mode.
*
* @param path The path to the file to open.
* @param mode The mode to open the file with.
@@ -318,7 +320,7 @@ public class FSAPI implements ILuaAPI
@LuaFunction
public final Object[] open( String path, String mode ) throws LuaException
{
this.environment.addTrackingChange( TrackingField.FS_OPS );
environment.addTrackingChange( TrackingField.FS_OPS );
try
{
switch( mode )
@@ -326,37 +328,37 @@ public class FSAPI implements ILuaAPI
case "r":
{
// Open the file for reading, then create a wrapper around the reader
FileSystemWrapper<BufferedReader> reader = this.fileSystem.openForRead( path, EncodedReadableHandle::openUtf8 );
FileSystemWrapper<BufferedReader> reader = 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 = this.fileSystem.openForWrite( path, false, EncodedWritableHandle::openUtf8 );
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 = this.fileSystem.openForWrite( path, true, EncodedWritableHandle::openUtf8 );
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 = this.fileSystem.openForRead( path, Function.identity() );
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 = this.fileSystem.openForWrite( path, false, Function.identity() );
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 = this.fileSystem.openForWrite( path, true, Function.identity() );
FileSystemWrapper<WritableByteChannel> writer = fileSystem.openForWrite( path, true, Function.identity() );
return new Object[] { BinaryWritableHandle.of( writer.get(), writer ) };
}
default:
@@ -365,10 +367,7 @@ public class FSAPI implements ILuaAPI
}
catch( FileSystemException e )
{
return new Object[] {
null,
e.getMessage()
};
return new Object[] { null, e.getMessage() };
}
}
@@ -385,7 +384,7 @@ public class FSAPI implements ILuaAPI
{
try
{
return this.fileSystem.exists( path ) ? new Object[] { this.fileSystem.getMountLabel( path ) } : null;
return fileSystem.exists( path ) ? new Object[] { fileSystem.getMountLabel( path ) } : null;
}
catch( FileSystemException e )
{
@@ -394,7 +393,8 @@ public class FSAPI implements ILuaAPI
}
/**
* Returns the amount of free space available on the drive the path is located on.
* Returns the amount of free space available on the drive the path is
* located on.
*
* @param path The path to check the free space for.
* @return The amount of free space available, in bytes.
@@ -406,7 +406,7 @@ public class FSAPI implements ILuaAPI
{
try
{
long freeSpace = this.fileSystem.getFreeSpace( path );
long freeSpace = fileSystem.getFreeSpace( path );
return freeSpace >= 0 ? freeSpace : "unlimited";
}
catch( FileSystemException e )
@@ -418,8 +418,10 @@ public class FSAPI implements ILuaAPI
/**
* Searches for files matching a string with wildcards.
*
* This string is formatted like a normal path string, but can include any number of wildcards ({@code *}) to look for files matching anything. For
* example, {@code rom/* /command*} will look for any path starting with {@code command} inside any subdirectory of {@code /rom}.
* This string is formatted like a normal path string, but can include any
* number of wildcards ({@code *}) to look for files matching anything.
* For example, {@code rom/* /command*} will look for any path starting with
* {@code command} inside any subdirectory of {@code /rom}.
*
* @param path The wildcard-qualified path to search for.
* @return A list of paths that match the search string.
@@ -430,8 +432,8 @@ public class FSAPI implements ILuaAPI
{
try
{
this.environment.addTrackingChange( TrackingField.FS_OPS );
return this.fileSystem.find( path );
environment.addTrackingChange( TrackingField.FS_OPS );
return fileSystem.find( path );
}
catch( FileSystemException e )
{
@@ -442,20 +444,22 @@ public class FSAPI implements ILuaAPI
/**
* Returns true if a path is mounted to the parent filesystem.
*
* The root filesystem "/" is considered a mount, along with disk folders and the rom folder. Other programs (such as network shares) can extend this to
* make other mount types by correctly assigning their return value for getDrive.
* The root filesystem "/" is considered a mount, along with disk folders and the rom folder. Other programs
* (such as network shares) can extend this to make other mount types by correctly assigning their return value for
* getDrive.
*
* @param path The path of the drive to get.
* @return The drive's capacity.
* @throws LuaException If the capacity cannot be determined.
* @cc.treturn number|nil This drive's capacity. This will be nil for "read-only" drives, such as the ROM or treasure disks.
* @cc.treturn number|nil This drive's capacity. This will be nil for "read-only" drives, such as the ROM or
* treasure disks.
*/
@LuaFunction
public final Object getCapacity( String path ) throws LuaException
{
try
{
OptionalLong capacity = this.fileSystem.getCapacity( path );
OptionalLong capacity = fileSystem.getCapacity( path );
return capacity.isPresent() ? capacity.getAsLong() : null;
}
catch( FileSystemException e )
@@ -470,8 +474,8 @@ public class FSAPI implements ILuaAPI
* The returned attributes table contains information about the size of the file, whether it is a directory,
* when it was created and last modified, and whether it is read only.
*
* The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be given to {@link OSAPI#date} in order to
* convert it to more usable form.
* The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be
* given to {@link OSAPI#date} in order to convert it to more usable form.
*
* @param path The path to get attributes for.
* @return The resulting attributes.
@@ -485,7 +489,7 @@ public class FSAPI implements ILuaAPI
{
try
{
BasicFileAttributes attributes = this.fileSystem.getAttributes( path );
BasicFileAttributes attributes = fileSystem.getAttributes( path );
Map<String, Object> result = new HashMap<>();
result.put( "modification", getFileTime( attributes.lastModifiedTime() ) );
result.put( "modified", getFileTime( attributes.lastModifiedTime() ) );

View File

@@ -37,7 +37,7 @@ public class HTTPAPI implements ILuaAPI
{
private final IAPIEnvironment apiEnvironment;
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>();
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>( ResourceGroup.DEFAULT );
private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests );
private final ResourceGroup<Websocket> websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets );
@@ -127,7 +127,10 @@ public class HTTPAPI implements ILuaAPI
HttpRequest request = new HttpRequest( requests, apiEnvironment, address, postString, headers, binary, redirect );
// Make the request
request.queue( r -> r.request( uri, httpMethod ) );
if( !request.queue( r -> r.request( uri, httpMethod ) ) )
{
throw new LuaException( "Too many ongoing HTTP requests" );
}
return new Object[] { true };
}
@@ -138,12 +141,15 @@ public class HTTPAPI implements ILuaAPI
}
@LuaFunction
public final Object[] checkURL( String address )
public final Object[] checkURL( String address ) throws LuaException
{
try
{
URI uri = HttpRequest.checkUri( address );
new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run );
if( !new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run ) )
{
throw new LuaException( "Too many ongoing checkUrl calls" );
}
return new Object[] { true };
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.peripheral.IPeripheral;
@@ -21,6 +20,12 @@ public interface IAPIEnvironment
{
String TIMER_EVENT = "timer";
@FunctionalInterface
interface IPeripheralChangeListener
{
void onPeripheralChanged( ComputerSide side, @Nullable IPeripheral newPeripheral );
}
int getComputerID();
@Nonnull
@@ -65,16 +70,10 @@ public interface IAPIEnvironment
void cancelTimer( int id );
default void addTrackingChange( @Nonnull TrackingField field )
{
this.addTrackingChange( field, 1 );
}
void addTrackingChange( @Nonnull TrackingField field, long change );
@FunctionalInterface
interface IPeripheralChangeListener
default void addTrackingChange( @Nonnull TrackingField field )
{
void onPeripheralChanged( ComputerSide side, @Nullable IPeripheral newPeripheral );
addTrackingChange( field, 1 );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.LuaException;
@@ -21,9 +20,6 @@ import java.util.function.LongUnaryOperator;
final class LuaDateTime
{
private static final TemporalField CENTURY = map( ChronoField.YEAR, ValueRange.of( 0, 6 ), x -> (x / 100) % 100 );
private static final TemporalField ZERO_WEEK = map( WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of( 0, 6 ), x -> x - 1 );
private LuaDateTime()
{
}
@@ -42,10 +38,7 @@ final class LuaDateTime
formatter.appendLiteral( c );
break;
case '%':
if( i >= format.length() )
{
break;
}
if( i >= format.length() ) break;
switch( c = format.charAt( i++ ) )
{
default:
@@ -81,8 +74,7 @@ final class LuaDateTime
format( formatter, "%m/%d/%y", offset );
break;
case 'e':
formatter.padNext( 2 )
.appendValue( ChronoField.DAY_OF_MONTH );
formatter.padNext( 2 ).appendValue( ChronoField.DAY_OF_MONTH );
break;
case 'F':
format( formatter, "%Y-%m-%d", offset );
@@ -97,7 +89,7 @@ final class LuaDateTime
formatter.appendValue( ChronoField.HOUR_OF_DAY, 2 );
break;
case 'I':
formatter.appendValue( ChronoField.HOUR_OF_AMPM );
formatter.appendValue( ChronoField.HOUR_OF_AMPM, 2 );
break;
case 'j':
formatter.appendValue( ChronoField.DAY_OF_YEAR, 3 );
@@ -176,49 +168,18 @@ final class LuaDateTime
if( isDst != null )
{
boolean requireDst = isDst;
for( ZoneOffset possibleOffset : ZoneOffset.systemDefault()
.getRules()
.getValidOffsets( time ) )
for( ZoneOffset possibleOffset : ZoneOffset.systemDefault().getRules().getValidOffsets( time ) )
{
Instant instant = time.toInstant( possibleOffset );
if( possibleOffset.getRules()
.getDaylightSavings( instant )
.isZero() == requireDst )
if( possibleOffset.getRules().getDaylightSavings( instant ).isZero() == requireDst )
{
return instant.getEpochSecond();
}
}
}
ZoneOffset offset = ZoneOffset.systemDefault()
.getRules()
.getOffset( time );
return time.toInstant( offset )
.getEpochSecond();
}
private static int getField( Map<?, ?> table, String field, int def ) throws LuaException
{
Object value = table.get( field );
if( value instanceof Number )
{
return ((Number) value).intValue();
}
if( def < 0 )
{
throw new LuaException( "field \"" + field + "\" missing in date table" );
}
return def;
}
private static Boolean getBoolField( Map<?, ?> table, String field ) throws LuaException
{
Object value = table.get( field );
if( value instanceof Boolean || value == null )
{
return (Boolean) value;
}
throw new LuaException( "field \"" + field + "\" missing in date table" );
ZoneOffset offset = ZoneOffset.systemDefault().getRules().getOffset( time );
return time.toInstant( offset ).getEpochSecond();
}
static Map<String, ?> toTable( TemporalAccessor date, ZoneId offset, Instant instant )
@@ -232,12 +193,28 @@ final class LuaDateTime
table.put( "sec", date.getLong( ChronoField.SECOND_OF_MINUTE ) );
table.put( "wday", date.getLong( WeekFields.SUNDAY_START.dayOfWeek() ) );
table.put( "yday", date.getLong( ChronoField.DAY_OF_YEAR ) );
table.put( "isdst",
offset.getRules()
.isDaylightSavings( instant ) );
table.put( "isdst", offset.getRules().isDaylightSavings( instant ) );
return table;
}
private static int getField( Map<?, ?> table, String field, int def ) throws LuaException
{
Object value = table.get( field );
if( value instanceof Number ) return ((Number) value).intValue();
if( def < 0 ) throw new LuaException( "field \"" + field + "\" missing in date table" );
return def;
}
private static Boolean getBoolField( Map<?, ?> table, String field ) throws LuaException
{
Object value = table.get( field );
if( value instanceof Boolean || value == null ) return (Boolean) value;
throw new LuaException( "field \"" + field + "\" missing in date table" );
}
private static final TemporalField CENTURY = map( ChronoField.YEAR, ValueRange.of( 0, 6 ), x -> (x / 100) % 100 );
private static final TemporalField ZERO_WEEK = map( WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of( 0, 6 ), x -> x - 1 );
private static TemporalField map( TemporalField field, ValueRange range, LongUnaryOperator convert )
{
return new TemporalField()
@@ -259,7 +236,7 @@ final class LuaDateTime
@Override
public ValueRange range()
{
return this.range;
return range;
}
@Override
@@ -283,7 +260,7 @@ final class LuaDateTime
@Override
public ValueRange rangeRefinedBy( TemporalAccessor temporal )
{
return this.range;
return range;
}
@Override

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.IArguments;
@@ -33,16 +32,36 @@ public class OSAPI implements ILuaAPI
{
private final IAPIEnvironment apiEnvironment;
private final Int2ObjectMap<Alarm> m_alarms = new Int2ObjectOpenHashMap<>();
private int m_clock;
private double m_time;
private int m_day;
private final Int2ObjectMap<Alarm> alarms = new Int2ObjectOpenHashMap<>();
private int clock;
private double time;
private int day;
private int m_nextAlarmToken = 0;
private int nextAlarmToken = 0;
private static class Alarm implements Comparable<Alarm>
{
final double time;
final int day;
Alarm( double time, int day )
{
this.time = time;
this.day = day;
}
@Override
public int compareTo( @Nonnull Alarm o )
{
double t = day * 24.0 + time;
double ot = day * 24.0 + time;
return Double.compare( t, ot );
}
}
public OSAPI( IAPIEnvironment environment )
{
this.apiEnvironment = environment;
apiEnvironment = environment;
}
@Override
@@ -54,252 +73,57 @@ public class OSAPI implements ILuaAPI
@Override
public void startup()
{
this.m_time = this.apiEnvironment.getComputerEnvironment()
.getTimeOfDay();
this.m_day = this.apiEnvironment.getComputerEnvironment()
.getDay();
this.m_clock = 0;
time = apiEnvironment.getComputerEnvironment().getTimeOfDay();
day = apiEnvironment.getComputerEnvironment().getDay();
clock = 0;
synchronized( this.m_alarms )
synchronized( alarms )
{
this.m_alarms.clear();
alarms.clear();
}
}
@Override
public void update()
{
this.m_clock++;
clock++;
// Wait for all of our alarms
synchronized( this.m_alarms )
synchronized( alarms )
{
double previousTime = this.m_time;
int previousDay = this.m_day;
double time = this.apiEnvironment.getComputerEnvironment()
.getTimeOfDay();
int day = this.apiEnvironment.getComputerEnvironment()
.getDay();
double previousTime = time;
int previousDay = day;
double time = apiEnvironment.getComputerEnvironment().getTimeOfDay();
int day = apiEnvironment.getComputerEnvironment().getDay();
if( time > previousTime || day > previousDay )
{
double now = this.m_day * 24.0 + this.m_time;
Iterator<Int2ObjectMap.Entry<Alarm>> it = this.m_alarms.int2ObjectEntrySet()
.iterator();
double now = this.day * 24.0 + this.time;
Iterator<Int2ObjectMap.Entry<Alarm>> it = alarms.int2ObjectEntrySet().iterator();
while( it.hasNext() )
{
Int2ObjectMap.Entry<Alarm> entry = it.next();
Alarm alarm = entry.getValue();
double t = alarm.m_day * 24.0 + alarm.m_time;
double t = alarm.day * 24.0 + alarm.time;
if( now >= t )
{
this.apiEnvironment.queueEvent( "alarm", entry.getIntKey() );
apiEnvironment.queueEvent( "alarm", entry.getIntKey() );
it.remove();
}
}
}
this.m_time = time;
this.m_day = day;
this.time = time;
this.day = day;
}
}
@Override
public void shutdown()
{
synchronized( this.m_alarms )
synchronized( alarms )
{
this.m_alarms.clear();
}
}
/**
* Adds an event to the event queue. This event can later be pulled with os.pullEvent.
*
* @param name The name of the event to queue.
* @param args The parameters of the event.
* @cc.tparam string name The name of the event to queue.
* @cc.param ... The parameters of the event.
* @cc.see os.pullEvent To pull the event queued
*/
@LuaFunction
public final void queueEvent( String name, IArguments args )
{
this.apiEnvironment.queueEvent( name,
args.drop( 1 )
.getAll() );
}
/**
* Starts a timer that will run for the specified number of seconds. Once the timer fires, a timer event will be added to the queue with the ID returned
* from this function as the first parameter.
*
* @param timer The number of seconds until the timer fires.
* @return The ID of the new timer.
* @throws LuaException If the time is below zero.
*/
@LuaFunction
public final int startTimer( double timer ) throws LuaException
{
return this.apiEnvironment.startTimer( Math.round( checkFinite( 0, timer ) / 0.05 ) );
}
/**
* Cancels a timer previously started with startTimer. This will stop the timer from firing.
*
* @param token The ID of the timer to cancel.
* @see #startTimer To start a timer.
*/
@LuaFunction
public final void cancelTimer( int token )
{
this.apiEnvironment.cancelTimer( token );
}
/**
* Sets an alarm that will fire at the specified world time. When it fires, an alarm event will be added to the event queue.
*
* @param time The time at which to fire the alarm, in the range [0.0, 24.0).
* @return The ID of the alarm that was set.
* @throws LuaException If the time is out of range.
*/
@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( this.m_alarms )
{
int day = time > this.m_time ? this.m_day : this.m_day + 1;
this.m_alarms.put( this.m_nextAlarmToken, new Alarm( time, day ) );
return this.m_nextAlarmToken++;
}
}
/**
* Cancels an alarm previously started with setAlarm. This will stop the alarm from firing.
*
* @param token The ID of the alarm to cancel.
* @see #setAlarm To set an alarm.
*/
@LuaFunction
public final void cancelAlarm( int token )
{
synchronized( this.m_alarms )
{
this.m_alarms.remove( token );
}
}
/**
* Shuts down the computer immediately.
*/
@LuaFunction( "shutdown" )
public final void doShutdown()
{
this.apiEnvironment.shutdown();
}
/**
* Reboots the computer immediately.
*/
@LuaFunction( "reboot" )
public final void doReboot()
{
this.apiEnvironment.reboot();
}
/**
* Returns the ID of the computer.
*
* @return The ID of the computer.
*/
@LuaFunction( {
"getComputerID",
"computerID"
} )
public final int getComputerID()
{
return this.apiEnvironment.getComputerID();
}
/**
* Returns the label of the computer, or {@code nil} if none is set.
*
* @return The label of the computer.
* @cc.treturn string The label of the computer.
*/
@LuaFunction( {
"getComputerLabel",
"computerLabel"
} )
public final Object[] getComputerLabel()
{
String label = this.apiEnvironment.getLabel();
return label == null ? null : new Object[] { label };
}
/**
* Set the label of this computer.
*
* @param label The new label. May be {@code nil} in order to clear it.
*/
@LuaFunction
public final void setComputerLabel( Optional<String> label )
{
this.apiEnvironment.setLabel( StringUtil.normaliseLabel( label.orElse( null ) ) );
}
/**
* Returns the number of seconds that the computer has been running.
*
* @return The computer's uptime.
*/
@LuaFunction
public final double clock()
{
return this.m_clock * 0.05;
}
/**
* Returns the current time depending on the string passed in. This will always be in the range [0.0, 24.0).
*
* * If called with {@code ingame}, the current world time will be returned. This is the default if nothing is passed. * If called with {@code utc},
* returns the hour of the day in UTC time. * If called with {@code local}, returns the hour of the day in the timezone the server is located in.
*
* This function can also be called with a table returned from {@link #date}, which will convert the date fields into a UNIX timestamp (number of
* seconds since 1 January 1970).
*
* @param args The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified.
* @return The hour of the selected locale, or a UNIX timestamp from the table, depending on the argument passed in.
* @throws LuaException If an invalid locale is passed.
* @cc.tparam [opt] string|table locale The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame}
* locale if not specified.
* @see #date To get a date table that can be converted with this function.
*/
@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 this.m_time;
default:
throw new LuaException( "Unsupported operation" );
alarms.clear();
}
}
@@ -311,34 +135,6 @@ public class OSAPI implements ILuaAPI
return time;
}
/**
* Returns the day depending on the locale specified.
*
* * If called with {@code ingame}, returns the number of days since the world was created. This is the default. * If called with {@code utc}, returns
* the number of days since 1 January 1970 in the UTC timezone. * If called with {@code local}, returns the number of days since 1 January 1970 in the
* server's local timezone.
*
* @param args The locale to get the day for. Defaults to {@code ingame} if not set.
* @return The day depending on the selected locale.
* @throws LuaException If an invalid locale is passed.
*/
@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 this.m_day;
default:
throw new LuaException( "Unsupported operation" );
}
}
private static int getDayForCalendar( Calendar c )
{
GregorianCalendar g = c instanceof GregorianCalendar ? (GregorianCalendar) c : new GregorianCalendar();
@@ -352,12 +148,244 @@ public class OSAPI implements ILuaAPI
return day;
}
private static long getEpochForCalendar( Calendar c )
{
return c.getTime().getTime();
}
/**
* Adds an event to the event queue. This event can later be pulled with
* os.pullEvent.
*
* @param name The name of the event to queue.
* @param args The parameters of the event.
* @cc.tparam string name The name of the event to queue.
* @cc.param ... The parameters of the event.
* @cc.see os.pullEvent To pull the event queued
*/
@LuaFunction
public final void queueEvent( String name, IArguments args )
{
apiEnvironment.queueEvent( name, args.drop( 1 ).getAll() );
}
/**
* Starts a timer that will run for the specified number of seconds. Once
* the timer fires, a {@code timer} event will be added to the queue with
* the ID returned from this function as the first parameter.
*
* As with @{os.sleep|sleep}, {@code timer} will automatically be rounded up
* to the nearest multiple of 0.05 seconds, as it waits for a fixed amount
* of world ticks.
*
* @param timer The number of seconds until the timer fires.
* @return The ID of the new timer. This can be used to filter the
* {@code timer} event, or {@link #cancelTimer cancel the timer}.
* @throws LuaException If the time is below zero.
* @see #cancelTimer To cancel a timer.
*/
@LuaFunction
public final int startTimer( double timer ) throws LuaException
{
return apiEnvironment.startTimer( Math.round( checkFinite( 0, timer ) / 0.05 ) );
}
/**
* Cancels a timer previously started with startTimer. This will stop the
* timer from firing.
*
* @param token The ID of the timer to cancel.
* @see #startTimer To start a timer.
*/
@LuaFunction
public final void cancelTimer( int token )
{
apiEnvironment.cancelTimer( token );
}
/**
* Sets an alarm that will fire at the specified world time. When it fires,
* an {@code alarm} event will be added to the event queue with the ID
* returned from this function as the first parameter.
*
* @param time The time at which to fire the alarm, in the range [0.0, 24.0).
* @return The ID of the new alarm. This can be used to filter the
* {@code alarm} event, or {@link #cancelAlarm cancel the alarm}.
* @throws LuaException If the time is out of range.
* @see #cancelAlarm To cancel an alarm.
*/
@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( alarms )
{
int day = time > this.time ? this.day : this.day + 1;
alarms.put( nextAlarmToken, new Alarm( time, day ) );
return nextAlarmToken++;
}
}
/**
* Cancels an alarm previously started with setAlarm. This will stop the
* alarm from firing.
*
* @param token The ID of the alarm to cancel.
* @see #setAlarm To set an alarm.
*/
@LuaFunction
public final void cancelAlarm( int token )
{
synchronized( alarms )
{
alarms.remove( token );
}
}
/**
* Shuts down the computer immediately.
*/
@LuaFunction( "shutdown" )
public final void doShutdown()
{
apiEnvironment.shutdown();
}
/**
* Reboots the computer immediately.
*/
@LuaFunction( "reboot" )
public final void doReboot()
{
apiEnvironment.reboot();
}
/**
* Returns the ID of the computer.
*
* @return The ID of the computer.
*/
@LuaFunction( { "getComputerID", "computerID" } )
public final int getComputerID()
{
return apiEnvironment.getComputerID();
}
/**
* Returns the label of the computer, or {@code nil} if none is set.
*
* @return The label of the computer.
* @cc.treturn string The label of the computer.
*/
@LuaFunction( { "getComputerLabel", "computerLabel" } )
public final Object[] getComputerLabel()
{
String label = apiEnvironment.getLabel();
return label == null ? null : new Object[] { label };
}
/**
* Set the label of this computer.
*
* @param label The new label. May be {@code nil} in order to clear it.
*/
@LuaFunction
public final void setComputerLabel( Optional<String> label )
{
apiEnvironment.setLabel( StringUtil.normaliseLabel( label.orElse( null ) ) );
}
/**
* Returns the number of seconds that the computer has been running.
*
* @return The computer's uptime.
*/
@LuaFunction
public final double clock()
{
return clock * 0.05;
}
/**
* Returns the current time depending on the string passed in. This will
* always be in the range [0.0, 24.0).
*
* * If called with {@code ingame}, the current world time will be returned.
* This is the default if nothing is passed.
* * If called with {@code utc}, returns the hour of the day in UTC time.
* * If called with {@code local}, returns the hour of the day in the
* timezone the server is located in.
*
* This function can also be called with a table returned from {@link #date},
* which will convert the date fields into a UNIX timestamp (number of
* seconds since 1 January 1970).
*
* @param args The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified.
* @return The hour of the selected locale, or a UNIX timestamp from the table, depending on the argument passed in.
* @throws LuaException If an invalid locale is passed.
* @cc.tparam [opt] string|table locale The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified.
* @see #date To get a date table that can be converted with this function.
*/
@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 time;
default:
throw new LuaException( "Unsupported operation" );
}
}
/**
* Returns the day depending on the locale specified.
*
* * If called with {@code ingame}, returns the number of days since the
* world was created. This is the default.
* * If called with {@code utc}, returns the number of days since 1 January
* 1970 in the UTC timezone.
* * If called with {@code local}, returns the number of days since 1
* January 1970 in the server's local timezone.
*
* @param args The locale to get the day for. Defaults to {@code ingame} if not set.
* @return The day depending on the selected locale.
* @throws LuaException If an invalid locale is passed.
*/
@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 day;
default:
throw new LuaException( "Unsupported operation" );
}
}
/**
* Returns the number of milliseconds since an epoch depending on the locale.
*
* * If called with {@code ingame}, returns the number of milliseconds since the world was created. This is the default. * If called with {@code utc},
* returns the number of milliseconds since 1 January 1970 in the UTC timezone. * If called with {@code local}, returns the number of seconds since 1 January
* 1970 in the server's local timezone.
* * If called with {@code ingame}, returns the number of milliseconds since the
* world was created. This is the default.
* * If called with {@code utc}, returns the number of milliseconds since 1
* January 1970 in the UTC timezone.
* * If called with {@code local}, returns the number of milliseconds since 1
* January 1970 in the server's local timezone.
*
* @param args The locale to get the milliseconds for. Defaults to {@code ingame} if not set.
* @return The milliseconds since the epoch depending on the selected locale.
@@ -366,8 +394,7 @@ public class OSAPI implements ILuaAPI
@LuaFunction
public final long epoch( Optional<String> args ) throws LuaException
{
switch( args.orElse( "ingame" )
.toLowerCase( Locale.ROOT ) )
switch( args.orElse( "ingame" ).toLowerCase( Locale.ROOT ) )
{
case "utc":
{
@@ -383,30 +410,29 @@ public class OSAPI implements ILuaAPI
}
case "ingame":
// Get in-game epoch
synchronized( this.m_alarms )
synchronized( alarms )
{
return this.m_day * 86400000L + (long) (this.m_time * 3600000.0);
return day * 86400000L + (long) (time * 3600000.0);
}
default:
throw new LuaException( "Unsupported operation" );
}
}
private static long getEpochForCalendar( Calendar c )
{
return c.getTime()
.getTime();
}
/**
* Returns a date string (or table) using a specified format string and optional time to format.
* Returns a date string (or table) using a specified format string and
* optional time to format.
*
* The format string takes the same formats as C's {@code strftime} function (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it can
* be prefixed with an exclamation mark ({@code !}) to use UTC time instead of the server's local timezone.
* The format string takes the same formats as C's {@code strftime} function
* (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it
* can be prefixed with an exclamation mark ({@code !}) to use UTC time
* instead of the server's local timezone.
*
* If the format is exactly {@code *t} (optionally prefixed with {@code !}), a table will be returned instead. This table has fields for the year,
* month, day, hour, minute, second, day of the week, day of the year, and whether Daylight Savings Time is in effect. This table can be converted to a
* UNIX timestamp (days since 1 January 1970) with {@link #date}.
* If the format is exactly {@code *t} (optionally prefixed with {@code !}), a
* table will be returned instead. This table has fields for the year, month,
* day, hour, minute, second, day of the week, day of the year, and whether
* Daylight Savings Time is in effect. This table can be converted to a UNIX
* timestamp (days since 1 January 1970) with {@link #date}.
*
* @param formatA The format of the string to return. This defaults to {@code %c}, which expands to a string similar to "Sat Dec 24 16:58:00 2011".
* @param timeA The time to convert to a string. This defaults to the current time.
@@ -417,8 +443,7 @@ public class OSAPI implements ILuaAPI
public final Object date( Optional<String> formatA, Optional<Long> timeA ) throws LuaException
{
String format = formatA.orElse( "%c" );
long time = timeA.orElseGet( () -> Instant.now()
.getEpochSecond() );
long time = timeA.orElseGet( () -> Instant.now().getEpochSecond() );
Instant instant = Instant.ofEpochSecond( time );
ZonedDateTime date;
@@ -432,40 +457,15 @@ public class OSAPI implements ILuaAPI
else
{
ZoneId id = ZoneId.systemDefault();
offset = id.getRules()
.getOffset( instant );
offset = id.getRules().getOffset( instant );
date = ZonedDateTime.ofInstant( instant, id );
}
if( format.equals( "*t" ) )
{
return LuaDateTime.toTable( date, offset, instant );
}
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 );
}
private static class Alarm implements Comparable<Alarm>
{
final double m_time;
final int m_day;
Alarm( double time, int day )
{
this.m_time = time;
this.m_day = day;
}
@Override
public int compareTo( @Nonnull Alarm o )
{
double t = this.m_day * 24.0 + this.m_time;
double ot = this.m_day * 24.0 + this.m_time;
return Double.compare( t, ot );
}
return formatter.toFormatter( Locale.ROOT ).format( date );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.filesystem.IMount;
@@ -31,213 +30,6 @@ import java.util.*;
*/
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener
{
private final IAPIEnvironment environment;
private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6];
private boolean running;
public PeripheralAPI( IAPIEnvironment environment )
{
this.environment = environment;
this.environment.setPeripheralChangeListener( this );
this.running = false;
}
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;
}
// IPeripheralChangeListener
@Override
public void onPeripheralChanged( ComputerSide side, IPeripheral newPeripheral )
{
synchronized( this.peripherals )
{
int index = side.ordinal();
if( this.peripherals[index] != null )
{
// Queue a detachment
final PeripheralWrapper wrapper = this.peripherals[index];
if( wrapper.isAttached() )
{
wrapper.detach();
}
// Queue a detachment event
this.environment.queueEvent( "peripheral_detach", side.getName() );
}
// Assign the new peripheral
this.peripherals[index] = newPeripheral == null ? null : new PeripheralWrapper( newPeripheral, side.getName() );
if( this.peripherals[index] != null )
{
// Queue an attachment
final PeripheralWrapper wrapper = this.peripherals[index];
if( this.running && !wrapper.isAttached() )
{
wrapper.attach();
}
// Queue an attachment event
this.environment.queueEvent( "peripheral", side.getName() );
}
}
}
@Override
public String[] getNames()
{
return new String[] { "peripheral" };
}
@Override
public void startup()
{
synchronized( this.peripherals )
{
this.running = true;
for( int i = 0; i < 6; i++ )
{
PeripheralWrapper wrapper = this.peripherals[i];
if( wrapper != null && !wrapper.isAttached() )
{
wrapper.attach();
}
}
}
}
@Override
public void shutdown()
{
synchronized( this.peripherals )
{
this.running = false;
for( int i = 0; i < 6; i++ )
{
PeripheralWrapper wrapper = this.peripherals[i];
if( wrapper != null && wrapper.isAttached() )
{
wrapper.detach();
}
}
}
}
@LuaFunction
public final boolean isPresent( String sideName )
{
ComputerSide side = ComputerSide.valueOfInsensitive( sideName );
if( side != null )
{
synchronized( this.peripherals )
{
PeripheralWrapper p = this.peripherals[side.ordinal()];
if( p != null )
{
return true;
}
}
}
return false;
}
@LuaFunction
public final Object[] getType( String sideName )
{
ComputerSide side = ComputerSide.valueOfInsensitive( sideName );
if( side == null )
{
return null;
}
synchronized( this.peripherals )
{
PeripheralWrapper p = this.peripherals[side.ordinal()];
if( p != null )
{
return new Object[] { p.getType() };
}
}
return null;
}
@LuaFunction
public final Object[] getMethods( String sideName )
{
ComputerSide side = ComputerSide.valueOfInsensitive( sideName );
if( side == null )
{
return null;
}
synchronized( this.peripherals )
{
PeripheralWrapper p = this.peripherals[side.ordinal()];
if( p != null )
{
return new Object[] { p.getMethods() };
}
}
return null;
}
@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 );
if( side == null )
{
throw new LuaException( "No peripheral attached" );
}
PeripheralWrapper p;
synchronized( this.peripherals )
{
p = this.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;
}
}
private class PeripheralWrapper extends ComputerAccess
{
private final String side;
@@ -249,54 +41,54 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
PeripheralWrapper( IPeripheral peripheral, String side )
{
super( PeripheralAPI.this.environment );
super( environment );
this.side = side;
this.peripheral = peripheral;
this.attached = false;
attached = false;
this.type = Objects.requireNonNull( peripheral.getType(), "Peripheral type cannot be null" );
type = Objects.requireNonNull( peripheral.getType(), "Peripheral type cannot be null" );
this.methodMap = PeripheralAPI.getMethods( peripheral );
}
public String getType()
{
return this.type;
methodMap = PeripheralAPI.getMethods( peripheral );
}
public IPeripheral getPeripheral()
{
return this.peripheral;
return peripheral;
}
public String getType()
{
return type;
}
public Collection<String> getMethods()
{
return this.methodMap.keySet();
return methodMap.keySet();
}
public synchronized boolean isAttached()
{
return attached;
}
public synchronized void attach()
{
this.attached = true;
this.peripheral.attach( this );
attached = true;
peripheral.attach( this );
}
public void detach()
{
// Call detach
this.peripheral.detach( this );
peripheral.detach( this );
synchronized( this )
{
// Unmount everything the detach function forgot to do
this.unmountAll();
unmountAll();
}
this.attached = false;
}
public synchronized boolean isAttached()
{
return this.attached;
attached = false;
}
public MethodResult call( ILuaContext context, String methodName, IArguments arguments ) throws LuaException
@@ -304,100 +96,64 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
PeripheralMethod method;
synchronized( this )
{
method = this.methodMap.get( methodName );
method = methodMap.get( methodName );
}
if( method == null )
{
throw new LuaException( "No such method " + methodName );
}
if( method == null ) throw new LuaException( "No such method " + methodName );
PeripheralAPI.this.environment.addTrackingChange( TrackingField.PERIPHERAL_OPS );
return method.apply( this.peripheral, context, this, arguments );
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( !this.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( !this.attached )
{
throw new NotAttachedException();
}
if( !attached ) throw new NotAttachedException();
return super.mountWritable( desiredLoc, mount, driveName );
}
@Override
public synchronized void unmount( String location )
{
if( !this.attached )
{
throw new NotAttachedException();
}
if( !attached ) throw new NotAttachedException();
super.unmount( location );
}
@Override
public int getID()
{
if( !this.attached )
{
throw new NotAttachedException();
}
if( !attached ) throw new NotAttachedException();
return super.getID();
}
@Override
public void queueEvent( @Nonnull String event, Object... arguments )
{
if( !this.attached )
{
throw new NotAttachedException();
}
if( !attached ) throw new NotAttachedException();
super.queueEvent( event, arguments );
}
@Nonnull
@Override
public IWorkMonitor getMainThreadMonitor()
{
if( !this.attached )
{
throw new NotAttachedException();
}
return super.getMainThreadMonitor();
}
@Nonnull
@Override
public String getAttachmentName()
{
if( !this.attached )
{
throw new NotAttachedException();
}
return this.side;
if( !attached ) throw new NotAttachedException();
return side;
}
@Nonnull
@Override
public Map<String, IPeripheral> getAvailablePeripherals()
{
if( !this.attached )
{
throw new NotAttachedException();
}
if( !attached ) throw new NotAttachedException();
Map<String, IPeripheral> peripherals = new HashMap<>();
for( PeripheralWrapper wrapper : PeripheralAPI.this.peripherals )
@@ -415,15 +171,11 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
@Override
public IPeripheral getAvailablePeripheral( @Nonnull String name )
{
if( !this.attached )
{
throw new NotAttachedException();
}
if( !attached ) throw new NotAttachedException();
for( PeripheralWrapper wrapper : PeripheralAPI.this.peripherals )
for( PeripheralWrapper wrapper : peripherals )
{
if( wrapper != null && wrapper.isAttached() && wrapper.getAttachmentName()
.equals( name ) )
if( wrapper != null && wrapper.isAttached() && wrapper.getAttachmentName().equals( name ) )
{
return wrapper.getPeripheral();
}
@@ -431,6 +183,186 @@ public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChange
return null;
}
@Nonnull
@Override
public IWorkMonitor getMainThreadMonitor()
{
if( !attached ) throw new NotAttachedException();
return super.getMainThreadMonitor();
}
}
private final IAPIEnvironment environment;
private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6];
private boolean running;
public PeripheralAPI( IAPIEnvironment environment )
{
this.environment = environment;
this.environment.setPeripheralChangeListener( this );
running = false;
}
// IPeripheralChangeListener
@Override
public void onPeripheralChanged( ComputerSide side, IPeripheral newPeripheral )
{
synchronized( peripherals )
{
int index = side.ordinal();
if( peripherals[index] != null )
{
// Queue a detachment
final PeripheralWrapper wrapper = peripherals[index];
if( wrapper.isAttached() ) wrapper.detach();
// Queue a detachment event
environment.queueEvent( "peripheral_detach", side.getName() );
}
// Assign the new peripheral
peripherals[index] = newPeripheral == null ? null
: new PeripheralWrapper( newPeripheral, side.getName() );
if( peripherals[index] != null )
{
// Queue an attachment
final PeripheralWrapper wrapper = peripherals[index];
if( running && !wrapper.isAttached() ) wrapper.attach();
// Queue an attachment event
environment.queueEvent( "peripheral", side.getName() );
}
}
}
@Override
public String[] getNames()
{
return new String[] { "peripheral" };
}
@Override
public void startup()
{
synchronized( peripherals )
{
running = true;
for( int i = 0; i < 6; i++ )
{
PeripheralWrapper wrapper = peripherals[i];
if( wrapper != null && !wrapper.isAttached() ) wrapper.attach();
}
}
}
@Override
public void shutdown()
{
synchronized( peripherals )
{
running = false;
for( int i = 0; i < 6; i++ )
{
PeripheralWrapper wrapper = peripherals[i];
if( wrapper != null && wrapper.isAttached() )
{
wrapper.detach();
}
}
}
}
@LuaFunction
public final boolean isPresent( String sideName )
{
ComputerSide side = ComputerSide.valueOfInsensitive( sideName );
if( side != null )
{
synchronized( peripherals )
{
PeripheralWrapper p = peripherals[side.ordinal()];
if( p != null ) return true;
}
}
return false;
}
@LuaFunction
public final Object[] getType( String sideName )
{
ComputerSide side = ComputerSide.valueOfInsensitive( sideName );
if( side == null ) return null;
synchronized( peripherals )
{
PeripheralWrapper p = peripherals[side.ordinal()];
if( p != null ) return new Object[] { p.getType() };
}
return null;
}
@LuaFunction
public final Object[] getMethods( String sideName )
{
ComputerSide side = ComputerSide.valueOfInsensitive( sideName );
if( side == null ) return null;
synchronized( peripherals )
{
PeripheralWrapper p = peripherals[side.ordinal()];
if( p != null ) return new Object[] { p.getMethods() };
}
return null;
}
@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 );
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;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.ILuaAPI;
@@ -14,37 +13,43 @@ import dan200.computercraft.core.computer.ComputerSide;
/**
* Interact with redstone attached to this computer.
*
* The {@link RedstoneAPI} library exposes three "types" of redstone control: - Binary input/output ({@link #setOutput}/{@link #getInput}): These simply
* check if a redstone wire has any input or output. A signal strength of 1 and 15 are treated the same. - Analogue input/output ({@link
* #setAnalogOutput}/{@link #getAnalogInput}): These work with the actual signal strength of the redstone wired, from 0 to 15. - Bundled cables ({@link
* #setBundledOutput}/{@link #getBundledInput}): These interact with "bundled" cables, such as those from Project:Red. These allow you to send 16 separate
* on/off signals. Each channel corresponds to a colour, with the first being @{colors.white} and the last @{colors.black}.
* The {@link RedstoneAPI} library exposes three "types" of redstone control:
* - Binary input/output ({@link #setOutput}/{@link #getInput}): These simply check if a redstone wire has any input or
* output. A signal strength of 1 and 15 are treated the same.
* - Analogue input/output ({@link #setAnalogOutput}/{@link #getAnalogInput}): These work with the actual signal
* strength of the redstone wired, from 0 to 15.
* - Bundled cables ({@link #setBundledOutput}/{@link #getBundledInput}): These interact with "bundled" cables, such
* as those from Project:Red. These allow you to send 16 separate on/off signals. Each channel corresponds to a
* colour, with the first being @{colors.white} and the last @{colors.black}.
*
* Whenever a redstone input changes, a {@code redstone} event will be fired. This may be used instead of repeativly polling.
* Whenever a redstone input changes, a {@code redstone} event will be fired. This may be used instead of repeativly
* polling.
*
* This module may also be referred to as {@code rs}. For example, one may call {@code rs.getSides()} instead of {@link #getSides}.
* This module may also be referred to as {@code rs}. For example, one may call {@code rs.getSides()} instead of
* {@link #getSides}.
*
* @cc.usage Toggle the redstone signal above the computer every 0.5 seconds.
*
* <pre>
* while true do
* redstone.setOutput("top", not redstone.getOutput("top"))
* sleep(0.5)
* end
* </pre>
* while true do
* redstone.setOutput("top", not redstone.getOutput("top"))
* sleep(0.5)
* end
* </pre>
* @cc.usage Mimic a redstone comparator in [subtraction mode][comparator].
*
* <pre>
* while true do
* local rear = rs.getAnalogueInput("back")
* local sides = math.max(rs.getAnalogueInput("left"), rs.getAnalogueInput("right"))
* rs.setAnalogueOutput("front", math.max(rear - sides, 0))
* while true do
* local rear = rs.getAnalogueInput("back")
* local sides = math.max(rs.getAnalogueInput("left"), rs.getAnalogueInput("right"))
* rs.setAnalogueOutput("front", math.max(rear - sides, 0))
*
* os.pullEvent("redstone") -- Wait for a change to inputs.
* end
* </pre>
* os.pullEvent("redstone") -- Wait for a change to inputs.
* end
* </pre>
*
* [comparator]: https://minecraft.gamepedia.com/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on the Minecraft wiki."
* [comparator]: https://minecraft.gamepedia.com/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on
* the Minecraft wiki."
* @cc.module redstone
*/
public class RedstoneAPI implements ILuaAPI
@@ -59,14 +64,12 @@ public class RedstoneAPI implements ILuaAPI
@Override
public String[] getNames()
{
return new String[] {
"rs",
"redstone"
};
return new String[] { "rs", "redstone" };
}
/**
* Returns a table containing the six sides of the computer. Namely, "top", "bottom", "left", "right", "front" and "back".
* Returns a table containing the six sides of the computer. Namely, "top", "bottom", "left", "right", "front" and
* "back".
*
* @return A table of valid sides.
*/
@@ -85,7 +88,7 @@ public class RedstoneAPI implements ILuaAPI
@LuaFunction
public final void setOutput( ComputerSide side, boolean on )
{
this.environment.setOutput( side, on ? 15 : 0 );
environment.setOutput( side, on ? 15 : 0 );
}
/**
@@ -98,7 +101,7 @@ public class RedstoneAPI implements ILuaAPI
@LuaFunction
public final boolean getOutput( ComputerSide side )
{
return this.environment.getOutput( side ) > 0;
return environment.getOutput( side ) > 0;
}
/**
@@ -110,7 +113,7 @@ public class RedstoneAPI implements ILuaAPI
@LuaFunction
public final boolean getInput( ComputerSide side )
{
return this.environment.getInput( side ) > 0;
return environment.getInput( side ) > 0;
}
/**
@@ -120,17 +123,11 @@ public class RedstoneAPI implements ILuaAPI
* @param value The signal strength between 0 and 15.
* @throws LuaException If {@code value} is not betwene 0 and 15.
*/
@LuaFunction( {
"setAnalogOutput",
"setAnalogueOutput"
} )
@LuaFunction( { "setAnalogOutput", "setAnalogueOutput" } )
public final void setAnalogOutput( ComputerSide side, int value ) throws LuaException
{
if( value < 0 || value > 15 )
{
throw new LuaException( "Expected number in range 0-15" );
}
this.environment.setOutput( side, value );
if( value < 0 || value > 15 ) throw new LuaException( "Expected number in range 0-15" );
environment.setOutput( side, value );
}
/**
@@ -140,13 +137,10 @@ public class RedstoneAPI implements ILuaAPI
* @return The output signal strength, between 0 and 15.
* @see #setAnalogOutput
*/
@LuaFunction( {
"getAnalogOutput",
"getAnalogueOutput"
} )
@LuaFunction( { "getAnalogOutput", "getAnalogueOutput" } )
public final int getAnalogOutput( ComputerSide side )
{
return this.environment.getOutput( side );
return environment.getOutput( side );
}
/**
@@ -155,13 +149,10 @@ public class RedstoneAPI implements ILuaAPI
* @param side The side to get.
* @return The input signal strength, between 0 and 15.
*/
@LuaFunction( {
"getAnalogInput",
"getAnalogueInput"
} )
@LuaFunction( { "getAnalogInput", "getAnalogueInput" } )
public final int getAnalogInput( ComputerSide side )
{
return this.environment.getInput( side );
return environment.getInput( side );
}
/**
@@ -175,7 +166,7 @@ public class RedstoneAPI implements ILuaAPI
@LuaFunction
public final void setBundledOutput( ComputerSide side, int output )
{
this.environment.setBundledOutput( side, output );
environment.setBundledOutput( side, output );
}
/**
@@ -187,7 +178,7 @@ public class RedstoneAPI implements ILuaAPI
@LuaFunction
public final int getBundledOutput( ComputerSide side )
{
return this.environment.getBundledOutput( side );
return environment.getBundledOutput( side );
}
/**
@@ -211,14 +202,14 @@ public class RedstoneAPI implements ILuaAPI
* @return If the colours are on.
* @cc.usage Check if @{colors.white} and @{colors.black} are on above the computer.
* <pre>
* print(redstone.testBundledInput("top", colors.combine(colors.white, colors.black)))
* </pre>
* print(redstone.testBundledInput("top", colors.combine(colors.white, colors.black)))
* </pre>
* @see #getBundledInput
*/
@LuaFunction
public final boolean testBundledInput( ComputerSide side, int mask )
{
int input = this.environment.getBundledInput( side );
int input = environment.getBundledInput( side );
return (input & mask) == mask;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.LuaException;
@@ -22,7 +21,32 @@ public final class TableHelper
{
private TableHelper()
{
throw new IllegalStateException( "Cannot instantiate singleton " + this.getClass().getName() );
throw new IllegalStateException( "Cannot instantiate singleton " + getClass().getName() );
}
@Nonnull
public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nullable Object actual )
{
return badKey( key, expected, LuaValues.getType( actual ) );
}
@Nonnull
public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nonnull String actual )
{
return new LuaException( "bad field '" + key + "' (" + expected + " expected, got " + actual + ")" );
}
public static double getNumberField( @Nonnull Map<?, ?> table, @Nonnull String key ) throws LuaException
{
Object value = table.get( key );
if( value instanceof Number )
{
return ((Number) value).doubleValue();
}
else
{
throw badKey( key, "number", value );
}
}
public static int getIntField( @Nonnull Map<?, ?> table, @Nonnull String key ) throws LuaException
@@ -38,45 +62,11 @@ public final class TableHelper
}
}
@Nonnull
public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nullable Object actual )
{
return badKey( key, expected, LuaValues.getType( actual ) );
}
@Nonnull
public static LuaException badKey( @Nonnull String key, @Nonnull String expected, @Nonnull String actual )
{
return new LuaException( "bad field '" + key + "' (" + expected + " expected, got " + actual + ")" );
}
public static double getRealField( @Nonnull Map<?, ?> table, @Nonnull String key ) throws LuaException
{
return checkReal( key, getNumberField( table, key ) );
}
private static double checkReal( @Nonnull String key, double value ) throws LuaException
{
if( !Double.isFinite( value ) )
{
throw badKey( key, "number", getNumericType( value ) );
}
return value;
}
public static double getNumberField( @Nonnull Map<?, ?> table, @Nonnull String key ) throws LuaException
{
Object value = table.get( key );
if( value instanceof Number )
{
return ((Number) value).doubleValue();
}
else
{
throw badKey( key, "number", value );
}
}
public static boolean getBooleanField( @Nonnull Map<?, ?> table, @Nonnull String key ) throws LuaException
{
Object value = table.get( key );
@@ -119,6 +109,23 @@ public final class TableHelper
}
}
public static double optNumberField( @Nonnull Map<?, ?> table, @Nonnull String key, double def ) throws LuaException
{
Object value = table.get( key );
if( value == null )
{
return def;
}
else if( value instanceof Number )
{
return ((Number) value).doubleValue();
}
else
{
throw badKey( key, "number", value );
}
}
public static int optIntField( @Nonnull Map<?, ?> table, @Nonnull String key, int def ) throws LuaException
{
Object value = table.get( key );
@@ -141,23 +148,6 @@ public final class TableHelper
return checkReal( key, optNumberField( table, key, def ) );
}
public static double optNumberField( @Nonnull Map<?, ?> table, @Nonnull String key, double def ) throws LuaException
{
Object value = table.get( key );
if( value == null )
{
return def;
}
else if( value instanceof Number )
{
return ((Number) value).doubleValue();
}
else
{
throw badKey( key, "number", value );
}
}
public static boolean optBooleanField( @Nonnull Map<?, ?> table, @Nonnull String key, boolean def ) throws LuaException
{
Object value = table.get( key );
@@ -209,4 +199,10 @@ public final class TableHelper
throw badKey( key, "table", value );
}
}
private static double checkReal( @Nonnull String key, double value ) throws LuaException
{
if( !Double.isFinite( value ) ) throw badKey( key, "number", getNumericType( value ) );
return value;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.IArguments;
@@ -28,7 +27,7 @@ public class TermAPI extends TermMethods implements ILuaAPI
public TermAPI( IAPIEnvironment environment )
{
this.terminal = environment.getTerminal();
terminal = environment.getTerminal();
this.environment = environment.getComputerEnvironment();
}
@@ -49,10 +48,7 @@ public class TermAPI extends TermMethods implements ILuaAPI
* @cc.treturn number The blue channel, will be between 0 and 1.
* @see TermMethods#setPaletteColour(IArguments) To change the palette colour.
*/
@LuaFunction( {
"nativePaletteColour",
"nativePaletteColor"
} )
@LuaFunction( { "nativePaletteColour", "nativePaletteColor" } )
public final Object[] nativePaletteColour( int colour ) throws LuaException
{
int actualColour = 15 - parseColour( colour );
@@ -61,10 +57,7 @@ public class TermAPI extends TermMethods implements ILuaAPI
float[] rgb = c.getRGB();
Object[] rgbObj = new Object[rgb.length];
for( int i = 0; i < rgbObj.length; ++i )
{
rgbObj[i] = rgb[i];
}
for( int i = 0; i < rgbObj.length; ++i ) rgbObj[i] = rgb[i];
return rgbObj;
}
@@ -72,12 +65,12 @@ public class TermAPI extends TermMethods implements ILuaAPI
@Override
public Terminal getTerminal()
{
return this.terminal;
return terminal;
}
@Override
public boolean isColour()
{
return this.environment.isColour();
return environment.isColour();
}
}

View File

@@ -22,10 +22,27 @@ import javax.annotation.Nonnull;
*/
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;
/**
* Write {@code text} at the current cursor position, moving the cursor to the end of the text.
*
* Unlike functions like {@code write} and {@code print}, this does not wrap the text - it simply copies the text to the current terminal line.
* Unlike functions like {@code write} and {@code print}, this does not wrap the text - it simply copies the
* text to the current terminal line.
*
* @param arguments The text to write.
* @throws LuaException (hidden) If the terminal cannot be found.
@@ -35,7 +52,7 @@ public abstract class TermMethods
public final void write( IArguments arguments ) throws LuaException
{
String text = StringUtil.toString( arguments.get( 0 ) );
Terminal terminal = this.getTerminal();
Terminal terminal = getTerminal();
synchronized( terminal )
{
terminal.write( text );
@@ -43,14 +60,11 @@ public abstract class TermMethods
}
}
@Nonnull
public abstract Terminal getTerminal() throws LuaException;
/**
* Move all positions up (or down) by {@code y} pixels.
*
* Every pixel in the terminal will be replaced by the line {@code y} pixels below it. If {@code y} is negative, it will copy pixels from above
* instead.
* Every pixel in the terminal will be replaced by the line {@code y} pixels below it. If {@code y} is negative, it
* will copy pixels from above instead.
*
* @param y The number of lines to move up by. This may be a negative number.
* @throws LuaException (hidden) If the terminal cannot be found.
@@ -58,7 +72,7 @@ public abstract class TermMethods
@LuaFunction
public final void scroll( int y ) throws LuaException
{
this.getTerminal().scroll( y );
getTerminal().scroll( y );
}
/**
@@ -72,11 +86,8 @@ public abstract class TermMethods
@LuaFunction
public final Object[] getCursorPos() throws LuaException
{
Terminal terminal = this.getTerminal();
return new Object[] {
terminal.getCursorX() + 1,
terminal.getCursorY() + 1
};
Terminal terminal = getTerminal();
return new Object[] { terminal.getCursorX() + 1, terminal.getCursorY() + 1 };
}
/**
@@ -89,7 +100,7 @@ public abstract class TermMethods
@LuaFunction
public final void setCursorPos( int x, int y ) throws LuaException
{
Terminal terminal = this.getTerminal();
Terminal terminal = getTerminal();
synchronized( terminal )
{
terminal.setCursorPos( x - 1, y - 1 );
@@ -105,7 +116,7 @@ public abstract class TermMethods
@LuaFunction
public final boolean getCursorBlink() throws LuaException
{
return this.getTerminal().getCursorBlink();
return getTerminal().getCursorBlink();
}
/**
@@ -117,7 +128,7 @@ public abstract class TermMethods
@LuaFunction
public final void setCursorBlink( boolean blink ) throws LuaException
{
Terminal terminal = this.getTerminal();
Terminal terminal = getTerminal();
synchronized( terminal )
{
terminal.setCursorBlink( blink );
@@ -135,11 +146,8 @@ public abstract class TermMethods
@LuaFunction
public final Object[] getSize() throws LuaException
{
Terminal terminal = this.getTerminal();
return new Object[] {
terminal.getWidth(),
terminal.getHeight()
};
Terminal terminal = getTerminal();
return new Object[] { terminal.getWidth(), terminal.getHeight() };
}
/**
@@ -150,18 +158,19 @@ public abstract class TermMethods
@LuaFunction
public final void clear() throws LuaException
{
this.getTerminal().clear();
getTerminal().clear();
}
/**
* Clears the line the cursor is currently on, filling it with the {@link #getBackgroundColour() current background colour}.
* Clears the line the cursor is currently on, filling it with the {@link #getBackgroundColour() current background
* colour}.
*
* @throws LuaException (hidden) If the terminal cannot be found.
*/
@LuaFunction
public final void clearLine() throws LuaException
{
this.getTerminal().clearLine();
getTerminal().clearLine();
}
/**
@@ -171,13 +180,10 @@ public abstract class TermMethods
* @throws LuaException (hidden) If the terminal cannot be found.
* @cc.see colors For a list of colour constants, returned by this function.
*/
@LuaFunction( {
"getTextColour",
"getTextColor"
} )
@LuaFunction( { "getTextColour", "getTextColor" } )
public final int getTextColour() throws LuaException
{
return encodeColour( this.getTerminal().getTextColour() );
return encodeColour( getTerminal().getTextColour() );
}
/**
@@ -187,81 +193,44 @@ public abstract class TermMethods
* @throws LuaException (hidden) If the terminal cannot be found.
* @cc.see colors For a list of colour constants.
*/
@LuaFunction( {
"setTextColour",
"setTextColor"
} )
@LuaFunction( { "setTextColour", "setTextColor" } )
public final void setTextColour( int colourArg ) throws LuaException
{
int colour = parseColour( colourArg );
Terminal terminal = this.getTerminal();
Terminal terminal = getTerminal();
synchronized( terminal )
{
terminal.setTextColour( colour );
}
}
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;
}
private static int getHighestBit( int group )
{
int bit = 0;
while( group > 0 )
{
group >>= 1;
bit++;
}
return bit;
}
public static int encodeColour( int colour )
{
return 1 << colour;
}
/**
* Return the current background colour. This is used when {@link #write writing text} and {@link #clear clearing} the terminal.
* Return the current background colour. This is used when {@link #write writing text} and {@link #clear clearing}
* the terminal.
*
* @return The current background colour.
* @throws LuaException (hidden) If the terminal cannot be found.
* @cc.see colors For a list of colour constants, returned by this function.
*/
@LuaFunction( {
"getBackgroundColour",
"getBackgroundColor"
} )
@LuaFunction( { "getBackgroundColour", "getBackgroundColor" } )
public final int getBackgroundColour() throws LuaException
{
return encodeColour( this.getTerminal().getBackgroundColour() );
return encodeColour( getTerminal().getBackgroundColour() );
}
/**
* Set the current background colour. This is used when {@link #write writing text} and {@link #clear clearing} the terminal.
* Set the current background colour. This is used when {@link #write writing text} and {@link #clear clearing} the
* terminal.
*
* @param colourArg The new background colour.
* @throws LuaException (hidden) If the terminal cannot be found.
* @cc.see colors For a list of colour constants.
*/
@LuaFunction( {
"setBackgroundColour",
"setBackgroundColor"
} )
@LuaFunction( { "setBackgroundColour", "setBackgroundColor" } )
public final void setBackgroundColour( int colourArg ) throws LuaException
{
int colour = parseColour( colourArg );
Terminal terminal = this.getTerminal();
Terminal terminal = getTerminal();
synchronized( terminal )
{
terminal.setBackgroundColour( colour );
@@ -271,29 +240,27 @@ public abstract class TermMethods
/**
* Determine if this terminal supports colour.
*
* Terminals which do not support colour will still allow writing coloured text/backgrounds, but it will be displayed in greyscale.
* Terminals which do not support colour will still allow writing coloured text/backgrounds, but it will be
* displayed in greyscale.
*
* @return Whether this terminal supports colour.
* @throws LuaException (hidden) If the terminal cannot be found.
*/
@LuaFunction( {
"isColour",
"isColor"
} )
@LuaFunction( { "isColour", "isColor" } )
public final boolean getIsColour() throws LuaException
{
return this.isColour();
return isColour();
}
public abstract boolean isColour() throws LuaException;
/**
* Writes {@code text} to the terminal with the specific foreground and background characters.
*
* As with {@link #write(IArguments)}, the text will be written at the current cursor location, with the cursor moving to the end of the text.
* As with {@link #write(IArguments)}, the text will be written at the current cursor location, with the cursor
* moving to the end of the text.
*
* {@code textColour} and {@code backgroundColour} must both be strings the same length as {@code text}. All characters represent a single hexadecimal
* digit, which is converted to one of CC's colours. For instance, {@code "a"} corresponds to purple.
* {@code textColour} and {@code backgroundColour} must both be strings the same length as {@code text}. All
* characters represent a single hexadecimal digit, which is converted to one of CC's colours. For instance,
* {@code "a"} corresponds to purple.
*
* @param text The text to write.
* @param textColour The corresponding text colours.
@@ -302,8 +269,8 @@ public abstract class TermMethods
* @cc.see colors For a list of colour constants, and their hexadecimal values.
* @cc.usage Prints "Hello, world!" in rainbow text.
* <pre>{@code
* term.blit("Hello, world!","01234456789ab","0000000000000")
* }</pre>
* term.blit("Hello, world!","01234456789ab","0000000000000")
* }</pre>
*/
@LuaFunction
public final void blit( String text, String textColour, String backgroundColour ) throws LuaException
@@ -313,7 +280,7 @@ public abstract class TermMethods
throw new LuaException( "Arguments must be the same length" );
}
Terminal terminal = this.getTerminal();
Terminal terminal = getTerminal();
synchronized( terminal )
{
terminal.blit( text, textColour, backgroundColour );
@@ -324,38 +291,36 @@ public abstract class TermMethods
/**
* Set the palette for a specific colour.
*
* ComputerCraft's palette system allows you to change how a specific colour should be displayed. For instance, you can make @{colors.red} <em>more
* red</em> by setting its palette to #FF0000. This does now allow you to draw more colours - you are still limited to 16 on the screen at one time -
* but you can change <em>which</em> colours are used.
* ComputerCraft's palette system allows you to change how a specific colour should be displayed. For instance, you
* can make @{colors.red} <em>more red</em> by setting its palette to #FF0000. This does now allow you to draw more
* colours - you are still limited to 16 on the screen at one time - but you can change <em>which</em> colours are
* used.
*
* @param args The new palette values.
* @throws LuaException (hidden) If the terminal cannot be found.
* @cc.tparam [1] number index The colour whose palette should be changed.
* @cc.tparam number colour A 24-bit integer representing the RGB value of the colour. For instance the integer `0xFF0000` corresponds to the colour
* #FF0000.
* @cc.tparam number colour A 24-bit integer representing the RGB value of the colour. For instance the integer
* `0xFF0000` corresponds to the colour #FF0000.
* @cc.tparam [2] number index The colour whose palette should be changed.
* @cc.tparam number r The intensity of the red channel, between 0 and 1.
* @cc.tparam number g The intensity of the green channel, between 0 and 1.
* @cc.tparam number b The intensity of the blue channel, between 0 and 1.
* @cc.usage Change the @{colors.red|red colour} from the default #CC4C4C to #FF0000.
* <pre>{@code
* term.setPaletteColour(colors.red, 0xFF0000)
* term.setTextColour(colors.red)
* print("Hello, world!")
* }</pre>
* term.setPaletteColour(colors.red, 0xFF0000)
* term.setTextColour(colors.red)
* print("Hello, world!")
* }</pre>
* @cc.usage As above, but specifying each colour channel separately.
* <pre>{@code
* term.setPaletteColour(colors.red, 1, 0, 0)
* term.setTextColour(colors.red)
* print("Hello, world!")
* }</pre>
* term.setPaletteColour(colors.red, 1, 0, 0)
* term.setTextColour(colors.red)
* print("Hello, world!")
* }</pre>
* @cc.see colors.unpackRGB To convert from the 24-bit format to three separate channels.
* @cc.see colors.packRGB To convert from three separate channels to the 24-bit format.
*/
@LuaFunction( {
"setPaletteColour",
"setPaletteColor"
} )
@LuaFunction( { "setPaletteColour", "setPaletteColor" } )
public final void setPaletteColour( IArguments args ) throws LuaException
{
int colour = 15 - parseColour( args.getInt( 0 ) );
@@ -363,24 +328,17 @@ public abstract class TermMethods
{
int hex = args.getInt( 1 );
double[] rgb = Palette.decodeRGB8( hex );
setColour( this.getTerminal(), colour, rgb[0], rgb[1], rgb[2] );
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( this.getTerminal(), colour, r, g, b );
setColour( getTerminal(), colour, r, g, b );
}
}
public static void setColour( Terminal terminal, int colour, double r, double g, double b )
{
terminal.getPalette()
.setColour( colour, r, g, b );
terminal.setChanged();
}
/**
* Get the current palette for a specific colour.
*
@@ -391,18 +349,34 @@ public abstract class TermMethods
* @cc.treturn number The green channel, will be between 0 and 1.
* @cc.treturn number The blue channel, will be between 0 and 1.
*/
@LuaFunction( {
"getPaletteColour",
"getPaletteColor"
} )
@LuaFunction( { "getPaletteColour", "getPaletteColor" } )
public final Object[] getPaletteColour( int colourArg ) throws LuaException
{
int colour = 15 - parseColour( colourArg );
Terminal terminal = this.getTerminal();
Terminal terminal = getTerminal();
synchronized( terminal )
{
return ArrayUtils.toObject( terminal.getPalette()
.getColour( colour ) );
return ArrayUtils.toObject( terminal.getPalette().getColour( colour ) );
}
}
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 )
{
terminal.getPalette().setColour( colour, r, g, b );
terminal.setChanged();
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http;
import dan200.computercraft.core.apis.IAPIEnvironment;
@@ -20,10 +19,12 @@ import java.util.concurrent.Future;
public class CheckUrl extends Resource<CheckUrl>
{
private static final String EVENT = "http_check";
private Future<?> future;
private final IAPIEnvironment environment;
private final String address;
private final URI uri;
private Future<?> future;
public CheckUrl( ResourceGroup<CheckUrl> limiter, IAPIEnvironment environment, String address, URI uri )
{
@@ -35,20 +36,14 @@ public class CheckUrl extends Resource<CheckUrl>
public void run()
{
if( this.isClosed() )
{
return;
}
this.future = NetworkUtils.EXECUTOR.submit( this::doRun );
this.checkClosed();
if( isClosed() ) return;
future = NetworkUtils.EXECUTOR.submit( this::doRun );
checkClosed();
}
private void doRun()
{
if( this.isClosed() )
{
return;
}
if( isClosed() ) return;
try
{
@@ -56,17 +51,11 @@ public class CheckUrl extends Resource<CheckUrl>
InetSocketAddress netAddress = NetworkUtils.getAddress( uri, ssl );
NetworkUtils.getOptions( uri.getHost(), netAddress );
if( this.tryClose() )
{
this.environment.queueEvent( EVENT, this.address, true );
}
if( tryClose() ) environment.queueEvent( EVENT, address, true );
}
catch( HTTPRequestException e )
{
if( this.tryClose() )
{
this.environment.queueEvent( EVENT, this.address, false, e.getMessage() );
}
if( tryClose() ) environment.queueEvent( EVENT, address, false, e.getMessage() );
}
}
@@ -74,6 +63,6 @@ public class CheckUrl extends Resource<CheckUrl>
protected void dispose()
{
super.dispose();
this.future = closeFuture( this.future );
future = closeFuture( future );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http;
public class HTTPRequestException extends Exception

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http;
import dan200.computercraft.ComputerCraft;
@@ -12,12 +11,19 @@ import dan200.computercraft.core.apis.http.options.AddressRule;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.shared.util.ThreadUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.timeout.ReadTimeoutException;
import javax.annotation.Nonnull;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;
import java.net.InetSocketAddress;
import java.net.URI;
@@ -32,69 +38,35 @@ import java.util.concurrent.TimeUnit;
*/
public final class NetworkUtils
{
public static final ExecutorService EXECUTOR = new ThreadPoolExecutor( 4,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
public static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
4, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
ThreadUtils.builder( "Network" )
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
.build() );
.build()
);
public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup( 4,
ThreadUtils.builder( "Netty" )
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
.build() );
private static final Object sslLock = new Object();
private static TrustManagerFactory trustManager;
private static SslContext sslContext;
private static boolean triedSslContext = false;
public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup( 4, ThreadUtils.builder( "Netty" )
.setPriority( Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2 )
.build()
);
private NetworkUtils()
{
}
public static SslContext getSslContext() throws HTTPRequestException
{
if( sslContext != null || triedSslContext )
{
return sslContext;
}
synchronized( sslLock )
{
if( sslContext != null || triedSslContext )
{
return sslContext;
}
try
{
return sslContext = SslContextBuilder.forClient()
.trustManager( getTrustManager() )
.build();
}
catch( SSLException e )
{
ComputerCraft.log.error( "Cannot construct SSL context", e );
triedSslContext = true;
sslContext = null;
throw new HTTPRequestException( "Cannot create a secure connection" );
}
}
}
private static final Object sslLock = new Object();
private static TrustManagerFactory trustManager;
private static SslContext sslContext;
private static boolean triedSslContext = false;
private static TrustManagerFactory getTrustManager()
{
if( trustManager != null )
{
return trustManager;
}
if( trustManager != null ) return trustManager;
synchronized( sslLock )
{
if( trustManager != null )
{
return trustManager;
}
if( trustManager != null ) return trustManager;
TrustManagerFactory tmf = null;
try
@@ -111,6 +83,30 @@ public final class NetworkUtils
}
}
public static SslContext getSslContext() throws HTTPRequestException
{
if( sslContext != null || triedSslContext ) return sslContext;
synchronized( sslLock )
{
if( sslContext != null || triedSslContext ) return sslContext;
try
{
return sslContext = SslContextBuilder
.forClient()
.trustManager( getTrustManager() )
.build();
}
catch( SSLException e )
{
ComputerCraft.log.error( "Cannot construct SSL context", e );
triedSslContext = true;
sslContext = null;
throw new HTTPRequestException( "Cannot create a secure connection" );
}
}
}
/**
* Create a {@link InetSocketAddress} from a {@link java.net.URI}.
*
@@ -139,15 +135,9 @@ public final class NetworkUtils
*/
public static InetSocketAddress getAddress( String host, int port, boolean ssl ) throws HTTPRequestException
{
if( port < 0 )
{
port = ssl ? 443 : 80;
}
if( port < 0 ) port = ssl ? 443 : 80;
InetSocketAddress socketAddress = new InetSocketAddress( host, port );
if( socketAddress.isUnresolved() )
{
throw new HTTPRequestException( "Unknown host" );
}
if( socketAddress.isUnresolved() ) throw new HTTPRequestException( "Unknown host" );
return socketAddress;
}
@@ -162,10 +152,7 @@ public final class NetworkUtils
public static Options getOptions( String host, InetSocketAddress address ) throws HTTPRequestException
{
Options options = AddressRule.apply( ComputerCraft.httpRules, host, address );
if( options.action == Action.DENY )
{
throw new HTTPRequestException( "Domain not permitted" );
}
if( options.action == Action.DENY ) throw new HTTPRequestException( "Domain not permitted" );
return options;
}
@@ -181,4 +168,29 @@ public final class NetworkUtils
buffer.readBytes( bytes );
return bytes;
}
@Nonnull
public static String toFriendlyError( @Nonnull Throwable cause )
{
if( cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException )
{
return cause.getMessage();
}
else if( cause instanceof TooLongFrameException )
{
return "Message is too large";
}
else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException )
{
return "Timed out";
}
else if( cause instanceof SSLHandshakeException || (cause instanceof DecoderException && cause.getCause() instanceof SSLHandshakeException) )
{
return "Could not create a secure connection";
}
else
{
return "Could not connect";
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http;
import dan200.computercraft.shared.util.IoUtil;
@@ -25,7 +24,6 @@ import java.util.function.Consumer;
*/
public abstract class Resource<T extends Resource<T>> implements Closeable
{
private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<>();
private final AtomicBoolean closed = new AtomicBoolean( false );
private final ResourceGroup<T> limiter;
@@ -34,66 +32,6 @@ public abstract class Resource<T extends Resource<T>> implements Closeable
this.limiter = limiter;
}
protected static <T extends Closeable> T closeCloseable( T closeable )
{
IoUtil.closeQuietly( closeable );
return null;
}
protected static ChannelFuture closeChannel( ChannelFuture future )
{
if( future != null )
{
future.cancel( false );
Channel channel = future.channel();
if( channel != null && channel.isOpen() )
{
channel.close();
}
}
return null;
}
protected static <T extends Future<?>> T closeFuture( T future )
{
if( future != null )
{
future.cancel( true );
}
return null;
}
public static void cleanup()
{
Reference<?> reference;
while( (reference = QUEUE.poll()) != null )
{
((CloseReference<?>) reference).resource.close();
}
}
@Override
public final void close()
{
this.tryClose();
}
/**
* Try to close the current resource.
*
* @return Whether this was successfully closed, or {@code false} if it has already been closed.
*/
protected final boolean tryClose()
{
if( this.closed.getAndSet( true ) )
{
return false;
}
this.dispose();
return true;
}
/**
* Whether this resource is closed.
*
@@ -101,7 +39,7 @@ public abstract class Resource<T extends Resource<T>> implements Closeable
*/
public final boolean isClosed()
{
return this.closed.get();
return closed.get();
}
/**
@@ -111,24 +49,34 @@ public abstract class Resource<T extends Resource<T>> implements Closeable
*/
public final boolean checkClosed()
{
if( !this.closed.get() )
{
return false;
}
this.dispose();
if( !closed.get() ) return false;
dispose();
return true;
}
/**
* Try to close the current resource.
*
* @return Whether this was successfully closed, or {@code false} if it has already been closed.
*/
protected final boolean tryClose()
{
if( closed.getAndSet( true ) ) return false;
dispose();
return true;
}
/**
* Clean up any pending resources
*
* Note, this may be called multiple times, and so should be thread-safe and avoid any major side effects.
* Note, this may be called multiple times, and so should be thread-safe and
* avoid any major side effects.
*/
protected void dispose()
{
@SuppressWarnings( "unchecked" )
T thisT = (T) this;
this.limiter.release( thisT );
limiter.release( thisT );
}
/**
@@ -143,13 +91,46 @@ public abstract class Resource<T extends Resource<T>> implements Closeable
return new CloseReference<>( this, object );
}
public boolean queue( Consumer<T> task )
@Override
public final void close()
{
tryClose();
}
public final boolean queue( Consumer<T> task )
{
@SuppressWarnings( "unchecked" )
T thisT = (T) this;
return this.limiter.queue( thisT, () -> task.accept( thisT ) );
return limiter.queue( thisT, () -> task.accept( thisT ) );
}
protected static <T extends Closeable> T closeCloseable( T closeable )
{
IoUtil.closeQuietly( closeable );
return null;
}
protected static ChannelFuture closeChannel( ChannelFuture future )
{
if( future != null )
{
future.cancel( false );
Channel channel = future.channel();
if( channel != null && channel.isOpen() ) channel.close();
}
return null;
}
protected static <T extends Future<?>> T closeFuture( T future )
{
if( future != null ) future.cancel( true );
return null;
}
private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<>();
private static class CloseReference<T> extends WeakReference<T>
{
final Resource<?> resource;
@@ -160,4 +141,10 @@ public abstract class Resource<T extends Resource<T>> implements Closeable
this.resource = resource;
}
}
public static void cleanup()
{
Reference<?> reference;
while( (reference = QUEUE.poll()) != null ) ((CloseReference<?>) reference).resource.close();
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http;
import java.util.Collections;
@@ -19,12 +18,17 @@ import java.util.function.Supplier;
*/
public class ResourceGroup<T extends Resource<T>>
{
public static final int DEFAULT_LIMIT = 512;
public static final IntSupplier DEFAULT = () -> DEFAULT_LIMIT;
private static final IntSupplier ZERO = () -> 0;
final IntSupplier limit;
final Set<T> resources = Collections.newSetFromMap( new ConcurrentHashMap<>() );
boolean active = false;
final Set<T> resources = Collections.newSetFromMap( new ConcurrentHashMap<>() );
public ResourceGroup( IntSupplier limit )
{
this.limit = limit;
@@ -32,23 +36,20 @@ public class ResourceGroup<T extends Resource<T>>
public ResourceGroup()
{
this.limit = ZERO;
limit = ZERO;
}
public void startup()
{
this.active = true;
active = true;
}
public synchronized void shutdown()
{
this.active = false;
active = false;
for( T resource : this.resources )
{
resource.close();
}
this.resources.clear();
for( T resource : resources ) resource.close();
resources.clear();
Resource.cleanup();
}
@@ -56,7 +57,7 @@ public class ResourceGroup<T extends Resource<T>>
public final boolean queue( T resource, Runnable setup )
{
return this.queue( () -> {
return queue( () -> {
setup.run();
return resource;
} );
@@ -65,15 +66,12 @@ public class ResourceGroup<T extends Resource<T>>
public synchronized boolean queue( Supplier<T> resource )
{
Resource.cleanup();
if( !this.active )
{
return false;
}
if( !active ) return false;
int limit = this.limit.getAsInt();
if( limit <= 0 || this.resources.size() < limit )
if( limit <= 0 || resources.size() < limit )
{
this.resources.add( resource.get() );
resources.add( resource.get() );
return true;
}
@@ -82,6 +80,6 @@ public class ResourceGroup<T extends Resource<T>>
public synchronized void release( T resource )
{
this.resources.remove( resource );
resources.remove( resource );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http;
import java.util.ArrayDeque;
@@ -32,21 +31,17 @@ public class ResourceQueue<T extends Resource<T>> extends ResourceGroup<T>
public synchronized void shutdown()
{
super.shutdown();
this.pending.clear();
pending.clear();
}
@Override
public synchronized boolean queue( Supplier<T> resource )
{
if( !this.active )
{
return false;
}
if( !active ) return false;
if( super.queue( resource ) ) return true;
if( pending.size() > DEFAULT_LIMIT ) return false;
if( !super.queue( resource ) )
{
this.pending.add( resource );
}
pending.add( resource );
return true;
}
@@ -55,19 +50,13 @@ public class ResourceQueue<T extends Resource<T>> extends ResourceGroup<T>
{
super.release( resource );
if( !this.active )
{
return;
}
if( !active ) return;
int limit = this.limit.getAsInt();
if( limit <= 0 || this.resources.size() < limit )
if( limit <= 0 || resources.size() < limit )
{
Supplier<T> next = this.pending.poll();
if( next != null )
{
this.resources.add( next.get() );
}
Supplier<T> next = pending.poll();
if( next != null ) resources.add( next.get() );
}
}
}

View File

@@ -9,13 +9,14 @@ import javax.annotation.Nonnull;
public enum Action
{
ALLOW, DENY;
ALLOW,
DENY;
private final PartialOptions partial = new PartialOptions( this, null, null, null, null );
@Nonnull
public PartialOptions toPartial()
{
return this.partial;
return partial;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http.options;
import com.google.common.net.InetAddresses;
@@ -62,6 +61,23 @@ public final class AddressRule
}
}
/**
* Determine whether the given address matches a series of patterns.
*
* @param domain The domain to match
* @param port The port of the address.
* @param address The address to check.
* @param ipv4Address An ipv4 version of the address, if the original was an ipv6 address.
* @return Whether it matches any of these patterns.
*/
private boolean matches( String domain, int port, InetAddress address, Inet4Address ipv4Address )
{
if( this.port != null && this.port != port ) return false;
return predicate.matches( domain )
|| predicate.matches( address )
|| (ipv4Address != null && predicate.matches( ipv4Address ));
}
public static Options apply( Iterable<? extends AddressRule> rules, String domain, InetSocketAddress socketAddress )
{
PartialOptions options = null;
@@ -95,21 +111,4 @@ public final class AddressRule
return (options == null ? PartialOptions.DEFAULT : options).toOptions();
}
/**
* Determine whether the given address matches a series of patterns.
*
* @param domain The domain to match
* @param port The port of the address.
* @param address The address to check.
* @param ipv4Address An ipv4 version of the address, if the original was an ipv6 address.
* @return Whether it matches any of these patterns.
*/
private boolean matches( String domain, int port, InetAddress address, Inet4Address ipv4Address )
{
if( this.port != null && this.port != port ) return false;
return predicate.matches( domain )
|| predicate.matches( address )
|| (ipv4Address != null && predicate.matches( ipv4Address ));
}
}

View File

@@ -5,8 +5,8 @@
*/
package dan200.computercraft.core.apis.http.options;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.InMemoryCommentedFormat;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
import dan200.computercraft.ComputerCraft;
@@ -16,10 +16,11 @@ import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Parses, checks and generates {@link Config}s for {@link AddressRule}.
*/
public class AddressRuleConfig
{
public static UnmodifiableConfig makeRule( String host, Action action )
{
CommentedConfig config = InMemoryCommentedFormat.defaultInstance().createConfig( ConcurrentHashMap::new );
@@ -61,10 +62,10 @@ public class AddressRuleConfig
public static AddressRule parseRule( UnmodifiableConfig builder )
{
String hostObj = get( builder, "host", String.class ).orElse( null );
Integer port = get( builder, "port", Number.class ).map( Number::intValue ).orElse( null );
if( hostObj == null ) return null;
Action action = getEnum( builder, "action", Action.class ).orElse( null );
Integer port = get( builder, "port", Number.class ).map( Number::intValue ).orElse( null );
Integer timeout = get( builder, "timeout", Number.class ).map( Number::intValue ).orElse( null );
Long maxUpload = get( builder, "max_upload", Number.class ).map( Number::longValue ).orElse( null );
Long maxDownload = get( builder, "max_download", Number.class ).map( Number::longValue ).orElse( null );

View File

@@ -31,43 +31,28 @@ public final class PartialOptions
@Nonnull
Options toOptions()
{
if( this.options != null )
{
return this.options;
}
if( options != null ) return options;
return this.options = new Options( this.action == null ? Action.DENY : this.action,
this.maxUpload == null ? AddressRule.MAX_UPLOAD : this.maxUpload,
this.maxDownload == null ? AddressRule.MAX_DOWNLOAD : this.maxDownload,
this.timeout == null ? AddressRule.TIMEOUT : this.timeout, this.websocketMessage == null ? AddressRule.WEBSOCKET_MESSAGE : this.websocketMessage );
return options = new Options(
action == null ? Action.DENY : action,
maxUpload == null ? AddressRule.MAX_UPLOAD : maxUpload,
maxDownload == null ? AddressRule.MAX_DOWNLOAD : maxDownload,
timeout == null ? AddressRule.TIMEOUT : timeout,
websocketMessage == null ? AddressRule.WEBSOCKET_MESSAGE : websocketMessage
);
}
void merge( @Nonnull PartialOptions other )
{
if( this.action == null && other.action != null )
{
this.action = other.action;
}
if( this.maxUpload == null && other.maxUpload != null )
{
this.maxUpload = other.maxUpload;
}
if( this.maxDownload == null && other.maxDownload != null )
{
this.maxDownload = other.maxDownload;
}
if( this.timeout == null && other.timeout != null )
{
this.timeout = other.timeout;
}
if( this.websocketMessage == null && other.websocketMessage != null )
{
this.websocketMessage = other.websocketMessage;
}
if( action == null && other.action != null ) action = other.action;
if( maxUpload == null && other.maxUpload != null ) maxUpload = other.maxUpload;
if( maxDownload == null && other.maxDownload != null ) maxDownload = other.maxDownload;
if( timeout == null && other.timeout != null ) timeout = other.timeout;
if( websocketMessage == null && other.websocketMessage != null ) websocketMessage = other.websocketMessage;
}
PartialOptions copy()
{
return new PartialOptions( this.action, this.maxUpload, this.maxDownload, this.timeout, this.websocketMessage );
return new PartialOptions( action, maxUpload, maxDownload, timeout, websocketMessage );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http.request;
import dan200.computercraft.ComputerCraft;
@@ -20,13 +19,10 @@ import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.net.InetSocketAddress;
@@ -48,26 +44,31 @@ public class HttpRequest extends Resource<HttpRequest>
private static final String FAILURE_EVENT = "http_failure";
private static final int MAX_REDIRECTS = 16;
final AtomicInteger redirects;
private final IAPIEnvironment environment;
private final String address;
private final ByteBuf postBuffer;
private final HttpHeaders headers;
private final boolean binary;
private Future<?> executorFuture;
private ChannelFuture connectFuture;
private HttpRequestHandler currentRequest;
public HttpRequest( ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, String postText, HttpHeaders headers,
boolean binary, boolean followRedirects )
private final IAPIEnvironment environment;
private final String address;
private final ByteBuf postBuffer;
private final HttpHeaders headers;
private final boolean binary;
final AtomicInteger redirects;
public HttpRequest( ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, String postText, HttpHeaders headers, boolean binary, boolean followRedirects )
{
super( limiter );
this.environment = environment;
this.address = address;
this.postBuffer = postText != null ? Unpooled.wrappedBuffer( postText.getBytes( StandardCharsets.UTF_8 ) ) : Unpooled.buffer( 0 );
postBuffer = postText != null
? Unpooled.wrappedBuffer( postText.getBytes( StandardCharsets.UTF_8 ) )
: Unpooled.buffer( 0 );
this.headers = headers;
this.binary = binary;
this.redirects = new AtomicInteger( followRedirects ? MAX_REDIRECTS : 0 );
redirects = new AtomicInteger( followRedirects ? MAX_REDIRECTS : 0 );
if( postText != null )
{
@@ -78,11 +79,16 @@ public class HttpRequest extends Resource<HttpRequest>
if( !headers.contains( HttpHeaderNames.CONTENT_LENGTH ) )
{
headers.set( HttpHeaderNames.CONTENT_LENGTH, this.postBuffer.readableBytes() );
headers.set( HttpHeaderNames.CONTENT_LENGTH, postBuffer.readableBytes() );
}
}
}
public IAPIEnvironment environment()
{
return environment;
}
public static URI checkUri( String address ) throws HTTPRequestException
{
URI url;
@@ -102,73 +108,52 @@ public class HttpRequest extends Resource<HttpRequest>
public static void checkUri( URI url ) throws HTTPRequestException
{
// Validate the URL
if( url.getScheme() == null )
{
throw new HTTPRequestException( "Must specify http or https" );
}
if( url.getHost() == null )
{
throw new HTTPRequestException( "URL malformed" );
}
if( url.getScheme() == null ) throw new HTTPRequestException( "Must specify http or https" );
if( url.getHost() == null ) throw new HTTPRequestException( "URL malformed" );
String scheme = url.getScheme()
.toLowerCase( Locale.ROOT );
String scheme = url.getScheme().toLowerCase( Locale.ROOT );
if( !scheme.equalsIgnoreCase( "http" ) && !scheme.equalsIgnoreCase( "https" ) )
{
throw new HTTPRequestException( "Invalid protocol '" + scheme + "'" );
}
}
public IAPIEnvironment environment()
{
return this.environment;
}
public void request( URI uri, HttpMethod method )
{
if( this.isClosed() )
{
return;
}
this.executorFuture = NetworkUtils.EXECUTOR.submit( () -> this.doRequest( uri, method ) );
this.checkClosed();
if( isClosed() ) return;
executorFuture = NetworkUtils.EXECUTOR.submit( () -> doRequest( uri, method ) );
checkClosed();
}
private void doRequest( URI uri, HttpMethod method )
{
// If we're cancelled, abort.
if( this.isClosed() )
{
return;
}
if( isClosed() ) return;
try
{
boolean ssl = uri.getScheme()
.equalsIgnoreCase( "https" );
boolean ssl = uri.getScheme().equalsIgnoreCase( "https" );
InetSocketAddress socketAddress = NetworkUtils.getAddress( uri, ssl );
Options options = NetworkUtils.getOptions( uri.getHost(), socketAddress );
SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null;
// getAddress may have a slight delay, so let's perform another cancellation check.
if( this.isClosed() )
{
return;
}
if( isClosed() ) return;
long requestBody = getHeaderSize( this.headers ) + this.postBuffer.capacity();
long requestBody = getHeaderSize( headers ) + postBuffer.capacity();
if( options.maxUpload != 0 && requestBody > options.maxUpload )
{
this.failure( "Request body is too large" );
failure( "Request body is too large" );
return;
}
// Add request size to the tracker before opening the connection
this.environment.addTrackingChange( TrackingField.HTTP_REQUESTS, 1 );
this.environment.addTrackingChange( TrackingField.HTTP_UPLOAD, requestBody );
environment.addTrackingChange( TrackingField.HTTP_REQUESTS, 1 );
environment.addTrackingChange( TrackingField.HTTP_UPLOAD, requestBody );
HttpRequestHandler handler = this.currentRequest = new HttpRequestHandler( this, uri, method, options );
this.connectFuture = new Bootstrap().group( NetworkUtils.LOOP_GROUP )
HttpRequestHandler handler = currentRequest = new HttpRequestHandler( this, uri, method, options );
connectFuture = new Bootstrap()
.group( NetworkUtils.LOOP_GROUP )
.channelFactory( NioSocketChannel::new )
.handler( new ChannelInitializer<SocketChannel>()
{
@@ -178,8 +163,7 @@ public class HttpRequest extends Resource<HttpRequest>
if( options.timeout > 0 )
{
ch.config()
.setConnectTimeoutMillis( options.timeout );
ch.config().setConnectTimeoutMillis( options.timeout );
}
ChannelPipeline p = ch.pipeline();
@@ -193,93 +177,46 @@ public class HttpRequest extends Resource<HttpRequest>
p.addLast( new ReadTimeoutHandler( options.timeout, TimeUnit.MILLISECONDS ) );
}
p.addLast( new HttpClientCodec(), new HttpContentDecompressor(), handler );
p.addLast(
new HttpClientCodec(),
new HttpContentDecompressor(),
handler
);
}
} )
.remoteAddress( socketAddress )
.connect()
.addListener( c -> {
if( !c.isSuccess() )
{
this.failure( c.cause() );
}
if( !c.isSuccess() ) failure( NetworkUtils.toFriendlyError( c.cause() ) );
} );
// Do an additional check for cancellation
this.checkClosed();
checkClosed();
}
catch( HTTPRequestException e )
{
this.failure( e.getMessage() );
failure( e.getMessage() );
}
catch( Exception e )
{
this.failure( "Could not connect" );
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.error( "Error in HTTP request", e );
}
failure( NetworkUtils.toFriendlyError( e ) );
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error in HTTP request", e );
}
}
public static long getHeaderSize( HttpHeaders headers )
{
long size = 0;
for( Map.Entry<String, String> header : headers )
{
size += header.getKey() == null ? 0 : header.getKey()
.length();
size += header.getValue() == null ? 0 : header.getValue()
.length() + 1;
}
return size;
}
void failure( String message )
{
if( this.tryClose() )
{
this.environment.queueEvent( FAILURE_EVENT, this.address, message );
}
}
void failure( Throwable cause )
{
String message;
if( cause instanceof HTTPRequestException )
{
message = cause.getMessage();
}
else if( cause instanceof TooLongFrameException )
{
message = "Response is too large";
}
else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException )
{
message = "Timed out";
}
else
{
message = "Could not connect";
}
this.failure( message );
if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message );
}
void failure( String message, HttpResponseHandle object )
{
if( this.tryClose() )
{
this.environment.queueEvent( FAILURE_EVENT, this.address, message, object );
}
if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message, object );
}
void success( HttpResponseHandle object )
{
if( this.tryClose() )
{
this.environment.queueEvent( SUCCESS_EVENT, this.address, object );
}
if( tryClose() ) environment.queueEvent( SUCCESS_EVENT, address, object );
}
@Override
@@ -287,23 +224,34 @@ public class HttpRequest extends Resource<HttpRequest>
{
super.dispose();
this.executorFuture = closeFuture( this.executorFuture );
this.connectFuture = closeChannel( this.connectFuture );
this.currentRequest = closeCloseable( this.currentRequest );
executorFuture = closeFuture( executorFuture );
connectFuture = closeChannel( connectFuture );
currentRequest = closeCloseable( currentRequest );
}
public static long getHeaderSize( HttpHeaders headers )
{
long size = 0;
for( Map.Entry<String, String> header : headers )
{
size += header.getKey() == null ? 0 : header.getKey().length();
size += header.getValue() == null ? 0 : header.getValue().length() + 1;
}
return size;
}
public ByteBuf body()
{
return this.postBuffer;
return postBuffer;
}
public HttpHeaders headers()
{
return this.headers;
return headers;
}
public boolean isBinary()
{
return this.binary;
return binary;
}
}

View File

@@ -183,7 +183,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
{
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error handling HTTP response", cause );
request.failure( cause );
request.failure( NetworkUtils.toFriendlyError( cause ) );
}
private void sendResponse()
@@ -256,4 +256,4 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
responseBody = null;
}
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http.request;
import dan200.computercraft.api.lua.IArguments;
@@ -19,8 +18,9 @@ import java.util.Collections;
import java.util.Map;
/**
* A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or {@link BinaryReadableHandle binary file} if the request used
* binary mode), though provides several request specific methods.
* A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or
* {@link BinaryReadableHandle binary file} if the request used binary mode), though provides several request specific
* methods.
*
* @cc.module http.Response
* @see HTTPAPI#request(IArguments) On how to make a http request.
@@ -50,38 +50,36 @@ public class HttpResponseHandle implements ObjectSource
@LuaFunction
public final Object[] getResponseCode()
{
return new Object[] {
this.responseCode,
this.responseStatus
};
return new Object[] { responseCode, responseStatus };
}
/**
* Get a table containing the response's headers, in a format similar to that required by {@link HTTPAPI#request}. If multiple headers are sent with the
* same name, they will be combined with a comma.
* Get a table containing the response's headers, in a format similar to that required by {@link HTTPAPI#request}.
* If multiple headers are sent with the same name, they will be combined with a comma.
*
* @return The response's headers.
* @cc.usage Make a request to [example.computercraft.cc](https://example.computercraft.cc), and print the returned headers.
* @cc.usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), and print the
* returned headers.
* <pre>{@code
* local request = http.get("https://example.computercraft.cc")
* print(textutils.serialize(request.getResponseHeaders()))
* -- => {
* -- [ "Content-Type" ] = "text/plain; charset=utf8",
* -- [ "content-length" ] = 17,
* -- ...
* -- }
* request.close()
* }</pre>
* local request = http.get("https://example.tweaked.cc")
* print(textutils.serialize(request.getResponseHeaders()))
* -- => {
* -- [ "Content-Type" ] = "text/plain; charset=utf8",
* -- [ "content-length" ] = 17,
* -- ...
* -- }
* request.close()
* }</pre>
*/
@LuaFunction
public final Map<String, String> getResponseHeaders()
{
return this.responseHeaders;
return responseHeaders;
}
@Override
public Iterable<Object> getExtra()
{
return Collections.singletonList( this.reader );
return Collections.singletonList( reader );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http.websocket;
import com.google.common.base.Strings;
@@ -43,8 +42,8 @@ import java.util.concurrent.Future;
public class Websocket extends Resource<Websocket>
{
/**
* We declare the maximum size to be 2^30 bytes. While messages can be much longer, we set an arbitrary limit as working with larger messages
* (especially within a Lua VM) is absurd.
* We declare the maximum size to be 2^30 bytes. While messages can be much longer, we set an arbitrary limit as
* working with larger messages (especially within a Lua VM) is absurd.
*/
public static final int MAX_MESSAGE_SIZE = 1 << 30;
@@ -52,13 +51,15 @@ public class Websocket extends Resource<Websocket>
static final String FAILURE_EVENT = "websocket_failure";
static final String CLOSE_EVENT = "websocket_closed";
static final String MESSAGE_EVENT = "websocket_message";
private Future<?> executorFuture;
private ChannelFuture connectFuture;
private WeakReference<WebsocketHandle> websocketHandle;
private final IAPIEnvironment environment;
private final URI uri;
private final String address;
private final HttpHeaders headers;
private Future<?> executorFuture;
private ChannelFuture connectFuture;
private WeakReference<WebsocketHandle> websocketHandle;
public Websocket( ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers )
{
@@ -91,10 +92,7 @@ public class Websocket extends Resource<Websocket>
}
}
if( uri == null || uri.getHost() == null )
{
throw new HTTPRequestException( "URL malformed" );
}
if( uri == null || uri.getHost() == null ) throw new HTTPRequestException( "URL malformed" );
String scheme = uri.getScheme();
if( scheme == null )
@@ -118,38 +116,28 @@ public class Websocket extends Resource<Websocket>
public void connect()
{
if( this.isClosed() )
{
return;
}
this.executorFuture = NetworkUtils.EXECUTOR.submit( this::doConnect );
this.checkClosed();
if( isClosed() ) return;
executorFuture = NetworkUtils.EXECUTOR.submit( this::doConnect );
checkClosed();
}
private void doConnect()
{
// If we're cancelled, abort.
if( this.isClosed() )
{
return;
}
if( isClosed() ) return;
try
{
boolean ssl = this.uri.getScheme()
.equalsIgnoreCase( "wss" );
boolean ssl = uri.getScheme().equalsIgnoreCase( "wss" );
InetSocketAddress socketAddress = NetworkUtils.getAddress( uri, ssl );
Options options = NetworkUtils.getOptions( this.uri.getHost(), socketAddress );
Options options = NetworkUtils.getOptions( uri.getHost(), socketAddress );
SslContext sslContext = ssl ? NetworkUtils.getSslContext() : null;
// getAddress may have a slight delay, so let's perform another cancellation check.
if( this.isClosed() )
{
return;
}
if( isClosed() ) return;
this.connectFuture = new Bootstrap().group( NetworkUtils.LOOP_GROUP )
connectFuture = new Bootstrap()
.group( NetworkUtils.LOOP_GROUP )
.channel( NioSocketChannel.class )
.handler( new ChannelInitializer<SocketChannel>()
{
@@ -159,81 +147,65 @@ public class Websocket extends Resource<Websocket>
ChannelPipeline p = ch.pipeline();
if( sslContext != null )
{
p.addLast( sslContext.newHandler( ch.alloc(), Websocket.this.uri.getHost(), socketAddress.getPort() ) );
p.addLast( sslContext.newHandler( ch.alloc(), uri.getHost(), socketAddress.getPort() ) );
}
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker( Websocket.this.uri,
WebSocketVersion.V13,
null,
true,
Websocket.this.headers,
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage );
WebSocketClientHandshaker handshaker = WebSocketClientHandshakerFactory.newHandshaker(
uri, WebSocketVersion.V13, null, true, headers,
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
);
p.addLast( new HttpClientCodec(),
p.addLast(
new HttpClientCodec(),
new HttpObjectAggregator( 8192 ),
WebSocketClientCompressionHandler.INSTANCE,
new WebsocketHandler( Websocket.this, handshaker, options ) );
new WebsocketHandler( Websocket.this, handshaker, options )
);
}
} )
.remoteAddress( socketAddress )
.connect()
.addListener( c -> {
if( !c.isSuccess() )
{
this.failure( c.cause()
.getMessage() );
}
if( !c.isSuccess() ) failure( NetworkUtils.toFriendlyError( c.cause() ) );
} );
// Do an additional check for cancellation
this.checkClosed();
checkClosed();
}
catch( HTTPRequestException e )
{
this.failure( e.getMessage() );
failure( e.getMessage() );
}
catch( Exception e )
{
this.failure( "Could not connect" );
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.error( "Error in websocket", e );
}
}
}
void failure( String message )
{
if( this.tryClose() )
{
this.environment.queueEvent( FAILURE_EVENT, this.address, message );
failure( NetworkUtils.toFriendlyError( e ) );
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error in websocket", e );
}
}
void success( Channel channel, Options options )
{
if( this.isClosed() )
{
return;
}
if( isClosed() ) return;
WebsocketHandle handle = new WebsocketHandle( this, options, channel );
this.environment().queueEvent( SUCCESS_EVENT, this.address, handle );
this.websocketHandle = this.createOwnerReference( handle );
environment().queueEvent( SUCCESS_EVENT, address, handle );
websocketHandle = createOwnerReference( handle );
this.checkClosed();
checkClosed();
}
public IAPIEnvironment environment()
void failure( String message )
{
return this.environment;
if( tryClose() ) environment.queueEvent( FAILURE_EVENT, address, message );
}
void close( int status, String reason )
{
if( this.tryClose() )
if( tryClose() )
{
this.environment.queueEvent( CLOSE_EVENT, this.address, Strings.isNullOrEmpty( reason ) ? null : reason, status < 0 ? null : status );
environment.queueEvent( CLOSE_EVENT, address,
Strings.isNullOrEmpty( reason ) ? null : reason,
status < 0 ? null : status );
}
}
@@ -242,17 +214,22 @@ public class Websocket extends Resource<Websocket>
{
super.dispose();
this.executorFuture = closeFuture( this.executorFuture );
this.connectFuture = closeChannel( this.connectFuture );
executorFuture = closeFuture( executorFuture );
connectFuture = closeChannel( connectFuture );
WeakReference<WebsocketHandle> websocketHandleRef = this.websocketHandle;
WeakReference<WebsocketHandle> websocketHandleRef = websocketHandle;
WebsocketHandle websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
IoUtil.closeQuietly( websocketHandle );
this.websocketHandle = null;
}
public IAPIEnvironment environment()
{
return environment;
}
public String address()
{
return this.address;
return address;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http.websocket;
import com.google.common.base.Objects;
@@ -60,21 +59,14 @@ public class WebsocketHandle implements Closeable
@LuaFunction
public final MethodResult receive( Optional<Double> timeout ) throws LuaException
{
this.checkOpen();
int timeoutId = timeout.isPresent() ? this.websocket.environment()
.startTimer( Math.round( checkFinite( 0, timeout.get() ) / 0.05 ) ) : -1;
checkOpen();
int timeoutId = timeout.isPresent()
? websocket.environment().startTimer( Math.round( checkFinite( 0, timeout.get() ) / 0.05 ) )
: -1;
return new ReceiveCallback( timeoutId ).pull;
}
private void checkOpen() throws LuaException
{
if( this.closed )
{
throw new LuaException( "attempt to use a closed file" );
}
}
/**
* Send a websocket message to the connected server.
*
@@ -86,39 +78,45 @@ public class WebsocketHandle implements Closeable
@LuaFunction
public final void send( Object message, Optional<Boolean> binary ) throws LuaException
{
this.checkOpen();
checkOpen();
String text = StringUtil.toString( message );
if( this.options.websocketMessage != 0 && text.length() > this.options.websocketMessage )
if( options.websocketMessage != 0 && text.length() > options.websocketMessage )
{
throw new LuaException( "Message is too large" );
}
this.websocket.environment()
.addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() );
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_OUTGOING, text.length() );
Channel channel = this.channel;
if( channel != null )
{
channel.writeAndFlush( binary.orElse( false ) ? new BinaryWebSocketFrame( Unpooled.wrappedBuffer( LuaValues.encode( text ) ) ) : new TextWebSocketFrame(
text ) );
channel.writeAndFlush( binary.orElse( false )
? new BinaryWebSocketFrame( Unpooled.wrappedBuffer( LuaValues.encode( text ) ) )
: new TextWebSocketFrame( text ) );
}
}
/**
* Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received along it.
* Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received
* along it.
*/
@LuaFunction( "close" )
public final void doClose()
{
this.close();
this.websocket.close();
close();
websocket.close();
}
private void checkOpen() throws LuaException
{
if( closed ) throw new LuaException( "attempt to use a closed file" );
}
@Override
public void close()
{
this.closed = true;
closed = true;
Channel channel = this.channel;
if( channel != null )
@@ -142,23 +140,23 @@ public class WebsocketHandle implements Closeable
@Override
public MethodResult resume( Object[] event )
{
if( event.length >= 3 && Objects.equal( event[0], MESSAGE_EVENT ) && Objects.equal( event[1], WebsocketHandle.this.websocket.address() ) )
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], WebsocketHandle.this.websocket.address() ) && WebsocketHandle.this.closed )
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 && this.timeoutId != -1 && Objects.equal( event[0],
TIMER_EVENT ) && event[1] instanceof Number && ((Number) event[1]).intValue() == this.timeoutId )
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 this.pull;
return pull;
}
}
}

View File

@@ -3,20 +3,15 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.apis.http.websocket;
import dan200.computercraft.core.apis.http.HTTPRequestException;
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.core.tracking.TrackingField;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ConnectTimeoutException;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.util.CharsetUtil;
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
@@ -37,70 +32,33 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object>
@Override
public void channelActive( ChannelHandlerContext ctx ) throws Exception
{
this.handshaker.handshake( ctx.channel() );
handshaker.handshake( ctx.channel() );
super.channelActive( ctx );
}
@Override
public void channelInactive( ChannelHandlerContext ctx ) throws Exception
{
this.websocket.close( -1, "Websocket is inactive" );
websocket.close( -1, "Websocket is inactive" );
super.channelInactive( ctx );
}
@Override
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
{
ctx.close();
String message;
if( cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException )
{
message = cause.getMessage();
}
else if( cause instanceof TooLongFrameException )
{
message = "Message is too large";
}
else if( cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException )
{
message = "Timed out";
}
else
{
message = "Could not connect";
}
if( this.handshaker.isHandshakeComplete() )
{
this.websocket.close( -1, message );
}
else
{
this.websocket.failure( message );
}
}
@Override
public void channelRead0( ChannelHandlerContext ctx, Object msg )
{
if( this.websocket.isClosed() )
{
return;
}
if( websocket.isClosed() ) return;
if( !this.handshaker.isHandshakeComplete() )
if( !handshaker.isHandshakeComplete() )
{
this.handshaker.finishHandshake( ctx.channel(), (FullHttpResponse) msg );
this.websocket.success( ctx.channel(), this.options );
handshaker.finishHandshake( ctx.channel(), (FullHttpResponse) msg );
websocket.success( ctx.channel(), options );
return;
}
if( msg instanceof FullHttpResponse )
{
FullHttpResponse response = (FullHttpResponse) msg;
throw new IllegalStateException( "Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content()
.toString( CharsetUtil.UTF_8 ) + ')' );
throw new IllegalStateException( "Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString( CharsetUtil.UTF_8 ) + ')' );
}
WebSocketFrame frame = (WebSocketFrame) msg;
@@ -108,31 +66,41 @@ public class WebsocketHandler extends SimpleChannelInboundHandler<Object>
{
String data = ((TextWebSocketFrame) frame).text();
this.websocket.environment()
.addTrackingChange( TrackingField.WEBSOCKET_INCOMING, data.length() );
this.websocket.environment()
.queueEvent( MESSAGE_EVENT, this.websocket.address(), data, false );
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, data.length() );
websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), data, false );
}
else if( frame instanceof BinaryWebSocketFrame )
{
byte[] converted = NetworkUtils.toBytes( frame.content() );
this.websocket.environment()
.addTrackingChange( TrackingField.WEBSOCKET_INCOMING, converted.length );
this.websocket.environment()
.queueEvent( MESSAGE_EVENT, this.websocket.address(), converted, true );
websocket.environment().addTrackingChange( TrackingField.WEBSOCKET_INCOMING, converted.length );
websocket.environment().queueEvent( MESSAGE_EVENT, websocket.address(), converted, true );
}
else if( frame instanceof CloseWebSocketFrame )
{
CloseWebSocketFrame closeFrame = (CloseWebSocketFrame) frame;
this.websocket.close( closeFrame.statusCode(), closeFrame.reasonText() );
websocket.close( closeFrame.statusCode(), closeFrame.reasonText() );
}
else if( frame instanceof PingWebSocketFrame )
{
frame.content()
.retain();
ctx.channel()
.writeAndFlush( new PongWebSocketFrame( frame.content() ) );
frame.content().retain();
ctx.channel().writeAndFlush( new PongWebSocketFrame( frame.content() ) );
}
}
@Override
public void exceptionCaught( ChannelHandlerContext ctx, Throwable cause )
{
ctx.close();
String message = NetworkUtils.toFriendlyError( cause );
if( handshaker.isHandshakeComplete() )
{
websocket.close( -1, message );
}
else
{
websocket.failure( message );
}
}
}

View File

@@ -18,6 +18,6 @@ final class DeclaringClassLoader extends ClassLoader
Class<?> define( String name, byte[] bytes, ProtectionDomain protectionDomain ) throws ClassFormatError
{
return this.defineClass( name, bytes, 0, bytes.length, protectionDomain );
return defineClass( name, bytes, 0, bytes.length, protectionDomain );
}
}

View File

@@ -11,10 +11,7 @@ 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 dan200.computercraft.api.lua.*;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
@@ -53,11 +50,15 @@ public final class Generator<T>
private final String methodDesc;
private final Function<T, T> wrap;
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder.newBuilder()
.build( CacheLoader.from( catching( this::build, Optional.empty() ) ) );
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder.newBuilder()
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder
.newBuilder()
.build( CacheLoader.from( catching( this::build, Collections.emptyList() ) ) );
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
.newBuilder()
.build( CacheLoader.from( catching( this::build, Optional.empty() ) ) );
Generator( Class<T> base, List<Class<?>> context, Function<T, T> wrap )
{
this.base = base;
@@ -66,13 +67,8 @@ public final class Generator<T>
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 );
for( Class<?> klass : context ) methodDesc.append( Type.getDescriptor( klass ) );
methodDesc.append( DESC_ARGUMENTS ).append( ")" ).append( DESC_METHOD_RESULT );
this.methodDesc = methodDesc.toString();
}
@@ -81,7 +77,7 @@ public final class Generator<T>
{
try
{
return this.classCache.get( klass );
return classCache.get( klass );
}
catch( ExecutionException e )
{
@@ -97,10 +93,7 @@ public final class Generator<T>
for( Method method : klass.getMethods() )
{
LuaFunction annotation = method.getAnnotation( LuaFunction.class );
if( annotation == null )
{
continue;
}
if( annotation == null ) continue;
if( Modifier.isStatic( method.getModifiers() ) )
{
@@ -108,52 +101,32 @@ public final class Generator<T>
continue;
}
T instance = this.methodCache.getUnchecked( method )
.orElse( null );
if( instance == null )
{
continue;
}
T instance = methodCache.getUnchecked( method ).orElse( null );
if( instance == null ) continue;
if( methods == null )
{
methods = new ArrayList<>();
}
this.addMethod( methods, method, annotation, instance );
if( methods == null ) methods = new ArrayList<>();
addMethod( methods, method, annotation, instance );
}
for( GenericMethod method : GenericMethod.all() )
{
if( !method.target.isAssignableFrom( klass ) ) continue;
T instance = this.methodCache.getUnchecked( method.method )
.orElse( null );
if( instance == null )
{
continue;
}
T instance = methodCache.getUnchecked( method.method ).orElse( null );
if( instance == null ) continue;
if( methods == null )
{
methods = new ArrayList<>();
}
this.addMethod( methods, method.method, method.annotation, instance );
if( methods == null ) methods = new ArrayList<>();
addMethod( methods, method.method, method.annotation, instance );
}
if( methods == null )
{
return Collections.emptyList();
}
if( methods == null ) return Collections.emptyList();
methods.trimToSize();
return Collections.unmodifiableList( methods );
}
private void addMethod( List<NamedMethod<T>> methods, Method method, LuaFunction annotation, T instance )
{
if( annotation.mainThread() )
{
instance = this.wrap.apply( instance );
}
if( annotation.mainThread() ) instance = wrap.apply( instance );
String[] names = annotation.value();
boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread();
@@ -173,8 +146,7 @@ public final class Generator<T>
@Nonnull
private Optional<T> build( Method method )
{
String name = method.getDeclaringClass()
.getName() + "." + method.getName();
String name = method.getDeclaringClass().getName() + "." + method.getName();
int modifiers = method.getModifiers();
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
@@ -189,8 +161,7 @@ public final class Generator<T>
return Optional.empty();
}
if( !Modifier.isPublic( method.getDeclaringClass()
.getModifiers() ) )
if( !Modifier.isPublic( method.getDeclaringClass().getModifiers() ) )
{
ComputerCraft.log.error( "Lua Method {} should be on a public class.", name );
return Optional.empty();
@@ -214,21 +185,12 @@ public final class Generator<T>
try
{
String className = method.getDeclaringClass()
.getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement();
byte[] bytes = this.generate( className, target, method );
if( bytes == null )
{
return Optional.empty();
}
String className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement();
byte[] bytes = generate( className, target, method );
if( bytes == null ) return Optional.empty();
Class<?> klass = DeclaringClassLoader.INSTANCE.define( className,
bytes,
method.getDeclaringClass()
.getProtectionDomain() );
return Optional.of( klass.asSubclass( this.base )
.getDeclaredConstructor()
.newInstance() );
Class<?> klass = DeclaringClassLoader.INSTANCE.define( className, bytes, method.getDeclaringClass().getProtectionDomain() );
return Optional.of( klass.asSubclass( base ).getDeclaredConstructor().newInstance() );
}
catch( ReflectiveOperationException | ClassFormatError | RuntimeException e )
{
@@ -245,7 +207,7 @@ public final class Generator<T>
// 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", this.interfaces );
cw.visit( V1_8, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces );
cw.visitSource( "CC generated method", null );
{ // Constructor just invokes super.
@@ -259,7 +221,7 @@ public final class Generator<T>
}
{
MethodVisitor mw = cw.visitMethod( ACC_PUBLIC, METHOD_NAME, this.methodDesc, null, EXCEPTIONS );
MethodVisitor mw = cw.visitMethod( ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS );
mw.visitCode();
// If we're an instance method, load the this parameter.
@@ -272,22 +234,16 @@ public final class Generator<T>
int argIndex = 0;
for( java.lang.reflect.Type genericArg : method.getGenericParameterTypes() )
{
Boolean loadedArg = this.loadArg( mw, target, method, genericArg, argIndex );
if( loadedArg == null )
{
return null;
}
if( loadedArg )
{
argIndex++;
}
Boolean loadedArg = loadArg( mw, target, method, genericArg, argIndex );
if( loadedArg == null ) return null;
if( loadedArg ) argIndex++;
}
mw.visitMethodInsn( Modifier.isStatic( method.getModifiers() ) ? INVOKESTATIC : INVOKEVIRTUAL,
Type.getInternalName( method.getDeclaringClass() ),
method.getName(),
Type.getMethodDescriptor( method ),
false );
mw.visitMethodInsn(
Modifier.isStatic( method.getModifiers() ) ? INVOKESTATIC : 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.
@@ -301,11 +257,7 @@ public final class Generator<T>
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, 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 )
@@ -339,18 +291,15 @@ public final class Generator<T>
}
Class<?> arg = Reflect.getRawType( method, genericArg, true );
if( arg == null )
{
return null;
}
if( arg == null ) return null;
if( arg == IArguments.class )
{
mw.visitVarInsn( ALOAD, 2 + this.context.size() );
mw.visitVarInsn( ALOAD, 2 + context.size() );
return false;
}
int idx = this.context.indexOf( arg );
int idx = context.indexOf( arg );
if( idx >= 0 )
{
mw.visitVarInsn( ALOAD, 2 + idx );
@@ -359,19 +308,12 @@ public final class Generator<T>
if( arg == Optional.class )
{
Class<?> klass = Reflect.getRawType( method,
TypeToken.of( genericArg )
.resolveType( Reflect.OPTIONAL_IN )
.getType(),
false );
if( klass == null )
{
return null;
}
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 + this.context.size() );
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 );
@@ -381,7 +323,7 @@ public final class Generator<T>
String name = Reflect.getLuaName( Primitives.unwrap( klass ) );
if( name != null )
{
mw.visitVarInsn( ALOAD, 2 + this.context.size() );
mw.visitVarInsn( ALOAD, 2 + context.size() );
Reflect.loadInt( mw, argIndex );
mw.visitMethodInsn( INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true );
return true;
@@ -390,7 +332,7 @@ public final class Generator<T>
if( Enum.class.isAssignableFrom( arg ) && arg != Enum.class )
{
mw.visitVarInsn( ALOAD, 2 + this.context.size() );
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 );
@@ -401,22 +343,16 @@ public final class Generator<T>
String name = arg == Object.class ? "" : Reflect.getLuaName( arg );
if( name != null )
{
if( Reflect.getRawType( method, genericArg, false ) == null )
{
return null;
}
if( Reflect.getRawType( method, genericArg, false ) == null ) return null;
mw.visitVarInsn( ALOAD, 2 + this.context.size() );
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() );
arg.getName(), method.getDeclaringClass().getName(), method.getName() );
return null;
}

View File

@@ -21,31 +21,19 @@ public final class IntCache<T>
@SuppressWarnings( "unchecked" )
public T get( int index )
{
if( index < 0 )
{
throw new IllegalArgumentException( "index < 0" );
}
if( index < 0 ) throw new IllegalArgumentException( "index < 0" );
if( index < this.cache.length )
if( index < cache.length )
{
T current = (T) this.cache[index];
if( current != null )
{
return current;
}
T current = (T) cache[index];
if( current != null ) return current;
}
synchronized( this )
{
if( index >= this.cache.length )
{
this.cache = Arrays.copyOf( this.cache, Math.max( this.cache.length * 2, index + 1 ) );
}
T current = (T) this.cache[index];
if( current == null )
{
this.cache[index] = current = this.factory.apply( index );
}
if( index >= cache.length ) cache = Arrays.copyOf( cache, Math.max( cache.length * 2, index + 1 ) );
T current = (T) cache[index];
if( current == null ) cache[index] = current = factory.apply( index );
return current;
}
}

View File

@@ -12,14 +12,13 @@ import java.util.Collections;
public interface LuaMethod
{
Generator<LuaMethod> GENERATOR = new Generator<>( LuaMethod.class,
Collections.singletonList( ILuaContext.class ),
m -> ( target, context, args ) -> TaskCallback.make( context,
() -> TaskCallback.checkUnwrap( m.apply( target,
context,
args ) ) ) );
Generator<LuaMethod> GENERATOR = new Generator<>( LuaMethod.class, Collections.singletonList( ILuaContext.class ),
m -> ( target, context, args ) -> TaskCallback.make( context, () -> TaskCallback.checkUnwrap( m.apply( target, context, args ) ) )
);
IntCache<LuaMethod> DYNAMIC = new IntCache<>( method -> ( instance, context, args ) -> ((IDynamicLuaObject) instance).callMethod( context, method, args ) );
IntCache<LuaMethod> DYNAMIC = new IntCache<>(
method -> ( instance, context, args ) -> ((IDynamicLuaObject) instance).callMethod( context, method, args )
);
String[] EMPTY_METHODS = new String[0];

View File

@@ -23,17 +23,17 @@ public final class NamedMethod<T>
@Nonnull
public String getName()
{
return this.name;
return name;
}
@Nonnull
public T getMethod()
{
return this.method;
return method;
}
public boolean nonYielding()
{
return this.nonYielding;
return nonYielding;
}
}

View File

@@ -10,29 +10,23 @@ 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.
* 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 );
}
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 );
}
for( NamedMethod<T> method : generator.getMethods( extra.getClass() ) ) accept.accept( extra, method );
}
}
}
Iterable<Object> getExtra();
}

View File

@@ -17,19 +17,13 @@ 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 ) -> TaskCallback.make( context,
() -> TaskCallback.checkUnwrap( m.apply(
target,
context,
computer,
args ) ) ) );
Generator<PeripheralMethod> GENERATOR = new Generator<>( PeripheralMethod.class, Arrays.asList( ILuaContext.class, IComputerAccess.class ),
m -> ( target, context, computer, args ) -> TaskCallback.make( context, () -> TaskCallback.checkUnwrap( m.apply( target, context, computer, args ) ) )
);
IntCache<PeripheralMethod> DYNAMIC = new IntCache<>( method -> ( instance, context, computer, args ) -> ((IDynamicPeripheral) instance).callMethod( computer,
context,
method,
args ) );
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;

View File

@@ -29,37 +29,16 @@ final class Reflect
{
if( klass.isPrimitive() )
{
if( klass == int.class )
{
return "Int";
}
if( klass == boolean.class )
{
return "Boolean";
}
if( klass == double.class )
{
return "Double";
}
if( klass == long.class )
{
return "Long";
}
if( klass == int.class ) return "Int";
if( klass == boolean.class ) return "Boolean";
if( klass == double.class ) return "Double";
if( klass == long.class ) return "Long";
}
else
{
if( klass == Map.class )
{
return "Table";
}
if( klass == String.class )
{
return "String";
}
if( klass == ByteBuffer.class )
{
return "Bytes";
}
if( klass == Map.class ) return "Table";
if( klass == String.class ) return "String";
if( klass == ByteBuffer.class ) return "Bytes";
}
return null;
@@ -71,10 +50,7 @@ final class Reflect
Type underlying = root;
while( true )
{
if( underlying instanceof Class<?> )
{
return (Class<?>) underlying;
}
if( underlying instanceof Class<?> ) return (Class<?>) underlying;
if( underlying instanceof ParameterizedType )
{
@@ -83,21 +59,13 @@ final class Reflect
{
for( java.lang.reflect.Type arg : type.getActualTypeArguments() )
{
if( arg instanceof WildcardType )
{
continue;
}
if( arg instanceof TypeVariable && ((TypeVariable<?>) arg).getName()
.startsWith( "capture#" ) )
if( arg instanceof WildcardType ) continue;
if( arg instanceof TypeVariable && ((TypeVariable<?>) arg).getName().startsWith( "capture#" ) )
{
continue;
}
ComputerCraft.log.error( "Method {}.{} has generic type {} with non-wildcard argument {}.",
method.getDeclaringClass(),
method.getName(),
root,
arg );
ComputerCraft.log.error( "Method {}.{} has generic type {} with non-wildcard argument {}.", method.getDeclaringClass(), method.getName(), root, arg );
return null;
}
}

View File

@@ -20,6 +20,33 @@ public final class TaskCallback implements ILuaCallback
this.task = task;
}
@Nonnull
@Override
public MethodResult resume( Object[] response ) throws LuaException
{
if( response.length < 3 || !(response[1] instanceof Number) || !(response[2] instanceof Boolean) )
{
return pull;
}
if( ((Number) response[1]).longValue() != task ) return pull;
if( (Boolean) response[2] )
{
// Extract the return values from the event and return them
return MethodResult.of( Arrays.copyOfRange( response, 3, response.length ) );
}
else if( response.length >= 4 && response[3] instanceof String )
{
// Extract the error message from the event and raise it
throw new LuaException( (String) response[3] );
}
else
{
throw new LuaException( "error" );
}
}
static Object[] checkUnwrap( MethodResult result )
{
if( result.getCallback() != null )
@@ -37,34 +64,4 @@ public final class TaskCallback implements ILuaCallback
long task = context.issueMainThreadTask( func );
return new TaskCallback( task ).pull;
}
@Nonnull
@Override
public MethodResult resume( Object[] response ) throws LuaException
{
if( response.length < 3 || !(response[1] instanceof Number) || !(response[2] instanceof Boolean) )
{
return this.pull;
}
if( ((Number) response[1]).longValue() != this.task )
{
return this.pull;
}
if( (Boolean) response[2] )
{
// Extract the return values from the event and return them
return MethodResult.of( Arrays.copyOfRange( response, 3, response.length ) );
}
else if( response.length >= 4 && response[3] instanceof String )
{
// Extract the error message from the event and raise it
throw new LuaException( (String) response[3] );
}
else
{
throw new LuaException( "error" );
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.api.lua.ILuaAPI;
@@ -25,30 +24,30 @@ final class ApiWrapper implements ILuaAPI
@Override
public String[] getNames()
{
return this.delegate.getNames();
return delegate.getNames();
}
@Override
public void startup()
{
this.delegate.startup();
delegate.startup();
}
@Override
public void update()
{
this.delegate.update();
delegate.update();
}
@Override
public void shutdown()
{
this.delegate.shutdown();
this.system.unmountAll();
delegate.shutdown();
system.unmountAll();
}
public ILuaAPI getDelegate()
{
return this.delegate;
return delegate;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import com.google.common.base.Objects;
@@ -31,79 +30,88 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class Computer
{
private static final int START_DELAY = 50;
// Various properties of the computer
private int id;
private String label = null;
// Read-only fields about the computer
private final IComputerEnvironment m_environment;
private final Terminal m_terminal;
private final IComputerEnvironment environment;
private final Terminal terminal;
private final ComputerExecutor executor;
private final MainThreadExecutor serverExecutor;
// Additional state about the computer and its environment.
private boolean blinking = false;
private final Environment internalEnvironment = new Environment( this );
private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
// Various properties of the computer
private int m_id;
private String m_label = null;
// Additional state about the computer and its environment.
private boolean m_blinking = false;
private boolean startRequested;
private int m_ticksSinceStart = -1;
private int ticksSinceStart = -1;
public Computer( IComputerEnvironment environment, Terminal terminal, int id )
{
this.m_id = id;
this.m_environment = environment;
this.m_terminal = terminal;
this.id = id;
this.environment = environment;
this.terminal = terminal;
this.executor = new ComputerExecutor( this );
this.serverExecutor = new MainThreadExecutor( this );
executor = new ComputerExecutor( this );
serverExecutor = new MainThreadExecutor( this );
}
IComputerEnvironment getComputerEnvironment()
{
return this.m_environment;
return environment;
}
FileSystem getFileSystem()
{
return this.executor.getFileSystem();
return executor.getFileSystem();
}
Terminal getTerminal()
{
return this.m_terminal;
return terminal;
}
public Environment getEnvironment()
{
return this.internalEnvironment;
return internalEnvironment;
}
public IAPIEnvironment getAPIEnvironment()
{
return this.internalEnvironment;
return internalEnvironment;
}
public boolean isOn()
{
return executor.isOn();
}
public void turnOn()
{
this.startRequested = true;
startRequested = true;
}
public void shutdown()
{
this.executor.queueStop( false, false );
executor.queueStop( false, false );
}
public void reboot()
{
this.executor.queueStop( true, false );
executor.queueStop( true, false );
}
public void unload()
{
this.executor.queueStop( false, true );
executor.queueStop( false, true );
}
public void queueEvent( String event, Object[] args )
{
this.executor.queueEvent( event, args );
executor.queueEvent( event, args );
}
/**
@@ -114,107 +122,98 @@ public class Computer
*/
public boolean queueMainThread( Runnable runnable )
{
return this.serverExecutor.enqueue( runnable );
return serverExecutor.enqueue( runnable );
}
public IWorkMonitor getMainThreadMonitor()
{
return this.serverExecutor;
return serverExecutor;
}
public int getID()
{
return this.m_id;
}
public void setID( int id )
{
this.m_id = id;
return id;
}
public int assignID()
{
if( this.m_id < 0 )
if( id < 0 )
{
this.m_id = this.m_environment.assignNewID();
id = environment.assignNewID();
}
return this.m_id;
return id;
}
public void setID( int id )
{
this.id = id;
}
public String getLabel()
{
return this.m_label;
return label;
}
public void setLabel( String label )
{
if( !Objects.equal( label, this.m_label ) )
if( !Objects.equal( label, this.label ) )
{
this.m_label = label;
this.externalOutputChanged.set( true );
this.label = label;
externalOutputChanged.set( true );
}
}
public void tick()
{
// We keep track of the number of ticks since the last start, only
if( this.m_ticksSinceStart >= 0 && this.m_ticksSinceStart <= START_DELAY )
{
this.m_ticksSinceStart++;
}
if( ticksSinceStart >= 0 && ticksSinceStart <= START_DELAY ) ticksSinceStart++;
if( this.startRequested && (this.m_ticksSinceStart < 0 || this.m_ticksSinceStart > START_DELAY) )
if( startRequested && (ticksSinceStart < 0 || ticksSinceStart > START_DELAY) )
{
this.startRequested = false;
if( !this.executor.isOn() )
startRequested = false;
if( !executor.isOn() )
{
this.m_ticksSinceStart = 0;
this.executor.queueStart();
ticksSinceStart = 0;
executor.queueStart();
}
}
this.executor.tick();
executor.tick();
// Update the environment's internal state.
this.internalEnvironment.tick();
internalEnvironment.tick();
// Propagate the environment's output to the world.
if( this.internalEnvironment.updateOutput() )
{
this.externalOutputChanged.set( true );
}
if( internalEnvironment.updateOutput() ) externalOutputChanged.set( true );
// Set output changed if the terminal has changed from blinking to not
boolean blinking = this.m_terminal.getCursorBlink() && this.m_terminal.getCursorX() >= 0 && this.m_terminal.getCursorX() < this.m_terminal.getWidth() && this.m_terminal.getCursorY() >= 0 && this.m_terminal.getCursorY() < this.m_terminal.getHeight();
if( blinking != this.m_blinking )
boolean blinking = terminal.getCursorBlink() &&
terminal.getCursorX() >= 0 && terminal.getCursorX() < terminal.getWidth() &&
terminal.getCursorY() >= 0 && terminal.getCursorY() < terminal.getHeight();
if( blinking != this.blinking )
{
this.m_blinking = blinking;
this.externalOutputChanged.set( true );
this.blinking = blinking;
externalOutputChanged.set( true );
}
}
void markChanged()
{
this.externalOutputChanged.set( true );
externalOutputChanged.set( true );
}
public boolean pollAndResetChanged()
{
return this.externalOutputChanged.getAndSet( false );
return externalOutputChanged.getAndSet( false );
}
public boolean isBlinking()
{
return this.isOn() && this.m_blinking;
}
public boolean isOn()
{
return this.executor.isOn();
return isOn() && blinking;
}
public void addApi( ILuaAPI api )
{
this.executor.addApi( api );
executor.addApi( api );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.ComputerCraft;
@@ -33,38 +32,50 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
/**
* The main task queue and executor for a single computer. This handles turning on and off a computer, as well as running events.
* The main task queue and executor for a single computer. This handles turning on and off a computer, as well as
* running events.
*
* When the computer is instructed to turn on or off, or handle an event, we queue a task and register this to be executed on the {@link ComputerThread}.
* Note, as we may be starting many events in a single tick, the external cannot lock on anything which may be held for a long time.
* When the computer is instructed to turn on or off, or handle an event, we queue a task and register this to be
* executed on the {@link ComputerThread}. Note, as we may be starting many events in a single tick, the external
* cannot lock on anything which may be held for a long time.
*
* The executor is effectively composed of two separate queues. Firstly, we have a "single element" queue {@link #command} which determines which state the
* computer should transition too. This is set by {@link #queueStart()} and {@link #queueStop(boolean, boolean)}.
* The executor is effectively composed of two separate queues. Firstly, we have a "single element" queue
* {@link #command} which determines which state the computer should transition too. This is set by
* {@link #queueStart()} and {@link #queueStop(boolean, boolean)}.
*
* When a computer is on, we simply push any events onto to the {@link #eventQueue}.
*
* Both queues are run from the {@link #work()} method, which tries to execute a command if one exists, or resumes the machine with an event otherwise.
* Both queues are run from the {@link #work()} method, which tries to execute a command if one exists, or resumes the
* machine with an event otherwise.
*
* One final responsibility for the executor is calling {@link ILuaAPI#update()} every tick, via the {@link #tick()} method. This should only be called when
* the computer is actually on ({@link #isOn}).
* One final responsibility for the executor is calling {@link ILuaAPI#update()} every tick, via the {@link #tick()}
* method. This should only be called when the computer is actually on ({@link #isOn}).
*/
final class ComputerExecutor
{
private static final int QUEUE_LIMIT = 256;
final TimeoutState timeout = new TimeoutState();
/**
* The thread the executor is running on. This is non-null when performing work. We use this to ensure we're only doing one bit of work at one time.
*
* @see ComputerThread
*/
final AtomicReference<Thread> executingThread = new AtomicReference<>();
private final Computer computer;
private final List<ILuaAPI> apis = new ArrayList<>();
final TimeoutState timeout = new TimeoutState();
private FileSystem fileSystem;
private ILuaMachine machine;
/**
* Whether the computer is currently on. This is set to false when a shutdown starts, or when turning on completes
* (but just before the Lua machine is started).
*
* @see #isOnLock
*/
private volatile boolean isOn = false;
/**
* The lock to acquire when you need to modify the "on state" of a computer.
*
* We hold this lock when running any command, and attempt to hold it when updating APIs. This ensures you don't update APIs while also
* starting/stopping them.
* We hold this lock when running any command, and attempt to hold it when updating APIs. This ensures you don't
* update APIs while also starting/stopping them.
*
* @see #isOn
* @see #tick()
@@ -72,17 +83,13 @@ final class ComputerExecutor
* @see #shutdown()
*/
private final ReentrantLock isOnLock = new ReentrantLock();
/**
* A lock used for any changes to {@link #eventQueue}, {@link #command} or {@link #onComputerQueue}. This will be used on the main thread, so locks
* should be kept as brief as possible.
* A lock used for any changes to {@link #eventQueue}, {@link #command} or {@link #onComputerQueue}. This will be
* used on the main thread, so locks should be kept as brief as possible.
*/
private final Object queueLock = new Object();
/**
* The queue of events which should be executed when this computer is on.
*
* Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again.
*/
private final Queue<Event> eventQueue = new ArrayDeque<>( 4 );
/**
* Determines if this executor is present within {@link ComputerThread}.
*
@@ -91,37 +98,39 @@ final class ComputerExecutor
* @see #afterWork()
*/
volatile boolean onComputerQueue = false;
/**
* The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers.
*
* @see ComputerThread
*/
long virtualRuntime = 0;
/**
* The last time at which we updated {@link #virtualRuntime}.
*
* @see ComputerThread
*/
long vRuntimeStart;
private FileSystem fileSystem;
private ILuaMachine machine;
/**
* Whether the computer is currently on. This is set to false when a shutdown starts, or when turning on completes (but just before the Lua machine is
* started).
*
* @see #isOnLock
*/
private volatile boolean isOn = false;
/**
* The command that {@link #work()} should execute on the computer thread.
*
* One sets the command with {@link #queueStart()} and {@link #queueStop(boolean, boolean)}. Neither of these will queue a new event if there is an
* existing one in the queue.
* One sets the command with {@link #queueStart()} and {@link #queueStop(boolean, boolean)}. Neither of these will
* queue a new event if there is an existing one in the queue.
*
* Note, if command is not {@code null}, then some command is scheduled to be executed. Otherwise it is not currently in the queue (or is currently
* being executed).
* Note, if command is not {@code null}, then some command is scheduled to be executed. Otherwise it is not
* currently in the queue (or is currently being executed).
*/
private volatile StateCommand command;
/**
* The queue of events which should be executed when this computer is on.
*
* Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again.
*/
private final Queue<Event> eventQueue = new ArrayDeque<>( 4 );
/**
* Whether we interrupted an event and so should resume it instead of executing another task.
*
@@ -129,14 +138,24 @@ final class ComputerExecutor
* @see #resumeMachine(String, Object[])
*/
private boolean interruptedEvent = false;
/**
* Whether this executor has been closed, and will no longer accept any incoming commands or events.
*
* @see #queueStop(boolean, boolean)
*/
private boolean closed;
private IWritableMount rootMount;
/**
* The thread the executor is running on. This is non-null when performing work. We use this to ensure we're only
* doing one bit of work at one time.
*
* @see ComputerThread
*/
final AtomicReference<Thread> executingThread = new AtomicReference<>();
ComputerExecutor( Computer computer )
{
// Ensure the computer thread is running as required.
@@ -147,41 +166,40 @@ final class ComputerExecutor
Environment environment = computer.getEnvironment();
// Add all default APIs to the loaded list.
this.apis.add( new TermAPI( environment ) );
this.apis.add( new RedstoneAPI( environment ) );
this.apis.add( new FSAPI( environment ) );
this.apis.add( new PeripheralAPI( environment ) );
this.apis.add( new OSAPI( environment ) );
if( ComputerCraft.httpEnabled )
{
this.apis.add( new HTTPAPI( environment ) );
}
apis.add( new TermAPI( environment ) );
apis.add( new RedstoneAPI( environment ) );
apis.add( new FSAPI( environment ) );
apis.add( new PeripheralAPI( environment ) );
apis.add( new OSAPI( environment ) );
if( ComputerCraft.httpEnabled ) apis.add( new HTTPAPI( environment ) );
// Load in the externally registered APIs.
for( ILuaAPIFactory factory : ApiFactories.getAll() )
{
ComputerSystem system = new ComputerSystem( environment );
ILuaAPI api = factory.create( system );
if( api != null )
{
this.apis.add( new ApiWrapper( api, system ) );
}
if( api != null ) apis.add( new ApiWrapper( api, system ) );
}
}
boolean isOn()
{
return this.isOn;
return isOn;
}
FileSystem getFileSystem()
{
return this.fileSystem;
return fileSystem;
}
Computer getComputer()
{
return computer;
}
void addApi( ILuaAPI api )
{
this.apis.add( api );
apis.add( api );
}
/**
@@ -189,30 +207,13 @@ final class ComputerExecutor
*/
void queueStart()
{
synchronized( this.queueLock )
synchronized( queueLock )
{
// We should only schedule a start if we're not currently on and there's turn on.
if( this.closed || this.isOn || this.command != null )
{
return;
}
if( closed || isOn || command != null ) return;
this.command = StateCommand.TURN_ON;
this.enqueue();
}
}
/**
* Add this executor to the {@link ComputerThread} if not already there.
*/
private void enqueue()
{
synchronized( this.queueLock )
{
if( !this.onComputerQueue )
{
ComputerThread.queue( this );
}
command = StateCommand.TURN_ON;
enqueue();
}
}
@@ -225,54 +226,54 @@ final class ComputerExecutor
*/
void queueStop( boolean reboot, boolean close )
{
synchronized( this.queueLock )
synchronized( queueLock )
{
if( this.closed )
{
return;
}
this.closed = close;
if( closed ) return;
closed = close;
StateCommand newCommand = reboot ? StateCommand.REBOOT : StateCommand.SHUTDOWN;
// We should only schedule a stop if we're currently on and there's no shutdown pending.
if( !this.isOn || this.command != null )
if( !isOn || command != null )
{
// If we're closing, set the command just in case.
if( close )
{
this.command = newCommand;
}
if( close ) command = newCommand;
return;
}
this.command = newCommand;
this.enqueue();
command = newCommand;
enqueue();
}
}
/**
* Abort this whole computer due to a timeout. This will immediately destroy the Lua machine, and then schedule a shutdown.
* Abort this whole computer due to a timeout. This will immediately destroy the Lua machine,
* and then schedule a shutdown.
*/
void abort()
{
ILuaMachine machine = this.machine;
if( machine != null )
{
machine.close();
}
immediateFail( StateCommand.ABORT );
}
synchronized( this.queueLock )
/**
* Abort this whole computer due to an internal error. This will immediately destroy the Lua machine,
* and then schedule a shutdown.
*/
void fastFail()
{
immediateFail( StateCommand.ERROR );
}
private void immediateFail( StateCommand command )
{
ILuaMachine machine = this.machine;
if( machine != null ) machine.close();
synchronized( queueLock )
{
if( this.closed )
{
return;
}
this.command = StateCommand.ABORT;
if( this.isOn )
{
this.enqueue();
}
if( closed ) return;
this.command = command;
if( isOn ) enqueue();
}
}
@@ -285,23 +286,28 @@ final class ComputerExecutor
void queueEvent( @Nonnull String event, @Nullable Object[] args )
{
// Events should be skipped if we're not on.
if( !this.isOn )
{
return;
}
if( !isOn ) return;
synchronized( this.queueLock )
synchronized( queueLock )
{
// And if we've got some command in the pipeline, then don't queue events - they'll
// probably be disposed of anyway.
// We also limit the number of events which can be queued.
if( this.closed || this.command != null || this.eventQueue.size() >= QUEUE_LIMIT )
{
return;
}
if( closed || command != null || eventQueue.size() >= QUEUE_LIMIT ) return;
this.eventQueue.offer( new Event( event, args ) );
this.enqueue();
eventQueue.offer( new Event( event, args ) );
enqueue();
}
}
/**
* Add this executor to the {@link ComputerThread} if not already there.
*/
private void enqueue()
{
synchronized( queueLock )
{
if( !onComputerQueue ) ComputerThread.queue( this );
}
}
@@ -310,7 +316,7 @@ final class ComputerExecutor
*/
void tick()
{
if( this.isOn && this.isOnLock.tryLock() )
if( isOn && isOnLock.tryLock() )
{
// This horrific structure means we don't try to update APIs while the state is being changed
// (and so they may be running startup/shutdown).
@@ -318,29 +324,191 @@ final class ComputerExecutor
// beginning or end of a computer's lifetime.
try
{
if( this.isOn )
if( isOn )
{
// Advance our APIs.
for( ILuaAPI api : this.apis )
{
api.update();
}
for( ILuaAPI api : apis ) api.update();
}
}
finally
{
this.isOnLock.unlock();
isOnLock.unlock();
}
}
}
private IMount getRomMount()
{
return computer.getComputerEnvironment().createResourceMount( "computercraft", "lua/rom" );
}
private IWritableMount getRootMount()
{
if( rootMount == null )
{
rootMount = computer.getComputerEnvironment().createSaveDirMount(
"computer/" + computer.assignID(),
computer.getComputerEnvironment().getComputerSpaceLimit()
);
}
return rootMount;
}
private FileSystem createFileSystem()
{
FileSystem filesystem = null;
try
{
filesystem = new FileSystem( "hdd", getRootMount() );
IMount romMount = getRomMount();
if( romMount == null )
{
displayFailure( "Cannot mount ROM", null );
return null;
}
filesystem.mount( "rom", "rom", romMount );
return filesystem;
}
catch( FileSystemException e )
{
if( filesystem != null ) filesystem.close();
ComputerCraft.log.error( "Cannot mount computer filesystem", e );
displayFailure( "Cannot mount computer system", null );
return null;
}
}
private ILuaMachine createLuaMachine()
{
// Load the bios resource
InputStream biosStream = null;
try
{
biosStream = computer.getComputerEnvironment().createResourceFile( "computercraft", "lua/bios.lua" );
}
catch( Exception ignored )
{
}
if( biosStream == null )
{
displayFailure( "Error loading bios.lua", null );
return null;
}
// Create the lua machine
ILuaMachine machine = new CobaltLuaMachine( computer, timeout );
// Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object.
for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api );
// Start the machine running the bios resource
MachineResult result = machine.loadBios( biosStream );
IoUtil.closeQuietly( biosStream );
if( result.isError() )
{
machine.close();
displayFailure( "Error loading bios.lua", result.getMessage() );
return null;
}
return machine;
}
private void turnOn() throws InterruptedException
{
isOnLock.lockInterruptibly();
try
{
// Reset the terminal and event queue
computer.getTerminal().reset();
interruptedEvent = false;
synchronized( queueLock )
{
eventQueue.clear();
}
// Init filesystem
if( (fileSystem = createFileSystem()) == null )
{
shutdown();
return;
}
// Init APIs
computer.getEnvironment().reset();
for( ILuaAPI api : apis ) api.startup();
// Init lua
if( (machine = createLuaMachine()) == null )
{
shutdown();
return;
}
// Initialisation has finished, so let's mark ourselves as on.
isOn = true;
computer.markChanged();
}
finally
{
isOnLock.unlock();
}
// Now actually start the computer, now that everything is set up.
resumeMachine( null, null );
}
private void shutdown() throws InterruptedException
{
isOnLock.lockInterruptibly();
try
{
isOn = false;
interruptedEvent = false;
synchronized( queueLock )
{
eventQueue.clear();
}
// Shutdown Lua machine
if( machine != null )
{
machine.close();
machine = null;
}
// Shutdown our APIs
for( ILuaAPI api : apis ) api.shutdown();
computer.getEnvironment().reset();
// Unload filesystem
if( fileSystem != null )
{
fileSystem.close();
fileSystem = null;
}
computer.getEnvironment().resetOutput();
computer.markChanged();
}
finally
{
isOnLock.unlock();
}
}
/**
* Called before calling {@link #work()}, setting up any important state.
*/
void beforeWork()
{
this.vRuntimeStart = System.nanoTime();
this.timeout.startTimer();
vRuntimeStart = System.nanoTime();
timeout.startTimer();
}
/**
@@ -350,35 +518,24 @@ final class ComputerExecutor
*/
boolean afterWork()
{
if( this.interruptedEvent )
if( interruptedEvent )
{
this.timeout.pauseTimer();
timeout.pauseTimer();
}
else
{
this.timeout.stopTimer();
timeout.stopTimer();
}
Tracking.addTaskTiming( this.getComputer(), this.timeout.nanoCurrent() );
Tracking.addTaskTiming( getComputer(), timeout.nanoCurrent() );
if( this.interruptedEvent )
if( interruptedEvent ) return true;
synchronized( queueLock )
{
if( eventQueue.isEmpty() && command == null ) return onComputerQueue = false;
return true;
}
synchronized( this.queueLock )
{
if( this.eventQueue.isEmpty() && this.command == null )
{
return this.onComputerQueue = false;
}
return true;
}
}
Computer getComputer()
{
return this.computer;
}
/**
@@ -392,19 +549,19 @@ final class ComputerExecutor
*/
void work() throws InterruptedException
{
if( this.interruptedEvent )
if( interruptedEvent )
{
this.interruptedEvent = false;
if( this.machine != null )
interruptedEvent = false;
if( machine != null )
{
this.resumeMachine( null, null );
resumeMachine( null, null );
return;
}
}
StateCommand command;
Event event = null;
synchronized( this.queueLock )
synchronized( queueLock )
{
command = this.command;
this.command = null;
@@ -412,15 +569,15 @@ final class ComputerExecutor
// If we've no command, pull something from the event queue instead.
if( command == null )
{
if( !this.isOn )
if( !isOn )
{
// We're not on and had no command, but we had work queued. This should never happen, so clear
// the event queue just in case.
this.eventQueue.clear();
eventQueue.clear();
return;
}
event = this.eventQueue.poll();
event = eventQueue.poll();
}
}
@@ -429,170 +586,52 @@ final class ComputerExecutor
switch( command )
{
case TURN_ON:
if( this.isOn )
{
return;
}
this.turnOn();
if( isOn ) return;
turnOn();
break;
case SHUTDOWN:
if( !this.isOn )
{
return;
}
this.computer.getTerminal()
.reset();
this.shutdown();
if( !isOn ) return;
computer.getTerminal().reset();
shutdown();
break;
case REBOOT:
if( !this.isOn )
{
return;
}
this.computer.getTerminal()
.reset();
this.shutdown();
if( !isOn ) return;
computer.getTerminal().reset();
shutdown();
this.computer.turnOn();
computer.turnOn();
break;
case ABORT:
if( !this.isOn )
{
return;
}
this.displayFailure( "Error running computer", TimeoutState.ABORT_MESSAGE );
this.shutdown();
if( !isOn ) return;
displayFailure( "Error running computer", TimeoutState.ABORT_MESSAGE );
shutdown();
break;
case ERROR:
if( !isOn ) return;
displayFailure( "Error running computer", "An internal error occurred, see logs." );
shutdown();
break;
}
}
else if( event != null )
{
this.resumeMachine( event.name, event.args );
}
}
private void resumeMachine( String event, Object[] args ) throws InterruptedException
{
MachineResult result = this.machine.handleEvent( event, args );
this.interruptedEvent = result.isPause();
if( !result.isError() )
{
return;
}
this.displayFailure( "Error running computer", result.getMessage() );
this.shutdown();
}
private void turnOn() throws InterruptedException
{
this.isOnLock.lockInterruptibly();
try
{
// Reset the terminal and event queue
this.computer.getTerminal()
.reset();
this.interruptedEvent = false;
synchronized( this.queueLock )
{
this.eventQueue.clear();
}
// Init filesystem
if( (this.fileSystem = this.createFileSystem()) == null )
{
this.shutdown();
return;
}
// Init APIs
this.computer.getEnvironment()
.reset();
for( ILuaAPI api : this.apis )
{
api.startup();
}
// Init lua
if( (this.machine = this.createLuaMachine()) == null )
{
this.shutdown();
return;
}
// Initialisation has finished, so let's mark ourselves as on.
this.isOn = true;
this.computer.markChanged();
}
finally
{
this.isOnLock.unlock();
}
// Now actually start the computer, now that everything is set up.
this.resumeMachine( null, null );
}
private void shutdown() throws InterruptedException
{
this.isOnLock.lockInterruptibly();
try
{
this.isOn = false;
this.interruptedEvent = false;
synchronized( this.queueLock )
{
this.eventQueue.clear();
}
// Shutdown Lua machine
if( this.machine != null )
{
this.machine.close();
this.machine = null;
}
// Shutdown our APIs
for( ILuaAPI api : this.apis )
{
api.shutdown();
}
this.computer.getEnvironment()
.reset();
// Unload filesystem
if( this.fileSystem != null )
{
this.fileSystem.close();
this.fileSystem = null;
}
this.computer.getEnvironment()
.resetOutput();
this.computer.markChanged();
}
finally
{
this.isOnLock.unlock();
resumeMachine( event.name, event.args );
}
}
private void displayFailure( String message, String extra )
{
Terminal terminal = this.computer.getTerminal();
boolean colour = this.computer.getComputerEnvironment()
.isColour();
Terminal terminal = computer.getTerminal();
boolean colour = computer.getComputerEnvironment().isColour();
terminal.reset();
// Display our primary error message
if( colour )
{
terminal.setTextColour( 15 - Colour.RED.ordinal() );
}
if( colour ) terminal.setTextColour( 15 - Colour.RED.ordinal() );
terminal.write( message );
if( extra != null )
@@ -605,105 +644,27 @@ final class ComputerExecutor
// And display our generic "CC may be installed incorrectly" message.
terminal.setCursorPos( 0, terminal.getCursorY() + 1 );
if( colour )
{
terminal.setTextColour( 15 - Colour.WHITE.ordinal() );
}
if( colour ) terminal.setTextColour( 15 - Colour.WHITE.ordinal() );
terminal.write( "ComputerCraft may be installed incorrectly" );
}
private FileSystem createFileSystem()
private void resumeMachine( String event, Object[] args ) throws InterruptedException
{
FileSystem filesystem = null;
try
{
filesystem = new FileSystem( "hdd", this.getRootMount() );
MachineResult result = machine.handleEvent( event, args );
interruptedEvent = result.isPause();
if( !result.isError() ) return;
IMount romMount = this.getRomMount();
if( romMount == null )
{
this.displayFailure( "Cannot mount ROM", null );
return null;
}
filesystem.mount( "rom", "rom", romMount );
return filesystem;
}
catch( FileSystemException e )
{
if( filesystem != null )
{
filesystem.close();
}
ComputerCraft.log.error( "Cannot mount computer filesystem", e );
this.displayFailure( "Cannot mount computer system", null );
return null;
}
}
private ILuaMachine createLuaMachine()
{
// Load the bios resource
InputStream biosStream = null;
try
{
biosStream = this.computer.getComputerEnvironment()
.createResourceFile( "computercraft", "lua/bios.lua" );
}
catch( Exception ignored )
{
}
if( biosStream == null )
{
this.displayFailure( "Error loading bios.lua", null );
return null;
}
// Create the lua machine
ILuaMachine machine = new CobaltLuaMachine( this.computer, this.timeout );
// Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object.
for( ILuaAPI api : this.apis )
{
machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api );
}
// Start the machine running the bios resource
MachineResult result = machine.loadBios( biosStream );
IoUtil.closeQuietly( biosStream );
if( result.isError() )
{
machine.close();
this.displayFailure( "Error loading bios.lua", result.getMessage() );
return null;
}
return machine;
}
private IWritableMount getRootMount()
{
if( this.rootMount == null )
{
this.rootMount = this.computer.getComputerEnvironment()
.createSaveDirMount( "computer/" + this.computer.assignID(), this.computer.getComputerEnvironment()
.getComputerSpaceLimit() );
}
return this.rootMount;
}
private IMount getRomMount()
{
return this.computer.getComputerEnvironment()
.createResourceMount( "computercraft", "lua/rom" );
displayFailure( "Error running computer", result.getMessage() );
shutdown();
}
private enum StateCommand
{
TURN_ON, SHUTDOWN, REBOOT, ABORT,
TURN_ON,
SHUTDOWN,
REBOOT,
ABORT,
ERROR,
}
private static final class Event

View File

@@ -3,27 +3,27 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import net.minecraft.util.math.Direction;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* A side on a computer. Unlike {@link Direction}, this is relative to the direction the computer is facing..
* A side on a computer. Unlike {@link Direction}, this is relative to the direction the computer is
* facing..
*/
public enum ComputerSide
{
BOTTOM( "bottom" ), TOP( "top" ), BACK( "back" ), FRONT( "front" ), RIGHT( "right" ), LEFT( "left" );
BOTTOM( "bottom" ),
TOP( "top" ),
BACK( "back" ),
FRONT( "front" ),
RIGHT( "right" ),
LEFT( "left" );
public static final String[] NAMES = new String[] {
"bottom",
"top",
"back",
"front",
"right",
"left"
};
public static final String[] NAMES = new String[] { "bottom", "top", "back", "front", "right", "left" };
public static final int COUNT = 6;
@@ -47,10 +47,7 @@ public enum ComputerSide
{
for( ComputerSide side : VALUES )
{
if( side.name.equalsIgnoreCase( name ) )
{
return side;
}
if( side.name.equalsIgnoreCase( name ) ) return side;
}
return null;
@@ -58,6 +55,6 @@ public enum ComputerSide
public String getName()
{
return this.name;
return name;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.api.filesystem.IFileSystem;
@@ -44,6 +43,21 @@ public class ComputerSystem extends ComputerAccess implements IComputerSystem
return "computer";
}
@Nullable
@Override
public IFileSystem getFileSystem()
{
FileSystem fs = environment.getFileSystem();
return fs == null ? null : fs.getMountWrapper();
}
@Nullable
@Override
public String getLabel()
{
return environment.getLabel();
}
@Nonnull
@Override
public Map<String, IPeripheral> getAvailablePeripherals()
@@ -58,19 +72,4 @@ public class ComputerSystem extends ComputerAccess implements IComputerSystem
{
return null;
}
@Nullable
@Override
public IFileSystem getFileSystem()
{
FileSystem fs = this.environment.getFileSystem();
return fs == null ? null : fs.getMountWrapper();
}
@Nullable
@Override
public String getLabel()
{
return this.environment.getLabel();
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.ComputerCraft;
@@ -25,20 +24,24 @@ import static dan200.computercraft.core.computer.TimeoutState.TIMEOUT;
/**
* Responsible for running all tasks from a {@link Computer}.
*
* This is split into two components: the {@link TaskRunner}s, which pull an executor from the queue and execute it, and a single {@link Monitor} which
* observes all runners and kills them if they have not been terminated by {@link TimeoutState#isSoftAborted()}.
* This is split into two components: the {@link TaskRunner}s, which pull an executor from the queue and execute it, and
* a single {@link Monitor} which observes all runners and kills them if they have not been terminated by
* {@link TimeoutState#isSoftAborted()}.
*
* Computers are executed using a priority system, with those who have spent less time executing having a higher priority than those hogging the thread.
* This, combined with {@link TimeoutState#isPaused()} means we can reduce the risk of badly behaved computers stalling execution for everyone else.
* Computers are executed using a priority system, with those who have spent less time executing having a higher
* priority than those hogging the thread. This, combined with {@link TimeoutState#isPaused()} means we can reduce the
* risk of badly behaved computers stalling execution for everyone else.
*
* This is done using an implementation of Linux's Completely Fair Scheduler. When a computer executes, we compute what share of execution time it has used
* (time executed/number of tasks). We then pick the computer who has the least "virtual execution time" (aka {@link ComputerExecutor#virtualRuntime}).
* This is done using an implementation of Linux's Completely Fair Scheduler. When a computer executes, we compute what
* share of execution time it has used (time executed/number of tasks). We then pick the computer who has the least
* "virtual execution time" (aka {@link ComputerExecutor#virtualRuntime}).
*
* When adding a computer to the queue, we make sure its "virtual runtime" is at least as big as the smallest runtime. This means that adding computers
* which have slept a lot do not then have massive priority over everyone else. See {@link #queue(ComputerExecutor)} for how this is implemented.
* When adding a computer to the queue, we make sure its "virtual runtime" is at least as big as the smallest runtime.
* This means that adding computers which have slept a lot do not then have massive priority over everyone else. See
* {@link #queue(ComputerExecutor)} for how this is implemented.
*
* In reality, it's unlikely that more than a few computers are waiting to execute at once, so this will not have much effect unless you have a computer
* hogging execution time. However, it is pretty effective in those situations.
* In reality, it's unlikely that more than a few computers are waiting to execute at once, so this will not have much
* effect unless you have a computer hogging execution time. However, it is pretty effective in those situations.
*
* @see TimeoutState For how hard timeouts are handled.
* @see ComputerExecutor For how computers actually do execution.
@@ -55,15 +58,16 @@ public final class ComputerThread
/**
* The target latency between executing two tasks on a single machine.
*
* An average tick takes 50ms, and so we ideally need to have handled a couple of events within that window in order to have a perceived low latency.
* An average tick takes 50ms, and so we ideally need to have handled a couple of events within that window in order
* to have a perceived low latency.
*/
private static final long DEFAULT_LATENCY = TimeUnit.MILLISECONDS.toNanos( 50 );
/**
* The minimum value that {@link #DEFAULT_LATENCY} can have when scaled.
*
* From statistics gathered on SwitchCraft, almost all machines will execute under 15ms, 75% under 1.5ms, with the mean being about 3ms. Most computers
* shouldn't be too impacted with having such a short period to execute in.
* From statistics gathered on SwitchCraft, almost all machines will execute under 15ms, 75% under 1.5ms, with the
* mean being about 3ms. Most computers shouldn't be too impacted with having such a short period to execute in.
*/
private static final long DEFAULT_MIN_PERIOD = TimeUnit.MILLISECONDS.toNanos( 5 );
@@ -76,45 +80,48 @@ public final class ComputerThread
* Lock used for modifications to the array of current threads.
*/
private static final Object threadLock = new Object();
private static final ReentrantLock computerLock = new ReentrantLock();
private static final Condition hasWork = computerLock.newCondition();
/**
* Active queues to execute.
*/
private static final TreeSet<ComputerExecutor> computerQueue = new TreeSet<>( ( a, b ) -> {
if( a == b )
{
return 0; // Should never happen, but let's be consistent here
}
long at = a.virtualRuntime, bt = b.virtualRuntime;
if( at == bt )
{
return Integer.compare( a.hashCode(), b.hashCode() );
}
return at < bt ? -1 : 1;
} );
private static final ThreadFactory monitorFactory = ThreadUtils.factory( "Computer-Monitor" );
private static final ThreadFactory runnerFactory = ThreadUtils.factory( "Computer-Runner" );
/**
* Whether the computer thread system is currently running.
*/
private static volatile boolean running = false;
/**
* The current task manager.
*/
private static Thread monitor;
/**
* The array of current runners, and their owning threads.
*/
private static TaskRunner[] runners;
private static long latency;
private static long minPeriod;
private static final ReentrantLock computerLock = new ReentrantLock();
private static final Condition hasWork = computerLock.newCondition();
/**
* Active queues to execute.
*/
private static final TreeSet<ComputerExecutor> computerQueue = new TreeSet<>( ( a, b ) -> {
if( a == b ) return 0; // Should never happen, but let's be consistent here
long at = a.virtualRuntime, bt = b.virtualRuntime;
if( at == bt ) return Integer.compare( a.hashCode(), b.hashCode() );
return at < bt ? -1 : 1;
} );
/**
* The minimum {@link ComputerExecutor#virtualRuntime} time on the tree.
*/
private static long minimumVirtualRuntime = 0;
private static final ThreadFactory monitorFactory = ThreadUtils.factory( "Computer-Monitor" );
private static final ThreadFactory runnerFactory = ThreadUtils.factory( "Computer-Runner" );
private ComputerThread() {}
/**
@@ -144,20 +151,13 @@ public final class ComputerThread
if( runner == null || runner.owner == null || !runner.owner.isAlive() )
{
// Mark the old runner as dead, just in case.
if( runner != null )
{
runner.running = false;
}
if( runner != null ) runner.running = false;
// And start a new runner
runnerFactory.newThread( runners[i] = new TaskRunner() )
.start();
runnerFactory.newThread( runners[i] = new TaskRunner() ).start();
}
}
if( monitor == null || !monitor.isAlive() )
{
(monitor = monitorFactory.newThread( new Monitor() )).start();
}
if( monitor == null || !monitor.isAlive() ) (monitor = monitorFactory.newThread( new Monitor() )).start();
}
}
@@ -173,16 +173,10 @@ public final class ComputerThread
{
for( TaskRunner runner : runners )
{
if( runner == null )
{
continue;
}
if( runner == null ) continue;
runner.running = false;
if( runner.owner != null )
{
runner.owner.interrupt();
}
if( runner.owner != null ) runner.owner.interrupt();
}
}
}
@@ -201,7 +195,8 @@ public final class ComputerThread
/**
* Mark a computer as having work, enqueuing it on the thread.
*
* You must be holding {@link ComputerExecutor}'s {@code queueLock} when calling this method - it should only be called from {@code enqueue}.
* You must be holding {@link ComputerExecutor}'s {@code queueLock} when calling this method - it should only
* be called from {@code enqueue}.
*
* @param executor The computer to execute work on.
*/
@@ -210,10 +205,7 @@ public final class ComputerThread
computerLock.lock();
try
{
if( executor.onComputerQueue )
{
throw new IllegalStateException( "Cannot queue already queued executor" );
}
if( executor.onComputerQueue ) throw new IllegalStateException( "Cannot queue already queued executor" );
executor.onComputerQueue = true;
updateRuntimes( null );
@@ -247,8 +239,8 @@ public final class ComputerThread
/**
* Update the {@link ComputerExecutor#virtualRuntime}s of all running tasks, and then update the {@link #minimumVirtualRuntime} based on the current
* tasks.
* Update the {@link ComputerExecutor#virtualRuntime}s of all running tasks, and then update the
* {@link #minimumVirtualRuntime} based on the current tasks.
*
* This is called before queueing tasks, to ensure that {@link #minimumVirtualRuntime} is up-to-date.
*
@@ -259,10 +251,7 @@ public final class ComputerThread
long minRuntime = Long.MAX_VALUE;
// If we've a task on the queue, use that as our base time.
if( !computerQueue.isEmpty() )
{
minRuntime = computerQueue.first().virtualRuntime;
}
if( !computerQueue.isEmpty() ) minRuntime = computerQueue.first().virtualRuntime;
// Update all the currently executing tasks
long now = System.nanoTime();
@@ -272,15 +261,9 @@ public final class ComputerThread
{
for( TaskRunner runner : currentRunners )
{
if( runner == null )
{
continue;
}
if( runner == null ) continue;
ComputerExecutor executor = runner.currentExecutor.get();
if( executor == null )
{
continue;
}
if( executor == null ) continue;
// We do two things here: first we update the task's virtual runtime based on when we
// last checked, and then we check the minimum.
@@ -301,6 +284,43 @@ public final class ComputerThread
}
}
/**
* Ensure the "currently working" state of the executor is reset, the timings are updated, and then requeue the
* executor if needed.
*
* @param runner The runner this task was on.
* @param executor The executor to requeue
*/
private static void afterWork( TaskRunner runner, ComputerExecutor executor )
{
// Clear the executor's thread.
Thread currentThread = executor.executingThread.getAndSet( null );
if( currentThread != runner.owner )
{
ComputerCraft.log.error(
"Expected computer #{} to be running on {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
executor.getComputer().getID(), runner.owner.getName(), currentThread == null ? "nothing" : currentThread.getName()
);
}
computerLock.lock();
try
{
updateRuntimes( executor );
// If we've no more tasks, just return.
if( !executor.afterWork() ) return;
// Otherwise, add to the queue, and signal any waiting workers.
computerQueue.add( executor );
hasWork.signal();
}
finally
{
computerLock.unlock();
}
}
/**
* The scaled period for a single task.
*
@@ -316,47 +336,6 @@ public final class ComputerThread
return count < LATENCY_MAX_TASKS ? latency / count : minPeriod;
}
/**
* Ensure the "currently working" state of the executor is reset, the timings are updated, and then requeue the executor if needed.
*
* @param runner The runner this task was on.
* @param executor The executor to requeue
*/
private static void afterWork( TaskRunner runner, ComputerExecutor executor )
{
// Clear the executor's thread.
Thread currentThread = executor.executingThread.getAndSet( null );
if( currentThread != runner.owner )
{
ComputerCraft.log.error(
"Expected computer #{} to be running on {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
executor.getComputer()
.getID(),
runner.owner.getName(),
currentThread == null ? "nothing" : currentThread.getName() );
}
computerLock.lock();
try
{
updateRuntimes( executor );
// If we've no more tasks, just return.
if( !executor.afterWork() )
{
return;
}
// Otherwise, add to the queue, and signal any waiting workers.
computerQueue.add( executor );
hasWork.signal();
}
finally
{
computerLock.unlock();
}
}
/**
* Determine if the thread has computers queued up.
*
@@ -367,40 +346,9 @@ public final class ComputerThread
return !computerQueue.isEmpty();
}
private static void timeoutTask( ComputerExecutor executor, Thread thread, long time )
{
if( !ComputerCraft.logComputerErrors )
{
return;
}
StringBuilder builder = new StringBuilder().append( "Terminating computer #" )
.append( executor.getComputer()
.getID() )
.append( " due to timeout (running for " )
.append( time * 1e-9 )
.append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " )
.append( thread.getName() )
.append( " is currently " )
.append( thread.getState() );
Object blocking = LockSupport.getBlocker( thread );
if( blocking != null )
{
builder.append( "\n on " )
.append( blocking );
}
for( StackTraceElement element : thread.getStackTrace() )
{
builder.append( "\n at " )
.append( element );
}
ComputerCraft.log.warn( builder.toString() );
}
/**
* Observes all currently active {@link TaskRunner}s and terminates their tasks once they have exceeded the hard abort limit.
* Observes all currently active {@link TaskRunner}s and terminates their tasks once they have exceeded the hard
* abort limit.
*
* @see TimeoutState
*/
@@ -424,37 +372,24 @@ public final class ComputerThread
// If we've no runner, skip.
if( runner == null || runner.owner == null || !runner.owner.isAlive() )
{
if( !running )
{
continue;
}
if( !running ) continue;
// Mark the old runner as dead and start a new one.
ComputerCraft.log.warn( "Previous runner ({}) has crashed, restarting!",
runner != null && runner.owner != null ? runner.owner.getName() : runner );
if( runner != null )
{
runner.running = false;
}
runnerFactory.newThread( runners[i] = new TaskRunner() )
.start();
if( runner != null ) runner.running = false;
runnerFactory.newThread( runners[i] = new TaskRunner() ).start();
}
// If the runner has no work, skip
ComputerExecutor executor = runner.currentExecutor.get();
if( executor == null )
{
continue;
}
if( executor == null ) continue;
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
// then we can let the Lua machine do its work.
long afterStart = executor.timeout.nanoCumulative();
long afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT;
if( afterHardAbort < 0 )
{
continue;
}
if( afterHardAbort < 0 ) continue;
// Set the hard abort flag.
executor.timeout.hardAbort();
@@ -469,17 +404,13 @@ public final class ComputerThread
runner.owner.interrupt();
ComputerExecutor thisExecutor = runner.currentExecutor.getAndSet( null );
if( thisExecutor != null )
{
afterWork( runner, executor );
}
if( thisExecutor != null ) afterWork( runner, executor );
synchronized( threadLock )
{
if( running && runners.length > i && runners[i] == runner )
{
runnerFactory.newThread( currentRunners[i] = new TaskRunner() )
.start();
runnerFactory.newThread( currentRunners[i] = new TaskRunner() ).start();
}
}
}
@@ -503,22 +434,24 @@ public final class ComputerThread
/**
* Pulls tasks from the {@link #computerQueue} queue and runs them.
*
* This is responsible for running the {@link ComputerExecutor#work()}, {@link ComputerExecutor#beforeWork()} and {@link ComputerExecutor#afterWork()}
* functions. Everything else is either handled by the executor, timeout state or monitor.
* This is responsible for running the {@link ComputerExecutor#work()}, {@link ComputerExecutor#beforeWork()} and
* {@link ComputerExecutor#afterWork()} functions. Everything else is either handled by the executor, timeout
* state or monitor.
*/
private static final class TaskRunner implements Runnable
{
final AtomicReference<ComputerExecutor> currentExecutor = new AtomicReference<>();
Thread owner;
volatile boolean running = true;
final AtomicReference<ComputerExecutor> currentExecutor = new AtomicReference<>();
@Override
public void run()
{
this.owner = Thread.currentThread();
owner = Thread.currentThread();
tasks:
while( this.running && ComputerThread.running )
while( running && ComputerThread.running )
{
// Wait for an active queue to execute
ComputerExecutor executor;
@@ -527,10 +460,7 @@ public final class ComputerThread
computerLock.lockInterruptibly();
try
{
while( computerQueue.isEmpty() )
{
hasWork.await();
}
while( computerQueue.isEmpty() ) hasWork.await();
executor = computerQueue.pollFirst();
assert executor != null : "hasWork should ensure we never receive null work";
}
@@ -548,16 +478,15 @@ public final class ComputerThread
// If we're trying to executing some task on this computer while someone else is doing work, something
// is seriously wrong.
while( !executor.executingThread.compareAndSet( null, this.owner ) )
while( !executor.executingThread.compareAndSet( null, owner ) )
{
Thread existing = executor.executingThread.get();
if( existing != null )
{
ComputerCraft.log.error(
"Trying to run computer #{} on thread {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
executor.getComputer()
.getID(), this.owner.getName(),
existing.getName() );
executor.getComputer().getID(), owner.getName(), existing.getName()
);
continue tasks;
}
}
@@ -567,7 +496,7 @@ public final class ComputerThread
// And then set the current executor. It's important to do it afterwards, as otherwise we introduce
// race conditions with the monitor.
this.currentExecutor.set( executor );
currentExecutor.set( executor );
// Execute the task
try
@@ -576,18 +505,38 @@ public final class ComputerThread
}
catch( Exception | LinkageError | VirtualMachineError e )
{
ComputerCraft.log.error( "Error running task on computer #" + executor.getComputer()
.getID(), e );
ComputerCraft.log.error( "Error running task on computer #" + executor.getComputer().getID(), e );
// Tear down the computer immediately. There's no guarantee it's well behaved from now on.
executor.fastFail();
}
finally
{
ComputerExecutor thisExecutor = this.currentExecutor.getAndSet( null );
if( thisExecutor != null )
{
afterWork( this, executor );
}
ComputerExecutor thisExecutor = currentExecutor.getAndSet( null );
if( thisExecutor != null ) afterWork( this, executor );
}
}
}
}
private static void timeoutTask( ComputerExecutor executor, Thread thread, long time )
{
if( !ComputerCraft.logComputerErrors ) return;
StringBuilder builder = new StringBuilder()
.append( "Terminating computer #" ).append( executor.getComputer().getID() )
.append( " due to timeout (running for " ).append( time * 1e-9 )
.append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " )
.append( thread.getName() )
.append( " is currently " )
.append( thread.getState() );
Object blocking = LockSupport.getBlocker( thread );
if( blocking != null ) builder.append( "\n on " ).append( blocking );
for( StackTraceElement element : thread.getStackTrace() )
{
builder.append( "\n at " ).append( element );
}
ComputerCraft.log.warn( builder.toString() );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.api.lua.ILuaAPI;
@@ -43,17 +42,22 @@ import java.util.Iterator;
public final class Environment implements IAPIEnvironment
{
private final Computer computer;
private boolean internalOutputChanged = false;
private final int[] internalOutput = new int[ComputerSide.COUNT];
private final int[] internalBundledOutput = new int[ComputerSide.COUNT];
private final int[] externalOutput = new int[ComputerSide.COUNT];
private final int[] externalBundledOutput = new int[ComputerSide.COUNT];
private boolean inputChanged = false;
private final int[] input = new int[ComputerSide.COUNT];
private final int[] bundledInput = new int[ComputerSide.COUNT];
private final IPeripheral[] peripherals = new IPeripheral[ComputerSide.COUNT];
private final Int2ObjectMap<Timer> timers = new Int2ObjectOpenHashMap<>();
private boolean internalOutputChanged = false;
private boolean inputChanged = false;
private IPeripheralChangeListener peripheralListener = null;
private final Int2ObjectMap<Timer> timers = new Int2ObjectOpenHashMap<>();
private int nextTimerToken = 0;
Environment( Computer computer )
@@ -64,64 +68,76 @@ public final class Environment implements IAPIEnvironment
@Override
public int getComputerID()
{
return this.computer.assignID();
return computer.assignID();
}
@Nonnull
@Override
public IComputerEnvironment getComputerEnvironment()
{
return this.computer.getComputerEnvironment();
return computer.getComputerEnvironment();
}
@Nonnull
@Override
public IWorkMonitor getMainThreadMonitor()
{
return this.computer.getMainThreadMonitor();
return computer.getMainThreadMonitor();
}
@Nonnull
@Override
public Terminal getTerminal()
{
return this.computer.getTerminal();
return computer.getTerminal();
}
@Override
public FileSystem getFileSystem()
{
return this.computer.getFileSystem();
return computer.getFileSystem();
}
@Override
public void shutdown()
{
this.computer.shutdown();
computer.shutdown();
}
@Override
public void reboot()
{
this.computer.reboot();
computer.reboot();
}
@Override
public void queueEvent( String event, Object... args )
{
this.computer.queueEvent( event, args );
computer.queueEvent( event, args );
}
@Override
public int getInput( ComputerSide side )
{
return input[side.ordinal()];
}
@Override
public int getBundledInput( ComputerSide side )
{
return bundledInput[side.ordinal()];
}
@Override
public void setOutput( ComputerSide side, int output )
{
int index = side.ordinal();
synchronized( this.internalOutput )
synchronized( internalOutput )
{
if( this.internalOutput[index] != output )
if( internalOutput[index] != output )
{
this.internalOutput[index] = output;
this.internalOutputChanged = true;
internalOutput[index] = output;
internalOutputChanged = true;
}
}
}
@@ -129,28 +145,22 @@ public final class Environment implements IAPIEnvironment
@Override
public int getOutput( ComputerSide side )
{
synchronized( this.internalOutput )
synchronized( internalOutput )
{
return this.computer.isOn() ? this.internalOutput[side.ordinal()] : 0;
return computer.isOn() ? internalOutput[side.ordinal()] : 0;
}
}
@Override
public int getInput( ComputerSide side )
{
return this.input[side.ordinal()];
}
@Override
public void setBundledOutput( ComputerSide side, int output )
{
int index = side.ordinal();
synchronized( this.internalOutput )
synchronized( internalOutput )
{
if( this.internalBundledOutput[index] != output )
if( internalBundledOutput[index] != output )
{
this.internalBundledOutput[index] = output;
this.internalOutputChanged = true;
internalBundledOutput[index] = output;
internalOutputChanged = true;
}
}
}
@@ -158,100 +168,39 @@ public final class Environment implements IAPIEnvironment
@Override
public int getBundledOutput( ComputerSide side )
{
synchronized( this.internalOutput )
synchronized( internalOutput )
{
return this.computer.isOn() ? this.internalBundledOutput[side.ordinal()] : 0;
return computer.isOn() ? internalBundledOutput[side.ordinal()] : 0;
}
}
@Override
public int getBundledInput( ComputerSide side )
{
return this.bundledInput[side.ordinal()];
}
@Override
public void setPeripheralChangeListener( IPeripheralChangeListener listener )
{
synchronized( this.peripherals )
{
this.peripheralListener = listener;
}
}
@Override
public IPeripheral getPeripheral( ComputerSide side )
{
synchronized( this.peripherals )
{
return this.peripherals[side.ordinal()];
}
}
@Override
public String getLabel()
{
return this.computer.getLabel();
}
@Override
public void setLabel( String label )
{
this.computer.setLabel( label );
}
@Override
public int startTimer( long ticks )
{
synchronized( this.timers )
{
this.timers.put( this.nextTimerToken, new Timer( ticks ) );
return this.nextTimerToken++;
}
}
@Override
public void cancelTimer( int id )
{
synchronized( this.timers )
{
this.timers.remove( id );
}
}
@Override
public void addTrackingChange( @Nonnull TrackingField field, long change )
{
Tracking.addValue( this.computer, field, change );
}
public int getExternalRedstoneOutput( ComputerSide side )
{
return this.computer.isOn() ? this.externalOutput[side.ordinal()] : 0;
return computer.isOn() ? externalOutput[side.ordinal()] : 0;
}
public int getExternalBundledRedstoneOutput( ComputerSide side )
{
return this.computer.isOn() ? this.externalBundledOutput[side.ordinal()] : 0;
return computer.isOn() ? externalBundledOutput[side.ordinal()] : 0;
}
public void setRedstoneInput( ComputerSide side, int level )
{
int index = side.ordinal();
if( this.input[index] != level )
if( input[index] != level )
{
this.input[index] = level;
this.inputChanged = true;
input[index] = level;
inputChanged = true;
}
}
public void setBundledRedstoneInput( ComputerSide side, int combination )
{
int index = side.ordinal();
if( this.bundledInput[index] != combination )
if( bundledInput[index] != combination )
{
this.bundledInput[index] = combination;
this.inputChanged = true;
bundledInput[index] = combination;
inputChanged = true;
}
}
@@ -263,9 +212,9 @@ public final class Environment implements IAPIEnvironment
*/
void reset()
{
synchronized( this.timers )
synchronized( timers )
{
this.timers.clear();
timers.clear();
}
}
@@ -274,17 +223,16 @@ public final class Environment implements IAPIEnvironment
*/
void tick()
{
if( this.inputChanged )
if( inputChanged )
{
this.inputChanged = false;
this.queueEvent( "redstone" );
inputChanged = false;
queueEvent( "redstone" );
}
synchronized( this.timers )
synchronized( timers )
{
// Countdown all of our active timers
Iterator<Int2ObjectMap.Entry<Timer>> it = this.timers.int2ObjectEntrySet()
.iterator();
Iterator<Int2ObjectMap.Entry<Timer>> it = timers.int2ObjectEntrySet().iterator();
while( it.hasNext() )
{
Int2ObjectMap.Entry<Timer> entry = it.next();
@@ -293,7 +241,7 @@ public final class Environment implements IAPIEnvironment
if( timer.ticksLeft <= 0 )
{
// Queue the "timer" event
this.queueEvent( TIMER_EVENT, entry.getIntKey() );
queueEvent( TIMER_EVENT, entry.getIntKey() );
it.remove();
}
}
@@ -308,31 +256,28 @@ public final class Environment implements IAPIEnvironment
boolean updateOutput()
{
// Mark output as changed if the internal redstone has changed
synchronized( this.internalOutput )
synchronized( internalOutput )
{
if( !this.internalOutputChanged )
{
return false;
}
if( !internalOutputChanged ) return false;
boolean changed = false;
for( int i = 0; i < ComputerSide.COUNT; i++ )
{
if( this.externalOutput[i] != this.internalOutput[i] )
if( externalOutput[i] != internalOutput[i] )
{
this.externalOutput[i] = this.internalOutput[i];
externalOutput[i] = internalOutput[i];
changed = true;
}
if( this.externalBundledOutput[i] != this.internalBundledOutput[i] )
if( externalBundledOutput[i] != internalBundledOutput[i] )
{
this.externalBundledOutput[i] = this.internalBundledOutput[i];
externalBundledOutput[i] = internalBundledOutput[i];
changed = true;
}
}
this.internalOutputChanged = false;
internalOutputChanged = false;
return changed;
}
@@ -341,31 +286,85 @@ public final class Environment implements IAPIEnvironment
void resetOutput()
{
// Reset redstone output
synchronized( this.internalOutput )
synchronized( internalOutput )
{
Arrays.fill( this.internalOutput, 0 );
Arrays.fill( this.internalBundledOutput, 0 );
this.internalOutputChanged = true;
Arrays.fill( internalOutput, 0 );
Arrays.fill( internalBundledOutput, 0 );
internalOutputChanged = true;
}
}
@Override
public IPeripheral getPeripheral( ComputerSide side )
{
synchronized( peripherals )
{
return peripherals[side.ordinal()];
}
}
public void setPeripheral( ComputerSide side, IPeripheral peripheral )
{
synchronized( this.peripherals )
synchronized( peripherals )
{
int index = side.ordinal();
IPeripheral existing = this.peripherals[index];
if( (existing == null && peripheral != null) || (existing != null && peripheral == null) || (existing != null && !existing.equals( peripheral )) )
IPeripheral existing = peripherals[index];
if( (existing == null && peripheral != null) ||
(existing != null && peripheral == null) ||
(existing != null && !existing.equals( peripheral )) )
{
this.peripherals[index] = peripheral;
if( this.peripheralListener != null )
{
this.peripheralListener.onPeripheralChanged( side, peripheral );
}
peripherals[index] = peripheral;
if( peripheralListener != null ) peripheralListener.onPeripheralChanged( side, peripheral );
}
}
}
@Override
public void setPeripheralChangeListener( IPeripheralChangeListener listener )
{
synchronized( peripherals )
{
peripheralListener = listener;
}
}
@Override
public String getLabel()
{
return computer.getLabel();
}
@Override
public void setLabel( String label )
{
computer.setLabel( label );
}
@Override
public int startTimer( long ticks )
{
synchronized( timers )
{
timers.put( nextTimerToken, new Timer( ticks ) );
return nextTimerToken++;
}
}
@Override
public void cancelTimer( int id )
{
synchronized( timers )
{
timers.remove( id );
}
}
@Override
public void addTrackingChange( @Nonnull TrackingField field, long change )
{
Tracking.addValue( computer, field, change );
}
private static class Timer
{
long ticksLeft;

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.api.filesystem.IMount;

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.ComputerCraft;
@@ -15,16 +14,17 @@ import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
/**
* Runs tasks on the main (server) thread, ticks {@link MainThreadExecutor}s, and limits how much time is used this tick.
* Runs tasks on the main (server) thread, ticks {@link MainThreadExecutor}s, and limits how much time is used this
* tick.
*
* Similar to {@link MainThreadExecutor}, the {@link MainThread} can be in one of three states: cool, hot and cooling. However, the implementation here is a
* little different:
* Similar to {@link MainThreadExecutor}, the {@link MainThread} can be in one of three states: cool, hot and cooling.
* However, the implementation here is a little different:
*
* {@link MainThread} starts cool, and runs as many tasks as it can in the current {@link #budget}ns. Any external tasks (those run by tile entities,
* etc...) will also consume the budget
* {@link MainThread} starts cool, and runs as many tasks as it can in the current {@link #budget}ns. Any external tasks
* (those run by tile entities, etc...) will also consume the budget
*
* Next tick, we put {@link ComputerCraft#maxMainGlobalTime} into our budget (and clamp it to that value to). If we're still over budget, then we should not
* execute <em>any</em> work (either as part of {@link MainThread} or externally).
* Next tick, we put {@link ComputerCraft#maxMainGlobalTime} into our budget (and clamp it to that value to). If we're
* still over budget, then we should not execute <em>any</em> work (either as part of {@link MainThread} or externally).
*/
public final class MainThread
{
@@ -40,16 +40,10 @@ public final class MainThread
* The queue of {@link MainThreadExecutor}s with tasks to perform.
*/
private static final TreeSet<MainThreadExecutor> executors = new TreeSet<>( ( a, b ) -> {
if( a == b )
{
return 0; // Should never happen, but let's be consistent here
}
if( a == b ) return 0; // Should never happen, but let's be consistent here
long at = a.virtualTime, bt = b.virtualTime;
if( at == bt )
{
return Integer.compare( a.hashCode(), b.hashCode() );
}
if( at == bt ) return Integer.compare( a.hashCode(), b.hashCode() );
return at < bt ? -1 : 1;
} );
@@ -62,7 +56,8 @@ public final class MainThread
private static final HashSet<MainThreadExecutor> cooling = new HashSet<>();
/**
* The current tick number. This is used by {@link MainThreadExecutor} to determine when to reset its own time counter.
* The current tick number. This is used by {@link MainThreadExecutor} to determine when to reset its own time
* counter.
*
* @see #currentTick()
*/
@@ -93,10 +88,7 @@ public final class MainThread
{
synchronized( executors )
{
if( executor.onQueue )
{
throw new IllegalStateException( "Cannot queue already queued executor" );
}
if( executor.onQueue ) throw new IllegalStateException( "Cannot queue already queued executor" );
executor.onQueue = true;
executor.updateTime();
@@ -105,10 +97,7 @@ public final class MainThread
long newRuntime = minimumTime;
// Slow down new computers a little bit.
if( executor.virtualTime == 0 )
{
newRuntime += ComputerCraft.maxMainComputerTime;
}
if( executor.virtualTime == 0 ) newRuntime += ComputerCraft.maxMainComputerTime;
executor.virtualTime = Math.max( newRuntime, executor.virtualTime );
@@ -121,6 +110,11 @@ public final class MainThread
cooling.add( executor );
}
static void consumeTime( long time )
{
budget -= time;
}
static boolean canExecute()
{
return canExecute;
@@ -145,10 +139,7 @@ public final class MainThread
// Cool down any warm computers.
cooling.removeIf( MainThreadExecutor::tickCooling );
if( !canExecute )
{
return;
}
if( !canExecute ) return;
// Run until we meet the deadline.
long start = System.nanoTime();
@@ -160,10 +151,7 @@ public final class MainThread
{
executor = executors.pollFirst();
}
if( executor == null )
{
break;
}
if( executor == null ) break;
long taskStart = System.nanoTime();
executor.execute();
@@ -171,10 +159,7 @@ public final class MainThread
long taskStop = System.nanoTime();
synchronized( executors )
{
if( executor.afterExecute( taskStop - taskStart ) )
{
executors.add( executor );
}
if( executor.afterExecute( taskStop - taskStart ) ) executors.add( executor );
// Compute the new minimum time (including the next task on the queue too). Note that this may also include
// time spent in external tasks.
@@ -182,28 +167,17 @@ public final class MainThread
if( !executors.isEmpty() )
{
MainThreadExecutor next = executors.first();
if( next.virtualTime < newMinimum )
{
newMinimum = next.virtualTime;
}
if( next.virtualTime < newMinimum ) newMinimum = next.virtualTime;
}
minimumTime = Math.max( minimumTime, newMinimum );
}
if( taskStop >= deadline )
{
break;
}
if( taskStop >= deadline ) break;
}
consumeTime( System.nanoTime() - start );
}
static void consumeTime( long time )
{
budget -= time;
}
public static void reset()
{
currentTick = 0;

View File

@@ -3,13 +3,13 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.peripheral.IWorkMonitor;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import net.minecraft.block.entity.BlockEntity;
import javax.annotation.Nonnull;
import java.util.ArrayDeque;
@@ -17,28 +17,32 @@ import java.util.Queue;
import java.util.concurrent.TimeUnit;
/**
* Keeps track of tasks that a {@link Computer} should run on the main thread and how long that has been spent executing them.
* Keeps track of tasks that a {@link Computer} should run on the main thread and how long that has been spent executing
* them.
*
* This provides rate-limiting mechanism for tasks enqueued with {@link Computer#queueMainThread(Runnable)}, but also those run elsewhere (such as during
* the turtle's tick - see {@link TurtleBrain#update()}). In order to handle this, the executor goes through three stages:
* This provides rate-limiting mechanism for tasks enqueued with {@link Computer#queueMainThread(Runnable)}, but also
* those run elsewhere (such as during the turtle's tick - see {@link TurtleBrain#update()}). In order to handle this,
* the executor goes through three stages:
*
* When {@link State#COOL}, the computer is allocated {@link ComputerCraft#maxMainComputerTime}ns to execute any work this tick. At the beginning of the
* tick, we execute as many {@link MainThread} tasks as possible, until our time-frame or the global time frame has expired.
* When {@link State#COOL}, the computer is allocated {@link ComputerCraft#maxMainComputerTime}ns to execute any work
* this tick. At the beginning of the tick, we execute as many {@link MainThread} tasks as possible, until our
* time-frame or the global time frame has expired.
*
* Then, when other objects (such as {@link TileEntity}) are ticked, we update how much time we've used using {@link IWorkMonitor#trackWork(long,
* TimeUnit)}.
* Then, when other objects (such as {@link BlockEntity}) are ticked, we update how much time we've used using
* {@link IWorkMonitor#trackWork(long, TimeUnit)}.
*
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as {@link State#HOT}. This means it will no longer
* be able to execute {@link MainThread} tasks (though will still execute tile entity tasks, in order to prevent the main thread from exhausting work every
* tick).
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as
* {@link State#HOT}. This means it will no longer be able to execute {@link MainThread} tasks (though will still
* execute tile entity tasks, in order to prevent the main thread from exhausting work every tick).
*
* At the beginning of the next tick, we increment the budget e by {@link ComputerCraft#maxMainComputerTime} and any {@link State#HOT} executors are marked
* as {@link State#COOLING}. They will remain cooling until their budget is fully replenished (is equal to {@link ComputerCraft#maxMainComputerTime}). Note,
* this is different to {@link MainThread}, which allows running when it has any budget left. When cooling, <em>no</em> tasks are executed - be they on the
* tile entity or main thread.
* At the beginning of the next tick, we increment the budget e by {@link ComputerCraft#maxMainComputerTime} and any
* {@link State#HOT} executors are marked as {@link State#COOLING}. They will remain cooling until their budget is
* fully replenished (is equal to {@link ComputerCraft#maxMainComputerTime}). Note, this is different to
* {@link MainThread}, which allows running when it has any budget left. When cooling, <em>no</em> tasks are executed -
* be they on the tile entity or main thread.
*
* This mechanism means that, on average, computers will use at most {@link ComputerCraft#maxMainComputerTime}ns per second, but one task source will not
* prevent others from executing.
* This mechanism means that, on average, computers will use at most {@link ComputerCraft#maxMainComputerTime}ns per
* second, but one task source will not prevent others from executing.
*
* @see MainThread
* @see IWorkMonitor
@@ -55,8 +59,8 @@ final class MainThreadExecutor implements IWorkMonitor
private final Computer computer;
/**
* A lock used for any changes to {@link #tasks}, or {@link #onQueue}. This will be used on the main thread, so locks should be kept as brief as
* possible.
* A lock used for any changes to {@link #tasks}, or {@link #onQueue}. This will be
* used on the main thread, so locks should be kept as brief as possible.
*/
private final Object queueLock = new Object();
@@ -77,7 +81,7 @@ final class MainThreadExecutor implements IWorkMonitor
* @see #afterExecute(long)
*/
volatile boolean onQueue;
long virtualTime;
/**
* The remaining budgeted time for this tick. This may be negative, in the case that we've gone over budget.
*
@@ -85,6 +89,7 @@ final class MainThreadExecutor implements IWorkMonitor
* @see #consumeTime(long)
*/
private long budget = 0;
/**
* The last tick that {@link #budget} was updated.
*
@@ -92,14 +97,18 @@ final class MainThreadExecutor implements IWorkMonitor
* @see #consumeTime(long)
*/
private int currentTick = -1;
/**
* The current state of this executor.
*
* @see #canWork()
*/
private State state = State.COOL;
private long pendingTime;
long virtualTime;
MainThreadExecutor( Computer computer )
{
this.computer = computer;
@@ -113,37 +122,25 @@ final class MainThreadExecutor implements IWorkMonitor
*/
boolean enqueue( Runnable runnable )
{
synchronized( this.queueLock )
synchronized( queueLock )
{
if( this.tasks.size() >= MAX_TASKS || !this.tasks.offer( runnable ) )
{
return false;
}
if( !this.onQueue && this.state == State.COOL )
{
MainThread.queue( this, true );
}
if( tasks.size() >= MAX_TASKS || !tasks.offer( runnable ) ) return false;
if( !onQueue && state == State.COOL ) MainThread.queue( this, true );
return true;
}
}
void execute()
{
if( this.state != State.COOL )
{
return;
}
if( state != State.COOL ) return;
Runnable task;
synchronized( this.queueLock )
synchronized( queueLock )
{
task = this.tasks.poll();
task = tasks.poll();
}
if( task != null )
{
task.run();
}
if( task != null ) task.run();
}
/**
@@ -154,54 +151,17 @@ final class MainThreadExecutor implements IWorkMonitor
*/
boolean afterExecute( long time )
{
this.consumeTime( time );
consumeTime( time );
synchronized( this.queueLock )
synchronized( queueLock )
{
this.virtualTime += time;
this.updateTime();
if( this.state != State.COOL || this.tasks.isEmpty() )
{
return this.onQueue = false;
}
virtualTime += time;
updateTime();
if( state != State.COOL || tasks.isEmpty() ) return onQueue = false;
return true;
}
}
private void consumeTime( long time )
{
Tracking.addServerTiming( this.computer, time );
// Reset the budget if moving onto a new tick. We know this is safe, as this will only have happened if
// #tickCooling() isn't called, and so we didn't overrun the previous tick.
if( this.currentTick != MainThread.currentTick() )
{
this.currentTick = MainThread.currentTick();
this.budget = ComputerCraft.maxMainComputerTime;
}
this.budget -= time;
// If we've gone over our limit, mark us as having to cool down.
if( this.budget < 0 && this.state == State.COOL )
{
this.state = State.HOT;
MainThread.cooling( this );
}
}
void updateTime()
{
this.virtualTime += this.pendingTime;
this.pendingTime = 0;
}
@Override
public boolean shouldWork()
{
return this.state == State.COOL && MainThread.canExecute();
}
/**
* Whether we should execute "external" tasks (ones not part of {@link #tasks}).
*
@@ -210,22 +170,50 @@ final class MainThreadExecutor implements IWorkMonitor
@Override
public boolean canWork()
{
return this.state != State.COOLING && MainThread.canExecute();
return state != State.COOLING && MainThread.canExecute();
}
@Override
public boolean shouldWork()
{
return state == State.COOL && MainThread.canExecute();
}
@Override
public void trackWork( long time, @Nonnull TimeUnit unit )
{
long nanoTime = unit.toNanos( time );
synchronized( this.queueLock )
synchronized( queueLock )
{
this.pendingTime += nanoTime;
pendingTime += nanoTime;
}
this.consumeTime( nanoTime );
consumeTime( nanoTime );
MainThread.consumeTime( nanoTime );
}
private void consumeTime( long time )
{
Tracking.addServerTiming( computer, time );
// Reset the budget if moving onto a new tick. We know this is safe, as this will only have happened if
// #tickCooling() isn't called, and so we didn't overrun the previous tick.
if( currentTick != MainThread.currentTick() )
{
currentTick = MainThread.currentTick();
budget = ComputerCraft.maxMainComputerTime;
}
budget -= time;
// If we've gone over our limit, mark us as having to cool down.
if( budget < 0 && state == State.COOL )
{
state = State.HOT;
MainThread.cooling( this );
}
}
/**
* Move this executor forward one tick, replenishing the budget by {@link ComputerCraft#maxMainComputerTime}.
*
@@ -233,27 +221,29 @@ final class MainThreadExecutor implements IWorkMonitor
*/
boolean tickCooling()
{
this.state = State.COOLING;
this.currentTick = MainThread.currentTick();
this.budget = Math.min( this.budget + ComputerCraft.maxMainComputerTime, ComputerCraft.maxMainComputerTime );
if( this.budget < ComputerCraft.maxMainComputerTime )
{
return false;
}
state = State.COOLING;
currentTick = MainThread.currentTick();
budget = Math.min( budget + ComputerCraft.maxMainComputerTime, ComputerCraft.maxMainComputerTime );
if( budget < ComputerCraft.maxMainComputerTime ) return false;
this.state = State.COOL;
synchronized( this.queueLock )
state = State.COOL;
synchronized( queueLock )
{
if( !this.tasks.isEmpty() && !this.onQueue )
{
MainThread.queue( this, false );
}
if( !tasks.isEmpty() && !onQueue ) MainThread.queue( this, false );
}
return true;
}
void updateTime()
{
virtualTime += pendingTime;
pendingTime = 0;
}
private enum State
{
COOL, HOT, COOLING,
COOL,
HOT,
COOLING,
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.computer;
import dan200.computercraft.core.lua.ILuaMachine;
@@ -14,18 +13,20 @@ import java.util.concurrent.TimeUnit;
/**
* Used to measure how long a computer has executed for, and thus the relevant timeout states.
*
* Timeouts are mostly used for execution of Lua code: we should ideally never have a state where constructing the APIs or machines themselves takes more
* than a fraction of a second.
* Timeouts are mostly used for execution of Lua code: we should ideally never have a state where constructing the APIs
* or machines themselves takes more than a fraction of a second.
*
* When a computer runs, it is allowed to run for 7 seconds ({@link #TIMEOUT}). After that point, the "soft abort" flag is set ({@link #isSoftAborted()}).
* Here, the Lua machine will attempt to abort the program in some safe manner (namely, throwing a "Too long without yielding" error).
* When a computer runs, it is allowed to run for 7 seconds ({@link #TIMEOUT}). After that point, the "soft abort" flag
* is set ({@link #isSoftAborted()}). Here, the Lua machine will attempt to abort the program in some safe manner
* (namely, throwing a "Too long without yielding" error).
*
* Now, if a computer still does not stop after that period, they're behaving really badly. 1.5 seconds after a soft abort ({@link #ABORT_TIMEOUT}), we
* trigger a hard abort (note, this is done from the computer thread manager). This will destroy the entire Lua runtime and shut the computer down.
* Now, if a computer still does not stop after that period, they're behaving really badly. 1.5 seconds after a soft
* abort ({@link #ABORT_TIMEOUT}), we trigger a hard abort (note, this is done from the computer thread manager). This
* will destroy the entire Lua runtime and shut the computer down.
*
* The Lua runtime is also allowed to pause execution if there are other computers contesting for work. All computers are allowed to run for {@link
* ComputerThread#scaledPeriod()} nanoseconds (see {@link #currentDeadline}). After that period, if any computers are waiting to be executed then we'll set
* the paused flag to true ({@link #isPaused()}.
* The Lua runtime is also allowed to pause execution if there are other computers contesting for work. All computers
* are allowed to run for {@link ComputerThread#scaledPeriod()} nanoseconds (see {@link #currentDeadline}). After that
* period, if any computers are waiting to be executed then we'll set the paused flag to true ({@link #isPaused()}.
*
* @see ComputerThread
* @see ILuaMachine
@@ -33,18 +34,21 @@ import java.util.concurrent.TimeUnit;
*/
public final class TimeoutState
{
/**
* The error message to display when we trigger an abort.
*/
public static final String ABORT_MESSAGE = "Too long without yielding";
/**
* The total time a task is allowed to run before aborting in nanoseconds.
*/
static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos( 7000 );
/**
* The time the task is allowed to run after each abort in nanoseconds.
*/
static final long ABORT_TIMEOUT = TimeUnit.MILLISECONDS.toNanos( 1500 );
/**
* The error message to display when we trigger an abort.
*/
public static final String ABORT_MESSAGE = "Too long without yielding";
private boolean paused;
private boolean softAbort;
private volatile boolean hardAbort;
@@ -71,12 +75,12 @@ public final class TimeoutState
long nanoCumulative()
{
return System.nanoTime() - this.cumulativeStart;
return System.nanoTime() - cumulativeStart;
}
long nanoCurrent()
{
return System.nanoTime() - this.currentStart;
return System.nanoTime() - currentStart;
}
/**
@@ -87,26 +91,21 @@ public final class TimeoutState
// Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we
// need to handle overflow.
long now = System.nanoTime();
if( !this.paused )
{
this.paused = this.currentDeadline - now <= 0 && ComputerThread.hasPendingWork(); // now >= currentDeadline
}
if( !this.softAbort )
{
this.softAbort = now - this.cumulativeStart - TIMEOUT >= 0; // now - cumulativeStart >= TIMEOUT
}
if( !paused ) paused = currentDeadline - now <= 0 && ComputerThread.hasPendingWork(); // now >= currentDeadline
if( !softAbort ) softAbort = now - cumulativeStart - TIMEOUT >= 0; // now - cumulativeStart >= TIMEOUT
}
/**
* Whether we should pause execution of this machine.
*
* This is determined by whether we've consumed our time slice, and if there are other computers waiting to perform work.
* This is determined by whether we've consumed our time slice, and if there are other computers waiting to perform
* work.
*
* @return Whether we should pause execution.
*/
public boolean isPaused()
{
return this.paused;
return paused;
}
/**
@@ -116,7 +115,7 @@ public final class TimeoutState
*/
public boolean isSoftAborted()
{
return this.softAbort;
return softAbort;
}
/**
@@ -126,7 +125,7 @@ public final class TimeoutState
*/
public boolean isHardAborted()
{
return this.hardAbort;
return hardAbort;
}
/**
@@ -134,7 +133,7 @@ public final class TimeoutState
*/
void hardAbort()
{
this.softAbort = this.hardAbort = true;
softAbort = hardAbort = true;
}
/**
@@ -143,10 +142,10 @@ public final class TimeoutState
void startTimer()
{
long now = System.nanoTime();
this.currentStart = now;
this.currentDeadline = now + ComputerThread.scaledPeriod();
currentStart = now;
currentDeadline = now + ComputerThread.scaledPeriod();
// Compute the "nominal start time".
this.cumulativeStart = now - this.cumulativeElapsed;
cumulativeStart = now - cumulativeElapsed;
}
/**
@@ -157,8 +156,8 @@ public final class TimeoutState
void pauseTimer()
{
// We set the cumulative time to difference between current time and "nominal start time".
this.cumulativeElapsed = System.nanoTime() - this.cumulativeStart;
this.paused = false;
cumulativeElapsed = System.nanoTime() - cumulativeStart;
paused = false;
}
/**
@@ -166,7 +165,7 @@ public final class TimeoutState
*/
void stopTimer()
{
this.cumulativeElapsed = 0;
this.paused = this.softAbort = this.hardAbort = false;
cumulativeElapsed = 0;
paused = softAbort = hardAbort = false;
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
@@ -20,24 +19,52 @@ import java.util.Set;
public class ComboMount implements IMount
{
private IMount[] m_parts;
private final IMount[] parts;
public ComboMount( IMount[] parts )
{
this.m_parts = parts;
this.parts = parts;
}
// IMount implementation
@Override
public boolean exists( @Nonnull String path ) throws IOException
{
for( int i = parts.length - 1; i >= 0; --i )
{
IMount part = parts[i];
if( part.exists( path ) )
{
return true;
}
}
return false;
}
@Override
public boolean isDirectory( @Nonnull String path ) throws IOException
{
for( int i = parts.length - 1; i >= 0; --i )
{
IMount part = parts[i];
if( part.isDirectory( path ) )
{
return true;
}
}
return false;
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
// Combine the lists from all the mounts
List<String> foundFiles = null;
int foundDirs = 0;
for( int i = this.m_parts.length - 1; i >= 0; --i )
for( int i = parts.length - 1; i >= 0; --i )
{
IMount part = this.m_parts[i];
IMount part = parts[i];
if( part.exists( path ) && part.isDirectory( path ) )
{
if( foundFiles == null )
@@ -72,13 +99,27 @@ public class ComboMount implements IMount
}
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
for( int i = parts.length - 1; i >= 0; --i )
{
IMount part = parts[i];
if( part.exists( path ) )
{
return part.getSize( path );
}
}
throw new FileOperationException( path, "No such file" );
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
for( int i = this.m_parts.length - 1; i >= 0; --i )
for( int i = parts.length - 1; i >= 0; --i )
{
IMount part = this.m_parts[i];
IMount part = parts[i];
if( part.exists( path ) && !part.isDirectory( path ) )
{
return part.openForRead( path );
@@ -91,9 +132,9 @@ public class ComboMount implements IMount
@Override
public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException
{
for( int i = this.m_parts.length - 1; i >= 0; --i )
for( int i = parts.length - 1; i >= 0; --i )
{
IMount part = this.m_parts[i];
IMount part = parts[i];
if( part.exists( path ) && !part.isDirectory( path ) )
{
return part.getAttributes( path );
@@ -101,46 +142,4 @@ public class ComboMount implements IMount
}
throw new FileOperationException( path, "No such file" );
}
@Override
public boolean exists( @Nonnull String path ) throws IOException
{
for( int i = this.m_parts.length - 1; i >= 0; --i )
{
IMount part = this.m_parts[i];
if( part.exists( path ) )
{
return true;
}
}
return false;
}
@Override
public boolean isDirectory( @Nonnull String path ) throws IOException
{
for( int i = this.m_parts.length - 1; i >= 0; --i )
{
IMount part = this.m_parts[i];
if( part.isDirectory( path ) )
{
return true;
}
}
return false;
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
for( int i = this.m_parts.length - 1; i >= 0; --i )
{
IMount part = this.m_parts[i];
if( part.exists( path ) )
{
return part.getSize( path );
}
}
throw new FileOperationException( path, "No such file" );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
@@ -16,18 +15,6 @@ import java.util.List;
public class EmptyMount implements IMount
{
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents )
{
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
throw new FileOperationException( path, "No such file" );
}
@Override
public boolean exists( @Nonnull String path )
{
@@ -40,9 +27,21 @@ public class EmptyMount implements IMount
return path.isEmpty();
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents )
{
}
@Override
public long getSize( @Nonnull String path )
{
return 0;
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
throw new FileOperationException( path, "No such file" );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import com.google.common.collect.Sets;
@@ -27,32 +26,383 @@ public class FileMount implements IWritableMount
{
private static final int MINIMUM_FILE_SIZE = 500;
private static final Set<OpenOption> READ_OPTIONS = Collections.singleton( StandardOpenOption.READ );
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet( StandardOpenOption.WRITE,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING );
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet( StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING );
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet( StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND );
private File m_rootPath;
private long m_capacity;
private long m_usedSpace;
private class WritableCountingChannel implements WritableByteChannel
{
private final WritableByteChannel inner;
long ignoredBytesLeft;
WritableCountingChannel( WritableByteChannel inner, long bytesToIgnore )
{
this.inner = inner;
ignoredBytesLeft = bytesToIgnore;
}
@Override
public int write( @Nonnull ByteBuffer b ) throws IOException
{
count( b.remaining() );
return inner.write( b );
}
void count( long n ) throws IOException
{
ignoredBytesLeft -= n;
if( ignoredBytesLeft < 0 )
{
long newBytes = -ignoredBytesLeft;
ignoredBytesLeft = 0;
long bytesLeft = capacity - usedSpace;
if( newBytes > bytesLeft ) throw new IOException( "Out of space" );
usedSpace += newBytes;
}
}
@Override
public boolean isOpen()
{
return inner.isOpen();
}
@Override
public void close() throws IOException
{
inner.close();
}
}
private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel
{
private final SeekableByteChannel inner;
SeekableCountingChannel( SeekableByteChannel inner, long bytesToIgnore )
{
super( inner, bytesToIgnore );
this.inner = inner;
}
@Override
public SeekableByteChannel position( long newPosition ) throws IOException
{
if( !isOpen() ) throw new ClosedChannelException();
if( newPosition < 0 )
{
throw new IllegalArgumentException( "Cannot seek before the beginning of the stream" );
}
long delta = newPosition - inner.position();
if( delta < 0 )
{
ignoredBytesLeft -= delta;
}
else
{
count( delta );
}
return inner.position( newPosition );
}
@Override
public SeekableByteChannel truncate( long size ) throws IOException
{
throw new IOException( "Not yet implemented" );
}
@Override
public int read( ByteBuffer dst ) throws ClosedChannelException
{
if( !inner.isOpen() ) throw new ClosedChannelException();
throw new NonReadableChannelException();
}
@Override
public long position() throws IOException
{
return inner.position();
}
@Override
public long size() throws IOException
{
return inner.size();
}
}
private final File rootPath;
private final long capacity;
private long usedSpace;
public FileMount( File rootPath, long capacity )
{
this.m_rootPath = rootPath;
this.m_capacity = capacity + MINIMUM_FILE_SIZE;
this.m_usedSpace = this.created() ? measureUsedSpace( this.m_rootPath ) : MINIMUM_FILE_SIZE;
this.rootPath = rootPath;
this.capacity = capacity + MINIMUM_FILE_SIZE;
usedSpace = created() ? measureUsedSpace( this.rootPath ) : MINIMUM_FILE_SIZE;
}
// IMount implementation
@Override
public boolean exists( @Nonnull String path )
{
if( !created() ) return path.isEmpty();
File file = getRealPath( path );
return file.exists();
}
@Override
public boolean isDirectory( @Nonnull String path )
{
if( !created() ) return path.isEmpty();
File file = getRealPath( path );
return file.exists() && file.isDirectory();
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
if( !created() )
{
if( !path.isEmpty() ) throw new FileOperationException( path, "Not a directory" );
return;
}
File file = getRealPath( path );
if( !file.exists() || !file.isDirectory() ) throw new FileOperationException( path, "Not a directory" );
String[] paths = file.list();
for( String subPath : paths )
{
if( new File( file, subPath ).exists() ) contents.add( subPath );
}
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
if( !created() )
{
if( path.isEmpty() ) return 0;
}
else
{
File file = getRealPath( path );
if( file.exists() ) return file.isDirectory() ? 0 : file.length();
}
throw new FileOperationException( path, "No such file" );
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
if( created() )
{
File file = getRealPath( path );
if( file.exists() && !file.isDirectory() ) return FileChannel.open( file.toPath(), READ_OPTIONS );
}
throw new FileOperationException( path, "No such file" );
}
@Nonnull
@Override
public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException
{
if( created() )
{
File file = getRealPath( path );
if( file.exists() ) return Files.readAttributes( file.toPath(), BasicFileAttributes.class );
}
throw new FileOperationException( path, "No such file" );
}
// IWritableMount implementation
@Override
public void makeDirectory( @Nonnull String path ) throws IOException
{
create();
File file = getRealPath( path );
if( file.exists() )
{
if( !file.isDirectory() ) throw new FileOperationException( path, "File exists" );
return;
}
int dirsToCreate = 1;
File parent = file.getParentFile();
while( !parent.exists() )
{
++dirsToCreate;
parent = parent.getParentFile();
}
if( getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE )
{
throw new FileOperationException( path, "Out of space" );
}
if( file.mkdirs() )
{
usedSpace += dirsToCreate * MINIMUM_FILE_SIZE;
}
else
{
throw new FileOperationException( path, "Access denied" );
}
}
@Override
public void delete( @Nonnull String path ) throws IOException
{
if( path.isEmpty() ) throw new FileOperationException( path, "Access denied" );
if( created() )
{
File file = getRealPath( path );
if( file.exists() ) deleteRecursively( file );
}
}
private void deleteRecursively( File file ) throws IOException
{
// Empty directories first
if( file.isDirectory() )
{
String[] children = file.list();
for( String aChildren : children )
{
deleteRecursively( new File( file, aChildren ) );
}
}
// Then delete
long fileSize = file.isDirectory() ? 0 : file.length();
boolean success = file.delete();
if( success )
{
usedSpace -= Math.max( MINIMUM_FILE_SIZE, fileSize );
}
else
{
throw new IOException( "Access denied" );
}
}
@Nonnull
@Override
public WritableByteChannel openForWrite( @Nonnull String path ) throws IOException
{
create();
File file = getRealPath( path );
if( file.exists() && file.isDirectory() ) throw new FileOperationException( path, "Cannot write to directory" );
if( file.exists() )
{
usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE );
}
else if( getRemainingSpace() < MINIMUM_FILE_SIZE )
{
throw new FileOperationException( path, "Out of space" );
}
usedSpace += MINIMUM_FILE_SIZE;
return new SeekableCountingChannel( Files.newByteChannel( file.toPath(), WRITE_OPTIONS ), MINIMUM_FILE_SIZE );
}
@Nonnull
@Override
public WritableByteChannel openForAppend( @Nonnull String path ) throws IOException
{
if( !created() )
{
throw new FileOperationException( path, "No such file" );
}
File file = getRealPath( path );
if( !file.exists() ) throw new FileOperationException( path, "No such file" );
if( file.isDirectory() ) throw new FileOperationException( path, "Cannot write to directory" );
// Allowing seeking when appending is not recommended, so we use a separate channel.
return new WritableCountingChannel(
Files.newByteChannel( file.toPath(), APPEND_OPTIONS ),
Math.max( MINIMUM_FILE_SIZE - file.length(), 0 )
);
}
@Override
public long getRemainingSpace()
{
return Math.max( capacity - usedSpace, 0 );
}
@Nonnull
@Override
public OptionalLong getCapacity()
{
return OptionalLong.of( capacity - MINIMUM_FILE_SIZE );
}
private File getRealPath( String path )
{
return new File( rootPath, path );
}
private boolean created()
{
return this.m_rootPath.exists();
return rootPath.exists();
}
private void create() throws IOException
{
if( !rootPath.exists() )
{
boolean success = rootPath.mkdirs();
if( !success )
{
throw new IOException( "Access denied" );
}
}
}
private static class Visitor extends SimpleFileVisitor<Path>
{
long size;
@Override
public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs )
{
size += MINIMUM_FILE_SIZE;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile( Path file, BasicFileAttributes attrs )
{
size += Math.max( attrs.size(), MINIMUM_FILE_SIZE );
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed( Path file, IOException exc )
{
ComputerCraft.log.error( "Error computing file size for {}", file, exc );
return FileVisitResult.CONTINUE;
}
}
private static long measureUsedSpace( File file )
{
if( !file.exists() )
{
return 0;
}
if( !file.exists() ) return 0;
try
{
@@ -66,410 +416,4 @@ public class FileMount implements IWritableMount
return 0;
}
}
// IMount implementation
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
if( !this.created() )
{
if( !path.isEmpty() )
{
throw new FileOperationException( path, "Not a directory" );
}
return;
}
File file = this.getRealPath( path );
if( !file.exists() || !file.isDirectory() )
{
throw new FileOperationException( path, "Not a directory" );
}
String[] paths = file.list();
for( String subPath : paths )
{
if( new File( file, subPath ).exists() )
{
contents.add( subPath );
}
}
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
if( this.created() )
{
File file = this.getRealPath( path );
if( file.exists() && !file.isDirectory() )
{
return FileChannel.open( file.toPath(), READ_OPTIONS );
}
}
throw new FileOperationException( path, "No such file" );
}
@Nonnull
@Override
public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException
{
if( this.created() )
{
File file = this.getRealPath( path );
if( file.exists() )
{
return Files.readAttributes( file.toPath(), BasicFileAttributes.class );
}
}
throw new FileOperationException( path, "No such file" );
}
@Override
public boolean exists( @Nonnull String path )
{
if( !this.created() )
{
return path.isEmpty();
}
File file = this.getRealPath( path );
return file.exists();
}
@Override
public boolean isDirectory( @Nonnull String path )
{
if( !this.created() )
{
return path.isEmpty();
}
File file = this.getRealPath( path );
return file.exists() && file.isDirectory();
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
if( !this.created() )
{
if( path.isEmpty() )
{
return 0;
}
}
else
{
File file = this.getRealPath( path );
if( file.exists() )
{
return file.isDirectory() ? 0 : file.length();
}
}
throw new FileOperationException( path, "No such file" );
}
// IWritableMount implementation
private File getRealPath( String path )
{
return new File( this.m_rootPath, path );
}
@Override
public void makeDirectory( @Nonnull String path ) throws IOException
{
this.create();
File file = this.getRealPath( path );
if( file.exists() )
{
if( !file.isDirectory() )
{
throw new FileOperationException( path, "File exists" );
}
return;
}
int dirsToCreate = 1;
File parent = file.getParentFile();
while( !parent.exists() )
{
++dirsToCreate;
parent = parent.getParentFile();
}
if( this.getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE )
{
throw new FileOperationException( path, "Out of space" );
}
if( file.mkdirs() )
{
this.m_usedSpace += dirsToCreate * MINIMUM_FILE_SIZE;
}
else
{
throw new FileOperationException( path, "Access denied" );
}
}
@Override
public void delete( @Nonnull String path ) throws IOException
{
if( path.isEmpty() )
{
throw new FileOperationException( path, "Access denied" );
}
if( this.created() )
{
File file = this.getRealPath( path );
if( file.exists() )
{
this.deleteRecursively( file );
}
}
}
@Nonnull
@Override
public WritableByteChannel openForWrite( @Nonnull String path ) throws IOException
{
this.create();
File file = this.getRealPath( path );
if( file.exists() && file.isDirectory() )
{
throw new FileOperationException( path, "Cannot write to directory" );
}
if( file.exists() )
{
this.m_usedSpace -= Math.max( file.length(), MINIMUM_FILE_SIZE );
}
else if( this.getRemainingSpace() < MINIMUM_FILE_SIZE )
{
throw new FileOperationException( path, "Out of space" );
}
this.m_usedSpace += MINIMUM_FILE_SIZE;
return new SeekableCountingChannel( Files.newByteChannel( file.toPath(), WRITE_OPTIONS ), MINIMUM_FILE_SIZE );
}
@Nonnull
@Override
public WritableByteChannel openForAppend( @Nonnull String path ) throws IOException
{
if( !this.created() )
{
throw new FileOperationException( path, "No such file" );
}
File file = this.getRealPath( path );
if( !file.exists() )
{
throw new FileOperationException( path, "No such file" );
}
if( file.isDirectory() )
{
throw new FileOperationException( path, "Cannot write to directory" );
}
// Allowing seeking when appending is not recommended, so we use a separate channel.
return new WritableCountingChannel( Files.newByteChannel( file.toPath(), APPEND_OPTIONS ), Math.max( MINIMUM_FILE_SIZE - file.length(), 0 ) );
}
@Override
public long getRemainingSpace()
{
return Math.max( this.m_capacity - this.m_usedSpace, 0 );
}
@Nonnull
@Override
public OptionalLong getCapacity()
{
return OptionalLong.of( this.m_capacity - MINIMUM_FILE_SIZE );
}
private void create() throws IOException
{
if( !this.m_rootPath.exists() )
{
boolean success = this.m_rootPath.mkdirs();
if( !success )
{
throw new IOException( "Access denied" );
}
}
}
private void deleteRecursively( File file ) throws IOException
{
// Empty directories first
if( file.isDirectory() )
{
String[] children = file.list();
for( String aChildren : children )
{
this.deleteRecursively( new File( file, aChildren ) );
}
}
// Then delete
long fileSize = file.isDirectory() ? 0 : file.length();
boolean success = file.delete();
if( success )
{
this.m_usedSpace -= Math.max( MINIMUM_FILE_SIZE, fileSize );
}
else
{
throw new IOException( "Access denied" );
}
}
private static class Visitor extends SimpleFileVisitor<Path>
{
long size;
@Override
public FileVisitResult preVisitDirectory( Path dir, BasicFileAttributes attrs )
{
this.size += MINIMUM_FILE_SIZE;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile( Path file, BasicFileAttributes attrs )
{
this.size += Math.max( attrs.size(), MINIMUM_FILE_SIZE );
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed( Path file, IOException exc )
{
ComputerCraft.log.error( "Error computing file size for {}", file, exc );
return FileVisitResult.CONTINUE;
}
}
private class WritableCountingChannel implements WritableByteChannel
{
private final WritableByteChannel m_inner;
long m_ignoredBytesLeft;
WritableCountingChannel( WritableByteChannel inner, long bytesToIgnore )
{
this.m_inner = inner;
this.m_ignoredBytesLeft = bytesToIgnore;
}
@Override
public int write( @Nonnull ByteBuffer b ) throws IOException
{
this.count( b.remaining() );
return this.m_inner.write( b );
}
void count( long n ) throws IOException
{
this.m_ignoredBytesLeft -= n;
if( this.m_ignoredBytesLeft < 0 )
{
long newBytes = -this.m_ignoredBytesLeft;
this.m_ignoredBytesLeft = 0;
long bytesLeft = FileMount.this.m_capacity - FileMount.this.m_usedSpace;
if( newBytes > bytesLeft )
{
throw new IOException( "Out of space" );
}
FileMount.this.m_usedSpace += newBytes;
}
}
@Override
public boolean isOpen()
{
return this.m_inner.isOpen();
}
@Override
public void close() throws IOException
{
this.m_inner.close();
}
}
private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel
{
private final SeekableByteChannel m_inner;
SeekableCountingChannel( SeekableByteChannel inner, long bytesToIgnore )
{
super( inner, bytesToIgnore );
this.m_inner = inner;
}
@Override
public int read( ByteBuffer dst ) throws ClosedChannelException
{
if( !this.m_inner.isOpen() )
{
throw new ClosedChannelException();
}
throw new NonReadableChannelException();
}
@Override
public long position() throws IOException
{
return this.m_inner.position();
}
@Override
public SeekableByteChannel position( long newPosition ) throws IOException
{
if( !this.isOpen() )
{
throw new ClosedChannelException();
}
if( newPosition < 0 )
{
throw new IllegalArgumentException( "Cannot seek before the beginning of the stream" );
}
long delta = newPosition - this.m_inner.position();
if( delta < 0 )
{
this.m_ignoredBytesLeft -= delta;
}
else
{
this.count( delta );
}
return this.m_inner.position( newPosition );
}
@Override
public long size() throws IOException
{
return this.m_inner.size();
}
@Override
public SeekableByteChannel truncate( long size ) throws IOException
{
throw new IOException( "Not yet implemented" );
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
public class FileSystemException extends Exception

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.IFileSystem;
@@ -18,11 +17,11 @@ import java.util.function.Function;
public class FileSystemWrapperMount implements IFileSystem
{
private final FileSystem m_filesystem;
private final FileSystem filesystem;
public FileSystemWrapperMount( FileSystem filesystem )
{
this.m_filesystem = filesystem;
this.filesystem = filesystem;
}
@Override
@@ -30,7 +29,7 @@ public class FileSystemWrapperMount implements IFileSystem
{
try
{
this.m_filesystem.makeDir( path );
filesystem.makeDir( path );
}
catch( FileSystemException e )
{
@@ -43,63 +42,7 @@ public class FileSystemWrapperMount implements IFileSystem
{
try
{
this.m_filesystem.delete( path );
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Nonnull
@Override
public WritableByteChannel openForWrite( @Nonnull String path ) throws IOException
{
try
{
return this.m_filesystem.openForWrite( path, false, Function.identity() )
.get();
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Nonnull
@Override
public WritableByteChannel openForAppend( @Nonnull String path ) throws IOException
{
try
{
return this.m_filesystem.openForWrite( path, true, Function.identity() )
.get();
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Override
public long getRemainingSpace() throws IOException
{
try
{
return this.m_filesystem.getFreeSpace( "/" );
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
try
{
Collections.addAll( contents, this.m_filesystem.list( path ) );
filesystem.delete( path );
}
catch( FileSystemException e )
{
@@ -114,8 +57,48 @@ public class FileSystemWrapperMount implements IFileSystem
try
{
// FIXME: Think of a better way of implementing this, so closing this will close on the computer.
return this.m_filesystem.openForRead( path, Function.identity() )
.get();
return filesystem.openForRead( path, Function.identity() ).get();
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Nonnull
@Override
public WritableByteChannel openForWrite( @Nonnull String path ) throws IOException
{
try
{
return filesystem.openForWrite( path, false, Function.identity() ).get();
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Nonnull
@Override
public WritableByteChannel openForAppend( @Nonnull String path ) throws IOException
{
try
{
return filesystem.openForWrite( path, true, Function.identity() ).get();
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Override
public long getRemainingSpace() throws IOException
{
try
{
return filesystem.getFreeSpace( "/" );
}
catch( FileSystemException e )
{
@@ -128,7 +111,7 @@ public class FileSystemWrapperMount implements IFileSystem
{
try
{
return this.m_filesystem.exists( path );
return filesystem.exists( path );
}
catch( FileSystemException e )
{
@@ -141,7 +124,20 @@ public class FileSystemWrapperMount implements IFileSystem
{
try
{
return this.m_filesystem.isDir( path );
return filesystem.isDir( path );
}
catch( FileSystemException e )
{
throw new IOException( e.getMessage() );
}
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
try
{
Collections.addAll( contents, filesystem.list( path ) );
}
catch( FileSystemException e )
{
@@ -154,7 +150,7 @@ public class FileSystemWrapperMount implements IFileSystem
{
try
{
return this.m_filesystem.getSize( path );
return filesystem.getSize( path );
}
catch( FileSystemException e )
{
@@ -165,7 +161,7 @@ public class FileSystemWrapperMount implements IFileSystem
@Override
public String combine( String path, String child )
{
return this.m_filesystem.combine( path, child );
return filesystem.combine( path, child );
}
@Override
@@ -173,7 +169,7 @@ public class FileSystemWrapperMount implements IFileSystem
{
try
{
this.m_filesystem.copy( from, to );
filesystem.copy( from, to );
}
catch( FileSystemException e )
{
@@ -186,7 +182,7 @@ public class FileSystemWrapperMount implements IFileSystem
{
try
{
this.m_filesystem.move( from, to );
filesystem.move( from, to );
}
catch( FileSystemException e )
{

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import com.google.common.cache.Cache;
@@ -48,18 +47,20 @@ public class JarMount implements IMount
private static final int MAX_CACHE_SIZE = 64 << 20;
/**
* We maintain a cache of the contents of all files in the mount. This allows us to allow seeking within ROM files, and reduces the amount we need to
* access disk for computer startup.
* We maintain a cache of the contents of all files in the mount. This allows us to allow
* seeking within ROM files, and reduces the amount we need to access disk for computer startup.
*/
private static final Cache<FileEntry, byte[]> CONTENTS_CACHE = CacheBuilder.newBuilder()
.concurrencyLevel( 4 )
.expireAfterAccess( 60, TimeUnit.SECONDS )
.maximumWeight( MAX_CACHE_SIZE )
.weakKeys().<FileEntry, byte[]>weigher( ( k, v ) -> v.length ).build();
.weakKeys()
.<FileEntry, byte[]>weigher( ( k, v ) -> v.length )
.build();
/**
* We have a {@link ReferenceQueue} of all mounts, a long with their corresponding {@link ZipFile}. If the mount has been destroyed, we clean up after
* it.
* We have a {@link ReferenceQueue} of all mounts, a long with their corresponding {@link ZipFile}. If
* the mount has been destroyed, we clean up after it.
*/
private static final ReferenceQueue<JarMount> MOUNT_QUEUE = new ReferenceQueue<>();
@@ -71,15 +72,12 @@ public class JarMount implements IMount
// Cleanup any old mounts. It's unlikely that there will be any, but it's best to be safe.
cleanup();
if( !jarFile.exists() || jarFile.isDirectory() )
{
throw new FileNotFoundException( "Cannot find " + jarFile );
}
if( !jarFile.exists() || jarFile.isDirectory() ) throw new FileNotFoundException( "Cannot find " + jarFile );
// Open the zip file
try
{
this.zip = new ZipFile( jarFile );
zip = new ZipFile( jarFile );
}
catch( IOException e )
{
@@ -87,9 +85,9 @@ public class JarMount implements IMount
}
// Ensure the root entry exists.
if( this.zip.getEntry( subPath ) == null )
if( zip.getEntry( subPath ) == null )
{
this.zip.close();
zip.close();
throw new FileNotFoundException( "Zip does not contain path" );
}
@@ -97,50 +95,49 @@ public class JarMount implements IMount
new MountReference( this );
// Read in all the entries
this.root = new FileEntry();
Enumeration<? extends ZipEntry> zipEntries = this.zip.entries();
root = new FileEntry();
Enumeration<? extends ZipEntry> zipEntries = zip.entries();
while( zipEntries.hasMoreElements() )
{
ZipEntry entry = zipEntries.nextElement();
String entryPath = entry.getName();
if( !entryPath.startsWith( subPath ) )
{
continue;
}
if( !entryPath.startsWith( subPath ) ) continue;
String localPath = FileSystem.toLocal( entryPath, subPath );
this.create( entry, localPath );
create( entry, localPath );
}
}
private static void cleanup()
private FileEntry get( String path )
{
Reference<? extends JarMount> next;
while( (next = MOUNT_QUEUE.poll()) != null )
FileEntry lastEntry = root;
int lastIndex = 0;
while( lastEntry != null && lastIndex < path.length() )
{
IoUtil.closeQuietly( ((MountReference) next).file );
int nextIndex = path.indexOf( '/', lastIndex );
if( nextIndex < 0 ) nextIndex = path.length();
lastEntry = lastEntry.children == null ? null : lastEntry.children.get( path.substring( lastIndex, nextIndex ) );
lastIndex = nextIndex + 1;
}
return lastEntry;
}
private void create( ZipEntry entry, String localPath )
{
FileEntry lastEntry = this.root;
FileEntry lastEntry = root;
int lastIndex = 0;
while( lastIndex < localPath.length() )
{
int nextIndex = localPath.indexOf( '/', lastIndex );
if( nextIndex < 0 )
{
nextIndex = localPath.length();
}
if( nextIndex < 0 ) nextIndex = localPath.length();
String part = localPath.substring( lastIndex, nextIndex );
if( lastEntry.children == null )
{
lastEntry.children = new HashMap<>( 0 );
}
if( lastEntry.children == null ) lastEntry.children = new HashMap<>( 0 );
FileEntry nextEntry = lastEntry.children.get( part );
if( nextEntry == null || !nextEntry.isDirectory() )
@@ -155,42 +152,54 @@ public class JarMount implements IMount
lastEntry.setup( entry );
}
@Override
public boolean exists( @Nonnull String path )
{
return get( path ) != null;
}
@Override
public boolean isDirectory( @Nonnull String path )
{
FileEntry file = get( path );
return file != null && file.isDirectory();
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
FileEntry file = this.get( path );
if( file == null || !file.isDirectory() )
{
throw new FileOperationException( path, "Not a directory" );
}
FileEntry file = get( path );
if( file == null || !file.isDirectory() ) throw new FileOperationException( path, "Not a directory" );
file.list( contents );
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
FileEntry file = get( path );
if( file != null ) return file.size;
throw new FileOperationException( path, "No such file" );
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
FileEntry file = this.get( path );
FileEntry file = get( path );
if( file != null && !file.isDirectory() )
{
byte[] contents = CONTENTS_CACHE.getIfPresent( file );
if( contents != null )
{
return new ArrayByteChannel( contents );
}
if( contents != null ) return new ArrayByteChannel( contents );
try
{
ZipEntry entry = this.zip.getEntry( file.path );
ZipEntry entry = zip.getEntry( file.path );
if( entry != null )
{
try( InputStream stream = this.zip.getInputStream( entry ) )
try( InputStream stream = zip.getInputStream( entry ) )
{
if( stream.available() > MAX_CACHED_SIZE )
{
return Channels.newChannel( stream );
}
if( stream.available() > MAX_CACHED_SIZE ) return Channels.newChannel( stream );
contents = ByteStreams.toByteArray( stream );
CONTENTS_CACHE.put( file, contents );
@@ -211,63 +220,16 @@ public class JarMount implements IMount
@Override
public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException
{
FileEntry file = this.get( path );
FileEntry file = get( path );
if( file != null )
{
ZipEntry entry = this.zip.getEntry( file.path );
if( entry != null )
{
return new ZipEntryAttributes( entry );
}
ZipEntry entry = zip.getEntry( file.path );
if( entry != null ) return new ZipEntryAttributes( entry );
}
throw new FileOperationException( path, "No such file" );
}
@Override
public boolean exists( @Nonnull String path )
{
return this.get( path ) != null;
}
private FileEntry get( String path )
{
FileEntry lastEntry = this.root;
int lastIndex = 0;
while( lastEntry != null && lastIndex < path.length() )
{
int nextIndex = path.indexOf( '/', lastIndex );
if( nextIndex < 0 )
{
nextIndex = path.length();
}
lastEntry = lastEntry.children == null ? null : lastEntry.children.get( path.substring( lastIndex, nextIndex ) );
lastIndex = nextIndex + 1;
}
return lastEntry;
}
@Override
public boolean isDirectory( @Nonnull String path )
{
FileEntry file = this.get( path );
return file != null && file.isDirectory();
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
FileEntry file = this.get( path );
if( file != null )
{
return file.size;
}
throw new FileOperationException( path, "No such file" );
}
private static class FileEntry
{
String path;
@@ -276,25 +238,19 @@ public class JarMount implements IMount
void setup( ZipEntry entry )
{
this.path = entry.getName();
this.size = entry.getSize();
if( this.children == null && entry.isDirectory() )
{
this.children = new HashMap<>( 0 );
}
path = entry.getName();
size = entry.getSize();
if( children == null && entry.isDirectory() ) children = new HashMap<>( 0 );
}
boolean isDirectory()
{
return this.children != null;
return children != null;
}
void list( List<String> contents )
{
if( this.children != null )
{
contents.addAll( this.children.keySet() );
}
if( children != null ) contents.addAll( children.keySet() );
}
}
@@ -309,9 +265,14 @@ public class JarMount implements IMount
}
}
private static void cleanup()
{
Reference<? extends JarMount> next;
while( (next = MOUNT_QUEUE.poll()) != null ) IoUtil.closeQuietly( ((MountReference) next).file );
}
private static class ZipEntryAttributes implements BasicFileAttributes
{
private static final FileTime EPOCH = FileTime.from( Instant.EPOCH );
private final ZipEntry entry;
ZipEntryAttributes( ZipEntry entry )
@@ -322,32 +283,32 @@ public class JarMount implements IMount
@Override
public FileTime lastModifiedTime()
{
return orEpoch( this.entry.getLastModifiedTime() );
return orEpoch( entry.getLastModifiedTime() );
}
@Override
public FileTime lastAccessTime()
{
return orEpoch( this.entry.getLastAccessTime() );
return orEpoch( entry.getLastAccessTime() );
}
@Override
public FileTime creationTime()
{
FileTime time = this.entry.getCreationTime();
return time == null ? this.lastModifiedTime() : time;
FileTime time = entry.getCreationTime();
return time == null ? lastModifiedTime() : time;
}
@Override
public boolean isRegularFile()
{
return !this.entry.isDirectory();
return !entry.isDirectory();
}
@Override
public boolean isDirectory()
{
return this.entry.isDirectory();
return entry.isDirectory();
}
@Override
@@ -365,7 +326,7 @@ public class JarMount implements IMount
@Override
public long size()
{
return this.entry.getSize();
return entry.getSize();
}
@Override
@@ -374,6 +335,7 @@ public class JarMount implements IMount
return null;
}
private static final FileTime EPOCH = FileTime.from( Instant.EPOCH );
private static FileTime orEpoch( FileTime time )
{

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.FileOperationException;
@@ -33,7 +32,7 @@ class MountWrapper
this.label = label;
this.location = location;
this.mount = mount;
this.writableMount = null;
writableMount = null;
}
MountWrapper( String label, String location, IWritableMount mount )
@@ -41,29 +40,26 @@ class MountWrapper
this.label = label;
this.location = location;
this.mount = mount;
this.writableMount = mount;
writableMount = mount;
}
public String getLabel()
{
return this.label;
return label;
}
public String getLocation()
{
return this.location;
return location;
}
public long getFreeSpace()
{
if( this.writableMount == null )
{
return 0;
}
if( writableMount == null ) return 0;
try
{
return this.writableMount.getRemainingSpace();
return writableMount.getRemainingSpace();
}
catch( IOException e )
{
@@ -73,50 +69,242 @@ class MountWrapper
public OptionalLong getCapacity()
{
return this.writableMount == null ? OptionalLong.empty() : this.writableMount.getCapacity();
return writableMount == null ? OptionalLong.empty() : writableMount.getCapacity();
}
public boolean isReadOnly( String path )
{
return this.writableMount == null;
return writableMount == null;
}
public boolean exists( String path ) throws FileSystemException
{
path = this.toLocal( path );
path = toLocal( path );
try
{
return this.mount.exists( path );
return mount.exists( path );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
throw localExceptionOf( path, e );
}
}
public boolean isDirectory( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
return mount.exists( path ) && mount.isDirectory( path );
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
public void list( String path, List<String> contents ) throws FileSystemException
{
path = toLocal( path );
try
{
if( !mount.exists( path ) || !mount.isDirectory( path ) )
{
throw localExceptionOf( path, "Not a directory" );
}
mount.list( path, contents );
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
public long getSize( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
if( !mount.exists( path ) ) throw localExceptionOf( path, "No such file" );
return mount.isDirectory( path ) ? 0 : mount.getSize( path );
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
@Nonnull
public BasicFileAttributes getAttributes( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
if( !mount.exists( path ) ) throw localExceptionOf( path, "No such file" );
return mount.getAttributes( path );
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
public ReadableByteChannel openForRead( String path ) throws FileSystemException
{
path = toLocal( path );
try
{
if( mount.exists( path ) && !mount.isDirectory( path ) )
{
return mount.openForRead( path );
}
else
{
throw localExceptionOf( path, "No such file" );
}
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
public void makeDirectory( String path ) throws FileSystemException
{
if( writableMount == null ) throw exceptionOf( path, "Access denied" );
path = toLocal( path );
try
{
if( mount.exists( path ) )
{
if( !mount.isDirectory( path ) ) throw localExceptionOf( path, "File exists" );
}
else
{
writableMount.makeDirectory( path );
}
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
public void delete( String path ) throws FileSystemException
{
if( writableMount == null ) throw exceptionOf( path, "Access denied" );
path = toLocal( path );
try
{
if( mount.exists( path ) )
{
writableMount.delete( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
public WritableByteChannel openForWrite( String path ) throws FileSystemException
{
if( writableMount == null ) throw exceptionOf( path, "Access denied" );
path = toLocal( path );
try
{
if( mount.exists( path ) && mount.isDirectory( path ) )
{
throw localExceptionOf( path, "Cannot write to directory" );
}
else
{
if( !path.isEmpty() )
{
String dir = FileSystem.getDirectory( path );
if( !dir.isEmpty() && !mount.exists( path ) )
{
writableMount.makeDirectory( dir );
}
}
return writableMount.openForWrite( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
public WritableByteChannel openForAppend( String path ) throws FileSystemException
{
if( writableMount == null ) throw exceptionOf( path, "Access denied" );
path = toLocal( path );
try
{
if( !mount.exists( path ) )
{
if( !path.isEmpty() )
{
String dir = FileSystem.getDirectory( path );
if( !dir.isEmpty() && !mount.exists( path ) )
{
writableMount.makeDirectory( dir );
}
}
return writableMount.openForWrite( path );
}
else if( mount.isDirectory( path ) )
{
throw localExceptionOf( path, "Cannot write to directory" );
}
else
{
return writableMount.openForAppend( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw localExceptionOf( path, e );
}
}
private String toLocal( String path )
{
return FileSystem.toLocal( path, this.location );
return FileSystem.toLocal( path, location );
}
private FileSystemException localExceptionOf( @Nullable String localPath, @Nonnull IOException e )
{
if( !this.location.isEmpty() && e instanceof FileOperationException )
if( !location.isEmpty() && e instanceof FileOperationException )
{
FileOperationException ex = (FileOperationException) e;
if( ex.getFilename() != null )
{
return this.localExceptionOf( ex.getFilename(), ex.getMessage() );
}
if( ex.getFilename() != null ) return localExceptionOf( ex.getFilename(), ex.getMessage() );
}
if( e instanceof java.nio.file.FileSystemException )
{
// This error will contain the absolute path, leaking information about where MC is installed. We drop that,
// just taking the reason. We assume that the error refers to the input path.
String message = ((java.nio.file.FileSystemException) e).getReason()
.trim();
return localPath == null ? new FileSystemException( message ) : this.localExceptionOf( localPath, message );
String message = ((java.nio.file.FileSystemException) e).getReason().trim();
return localPath == null ? new FileSystemException( message ) : localExceptionOf( localPath, message );
}
return new FileSystemException( e.getMessage() );
@@ -124,10 +312,7 @@ class MountWrapper
private FileSystemException localExceptionOf( String path, String message )
{
if( !this.location.isEmpty() )
{
path = path.isEmpty() ? this.location : this.location + "/" + path;
}
if( !location.isEmpty() ) path = path.isEmpty() ? location : location + "/" + path;
return exceptionOf( path, message );
}
@@ -135,221 +320,4 @@ class MountWrapper
{
return new FileSystemException( "/" + path + ": " + message );
}
public boolean isDirectory( String path ) throws FileSystemException
{
path = this.toLocal( path );
try
{
return this.mount.exists( path ) && this.mount.isDirectory( path );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
public void list( String path, List<String> contents ) throws FileSystemException
{
path = this.toLocal( path );
try
{
if( !this.mount.exists( path ) || !this.mount.isDirectory( path ) )
{
throw this.localExceptionOf( path, "Not a directory" );
}
this.mount.list( path, contents );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
public long getSize( String path ) throws FileSystemException
{
path = this.toLocal( path );
try
{
if( !this.mount.exists( path ) )
{
throw this.localExceptionOf( path, "No such file" );
}
return this.mount.isDirectory( path ) ? 0 : this.mount.getSize( path );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
@Nonnull
public BasicFileAttributes getAttributes( String path ) throws FileSystemException
{
path = this.toLocal( path );
try
{
if( !this.mount.exists( path ) )
{
throw this.localExceptionOf( path, "No such file" );
}
return this.mount.getAttributes( path );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
public ReadableByteChannel openForRead( String path ) throws FileSystemException
{
path = this.toLocal( path );
try
{
if( this.mount.exists( path ) && !this.mount.isDirectory( path ) )
{
return this.mount.openForRead( path );
}
else
{
throw this.localExceptionOf( path, "No such file" );
}
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
public void makeDirectory( String path ) throws FileSystemException
{
if( this.writableMount == null )
{
throw exceptionOf( path, "Access denied" );
}
path = this.toLocal( path );
try
{
if( this.mount.exists( path ) )
{
if( !this.mount.isDirectory( path ) )
{
throw this.localExceptionOf( path, "File exists" );
}
}
else
{
this.writableMount.makeDirectory( path );
}
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
public void delete( String path ) throws FileSystemException
{
if( this.writableMount == null )
{
throw exceptionOf( path, "Access denied" );
}
path = this.toLocal( path );
try
{
if( this.mount.exists( path ) )
{
this.writableMount.delete( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
public WritableByteChannel openForWrite( String path ) throws FileSystemException
{
if( this.writableMount == null )
{
throw exceptionOf( path, "Access denied" );
}
path = this.toLocal( path );
try
{
if( this.mount.exists( path ) && this.mount.isDirectory( path ) )
{
throw this.localExceptionOf( path, "Cannot write to directory" );
}
else
{
if( !path.isEmpty() )
{
String dir = FileSystem.getDirectory( path );
if( !dir.isEmpty() && !this.mount.exists( path ) )
{
this.writableMount.makeDirectory( dir );
}
}
return this.writableMount.openForWrite( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
public WritableByteChannel openForAppend( String path ) throws FileSystemException
{
if( this.writableMount == null )
{
throw exceptionOf( path, "Access denied" );
}
path = this.toLocal( path );
try
{
if( !this.mount.exists( path ) )
{
if( !path.isEmpty() )
{
String dir = FileSystem.getDirectory( path );
if( !dir.isEmpty() && !this.mount.exists( path ) )
{
this.writableMount.makeDirectory( dir );
}
}
return this.writableMount.openForWrite( path );
}
else if( this.mount.isDirectory( path ) )
{
throw this.localExceptionOf( path, "Cannot write to directory" );
}
else
{
return this.writableMount.openForAppend( path );
}
}
catch( AccessDeniedException e )
{
throw new FileSystemException( "Access denied" );
}
catch( IOException e )
{
throw this.localExceptionOf( path, e );
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import com.google.common.cache.Cache;
@@ -49,17 +48,18 @@ public final class ResourceMount implements IMount
private static final byte[] TEMP_BUFFER = new byte[8192];
/**
* We maintain a cache of the contents of all files in the mount. This allows us to allow seeking within ROM files, and reduces the amount we need to
* access disk for computer startup.
* We maintain a cache of the contents of all files in the mount. This allows us to allow
* seeking within ROM files, and reduces the amount we need to access disk for computer startup.
*/
private static final Cache<FileEntry, byte[]> CONTENTS_CACHE = CacheBuilder.newBuilder()
.concurrencyLevel( 4 )
.expireAfterAccess( 60, TimeUnit.SECONDS )
.maximumWeight( MAX_CACHE_SIZE )
.weakKeys().<FileEntry, byte[]>weigher( ( k, v ) -> v.length ).build();
.weakKeys()
.<FileEntry, byte[]>weigher( ( k, v ) -> v.length )
.build();
private static final MapMaker CACHE_TEMPLATE = new MapMaker().weakValues()
.concurrencyLevel( 1 );
private static final MapMaker CACHE_TEMPLATE = new MapMaker().weakValues().concurrencyLevel( 1 );
/**
* Maintain a cache of currently loaded resource mounts. This cache is invalidated when currentManager changes.
@@ -73,6 +73,25 @@ public final class ResourceMount implements IMount
@Nullable
private FileEntry root;
public static ResourceMount get( String namespace, String subPath, ReloadableResourceManager manager )
{
Map<Identifier, ResourceMount> cache;
synchronized( MOUNT_CACHE )
{
cache = MOUNT_CACHE.get( manager );
if( cache == null ) MOUNT_CACHE.put( manager, cache = CACHE_TEMPLATE.makeMap() );
}
Identifier path = new Identifier( namespace, subPath );
synchronized( cache )
{
ResourceMount mount = cache.get( path );
if( mount == null ) cache.put( path, mount = new ResourceMount( namespace, subPath, manager ) );
return mount;
}
}
private ResourceMount( String namespace, String subPath, ReloadableResourceManager manager )
{
this.namespace = namespace;
@@ -80,10 +99,7 @@ public final class ResourceMount implements IMount
this.manager = manager;
Listener.INSTANCE.add( manager, this );
if( this.root == null )
{
this.load();
}
if( root == null ) load();
}
private void load()
@@ -98,8 +114,8 @@ public final class ResourceMount implements IMount
if( !file.getNamespace().equals( namespace ) ) continue;
String localPath = FileSystem.toLocal( file.getPath(), this.subPath );
this.create( newRoot, localPath );
String localPath = FileSystem.toLocal( file.getPath(), subPath );
create( newRoot, localPath );
hasAny = true;
}
@@ -115,22 +131,33 @@ public final class ResourceMount implements IMount
}
}
private FileEntry get( String path )
{
FileEntry lastEntry = root;
int lastIndex = 0;
while( lastEntry != null && lastIndex < path.length() )
{
int nextIndex = path.indexOf( '/', lastIndex );
if( nextIndex < 0 ) nextIndex = path.length();
lastEntry = lastEntry.children == null ? null : lastEntry.children.get( path.substring( lastIndex, nextIndex ) );
lastIndex = nextIndex + 1;
}
return lastEntry;
}
private void create( FileEntry lastEntry, String path )
{
int lastIndex = 0;
while( lastIndex < path.length() )
{
int nextIndex = path.indexOf( '/', lastIndex );
if( nextIndex < 0 )
{
nextIndex = path.length();
}
if( nextIndex < 0 ) nextIndex = path.length();
String part = path.substring( lastIndex, nextIndex );
if( lastEntry.children == null )
{
lastEntry.children = new HashMap<>();
}
if( lastEntry.children == null ) lastEntry.children = new HashMap<>();
FileEntry nextEntry = lastEntry.children.get( part );
if( nextEntry == null )
@@ -138,7 +165,7 @@ public final class ResourceMount implements IMount
Identifier childPath;
try
{
childPath = new Identifier( this.namespace, this.subPath + "/" + path );
childPath = new Identifier( namespace, subPath + "/" + path );
}
catch( InvalidIdentifierException e )
{
@@ -153,137 +180,43 @@ public final class ResourceMount implements IMount
}
}
public static ResourceMount get( String namespace, String subPath, ReloadableResourceManager manager )
{
Map<Identifier, ResourceMount> cache;
synchronized( MOUNT_CACHE )
{
cache = MOUNT_CACHE.get( manager );
if( cache == null )
{
MOUNT_CACHE.put( manager, cache = CACHE_TEMPLATE.makeMap() );
}
}
Identifier path = new Identifier( namespace, subPath );
synchronized( cache )
{
ResourceMount mount = cache.get( path );
if( mount == null )
{
cache.put( path, mount = new ResourceMount( namespace, subPath, manager ) );
}
return mount;
}
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
FileEntry file = this.get( path );
if( file == null || !file.isDirectory() )
{
throw new IOException( "/" + path + ": Not a directory" );
}
file.list( contents );
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
FileEntry file = this.get( path );
if( file != null && !file.isDirectory() )
{
byte[] contents = CONTENTS_CACHE.getIfPresent( file );
if( contents != null )
{
return new ArrayByteChannel( contents );
}
try
{
InputStream stream = manager.getResource( file.identifier ).getInputStream();
if( stream.available() > MAX_CACHED_SIZE ) return Channels.newChannel( stream );
try
{
contents = ByteStreams.toByteArray( stream );
}
finally
{
IoUtil.closeQuietly( stream );
}
CONTENTS_CACHE.put( file, contents );
return new ArrayByteChannel( contents );
}
catch( FileNotFoundException ignored )
{
}
}
throw new IOException( "/" + path + ": No such file" );
}
@Override
public boolean exists( @Nonnull String path )
{
return this.get( path ) != null;
}
private FileEntry get( String path )
{
FileEntry lastEntry = this.root;
int lastIndex = 0;
while( lastEntry != null && lastIndex < path.length() )
{
int nextIndex = path.indexOf( '/', lastIndex );
if( nextIndex < 0 )
{
nextIndex = path.length();
}
lastEntry = lastEntry.children == null ? null : lastEntry.children.get( path.substring( lastIndex, nextIndex ) );
lastIndex = nextIndex + 1;
}
return lastEntry;
return get( path ) != null;
}
@Override
public boolean isDirectory( @Nonnull String path )
{
FileEntry file = this.get( path );
FileEntry file = get( path );
return file != null && file.isDirectory();
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
FileEntry file = get( path );
if( file == null || !file.isDirectory() ) throw new IOException( "/" + path + ": Not a directory" );
file.list( contents );
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
FileEntry file = this.get( path );
FileEntry file = get( path );
if( file != null )
{
if( file.size != -1 )
{
return file.size;
}
if( file.isDirectory() )
{
return file.size = 0;
}
if( file.size != -1 ) return file.size;
if( file.isDirectory() ) return file.size = 0;
byte[] contents = CONTENTS_CACHE.getIfPresent( file );
if( contents != null )
{
return file.size = contents.length;
}
if( contents != null ) return file.size = contents.length;
try
{
Resource resource = this.manager.getResource( file.identifier );
Resource resource = manager.getResource( file.identifier );
InputStream s = resource.getInputStream();
int total = 0, read = 0;
do
@@ -303,6 +236,41 @@ public final class ResourceMount implements IMount
throw new IOException( "/" + path + ": No such file" );
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
FileEntry file = get( path );
if( file != null && !file.isDirectory() )
{
byte[] contents = CONTENTS_CACHE.getIfPresent( file );
if( contents != null ) return new ArrayByteChannel( contents );
try
{
InputStream stream = manager.getResource( file.identifier ).getInputStream();
if( stream.available() > MAX_CACHED_SIZE ) return Channels.newChannel( stream );
try
{
contents = ByteStreams.toByteArray( stream );
}
finally
{
IoUtil.closeQuietly( stream );
}
CONTENTS_CACHE.put( file, contents );
return new ArrayByteChannel( contents );
}
catch( FileNotFoundException ignored )
{
}
}
throw new IOException( "/" + path + ": No such file" );
}
private static class FileEntry
{
final Identifier identifier;
@@ -316,21 +284,20 @@ public final class ResourceMount implements IMount
boolean isDirectory()
{
return this.children != null;
return children != null;
}
void list( List<String> contents )
{
if( this.children != null )
{
contents.addAll( this.children.keySet() );
}
if( children != null ) contents.addAll( children.keySet() );
}
}
/**
* While people should really be keeping a permanent reference to this, some people construct it every method call, so let's make this as small as
* possible.
* A {@link ResourceReloadListener} which reloads any associated mounts.
*
* While people should really be keeping a permanent reference to this, some people construct it every
* method call, so let's make this as small as possible.
*/
static class Listener implements ResourceReloadListener
{
@@ -340,32 +307,25 @@ public final class ResourceMount implements IMount
private final Set<ReloadableResourceManager> managers = Collections.newSetFromMap( new WeakHashMap<>() );
@Override
public CompletableFuture<Void> reload( Synchronizer synchronizer, ResourceManager resourceManager, Profiler profiler, Profiler profiler1,
Executor executor, Executor executor1 )
public CompletableFuture<Void> reload( Synchronizer synchronizer, ResourceManager manager, Profiler prepareProfiler, Profiler applyProfiler, Executor prepareExecutor, Executor applyExecutor )
{
return CompletableFuture.runAsync( () -> {
profiler.push( "Mount reloading" );
prepareProfiler.push( "Mount reloading" );
try
{
for( ResourceMount mount : this.mounts )
{
mount.load();
}
for( ResourceMount mount : this.mounts ) mount.load();
}
finally
{
profiler.pop();
prepareProfiler.pop();
}
}, executor );
}, prepareExecutor );
}
synchronized void add( ReloadableResourceManager manager, ResourceMount mount )
{
if( this.managers.add( manager ) )
{
manager.registerListener( this );
}
this.mounts.add( mount );
if( managers.add( manager ) ) manager.registerListener( this );
mounts.add( mount );
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.filesystem;
import dan200.computercraft.api.filesystem.IMount;
@@ -16,8 +15,8 @@ import java.util.List;
public class SubMount implements IMount
{
private IMount parent;
private String subPath;
private final IMount parent;
private final String subPath;
public SubMount( IMount parent, String subPath )
{
@@ -25,46 +24,46 @@ public class SubMount implements IMount
this.subPath = subPath;
}
@Override
public boolean exists( @Nonnull String path ) throws IOException
{
return parent.exists( getFullPath( path ) );
}
@Override
public boolean isDirectory( @Nonnull String path ) throws IOException
{
return parent.isDirectory( getFullPath( path ) );
}
@Override
public void list( @Nonnull String path, @Nonnull List<String> contents ) throws IOException
{
this.parent.list( this.getFullPath( path ), contents );
parent.list( getFullPath( path ), contents );
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
return parent.getSize( getFullPath( path ) );
}
@Nonnull
@Override
public ReadableByteChannel openForRead( @Nonnull String path ) throws IOException
{
return this.parent.openForRead( this.getFullPath( path ) );
return parent.openForRead( getFullPath( path ) );
}
@Nonnull
@Override
public BasicFileAttributes getAttributes( @Nonnull String path ) throws IOException
{
return this.parent.getAttributes( this.getFullPath( path ) );
}
@Override
public boolean exists( @Nonnull String path ) throws IOException
{
return this.parent.exists( this.getFullPath( path ) );
}
@Override
public boolean isDirectory( @Nonnull String path ) throws IOException
{
return this.parent.isDirectory( this.getFullPath( path ) );
}
@Override
public long getSize( @Nonnull String path ) throws IOException
{
return this.parent.getSize( this.getFullPath( path ) );
return parent.getAttributes( getFullPath( path ) );
}
private String getFullPath( String path )
{
return path.isEmpty() ? this.subPath : this.subPath + "/" + path;
return path.isEmpty() ? subPath : subPath + "/" + path;
}
}

View File

@@ -45,7 +45,7 @@ class BasicFunction extends VarArgFunction
MethodResult results;
try
{
results = this.method.apply( this.instance, this.context, arguments );
results = method.apply( instance, context, arguments );
}
catch( LuaException e )
{
@@ -55,7 +55,7 @@ class BasicFunction extends VarArgFunction
{
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.error( "Error calling " + this.name + " on " + this.instance, t );
ComputerCraft.log.error( "Error calling " + name + " on " + instance, t );
}
throw new LuaError( "Java Exception Thrown: " + t, 0 );
}
@@ -64,7 +64,7 @@ class BasicFunction extends VarArgFunction
{
throw new IllegalStateException( "Cannot have a yielding non-yielding function" );
}
return this.machine.toValues( results.getResult() );
return machine.toValues( results.getResult() );
}
public static LuaError wrap( LuaException exception )

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.lua;
import dan200.computercraft.ComputerCraft;
@@ -11,7 +10,6 @@ import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.MainThread;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingField;
@@ -42,38 +40,39 @@ import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKYIELD;
public class CobaltLuaMachine implements ILuaMachine
{
private static final ThreadPoolExecutor COROUTINES = new ThreadPoolExecutor( 0,
Integer.MAX_VALUE,
5L,
TimeUnit.MINUTES,
private static final ThreadPoolExecutor COROUTINES = new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
5L, TimeUnit.MINUTES,
new SynchronousQueue<>(),
ThreadUtils.factory( "Coroutine" ) );
ThreadUtils.factory( "Coroutine" )
);
private static final LuaMethod FUNCTION_METHOD = ( target, context, args ) -> ((ILuaFunction) target).call( args );
private final Computer m_computer;
private final Computer computer;
private final TimeoutState timeout;
private final TimeoutDebugHandler debug;
private final ILuaContext context = new CobaltLuaContext();
private final ILuaContext context;
private LuaState m_state;
private LuaTable m_globals;
private LuaState state;
private LuaTable globals;
private LuaThread m_mainRoutine = null;
private String m_eventFilter = null;
private LuaThread mainRoutine = null;
private String eventFilter = null;
public CobaltLuaMachine( Computer computer, TimeoutState timeout )
{
this.m_computer = computer;
this.computer = computer;
this.timeout = timeout;
this.debug = new TimeoutDebugHandler();
this.context = new LuaContext( computer );
debug = new TimeoutDebugHandler();
// Create an environment to run in
LuaState state = this.m_state = LuaState.builder()
LuaState state = this.state = LuaState.builder()
.resourceManipulator( new VoidResourceManipulator() )
.debug( this.debug )
.debug( debug )
.coroutineExecutor( command -> {
Tracking.addValue( this.m_computer, TrackingField.COROUTINES_CREATED, 1 );
Tracking.addValue( this.computer, TrackingField.COROUTINES_CREATED, 1 );
COROUTINES.execute( () -> {
try
{
@@ -81,47 +80,278 @@ public class CobaltLuaMachine implements ILuaMachine
}
finally
{
Tracking.addValue( this.m_computer, TrackingField.COROUTINES_DISPOSED, 1 );
Tracking.addValue( this.computer, TrackingField.COROUTINES_DISPOSED, 1 );
}
} );
} )
.build();
this.m_globals = new LuaTable();
state.setupThread( this.m_globals );
globals = new LuaTable();
state.setupThread( globals );
// Add basic libraries
this.m_globals.load( state, new BaseLib() );
this.m_globals.load( state, new TableLib() );
this.m_globals.load( state, new StringLib() );
this.m_globals.load( state, new MathLib() );
this.m_globals.load( state, new CoroutineLib() );
this.m_globals.load( state, new Bit32Lib() );
this.m_globals.load( state, new Utf8Lib() );
if( ComputerCraft.debugEnable )
{
this.m_globals.load( state, new DebugLib() );
}
globals.load( state, new BaseLib() );
globals.load( state, new TableLib() );
globals.load( state, new StringLib() );
globals.load( state, new MathLib() );
globals.load( state, new CoroutineLib() );
globals.load( state, new Bit32Lib() );
globals.load( state, new Utf8Lib() );
if( ComputerCraft.debugEnable ) globals.load( state, new DebugLib() );
// Remove globals we don't want to expose
this.m_globals.rawset( "collectgarbage", Constants.NIL );
this.m_globals.rawset( "dofile", Constants.NIL );
this.m_globals.rawset( "loadfile", Constants.NIL );
this.m_globals.rawset( "print", Constants.NIL );
globals.rawset( "collectgarbage", Constants.NIL );
globals.rawset( "dofile", Constants.NIL );
globals.rawset( "loadfile", Constants.NIL );
globals.rawset( "print", Constants.NIL );
// Add version globals
this.m_globals.rawset( "_VERSION", valueOf( "Lua 5.1" ) );
this.m_globals.rawset( "_HOST",
valueOf( computer.getAPIEnvironment()
.getComputerEnvironment()
.getHostString() ) );
this.m_globals.rawset( "_CC_DEFAULT_SETTINGS", valueOf( ComputerCraft.defaultComputerSettings ) );
globals.rawset( "_VERSION", valueOf( "Lua 5.1" ) );
globals.rawset( "_HOST", valueOf( computer.getAPIEnvironment().getComputerEnvironment().getHostString() ) );
globals.rawset( "_CC_DEFAULT_SETTINGS", valueOf( ComputerCraft.defaultComputerSettings ) );
if( ComputerCraft.disableLua51Features )
{
this.m_globals.rawset( "_CC_DISABLE_LUA51_FEATURES", Constants.TRUE );
globals.rawset( "_CC_DISABLE_LUA51_FEATURES", Constants.TRUE );
}
}
@Override
public void addAPI( @Nonnull ILuaAPI api )
{
// Add the methods of an API to the global table
LuaTable table = wrapLuaObject( api );
if( table == null )
{
ComputerCraft.log.warn( "API {} does not provide any methods", api );
table = new LuaTable();
}
String[] names = api.getNames();
for( String name : names ) globals.rawset( name, table );
}
@Override
public MachineResult loadBios( @Nonnull InputStream bios )
{
// Begin executing a file (ie, the bios)
if( mainRoutine != null ) return MachineResult.OK;
try
{
LuaFunction value = LoadState.load( state, bios, "@bios.lua", globals );
mainRoutine = new LuaThread( state, value, globals );
return MachineResult.OK;
}
catch( CompileException e )
{
close();
return MachineResult.error( e );
}
catch( Exception e )
{
ComputerCraft.log.warn( "Could not load bios.lua", e );
close();
return MachineResult.GENERIC_ERROR;
}
}
@Override
public MachineResult handleEvent( String eventName, Object[] arguments )
{
if( mainRoutine == null ) return MachineResult.OK;
if( eventFilter != null && eventName != null && !eventName.equals( eventFilter ) && !eventName.equals( "terminate" ) )
{
return MachineResult.OK;
}
// If the soft abort has been cleared then we can reset our flag.
timeout.refresh();
if( !timeout.isSoftAborted() ) debug.thrownSoftAbort = false;
try
{
Varargs resumeArgs = Constants.NONE;
if( eventName != null )
{
resumeArgs = varargsOf( valueOf( eventName ), toValues( arguments ) );
}
// Resume the current thread, or the main one when first starting off.
LuaThread thread = state.getCurrentThread();
if( thread == null || thread == state.getMainThread() ) thread = mainRoutine;
Varargs results = LuaThread.run( thread, resumeArgs );
if( timeout.isHardAborted() ) throw HardAbortError.INSTANCE;
if( results == null ) return MachineResult.PAUSE;
LuaValue filter = results.first();
eventFilter = filter.isString() ? filter.toString() : null;
if( mainRoutine.getStatus().equals( "dead" ) )
{
close();
return MachineResult.GENERIC_ERROR;
}
else
{
return MachineResult.OK;
}
}
catch( HardAbortError | InterruptedException e )
{
close();
return MachineResult.TIMEOUT;
}
catch( LuaError e )
{
close();
ComputerCraft.log.warn( "Top level coroutine errored", e );
return MachineResult.error( e );
}
}
@Override
public void close()
{
LuaState state = this.state;
if( state == null ) return;
state.abandon();
mainRoutine = null;
this.state = null;
globals = null;
}
@Nullable
private LuaTable wrapLuaObject( Object object )
{
String[] dynamicMethods = object instanceof IDynamicLuaObject
? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(), "Methods cannot be null" )
: LuaMethod.EMPTY_METHODS;
LuaTable table = new LuaTable();
for( int i = 0; i < dynamicMethods.length; i++ )
{
String method = dynamicMethods[i];
table.rawset( method, new ResultInterpreterFunction( this, LuaMethod.DYNAMIC.get( i ), object, context, method ) );
}
ObjectSource.allMethods( LuaMethod.GENERATOR, object, ( instance, method ) ->
table.rawset( method.getName(), method.nonYielding()
? new BasicFunction( this, method.getMethod(), instance, context, method.getName() )
: new ResultInterpreterFunction( this, method.getMethod(), instance, context, method.getName() ) ) );
try
{
if( table.keyCount() == 0 ) return null;
}
catch( LuaError ignored )
{
}
return table;
}
@Nonnull
private LuaValue toValue( @Nullable Object object, @Nullable Map<Object, LuaValue> values )
{
if( object == null ) return Constants.NIL;
if( object instanceof Number ) return valueOf( ((Number) object).doubleValue() );
if( object instanceof Boolean ) return valueOf( (Boolean) object );
if( object instanceof String ) return valueOf( object.toString() );
if( object instanceof byte[] )
{
byte[] b = (byte[]) object;
return valueOf( Arrays.copyOf( b, b.length ) );
}
if( object instanceof ByteBuffer )
{
ByteBuffer b = (ByteBuffer) object;
byte[] bytes = new byte[b.remaining()];
b.get( bytes );
return valueOf( bytes );
}
if( values == null ) values = new IdentityHashMap<>( 1 );
LuaValue result = values.get( object );
if( result != null ) return result;
if( object instanceof ILuaFunction )
{
return new ResultInterpreterFunction( this, FUNCTION_METHOD, object, context, object.toString() );
}
if( object instanceof IDynamicLuaObject )
{
LuaValue wrapped = wrapLuaObject( object );
if( wrapped == null ) wrapped = new LuaTable();
values.put( object, wrapped );
return wrapped;
}
if( object instanceof Map )
{
LuaTable table = new LuaTable();
values.put( object, table );
for( Map.Entry<?, ?> pair : ((Map<?, ?>) object).entrySet() )
{
LuaValue key = toValue( pair.getKey(), values );
LuaValue value = toValue( pair.getValue(), values );
if( !key.isNil() && !value.isNil() ) table.rawset( key, value );
}
return table;
}
if( object instanceof Collection )
{
Collection<?> objects = (Collection<?>) object;
LuaTable table = new LuaTable( objects.size(), 0 );
values.put( object, table );
int i = 0;
for( Object child : objects ) table.rawset( ++i, toValue( child, values ) );
return table;
}
if( object instanceof Object[] )
{
Object[] objects = (Object[]) object;
LuaTable table = new LuaTable( objects.length, 0 );
values.put( object, table );
for( int i = 0; i < objects.length; i++ ) table.rawset( i + 1, toValue( objects[i], values ) );
return table;
}
LuaTable wrapped = wrapLuaObject( object );
if( wrapped != null )
{
values.put( object, wrapped );
return wrapped;
}
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.warn( "Received unknown type '{}', returning nil.", object.getClass().getName() );
}
return Constants.NIL;
}
Varargs toValues( Object[] objects )
{
if( objects == null || objects.length == 0 ) return Constants.NONE;
if( objects.length == 1 ) return toValue( objects[0], null );
Map<Object, LuaValue> result = new IdentityHashMap<>( 0 );
LuaValue[] values = new LuaValue[objects.length];
for( int i = 0; i < values.length; i++ )
{
Object object = objects[i];
values[i] = toValue( object, result );
}
return varargsOf( values );
}
static Object toObject( LuaValue value, Map<LuaValue, Object> objects )
{
switch( value.type() )
@@ -147,10 +377,7 @@ public class CobaltLuaMachine implements ILuaMachine
else
{
Object existing = objects.get( value );
if( existing != null )
{
return existing;
}
if( existing != null ) return existing;
}
Map<Object, Object> table = new HashMap<>();
objects.put( value, table );
@@ -171,10 +398,7 @@ public class CobaltLuaMachine implements ILuaMachine
break;
}
k = keyValue.first();
if( k.isNil() )
{
break;
}
if( k.isNil() ) break;
LuaValue v = keyValue.arg( 2 );
Object keyObject = toObject( k, objects );
@@ -195,10 +419,7 @@ public class CobaltLuaMachine implements ILuaMachine
{
int count = values.count();
Object[] objects = new Object[count];
for( int i = 0; i < count; i++ )
{
objects[i] = toObject( values.arg( i + 1 ), null );
}
for( int i = 0; i < count; i++ ) objects[i] = toObject( values.arg( i + 1 ), null );
return objects;
}
@@ -207,342 +428,22 @@ public class CobaltLuaMachine implements ILuaMachine
return values == Constants.NONE ? VarargArguments.EMPTY : new VarargArguments( values );
}
@Override
public void addAPI( @Nonnull ILuaAPI api )
{
// Add the methods of an API to the global table
LuaTable table = this.wrapLuaObject( api );
if( table == null )
{
ComputerCraft.log.warn( "API {} does not provide any methods", api );
table = new LuaTable();
}
String[] names = api.getNames();
for( String name : names )
{
this.m_globals.rawset( name, table );
}
}
@Override
public MachineResult loadBios( @Nonnull InputStream bios )
{
// Begin executing a file (ie, the bios)
if( this.m_mainRoutine != null )
{
return MachineResult.OK;
}
try
{
LuaFunction value = LoadState.load( this.m_state, bios, "@bios.lua", this.m_globals );
this.m_mainRoutine = new LuaThread( this.m_state, value, this.m_globals );
return MachineResult.OK;
}
catch( CompileException e )
{
this.close();
return MachineResult.error( e );
}
catch( Exception e )
{
ComputerCraft.log.warn( "Could not load bios.lua", e );
this.close();
return MachineResult.GENERIC_ERROR;
}
}
@Override
public MachineResult handleEvent( String eventName, Object[] arguments )
{
if( this.m_mainRoutine == null )
{
return MachineResult.OK;
}
if( this.m_eventFilter != null && eventName != null && !eventName.equals( this.m_eventFilter ) && !eventName.equals( "terminate" ) )
{
return MachineResult.OK;
}
// If the soft abort has been cleared then we can reset our flag.
this.timeout.refresh();
if( !this.timeout.isSoftAborted() )
{
this.debug.thrownSoftAbort = false;
}
try
{
Varargs resumeArgs = Constants.NONE;
if( eventName != null )
{
resumeArgs = varargsOf( valueOf( eventName ), this.toValues( arguments ) );
}
// Resume the current thread, or the main one when first starting off.
LuaThread thread = this.m_state.getCurrentThread();
if( thread == null || thread == this.m_state.getMainThread() )
{
thread = this.m_mainRoutine;
}
Varargs results = LuaThread.run( thread, resumeArgs );
if( this.timeout.isHardAborted() )
{
throw HardAbortError.INSTANCE;
}
if( results == null )
{
return MachineResult.PAUSE;
}
LuaValue filter = results.first();
this.m_eventFilter = filter.isString() ? filter.toString() : null;
if( this.m_mainRoutine.getStatus()
.equals( "dead" ) )
{
this.close();
return MachineResult.GENERIC_ERROR;
}
else
{
return MachineResult.OK;
}
}
catch( HardAbortError | InterruptedException e )
{
this.close();
return MachineResult.TIMEOUT;
}
catch( LuaError e )
{
this.close();
ComputerCraft.log.warn( "Top level coroutine errored", e );
return MachineResult.error( e );
}
}
@Override
public void close()
{
LuaState state = this.m_state;
if( state == null )
{
return;
}
state.abandon();
this.m_mainRoutine = null;
this.m_state = null;
this.m_globals = null;
}
@Nullable
private LuaTable wrapLuaObject( Object object )
{
String[] dynamicMethods = object instanceof IDynamicLuaObject ? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(),
"Methods cannot be null" ) : LuaMethod.EMPTY_METHODS;
LuaTable table = new LuaTable();
for( int i = 0; i < dynamicMethods.length; i++ )
{
String method = dynamicMethods[i];
table.rawset( method, new ResultInterpreterFunction( this, LuaMethod.DYNAMIC.get( i ), object, this.context, method ) );
}
ObjectSource.allMethods( LuaMethod.GENERATOR,
object,
( instance, method ) -> table.rawset( method.getName(),
method.nonYielding() ? new BasicFunction( this,
(LuaMethod) method.getMethod(),
instance, this.context,
method.getName() ) :
new ResultInterpreterFunction(
this,
(LuaMethod) method.getMethod(),
instance, this.context,
method.getName() ) ) );
try
{
if( table.keyCount() == 0 )
{
return null;
}
}
catch( LuaError ignored )
{
}
return table;
}
@Nonnull
private LuaValue toValue( @Nullable Object object, @Nullable Map<Object, LuaValue> values )
{
if( object == null )
{
return Constants.NIL;
}
if( object instanceof Number )
{
return valueOf( ((Number) object).doubleValue() );
}
if( object instanceof Boolean )
{
return valueOf( (Boolean) object );
}
if( object instanceof String )
{
return valueOf( object.toString() );
}
if( object instanceof byte[] )
{
byte[] b = (byte[]) object;
return valueOf( Arrays.copyOf( b, b.length ) );
}
if( object instanceof ByteBuffer )
{
ByteBuffer b = (ByteBuffer) object;
byte[] bytes = new byte[b.remaining()];
b.get( bytes );
return valueOf( bytes );
}
if( values == null )
{
values = new IdentityHashMap<>( 1 );
}
LuaValue result = values.get( object );
if( result != null )
{
return result;
}
if( object instanceof ILuaFunction )
{
return new ResultInterpreterFunction( this, FUNCTION_METHOD, object, this.context, object.toString() );
}
if( object instanceof IDynamicLuaObject )
{
LuaValue wrapped = this.wrapLuaObject( object );
if( wrapped == null )
{
wrapped = new LuaTable();
}
values.put( object, wrapped );
return wrapped;
}
if( object instanceof Map )
{
LuaTable table = new LuaTable();
values.put( object, table );
for( Map.Entry<?, ?> pair : ((Map<?, ?>) object).entrySet() )
{
LuaValue key = this.toValue( pair.getKey(), values );
LuaValue value = this.toValue( pair.getValue(), values );
if( !key.isNil() && !value.isNil() )
{
table.rawset( key, value );
}
}
return table;
}
if( object instanceof Collection )
{
Collection<?> objects = (Collection<?>) object;
LuaTable table = new LuaTable( objects.size(), 0 );
values.put( object, table );
int i = 0;
for( Object child : objects )
{
table.rawset( ++i, this.toValue( child, values ) );
}
return table;
}
if( object instanceof Object[] )
{
Object[] objects = (Object[]) object;
LuaTable table = new LuaTable( objects.length, 0 );
values.put( object, table );
for( int i = 0; i < objects.length; i++ )
{
table.rawset( i + 1, this.toValue( objects[i], values ) );
}
return table;
}
LuaTable wrapped = this.wrapLuaObject( object );
if( wrapped != null )
{
values.put( object, wrapped );
return wrapped;
}
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.warn( "Received unknown type '{}', returning nil.",
object.getClass()
.getName() );
}
return Constants.NIL;
}
Varargs toValues( Object[] objects )
{
if( objects == null || objects.length == 0 )
{
return Constants.NONE;
}
if( objects.length == 1 )
{
return this.toValue( objects[0], null );
}
Map<Object, LuaValue> result = new IdentityHashMap<>( 0 );
LuaValue[] values = new LuaValue[objects.length];
for( int i = 0; i < values.length; i++ )
{
Object object = objects[i];
values[i] = this.toValue( object, result );
}
return varargsOf( values );
}
private static final class HardAbortError extends Error
{
static final HardAbortError INSTANCE = new HardAbortError();
private static final long serialVersionUID = 7954092008586367501L;
private HardAbortError()
{
super( "Hard Abort", null, true, false );
}
}
/**
* A {@link DebugHandler} which observes the {@link TimeoutState} and responds accordingly.
*/
private class TimeoutDebugHandler extends DebugHandler
{
private final TimeoutState timeout;
boolean thrownSoftAbort;
private int count = 0;
boolean thrownSoftAbort;
private boolean isPaused;
private int oldFlags;
private boolean oldInHook;
TimeoutDebugHandler()
{
this.timeout = CobaltLuaMachine.this.timeout;
timeout = CobaltLuaMachine.this.timeout;
}
@Override
@@ -550,35 +451,29 @@ public class CobaltLuaMachine implements ILuaMachine
{
di.pc = pc;
if( this.isPaused )
{
this.resetPaused( ds, di );
}
if( isPaused ) resetPaused( ds, di );
// We check our current pause/abort state every 128 instructions.
if( (this.count = (this.count + 1) & 127) == 0 )
if( (count = (count + 1) & 127) == 0 )
{
// If we've been hard aborted or closed then abort.
if( this.timeout.isHardAborted() || CobaltLuaMachine.this.m_state == null )
{
throw HardAbortError.INSTANCE;
}
if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE;
this.timeout.refresh();
if( this.timeout.isPaused() )
timeout.refresh();
if( timeout.isPaused() )
{
// Preserve the current state
this.isPaused = true;
this.oldInHook = ds.inhook;
this.oldFlags = di.flags;
isPaused = true;
oldInHook = ds.inhook;
oldFlags = di.flags;
// Suspend the state. This will probably throw, but we need to handle the case where it won't.
di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED;
LuaThread.suspend( ds.getLuaState() );
this.resetPaused( ds, di );
resetPaused( ds, di );
}
this.handleSoftAbort();
handleSoftAbort();
}
super.onInstruction( ds, di, pc );
@@ -588,99 +483,41 @@ public class CobaltLuaMachine implements ILuaMachine
public void poll() throws LuaError
{
// If we've been hard aborted or closed then abort.
LuaState state = CobaltLuaMachine.this.m_state;
if( this.timeout.isHardAborted() || state == null )
{
throw HardAbortError.INSTANCE;
}
LuaState state = CobaltLuaMachine.this.state;
if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE;
this.timeout.refresh();
if( this.timeout.isPaused() )
{
LuaThread.suspendBlocking( state );
}
this.handleSoftAbort();
timeout.refresh();
if( timeout.isPaused() ) LuaThread.suspendBlocking( state );
handleSoftAbort();
}
private void resetPaused( DebugState ds, DebugFrame di )
{
// Restore the previous paused state
this.isPaused = false;
ds.inhook = this.oldInHook;
di.flags = this.oldFlags;
isPaused = false;
ds.inhook = oldInHook;
di.flags = oldFlags;
}
private void handleSoftAbort() throws LuaError
{
// If we already thrown our soft abort error then don't do it again.
if( !this.timeout.isSoftAborted() || this.thrownSoftAbort )
{
return;
}
if( !timeout.isSoftAborted() || thrownSoftAbort ) return;
this.thrownSoftAbort = true;
thrownSoftAbort = true;
throw new LuaError( TimeoutState.ABORT_MESSAGE );
}
}
private class CobaltLuaContext implements ILuaContext
private static final class HardAbortError extends Error
{
@Override
public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException
private static final long serialVersionUID = 7954092008586367501L;
static final HardAbortError INSTANCE = new HardAbortError();
private HardAbortError()
{
// Issue command
final long taskID = MainThread.getUniqueTaskID();
final Runnable iTask = () -> {
try
{
Object[] results = task.execute();
if( results != null )
{
Object[] eventArguments = new Object[results.length + 2];
eventArguments[0] = taskID;
eventArguments[1] = true;
System.arraycopy( results, 0, eventArguments, 2, results.length );
CobaltLuaMachine.this.m_computer.queueEvent( "task_complete", eventArguments );
}
else
{
CobaltLuaMachine.this.m_computer.queueEvent( "task_complete",
new Object[] {
taskID,
true
} );
}
}
catch( LuaException e )
{
CobaltLuaMachine.this.m_computer.queueEvent( "task_complete",
new Object[] {
taskID,
false,
e.getMessage()
} );
}
catch( Throwable t )
{
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.error( "Error running task", t );
}
CobaltLuaMachine.this.m_computer.queueEvent( "task_complete", new Object[] {
taskID,
false,
"Java Exception Thrown: " + t,
} );
}
};
if( CobaltLuaMachine.this.m_computer.queueMainThread( iTask ) )
{
return taskID;
}
else
{
throw new LuaException( "Task limit exceeded" );
}
super( "Hard Abort", null, true, false );
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.lua;
import dan200.computercraft.api.lua.IDynamicLuaObject;
@@ -14,20 +13,21 @@ import javax.annotation.Nullable;
import java.io.InputStream;
/**
* Represents a machine which will execute Lua code. Technically this API is flexible enough to support many languages, but you'd need a way to provide
* alternative ROMs, BIOSes, etc...
* Represents a machine which will execute Lua code. Technically this API is flexible enough to support many languages,
* but you'd need a way to provide alternative ROMs, BIOSes, etc...
*
* There should only be one concrete implementation at any one time, which is currently {@link CobaltLuaMachine}. If external mod authors are interested in
* registering their own machines, we can look into how we can provide some mechanism for registering these.
* There should only be one concrete implementation at any one time, which is currently {@link CobaltLuaMachine}. If
* external mod authors are interested in registering their own machines, we can look into how we can provide some
* mechanism for registering these.
*
* This should provide implementations of {@link dan200.computercraft.api.lua.ILuaContext}, and the ability to convert {@link IDynamicLuaObject}s into
* something the VM understands, as well as handling method calls.
* This should provide implementations of {@link dan200.computercraft.api.lua.ILuaContext}, and the ability to convert
* {@link IDynamicLuaObject}s into something the VM understands, as well as handling method calls.
*/
public interface ILuaMachine
{
/**
* Inject an API into the global environment of this machine. This should construct an object, as it would for any {@link IDynamicLuaObject} and set it
* to all names in {@link ILuaAPI#getNames()}.
* Inject an API into the global environment of this machine. This should construct an object, as it would for any
* {@link IDynamicLuaObject} and set it to all names in {@link ILuaAPI#getNames()}.
*
* Called before {@link #loadBios(InputStream)}.
*
@@ -36,7 +36,8 @@ public interface ILuaMachine
void addAPI( @Nonnull ILuaAPI api );
/**
* Create a function from the provided program, and set it up to run when {@link #handleEvent(String, Object[])} is called.
* Create a function from the provided program, and set it up to run when {@link #handleEvent(String, Object[])} is
* called.
*
* This should destroy the machine if it failed to load the bios.
*
@@ -50,10 +51,11 @@ public interface ILuaMachine
*
* This should destroy the machine if it failed to execute successfully.
*
* @param eventName The name of the event. This is {@code null} when first starting the machine. Note, this may do nothing if it does not match the
* event filter.
* @param eventName The name of the event. This is {@code null} when first starting the machine. Note, this may
* do nothing if it does not match the event filter.
* @param arguments The arguments for this event.
* @return The result of loading this machine. Will either be OK, or the error message that occurred when executing.
* @return The result of loading this machine. Will either be OK, or the error message that occurred when
* executing.
*/
MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments );

View File

@@ -0,0 +1,69 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.lua;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.ILuaTask;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.MainThread;
import javax.annotation.Nonnull;
class LuaContext implements ILuaContext
{
private final Computer computer;
LuaContext( Computer computer )
{
this.computer = computer;
}
@Override
public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException
{
// Issue command
final long taskID = MainThread.getUniqueTaskID();
final Runnable iTask = () -> {
try
{
Object[] results = task.execute();
if( results != null )
{
Object[] eventArguments = new Object[results.length + 2];
eventArguments[0] = taskID;
eventArguments[1] = true;
System.arraycopy( results, 0, eventArguments, 2, results.length );
computer.queueEvent( "task_complete", eventArguments );
}
else
{
computer.queueEvent( "task_complete", new Object[] { taskID, true } );
}
}
catch( LuaException e )
{
computer.queueEvent( "task_complete", new Object[] { taskID, false, e.getMessage() } );
}
catch( Exception t )
{
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t );
computer.queueEvent( "task_complete", new Object[] {
taskID, false, "Java Exception Thrown: " + t,
} );
}
};
if( computer.queueMainThread( iTask ) )
{
return taskID;
}
else
{
throw new LuaException( "Task limit exceeded" );
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.lua;
import dan200.computercraft.core.computer.TimeoutState;
@@ -65,17 +64,17 @@ public final class MachineResult
public boolean isError()
{
return this.error;
return error;
}
public boolean isPause()
{
return this.pause;
return pause;
}
@Nullable
public String getMessage()
{
return this.message;
return message;
}
}

View File

@@ -15,11 +15,24 @@ import org.squiddev.cobalt.function.ResumableVarArgFunction;
import javax.annotation.Nonnull;
/**
* Calls a {@link LuaMethod}, and interprets the resulting {@link MethodResult}, either returning the result or yielding and resuming the supplied
* continuation.
* Calls a {@link LuaMethod}, and interprets the resulting {@link MethodResult}, either returning the result or yielding
* and resuming the supplied continuation.
*/
class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterpreterFunction.Container>
{
@Nonnull
static class Container
{
ILuaCallback callback;
int errorAdjust;
Container( ILuaCallback callback, int errorAdjust )
{
this.callback = callback;
this.errorAdjust = errorAdjust;
}
}
private final CobaltLuaMachine machine;
private final LuaMethod method;
private final Object instance;
@@ -42,7 +55,7 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
MethodResult results;
try
{
results = this.method.apply( this.instance, this.context, arguments );
results = method.apply( instance, context, arguments );
}
catch( LuaException e )
{
@@ -52,18 +65,15 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
{
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.error( "Error calling " + this.name + " on " + this.instance, t );
ComputerCraft.log.error( "Error calling " + name + " on " + instance, t );
}
throw new LuaError( "Java Exception Thrown: " + t, 0 );
}
ILuaCallback callback = results.getCallback();
Varargs ret = this.machine.toValues( results.getResult() );
Varargs ret = machine.toValues( results.getResult() );
if( callback == null )
{
return ret;
}
if( callback == null ) return ret;
debugFrame.state = new Container( callback, results.getErrorAdjust() );
return LuaThread.yield( state, ret );
@@ -86,18 +96,15 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
{
if( ComputerCraft.logComputerErrors )
{
ComputerCraft.log.error( "Error calling " + this.name + " on " + container.callback, t );
ComputerCraft.log.error( "Error calling " + name + " on " + container.callback, t );
}
throw new LuaError( "Java Exception Thrown: " + t, 0 );
}
Varargs ret = this.machine.toValues( results.getResult() );
Varargs ret = machine.toValues( results.getResult() );
ILuaCallback callback = results.getCallback();
if( callback == null )
{
return ret;
}
if( callback == null ) return ret;
container.callback = callback;
return LuaThread.yield( state, ret );
@@ -105,25 +112,9 @@ class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterprete
public static LuaError wrap( LuaException exception, int adjust )
{
if( !exception.hasLevel() && adjust == 0 )
{
return new LuaError( exception.getMessage() );
}
if( !exception.hasLevel() && adjust == 0 ) return new LuaError( exception.getMessage() );
int level = exception.getLevel();
return new LuaError( exception.getMessage(), level <= 0 ? level : level + adjust + 1 );
}
@Nonnull
static class Container
{
ILuaCallback callback;
int errorAdjust;
Container( ILuaCallback callback, int errorAdjust )
{
this.callback = callback;
this.errorAdjust = errorAdjust;
}
}
}

View File

@@ -27,104 +27,75 @@ class VarargArguments implements IArguments
this.varargs = varargs;
}
@Override
public IArguments drop( int count )
{
if( count < 0 )
{
throw new IllegalStateException( "count cannot be negative" );
}
if( count == 0 )
{
return this;
}
return new VarargArguments( this.varargs.subargs( count + 1 ) );
}
@Override
public int count()
{
return this.varargs.count();
return varargs.count();
}
@Nullable
@Override
public Object get( int index )
{
if( index < 0 || index >= this.varargs.count() )
{
return null;
}
if( index < 0 || index >= varargs.count() ) return null;
Object[] cache = this.cache;
if( cache == null )
{
cache = this.cache = new Object[this.varargs.count()];
cache = this.cache = new Object[varargs.count()];
}
else
{
Object existing = cache[index];
if( existing != null )
{
return existing;
}
if( existing != null ) return existing;
}
return cache[index] = CobaltLuaMachine.toObject( this.varargs.arg( index + 1 ), null );
return cache[index] = CobaltLuaMachine.toObject( varargs.arg( index + 1 ), null );
}
@Override
public long getLong( int index ) throws LuaException
public IArguments drop( int count )
{
LuaValue value = this.varargs.arg( index + 1 );
if( !(value instanceof LuaNumber) )
{
throw LuaValues.badArgument( index, "number", value.typeName() );
}
return value instanceof LuaInteger ? value.toInteger() : (long) LuaValues.checkFinite( index, value.toDouble() );
if( count < 0 ) throw new IllegalStateException( "count cannot be negative" );
if( count == 0 ) return this;
return new VarargArguments( varargs.subargs( count + 1 ) );
}
@Override
public double getDouble( int index ) throws LuaException
{
LuaValue value = this.varargs.arg( index + 1 );
if( !(value instanceof LuaNumber) )
{
throw LuaValues.badArgument( index, "number", value.typeName() );
}
LuaValue value = varargs.arg( index + 1 );
if( !(value instanceof LuaNumber) ) throw LuaValues.badArgument( index, "number", value.typeName() );
return value.toDouble();
}
@Override
public long getLong( int index ) throws LuaException
{
LuaValue value = varargs.arg( index + 1 );
if( !(value instanceof LuaNumber) ) throw LuaValues.badArgument( index, "number", value.typeName() );
return value instanceof LuaInteger ? value.toInteger() : (long) LuaValues.checkFinite( index, value.toDouble() );
}
@Nonnull
@Override
public ByteBuffer getBytes( int index ) throws LuaException
{
LuaValue value = this.varargs.arg( index + 1 );
if( !(value instanceof LuaBaseString) )
{
throw LuaValues.badArgument( index, "string", value.typeName() );
}
LuaValue value = varargs.arg( index + 1 );
if( !(value instanceof LuaBaseString) ) throw LuaValues.badArgument( index, "string", value.typeName() );
LuaString str = ((LuaBaseString) value).strvalue();
return ByteBuffer.wrap( str.bytes, str.offset, str.length )
.asReadOnlyBuffer();
return ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer();
}
@Override
public Optional<ByteBuffer> optBytes( int index ) throws LuaException
{
LuaValue value = this.varargs.arg( index + 1 );
if( value.isNil() )
{
return Optional.empty();
}
if( !(value instanceof LuaBaseString) )
{
throw LuaValues.badArgument( index, "string", value.typeName() );
}
LuaValue value = varargs.arg( index + 1 );
if( value.isNil() ) return Optional.empty();
if( !(value instanceof LuaBaseString) ) throw LuaValues.badArgument( index, "string", value.typeName() );
LuaString str = ((LuaBaseString) value).strvalue();
return Optional.of( ByteBuffer.wrap( str.bytes, str.offset, str.length )
.asReadOnlyBuffer() );
return Optional.of( ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer() );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.terminal;
import dan200.computercraft.shared.util.Colour;
@@ -16,18 +15,23 @@ import javax.annotation.Nonnull;
public class Terminal
{
private static final String base16 = "0123456789abcdef";
private final Palette m_palette = new Palette();
private int cursorX = 0;
private int cursorY = 0;
private boolean cursorBlink = false;
private int cursorColour = 0;
private int cursorBackgroundColour = 15;
private int width;
private int height;
private TextBuffer[] text;
private TextBuffer[] textColour;
private TextBuffer[] backgroundColour;
private final Palette palette = new Palette();
private final Runnable onChanged;
private int m_cursorX = 0;
private int m_cursorY = 0;
private boolean m_cursorBlink = false;
private int m_cursorColour = 0;
private int m_cursorBackgroundColour = 15;
private int m_width;
private int m_height;
private TextBuffer[] m_text;
private TextBuffer[] m_textColour;
private TextBuffer[] m_backgroundColour;
public Terminal( int width, int height )
{
@@ -36,199 +40,180 @@ public class Terminal
public Terminal( int width, int height, Runnable changedCallback )
{
this.m_width = width;
this.m_height = height;
this.onChanged = changedCallback;
this.width = width;
this.height = height;
onChanged = changedCallback;
this.m_text = new TextBuffer[this.m_height];
this.m_textColour = new TextBuffer[this.m_height];
this.m_backgroundColour = new TextBuffer[this.m_height];
for( int i = 0; i < this.m_height; i++ )
text = new TextBuffer[this.height];
textColour = new TextBuffer[this.height];
backgroundColour = new TextBuffer[this.height];
for( int i = 0; i < this.height; i++ )
{
this.m_text[i] = new TextBuffer( ' ', this.m_width );
this.m_textColour[i] = new TextBuffer( base16.charAt( this.m_cursorColour ), this.m_width );
this.m_backgroundColour[i] = new TextBuffer( base16.charAt( this.m_cursorBackgroundColour ), this.m_width );
text[i] = new TextBuffer( ' ', this.width );
textColour[i] = new TextBuffer( base16.charAt( cursorColour ), this.width );
backgroundColour[i] = new TextBuffer( base16.charAt( cursorBackgroundColour ), this.width );
}
}
public synchronized void reset()
{
this.m_cursorColour = 0;
this.m_cursorBackgroundColour = 15;
this.m_cursorX = 0;
this.m_cursorY = 0;
this.m_cursorBlink = false;
this.clear();
this.setChanged();
this.m_palette.resetColours();
}
public synchronized void clear()
{
for( int y = 0; y < this.m_height; y++ )
{
this.m_text[y].fill( ' ' );
this.m_textColour[y].fill( base16.charAt( this.m_cursorColour ) );
this.m_backgroundColour[y].fill( base16.charAt( this.m_cursorBackgroundColour ) );
}
this.setChanged();
}
public final void setChanged()
{
if( this.onChanged != null )
{
this.onChanged.run();
}
cursorColour = 0;
cursorBackgroundColour = 15;
cursorX = 0;
cursorY = 0;
cursorBlink = false;
clear();
setChanged();
palette.resetColours();
}
public int getWidth()
{
return this.m_width;
return width;
}
public int getHeight()
{
return this.m_height;
return height;
}
public synchronized void resize( int width, int height )
{
if( width == this.m_width && height == this.m_height )
if( width == this.width && height == this.height )
{
return;
}
int oldHeight = this.m_height;
int oldWidth = this.m_width;
TextBuffer[] oldText = this.m_text;
TextBuffer[] oldTextColour = this.m_textColour;
TextBuffer[] oldBackgroundColour = this.m_backgroundColour;
int oldHeight = this.height;
int oldWidth = this.width;
TextBuffer[] oldText = text;
TextBuffer[] oldTextColour = textColour;
TextBuffer[] oldBackgroundColour = backgroundColour;
this.m_width = width;
this.m_height = height;
this.width = width;
this.height = height;
this.m_text = new TextBuffer[this.m_height];
this.m_textColour = new TextBuffer[this.m_height];
this.m_backgroundColour = new TextBuffer[this.m_height];
for( int i = 0; i < this.m_height; i++ )
text = new TextBuffer[this.height];
textColour = new TextBuffer[this.height];
backgroundColour = new TextBuffer[this.height];
for( int i = 0; i < this.height; i++ )
{
if( i >= oldHeight )
{
this.m_text[i] = new TextBuffer( ' ', this.m_width );
this.m_textColour[i] = new TextBuffer( base16.charAt( this.m_cursorColour ), this.m_width );
this.m_backgroundColour[i] = new TextBuffer( base16.charAt( this.m_cursorBackgroundColour ), this.m_width );
text[i] = new TextBuffer( ' ', this.width );
textColour[i] = new TextBuffer( base16.charAt( cursorColour ), this.width );
backgroundColour[i] = new TextBuffer( base16.charAt( cursorBackgroundColour ), this.width );
}
else if( this.m_width == oldWidth )
else if( this.width == oldWidth )
{
this.m_text[i] = oldText[i];
this.m_textColour[i] = oldTextColour[i];
this.m_backgroundColour[i] = oldBackgroundColour[i];
text[i] = oldText[i];
textColour[i] = oldTextColour[i];
backgroundColour[i] = oldBackgroundColour[i];
}
else
{
this.m_text[i] = new TextBuffer( ' ', this.m_width );
this.m_textColour[i] = new TextBuffer( base16.charAt( this.m_cursorColour ), this.m_width );
this.m_backgroundColour[i] = new TextBuffer( base16.charAt( this.m_cursorBackgroundColour ), this.m_width );
this.m_text[i].write( oldText[i] );
this.m_textColour[i].write( oldTextColour[i] );
this.m_backgroundColour[i].write( oldBackgroundColour[i] );
text[i] = new TextBuffer( ' ', this.width );
textColour[i] = new TextBuffer( base16.charAt( cursorColour ), this.width );
backgroundColour[i] = new TextBuffer( base16.charAt( cursorBackgroundColour ), this.width );
text[i].write( oldText[i] );
textColour[i].write( oldTextColour[i] );
backgroundColour[i].write( oldBackgroundColour[i] );
}
}
this.setChanged();
setChanged();
}
public void setCursorPos( int x, int y )
{
if( this.m_cursorX != x || this.m_cursorY != y )
if( cursorX != x || cursorY != y )
{
this.m_cursorX = x;
this.m_cursorY = y;
this.setChanged();
cursorX = x;
cursorY = y;
setChanged();
}
}
public void setCursorBlink( boolean blink )
{
if( cursorBlink != blink )
{
cursorBlink = blink;
setChanged();
}
}
public void setTextColour( int colour )
{
if( cursorColour != colour )
{
cursorColour = colour;
setChanged();
}
}
public void setBackgroundColour( int colour )
{
if( cursorBackgroundColour != colour )
{
cursorBackgroundColour = colour;
setChanged();
}
}
public int getCursorX()
{
return this.m_cursorX;
return cursorX;
}
public int getCursorY()
{
return this.m_cursorY;
return cursorY;
}
public boolean getCursorBlink()
{
return this.m_cursorBlink;
}
public void setCursorBlink( boolean blink )
{
if( this.m_cursorBlink != blink )
{
this.m_cursorBlink = blink;
this.setChanged();
}
return cursorBlink;
}
public int getTextColour()
{
return this.m_cursorColour;
}
public void setTextColour( int colour )
{
if( this.m_cursorColour != colour )
{
this.m_cursorColour = colour;
this.setChanged();
}
return cursorColour;
}
public int getBackgroundColour()
{
return this.m_cursorBackgroundColour;
}
public void setBackgroundColour( int colour )
{
if( this.m_cursorBackgroundColour != colour )
{
this.m_cursorBackgroundColour = colour;
this.setChanged();
}
return cursorBackgroundColour;
}
@Nonnull
public Palette getPalette()
{
return this.m_palette;
return palette;
}
public synchronized void blit( String text, String textColour, String backgroundColour )
{
int x = this.m_cursorX;
int y = this.m_cursorY;
if( y >= 0 && y < this.m_height )
int x = cursorX;
int y = cursorY;
if( y >= 0 && y < height )
{
this.m_text[y].write( text, x );
this.m_textColour[y].write( textColour, x );
this.m_backgroundColour[y].write( backgroundColour, x );
this.setChanged();
this.text[y].write( text, x );
this.textColour[y].write( textColour, x );
this.backgroundColour[y].write( backgroundColour, x );
setChanged();
}
}
public synchronized void write( String text )
{
int x = this.m_cursorX;
int y = this.m_cursorY;
if( y >= 0 && y < this.m_height )
int x = cursorX;
int y = cursorY;
if( y >= 0 && y < height )
{
this.m_text[y].write( text, x );
this.m_textColour[y].fill( base16.charAt( this.m_cursorColour ), x, x + text.length() );
this.m_backgroundColour[y].fill( base16.charAt( this.m_cursorBackgroundColour ), x, x + text.length() );
this.setChanged();
this.text[y].write( text, x );
textColour[y].fill( base16.charAt( cursorColour ), x, x + text.length() );
backgroundColour[y].fill( base16.charAt( cursorBackgroundColour ), x, x + text.length() );
setChanged();
}
}
@@ -236,132 +221,138 @@ public class Terminal
{
if( yDiff != 0 )
{
TextBuffer[] newText = new TextBuffer[this.m_height];
TextBuffer[] newTextColour = new TextBuffer[this.m_height];
TextBuffer[] newBackgroundColour = new TextBuffer[this.m_height];
for( int y = 0; y < this.m_height; y++ )
TextBuffer[] newText = new TextBuffer[height];
TextBuffer[] newTextColour = new TextBuffer[height];
TextBuffer[] newBackgroundColour = new TextBuffer[height];
for( int y = 0; y < height; y++ )
{
int oldY = y + yDiff;
if( oldY >= 0 && oldY < this.m_height )
if( oldY >= 0 && oldY < height )
{
newText[y] = this.m_text[oldY];
newTextColour[y] = this.m_textColour[oldY];
newBackgroundColour[y] = this.m_backgroundColour[oldY];
newText[y] = text[oldY];
newTextColour[y] = textColour[oldY];
newBackgroundColour[y] = backgroundColour[oldY];
}
else
{
newText[y] = new TextBuffer( ' ', this.m_width );
newTextColour[y] = new TextBuffer( base16.charAt( this.m_cursorColour ), this.m_width );
newBackgroundColour[y] = new TextBuffer( base16.charAt( this.m_cursorBackgroundColour ), this.m_width );
newText[y] = new TextBuffer( ' ', width );
newTextColour[y] = new TextBuffer( base16.charAt( cursorColour ), width );
newBackgroundColour[y] = new TextBuffer( base16.charAt( cursorBackgroundColour ), width );
}
}
this.m_text = newText;
this.m_textColour = newTextColour;
this.m_backgroundColour = newBackgroundColour;
this.setChanged();
text = newText;
textColour = newTextColour;
backgroundColour = newBackgroundColour;
setChanged();
}
}
public synchronized void clear()
{
for( int y = 0; y < height; y++ )
{
text[y].fill( ' ' );
textColour[y].fill( base16.charAt( cursorColour ) );
backgroundColour[y].fill( base16.charAt( cursorBackgroundColour ) );
}
setChanged();
}
public synchronized void clearLine()
{
int y = this.m_cursorY;
if( y >= 0 && y < this.m_height )
int y = cursorY;
if( y >= 0 && y < height )
{
this.m_text[y].fill( ' ' );
this.m_textColour[y].fill( base16.charAt( this.m_cursorColour ) );
this.m_backgroundColour[y].fill( base16.charAt( this.m_cursorBackgroundColour ) );
this.setChanged();
text[y].fill( ' ' );
textColour[y].fill( base16.charAt( cursorColour ) );
backgroundColour[y].fill( base16.charAt( cursorBackgroundColour ) );
setChanged();
}
}
public synchronized TextBuffer getLine( int y )
{
if( y >= 0 && y < this.m_height )
if( y >= 0 && y < height )
{
return this.m_text[y];
return text[y];
}
return null;
}
public synchronized void setLine( int y, String text, String textColour, String backgroundColour )
{
this.m_text[y].write( text );
this.m_textColour[y].write( textColour );
this.m_backgroundColour[y].write( backgroundColour );
this.setChanged();
this.text[y].write( text );
this.textColour[y].write( textColour );
this.backgroundColour[y].write( backgroundColour );
setChanged();
}
public synchronized TextBuffer getTextColourLine( int y )
{
if( y >= 0 && y < this.m_height )
if( y >= 0 && y < height )
{
return this.m_textColour[y];
return textColour[y];
}
return null;
}
public synchronized TextBuffer getBackgroundColourLine( int y )
{
if( y >= 0 && y < this.m_height )
if( y >= 0 && y < height )
{
return this.m_backgroundColour[y];
return backgroundColour[y];
}
return null;
}
public final void setChanged()
{
if( onChanged != null ) onChanged.run();
}
public synchronized void write( PacketByteBuf buffer )
{
buffer.writeInt( this.m_cursorX );
buffer.writeInt( this.m_cursorY );
buffer.writeBoolean( this.m_cursorBlink );
buffer.writeByte( this.m_cursorBackgroundColour << 4 | this.m_cursorColour );
buffer.writeInt( cursorX );
buffer.writeInt( cursorY );
buffer.writeBoolean( cursorBlink );
buffer.writeByte( cursorBackgroundColour << 4 | cursorColour );
for( int y = 0; y < this.m_height; y++ )
for( int y = 0; y < height; y++ )
{
TextBuffer text = this.m_text[y];
TextBuffer textColour = this.m_textColour[y];
TextBuffer backColour = this.m_backgroundColour[y];
TextBuffer text = this.text[y];
TextBuffer textColour = this.textColour[y];
TextBuffer backColour = backgroundColour[y];
for( int x = 0; x < this.m_width; x++ )
for( int x = 0; x < width; x++ )
{
buffer.writeByte( text.charAt( x ) & 0xFF );
buffer.writeByte( getColour( backColour.charAt( x ), Colour.BLACK ) << 4 | getColour( textColour.charAt( x ), Colour.WHITE ) );
buffer.writeByte( getColour(
backColour.charAt( x ), Colour.BLACK ) << 4 |
getColour( textColour.charAt( x ), Colour.WHITE )
);
}
}
this.m_palette.write( buffer );
}
public static int getColour( char c, Colour def )
{
if( c >= '0' && c <= '9' )
{
return c - '0';
}
if( c >= 'a' && c <= 'f' )
{
return c - 'a' + 10;
}
return 15 - def.ordinal();
palette.write( buffer );
}
public synchronized void read( PacketByteBuf buffer )
{
this.m_cursorX = buffer.readInt();
this.m_cursorY = buffer.readInt();
this.m_cursorBlink = buffer.readBoolean();
cursorX = buffer.readInt();
cursorY = buffer.readInt();
cursorBlink = buffer.readBoolean();
byte cursorColour = buffer.readByte();
this.m_cursorBackgroundColour = (cursorColour >> 4) & 0xF;
this.m_cursorColour = cursorColour & 0xF;
cursorBackgroundColour = (cursorColour >> 4) & 0xF;
this.cursorColour = cursorColour & 0xF;
for( int y = 0; y < this.m_height; y++ )
for( int y = 0; y < height; y++ )
{
TextBuffer text = this.m_text[y];
TextBuffer textColour = this.m_textColour[y];
TextBuffer backColour = this.m_backgroundColour[y];
TextBuffer text = this.text[y];
TextBuffer textColour = this.textColour[y];
TextBuffer backColour = backgroundColour[y];
for( int x = 0; x < this.m_width; x++ )
for( int x = 0; x < width; x++ )
{
text.setChar( x, (char) (buffer.readByte() & 0xFF) );
@@ -371,56 +362,63 @@ public class Terminal
}
}
this.m_palette.read( buffer );
this.setChanged();
palette.read( buffer );
setChanged();
}
public synchronized CompoundTag writeToNBT( CompoundTag nbt )
{
nbt.putInt( "term_cursorX", this.m_cursorX );
nbt.putInt( "term_cursorY", this.m_cursorY );
nbt.putBoolean( "term_cursorBlink", this.m_cursorBlink );
nbt.putInt( "term_textColour", this.m_cursorColour );
nbt.putInt( "term_bgColour", this.m_cursorBackgroundColour );
for( int n = 0; n < this.m_height; n++ )
nbt.putInt( "term_cursorX", cursorX );
nbt.putInt( "term_cursorY", cursorY );
nbt.putBoolean( "term_cursorBlink", cursorBlink );
nbt.putInt( "term_textColour", cursorColour );
nbt.putInt( "term_bgColour", cursorBackgroundColour );
for( int n = 0; n < height; n++ )
{
nbt.putString( "term_text_" + n, this.m_text[n].toString() );
nbt.putString( "term_textColour_" + n, this.m_textColour[n].toString() );
nbt.putString( "term_textBgColour_" + n, this.m_backgroundColour[n].toString() );
nbt.putString( "term_text_" + n, text[n].toString() );
nbt.putString( "term_textColour_" + n, textColour[n].toString() );
nbt.putString( "term_textBgColour_" + n, backgroundColour[n].toString() );
}
this.m_palette.writeToNBT( nbt );
palette.writeToNBT( nbt );
return nbt;
}
public synchronized void readFromNBT( CompoundTag nbt )
{
this.m_cursorX = nbt.getInt( "term_cursorX" );
this.m_cursorY = nbt.getInt( "term_cursorY" );
this.m_cursorBlink = nbt.getBoolean( "term_cursorBlink" );
this.m_cursorColour = nbt.getInt( "term_textColour" );
this.m_cursorBackgroundColour = nbt.getInt( "term_bgColour" );
cursorX = nbt.getInt( "term_cursorX" );
cursorY = nbt.getInt( "term_cursorY" );
cursorBlink = nbt.getBoolean( "term_cursorBlink" );
cursorColour = nbt.getInt( "term_textColour" );
cursorBackgroundColour = nbt.getInt( "term_bgColour" );
for( int n = 0; n < this.m_height; n++ )
for( int n = 0; n < height; n++ )
{
this.m_text[n].fill( ' ' );
text[n].fill( ' ' );
if( nbt.contains( "term_text_" + n ) )
{
this.m_text[n].write( nbt.getString( "term_text_" + n ) );
text[n].write( nbt.getString( "term_text_" + n ) );
}
this.m_textColour[n].fill( base16.charAt( this.m_cursorColour ) );
textColour[n].fill( base16.charAt( cursorColour ) );
if( nbt.contains( "term_textColour_" + n ) )
{
this.m_textColour[n].write( nbt.getString( "term_textColour_" + n ) );
textColour[n].write( nbt.getString( "term_textColour_" + n ) );
}
this.m_backgroundColour[n].fill( base16.charAt( this.m_cursorBackgroundColour ) );
backgroundColour[n].fill( base16.charAt( cursorBackgroundColour ) );
if( nbt.contains( "term_textBgColour_" + n ) )
{
this.m_backgroundColour[n].write( nbt.getString( "term_textBgColour_" + n ) );
backgroundColour[n].write( nbt.getString( "term_textBgColour_" + n ) );
}
}
this.m_palette.readFromNBT( nbt );
this.setChanged();
palette.readFromNBT( nbt );
setChanged();
}
public static int getColour( char c, Colour def )
{
if( c >= '0' && c <= '9' ) return c - '0';
if( c >= 'a' && c <= 'f' ) return c - 'a' + 10;
return 15 - def.ordinal();
}
}

View File

@@ -83,4 +83,4 @@ public class TextBuffer
{
return new String( text );
}
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.tracking;
import dan200.computercraft.core.computer.Computer;
@@ -16,126 +15,108 @@ public class ComputerTracker
{
private final WeakReference<Computer> computer;
private final int computerId;
private final Object2LongOpenHashMap<TrackingField> fields;
private long tasks;
private long totalTime;
private long maxTime;
private long serverCount;
private long serverTime;
private final Object2LongOpenHashMap<TrackingField> fields;
public ComputerTracker( Computer computer )
{
this.computer = new WeakReference<>( computer );
this.computerId = computer.getID();
this.fields = new Object2LongOpenHashMap<>();
computerId = computer.getID();
fields = new Object2LongOpenHashMap<>();
}
ComputerTracker( ComputerTracker timings )
{
this.computer = timings.computer;
this.computerId = timings.computerId;
computer = timings.computer;
computerId = timings.computerId;
this.tasks = timings.tasks;
this.totalTime = timings.totalTime;
this.maxTime = timings.maxTime;
tasks = timings.tasks;
totalTime = timings.totalTime;
maxTime = timings.maxTime;
this.serverCount = timings.serverCount;
this.serverTime = timings.serverTime;
serverCount = timings.serverCount;
serverTime = timings.serverTime;
this.fields = new Object2LongOpenHashMap<>( timings.fields );
fields = new Object2LongOpenHashMap<>( timings.fields );
}
@Nullable
public Computer getComputer()
{
return this.computer.get();
return computer.get();
}
public int getComputerId()
{
return this.computerId;
return computerId;
}
public long getTasks()
{
return this.tasks;
return tasks;
}
public long getTotalTime()
{
return this.totalTime;
return totalTime;
}
public long getMaxTime()
{
return this.maxTime;
return maxTime;
}
public long getAverage()
{
return this.totalTime / this.tasks;
return totalTime / tasks;
}
void addTaskTiming( long time )
{
this.tasks++;
this.totalTime += time;
if( time > this.maxTime )
{
this.maxTime = time;
}
tasks++;
totalTime += time;
if( time > maxTime ) maxTime = time;
}
void addMainTiming( long time )
{
this.serverCount++;
this.serverTime += time;
serverCount++;
serverTime += time;
}
void addValue( TrackingField field, long change )
{
synchronized( this.fields )
synchronized( fields )
{
this.fields.addTo( field, change );
fields.addTo( field, change );
}
}
public long get( TrackingField field )
{
if( field == TrackingField.TASKS ) return tasks;
if( field == TrackingField.MAX_TIME ) return maxTime;
if( field == TrackingField.TOTAL_TIME ) return totalTime;
if( field == TrackingField.AVERAGE_TIME ) return tasks == 0 ? 0 : totalTime / tasks;
if( field == TrackingField.SERVER_COUNT ) return serverCount;
if( field == TrackingField.SERVER_TIME ) return serverTime;
synchronized( fields )
{
return fields.getLong( field );
}
}
public String getFormatted( TrackingField field )
{
return field.format( this.get( field ) );
}
public long get( TrackingField field )
{
if( field == TrackingField.TASKS )
{
return this.tasks;
}
if( field == TrackingField.MAX_TIME )
{
return this.maxTime;
}
if( field == TrackingField.TOTAL_TIME )
{
return this.totalTime;
}
if( field == TrackingField.AVERAGE_TIME )
{
return this.tasks == 0 ? 0 : this.totalTime / this.tasks;
}
if( field == TrackingField.SERVER_COUNT )
{
return this.serverCount;
}
if( field == TrackingField.SERVER_TIME )
{
return this.serverTime;
}
synchronized( this.fields )
{
return this.fields.getLong( field );
}
return field.format( get( field ) );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.tracking;
import dan200.computercraft.core.computer.Computer;
@@ -35,8 +34,8 @@ public interface Tracker
}
/**
* Increment an arbitrary field by some value. Implementations may track how often this is called as well as the change, to compute some level of
* "average".
* Increment an arbitrary field by some value. Implementations may track how often this is called
* as well as the change, to compute some level of "average".
*
* @param computer The computer to increment
* @param field The field to increment.

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.tracking;
import dan200.computercraft.core.computer.Computer;
@@ -29,10 +28,7 @@ public final class Tracking
synchronized( lock )
{
TrackingContext context = contexts.get( uuid );
if( context == null )
{
contexts.put( uuid, context = new TrackingContext() );
}
if( context == null ) contexts.put( uuid, context = new TrackingContext() );
return context;
}
}
@@ -48,61 +44,34 @@ public final class Tracking
public static void addTaskTiming( Computer computer, long time )
{
if( tracking.get() == 0 )
{
return;
}
if( tracking.get() == 0 ) return;
synchronized( contexts )
{
for( TrackingContext context : contexts.values() )
{
context.addTaskTiming( computer, time );
}
for( Tracker tracker : trackers )
{
tracker.addTaskTiming( computer, time );
}
for( TrackingContext context : contexts.values() ) context.addTaskTiming( computer, time );
for( Tracker tracker : trackers ) tracker.addTaskTiming( computer, time );
}
}
public static void addServerTiming( Computer computer, long time )
{
if( tracking.get() == 0 )
{
return;
}
if( tracking.get() == 0 ) return;
synchronized( contexts )
{
for( TrackingContext context : contexts.values() )
{
context.addServerTiming( computer, time );
}
for( Tracker tracker : trackers )
{
tracker.addServerTiming( computer, time );
}
for( TrackingContext context : contexts.values() ) context.addServerTiming( computer, time );
for( Tracker tracker : trackers ) tracker.addServerTiming( computer, time );
}
}
public static void addValue( Computer computer, TrackingField field, long change )
{
if( tracking.get() == 0 )
{
return;
}
if( tracking.get() == 0 ) return;
synchronized( lock )
{
for( TrackingContext context : contexts.values() )
{
context.addValue( computer, field, change );
}
for( Tracker tracker : trackers )
{
tracker.addValue( computer, field, change );
}
for( TrackingContext context : contexts.values() ) context.addValue( computer, field, change );
for( Tracker tracker : trackers ) tracker.addValue( computer, field, change );
}
}

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.tracking;
import com.google.common.collect.MapMaker;
@@ -14,73 +13,63 @@ import java.util.List;
import java.util.Map;
/**
* Tracks timing information about computers, including how long they ran for and the number of events they handled.
* Tracks timing information about computers, including how long they ran for
* and the number of events they handled.
*
* Note that this <em>will</em> track computers which have been deleted (hence the presence of {@link #timingLookup} and {@link #timings}
* Note that this <em>will</em> track computers which have been deleted (hence
* the presence of {@link #timingLookup} and {@link #timings}
*/
public class TrackingContext implements Tracker
{
private final List<ComputerTracker> timings = new ArrayList<>();
private final Map<Computer, ComputerTracker> timingLookup = new MapMaker().weakKeys()
.makeMap();
private boolean tracking = false;
private final List<ComputerTracker> timings = new ArrayList<>();
private final Map<Computer, ComputerTracker> timingLookup = new MapMaker().weakKeys().makeMap();
public synchronized void start()
{
if( !this.tracking )
{
Tracking.tracking.incrementAndGet();
}
this.tracking = true;
if( !tracking ) Tracking.tracking.incrementAndGet();
tracking = true;
this.timings.clear();
this.timingLookup.clear();
timings.clear();
timingLookup.clear();
}
public synchronized boolean stop()
{
if( !this.tracking )
{
return false;
}
if( !tracking ) return false;
Tracking.tracking.decrementAndGet();
this.tracking = false;
this.timingLookup.clear();
tracking = false;
timingLookup.clear();
return true;
}
public synchronized List<ComputerTracker> getImmutableTimings()
{
ArrayList<ComputerTracker> timings = new ArrayList<>( this.timings.size() );
for( ComputerTracker timing : this.timings )
{
timings.add( new ComputerTracker( timing ) );
}
for( ComputerTracker timing : this.timings ) timings.add( new ComputerTracker( timing ) );
return timings;
}
public synchronized List<ComputerTracker> getTimings()
{
return new ArrayList<>( this.timings );
return new ArrayList<>( timings );
}
@Override
public void addTaskTiming( Computer computer, long time )
{
if( !this.tracking )
{
return;
}
if( !tracking ) return;
synchronized( this )
{
ComputerTracker computerTimings = this.timingLookup.get( computer );
ComputerTracker computerTimings = timingLookup.get( computer );
if( computerTimings == null )
{
computerTimings = new ComputerTracker( computer );
this.timingLookup.put( computer, computerTimings );
this.timings.add( computerTimings );
timingLookup.put( computer, computerTimings );
timings.add( computerTimings );
}
computerTimings.addTaskTiming( time );
@@ -90,19 +79,16 @@ public class TrackingContext implements Tracker
@Override
public void addServerTiming( Computer computer, long time )
{
if( !this.tracking )
{
return;
}
if( !tracking ) return;
synchronized( this )
{
ComputerTracker computerTimings = this.timingLookup.get( computer );
ComputerTracker computerTimings = timingLookup.get( computer );
if( computerTimings == null )
{
computerTimings = new ComputerTracker( computer );
this.timingLookup.put( computer, computerTimings );
this.timings.add( computerTimings );
timingLookup.put( computer, computerTimings );
timings.add( computerTimings );
}
computerTimings.addMainTiming( time );
@@ -112,19 +98,16 @@ public class TrackingContext implements Tracker
@Override
public void addValue( Computer computer, TrackingField field, long change )
{
if( !this.tracking )
{
return;
}
if( !tracking ) return;
synchronized( this )
{
ComputerTracker computerTimings = this.timingLookup.get( computer );
ComputerTracker computerTimings = timingLookup.get( computer );
if( computerTimings == null )
{
computerTimings = new ComputerTracker( computer );
this.timingLookup.put( computer, computerTimings );
this.timings.add( computerTimings );
timingLookup.put( computer, computerTimings );
timings.add( computerTimings );
}
computerTimings.addValue( field, change );

View File

@@ -3,7 +3,6 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.tracking;
import java.util.Collections;
@@ -28,28 +27,41 @@ public final class TrackingField
public static final TrackingField TURTLE_OPS = TrackingField.of( "turtle", TrackingField::formatDefault );
public static final TrackingField HTTP_REQUESTS = TrackingField.of( "http", TrackingField::formatDefault );
public static final TrackingField COROUTINES_CREATED = TrackingField.of( "coroutines_created", x -> String.format( "%4d", x ) );
public static final TrackingField COROUTINES_DISPOSED = TrackingField.of( "coroutines_dead", x -> String.format( "%4d", x ) );
/**
* So technically a kibibyte, but let's not argue here.
*/
private static final int KILOBYTE_SIZE = 1024;
private static final String SI_PREFIXES = "KMGT";
public static final TrackingField HTTP_UPLOAD = TrackingField.of( "http_upload", TrackingField::formatBytes );
public static final TrackingField HTTP_DOWNLOAD = TrackingField.of( "http_download", TrackingField::formatBytes );
public static final TrackingField WEBSOCKET_INCOMING = TrackingField.of( "websocket_incoming", TrackingField::formatBytes );
public static final TrackingField WEBSOCKET_OUTGOING = TrackingField.of( "websocket_outgoing", TrackingField::formatBytes );
public static final TrackingField COROUTINES_CREATED = TrackingField.of( "coroutines_created", x -> String.format( "%4d", x ) );
public static final TrackingField COROUTINES_DISPOSED = TrackingField.of( "coroutines_dead", x -> String.format( "%4d", x ) );
private final String id;
private final String translationKey;
private final LongFunction<String> format;
public String id()
{
return id;
}
public String translationKey()
{
return translationKey;
}
private TrackingField( String id, LongFunction<String> format )
{
this.id = id;
this.translationKey = "tracking_field.computercraft." + id + ".name";
translationKey = "tracking_field.computercraft." + id + ".name";
this.format = format;
}
public String format( long value )
{
return format.apply( value );
}
public static TrackingField of( String id, LongFunction<String> format )
{
TrackingField field = new TrackingField( id, format );
@@ -67,32 +79,18 @@ public final class TrackingField
return String.format( "%6d", value );
}
/**
* So technically a kibibyte, but let's not argue here.
*/
private static final int KILOBYTE_SIZE = 1024;
private static final String SI_PREFIXES = "KMGT";
private static String formatBytes( long bytes )
{
if( bytes < 1024 )
{
return String.format( "%10d B", bytes );
}
if( bytes < 1024 ) return String.format( "%10d B", bytes );
int exp = (int) (Math.log( bytes ) / Math.log( KILOBYTE_SIZE ));
if( exp > SI_PREFIXES.length() )
{
exp = SI_PREFIXES.length();
}
if( exp > SI_PREFIXES.length() ) exp = SI_PREFIXES.length();
return String.format( "%10.1f %siB", bytes / Math.pow( KILOBYTE_SIZE, exp ), SI_PREFIXES.charAt( exp - 1 ) );
}
public String id()
{
return this.id;
}
public String translationKey()
{
return this.translationKey;
}
public String format( long value )
{
return this.format.apply( value );
}
}

View File

@@ -5,10 +5,7 @@
*/
package dan200.computercraft.shared.util;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.ConfigSpec;
import com.electronwill.nightconfig.core.EnumGetMethod;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
import com.electronwill.nightconfig.core.*;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
import com.google.common.base.CaseFormat;
@@ -30,8 +27,7 @@ import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class Config
{
public class Config {
private static final int MODEM_MAX_RANGE = 100000;
public static final String TRANSLATION_PREFIX = "gui.computercraft.config.";
@@ -42,346 +38,327 @@ public class Config
public static CommentedConfig serverConfig;
public static CommentedConfig clientConfig;
private static final WorldSavePath serverDir = WorldSavePathAccess.createWorldSavePath( "serverconfig" );
private static final WorldSavePath serverDir = WorldSavePathAccess.createWorldSavePath("serverconfig");
private static final String serverFileName = "computercraft-server.toml";
private static final Path clientPath = FabricLoader.INSTANCE.getConfigDir().resolve( "computercraft-client.toml" );
private static final Path clientPath = FabricLoader.INSTANCE.getConfigDir().resolve("computercraft-client.toml");
private Config()
{
private Config() {
}
static
{
System.setProperty( "nightconfig.preserveInsertionOrder", "true" );
static {
System.setProperty("nightconfig.preserveInsertionOrder", "true");
serverSpec = new CommentedConfigSpec();
{ // General computers
serverSpec.comment( "computer_space_limit",
"The disk space limit for computers and turtles, in bytes" );
serverSpec.define( "computer_space_limit", ComputerCraft.computerSpaceLimit );
serverSpec.comment("computer_space_limit",
"The disk space limit for computers and turtles, in bytes");
serverSpec.define("computer_space_limit", ComputerCraft.computerSpaceLimit);
serverSpec.comment( "floppy_space_limit",
"The disk space limit for floppy disks, in bytes" );
serverSpec.define( "floppy_space_limit", ComputerCraft.floppySpaceLimit );
serverSpec.comment("floppy_space_limit",
"The disk space limit for floppy disks, in bytes");
serverSpec.define("floppy_space_limit", ComputerCraft.floppySpaceLimit);
serverSpec.comment( "maximum_open_files",
"Set how many files a computer can have open at the same time. Set to 0 for unlimited." );
serverSpec.defineInRange( "maximum_open_files", ComputerCraft.maximumFilesOpen, 0, Integer.MAX_VALUE );
serverSpec.comment("maximum_open_files",
"Set how many files a computer can have open at the same time. Set to 0 for unlimited.");
serverSpec.defineInRange("maximum_open_files", ComputerCraft.maximumFilesOpen, 0, Integer.MAX_VALUE);
serverSpec.comment( "disable_lua51_features",
"Set this to true to disable Lua 5.1 functions that will be removed in a future update. " +
"Useful for ensuring forward compatibility of your programs now." );
serverSpec.define( "disable_lua51_features", ComputerCraft.disableLua51Features );
serverSpec.comment("disable_lua51_features",
"Set this to true to disable Lua 5.1 functions that will be removed in a future update. " +
"Useful for ensuring forward compatibility of your programs now.");
serverSpec.define("disable_lua51_features", ComputerCraft.disableLua51Features);
serverSpec.comment( "default_computer_settings",
"A comma separated list of default system settings to set on new computers. Example: " +
serverSpec.comment("default_computer_settings",
"A comma separated list of default system settings to set on new computers. Example: " +
"\"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\" will disable all " +
"autocompletion" );
serverSpec.define( "default_computer_settings", ComputerCraft.defaultComputerSettings );
"autocompletion");
serverSpec.define("default_computer_settings", ComputerCraft.defaultComputerSettings);
serverSpec.comment( "debug_enabled",
"Enable Lua's debug library. This is sandboxed to each computer, so is generally safe to be used by players." );
serverSpec.define( "debug_enabled", ComputerCraft.debugEnable );
serverSpec.comment("debug_enabled",
"Enable Lua's debug library. This is sandboxed to each computer, so is generally safe to be used by players.");
serverSpec.define("debug_enabled", ComputerCraft.debugEnable);
serverSpec.comment( "log_computer_errors",
"Log exceptions thrown by peripherals and other Lua objects.\n" +
"This makes it easier for mod authors to debug problems, but may result in log spam should people use buggy methods." );
serverSpec.define( "log_computer_errors", ComputerCraft.logComputerErrors );
serverSpec.comment("log_computer_errors",
"Log exceptions thrown by peripherals and other Lua objects.\n" +
"This makes it easier for mod authors to debug problems, but may result in log spam should people use buggy methods.");
serverSpec.define("log_computer_errors", ComputerCraft.logComputerErrors);
serverSpec.comment( "command_require_creative",
"Require players to be in creative mode and be opped in order to interact with command computers." +
"This is the default behaviour for vanilla's Command blocks." );
serverSpec.define( "command_require_creative", ComputerCraft.commandRequireCreative );
serverSpec.comment("command_require_creative",
"Require players to be in creative mode and be opped in order to interact with command computers." +
"This is the default behaviour for vanilla's Command blocks.");
serverSpec.define("command_require_creative", ComputerCraft.commandRequireCreative);
}
{ // Execution
serverSpec.comment( "execution",
"Controls execution behaviour of computers. This is largely intended for fine-tuning " +
"servers, and generally shouldn't need to be touched" );
serverSpec.comment("execution",
"Controls execution behaviour of computers. This is largely intended for fine-tuning " +
"servers, and generally shouldn't need to be touched");
serverSpec.comment( "execution.computer_threads",
"Set the number of threads computers can run on. A higher number means more computers can run " +
serverSpec.comment("execution.computer_threads",
"Set the number of threads computers can run on. A higher number means more computers can run " +
"at once, but may induce lag.\n" +
"Please note that some mods may not work with a thread count higher than 1. Use with caution." );
serverSpec.defineInRange( "execution.computer_threads", ComputerCraft.computerThreads, 1, Integer.MAX_VALUE );
"Please note that some mods may not work with a thread count higher than 1. Use with caution.");
serverSpec.defineInRange("execution.computer_threads", ComputerCraft.computerThreads, 1, Integer.MAX_VALUE);
serverSpec.comment( "execution.max_main_global_time",
"The maximum time that can be spent executing tasks in a single tick, in milliseconds.\n" +
serverSpec.comment("execution.max_main_global_time",
"The maximum time that can be spent executing tasks in a single tick, in milliseconds.\n" +
"Note, we will quite possibly go over this limit, as there's no way to tell how long a will take " +
"- this aims to be the upper bound of the average time." );
serverSpec.defineInRange( "execution.max_main_global_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainGlobalTime ), 1, Integer.MAX_VALUE );
"- this aims to be the upper bound of the average time.");
serverSpec.defineInRange("execution.max_main_global_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainGlobalTime ), 1, Integer.MAX_VALUE);
serverSpec.comment( "execution.max_main_computer_time",
"The ideal maximum time a computer can execute for in a tick, in milliseconds.\n" +
serverSpec.comment("execution.max_main_computer_time",
"The ideal maximum time a computer can execute for in a tick, in milliseconds.\n" +
"Note, we will quite possibly go over this limit, as there's no way to tell how long a will take " +
"- this aims to be the upper bound of the average time." );
serverSpec.defineInRange( "execution.max_main_computer_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainComputerTime ), 1, Integer.MAX_VALUE );
"- this aims to be the upper bound of the average time.");
serverSpec.defineInRange("execution.max_main_computer_time", (int) TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainComputerTime ), 1, Integer.MAX_VALUE);
}
{ // HTTP
serverSpec.comment( "http", "Controls the HTTP API" );
serverSpec.comment("http", "Controls the HTTP API");
serverSpec.comment( "http.enabled",
"Enable the \"http\" API on Computers (see \"rules\" for more fine grained control than this)." );
serverSpec.define( "http.enabled", ComputerCraft.httpEnabled );
serverSpec.comment("http.enabled",
"Enable the \"http\" API on Computers (see \"rules\" for more fine grained control than this).");
serverSpec.define("http.enabled", ComputerCraft.httpEnabled);
serverSpec.comment( "http.websocket_enabled",
"Enable use of http websockets. This requires the \"http_enable\" option to also be true." );
serverSpec.define( "http.websocket_enabled", ComputerCraft.httpWebsocketEnabled );
serverSpec.comment("http.websocket_enabled",
"Enable use of http websockets. This requires the \"http_enable\" option to also be true.");
serverSpec.define("http.websocket_enabled", ComputerCraft.httpWebsocketEnabled);
serverSpec.comment( "http.rules",
"A list of rules which control behaviour of the \"http\" API for specific domains or IPs.\n" +
serverSpec.comment("http.rules",
"A list of rules which control behaviour of the \"http\" API for specific domains or IPs.\n" +
"Each rule is an item with a 'host' to match against, and a series of properties. " +
"The host may be a domain name (\"pastebin.com\"),\n" +
"wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\"). If no rules, the domain is blocked." );
serverSpec.defineList( "http.rules", Arrays.asList(
AddressRuleConfig.makeRule( "$private", Action.DENY ),
AddressRuleConfig.makeRule( "*", Action.ALLOW )
), x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule( (UnmodifiableConfig) x ) );
"wildcard (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\"). If no rules, the domain is blocked.");
serverSpec.defineList("http.rules", Arrays.asList(
AddressRuleConfig.makeRule("$private", Action.DENY),
AddressRuleConfig.makeRule("*", Action.ALLOW)
), x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule((UnmodifiableConfig) x));
serverSpec.comment( "http.max_requests",
"The number of http requests a computer can make at one time. Additional requests will be queued, and sent when the running requests have finished. Set to 0 for unlimited." );
serverSpec.defineInRange( "http.max_requests", ComputerCraft.httpMaxRequests, 0, Integer.MAX_VALUE );
serverSpec.comment("http.max_requests",
"The number of http requests a computer can make at one time. Additional requests will be queued, and sent when the running requests have finished. Set to 0 for unlimited.");
serverSpec.defineInRange("http.max_requests", ComputerCraft.httpMaxRequests, 0, Integer.MAX_VALUE);
serverSpec.comment( "http.max_websockets",
"The number of websockets a computer can have open at one time. Set to 0 for unlimited." );
serverSpec.defineInRange( "http.max_websockets", ComputerCraft.httpMaxWebsockets, 1, Integer.MAX_VALUE );
serverSpec.comment("http.max_websockets",
"The number of websockets a computer can have open at one time. Set to 0 for unlimited.");
serverSpec.defineInRange("http.max_websockets", ComputerCraft.httpMaxWebsockets, 1, Integer.MAX_VALUE);
}
{ // Peripherals
serverSpec.comment( "peripheral", "Various options relating to peripherals." );
serverSpec.comment("peripheral", "Various options relating to peripherals.");
serverSpec.comment( "peripheral.command_block_enabled",
"Enable Command Block peripheral support" );
serverSpec.define( "peripheral.command_block_enabled", ComputerCraft.enableCommandBlock );
serverSpec.comment("peripheral.command_block_enabled",
"Enable Command Block peripheral support");
serverSpec.define("peripheral.command_block_enabled", ComputerCraft.enableCommandBlock);
serverSpec.comment( "peripheral.modem_range",
"The range of Wireless Modems at low altitude in clear weather, in meters" );
serverSpec.defineInRange( "peripheral.modem_range", ComputerCraft.modemRange, 0, MODEM_MAX_RANGE );
serverSpec.comment("peripheral.modem_range",
"The range of Wireless Modems at low altitude in clear weather, in meters");
serverSpec.defineInRange("peripheral.modem_range", ComputerCraft.modemRange, 0, MODEM_MAX_RANGE);
serverSpec.comment( "peripheral.modem_high_altitude_range",
"The range of Wireless Modems at maximum altitude in clear weather, in meters" );
serverSpec.defineInRange( "peripheral.modem_high_altitude_range", ComputerCraft.modemHighAltitudeRange, 0, MODEM_MAX_RANGE );
serverSpec.comment("peripheral.modem_high_altitude_range",
"The range of Wireless Modems at maximum altitude in clear weather, in meters");
serverSpec.defineInRange("peripheral.modem_high_altitude_range", ComputerCraft.modemHighAltitudeRange, 0, MODEM_MAX_RANGE);
serverSpec.comment( "peripheral.modem_range_during_storm",
"The range of Wireless Modems at low altitude in stormy weather, in meters" );
serverSpec.defineInRange( "peripheral.modem_range_during_storm", ComputerCraft.modemRangeDuringStorm, 0, MODEM_MAX_RANGE );
serverSpec.comment("peripheral.modem_range_during_storm",
"The range of Wireless Modems at low altitude in stormy weather, in meters");
serverSpec.defineInRange("peripheral.modem_range_during_storm", ComputerCraft.modemRangeDuringStorm, 0, MODEM_MAX_RANGE);
serverSpec.comment( "peripheral.modem_high_altitude_range_during_storm",
"The range of Wireless Modems at maximum altitude in stormy weather, in meters" );
serverSpec.defineInRange( "peripheral.modem_high_altitude_range_during_storm", ComputerCraft.modemHighAltitudeRangeDuringStorm, 0, MODEM_MAX_RANGE );
serverSpec.comment("peripheral.modem_high_altitude_range_during_storm",
"The range of Wireless Modems at maximum altitude in stormy weather, in meters");
serverSpec.defineInRange("peripheral.modem_high_altitude_range_during_storm", ComputerCraft.modemHighAltitudeRangeDuringStorm, 0, MODEM_MAX_RANGE);
serverSpec.comment( "peripheral.max_notes_per_tick",
"Maximum amount of notes a speaker can play at once" );
serverSpec.defineInRange( "peripheral.max_notes_per_tick", ComputerCraft.maxNotesPerTick, 1, Integer.MAX_VALUE );
serverSpec.comment("peripheral.max_notes_per_tick",
"Maximum amount of notes a speaker can play at once");
serverSpec.defineInRange("peripheral.max_notes_per_tick", ComputerCraft.maxNotesPerTick, 1, Integer.MAX_VALUE);
serverSpec.comment( "peripheral.monitor_bandwidth",
"The limit to how much monitor data can be sent *per tick*. Note:\n" +
serverSpec.comment("peripheral.monitor_bandwidth",
"The limit to how much monitor data can be sent *per tick*. Note:\n" +
" - Bandwidth is measured before compression, so the data sent to the client is smaller.\n" +
" - This ignores the number of players a packet is sent to. Updating a monitor for one player consumes " +
"the same bandwidth limit as sending to 20.\n" +
" - A full sized monitor sends ~25kb of data. So the default (1MB) allows for ~40 monitors to be updated " +
"in a single tick. \n" +
"Set to 0 to disable." );
serverSpec.defineInRange( "peripheral.monitor_bandwidth", (int) ComputerCraft.monitorBandwidth, 0, Integer.MAX_VALUE );
"Set to 0 to disable.");
serverSpec.defineInRange("peripheral.monitor_bandwidth", (int) ComputerCraft.monitorBandwidth, 0, Integer.MAX_VALUE);
}
{ // Turtles
serverSpec.comment( "turtle", "Various options relating to turtles." );
serverSpec.comment("turtle", "Various options relating to turtles.");
serverSpec.comment( "turtle.need_fuel",
"Set whether Turtles require fuel to move" );
serverSpec.define( "turtle.need_fuel", ComputerCraft.turtlesNeedFuel );
serverSpec.comment("turtle.need_fuel",
"Set whether Turtles require fuel to move");
serverSpec.define("turtle.need_fuel", ComputerCraft.turtlesNeedFuel);
serverSpec.comment( "turtle.normal_fuel_limit", "The fuel limit for Turtles" );
serverSpec.defineInRange( "turtle.normal_fuel_limit", ComputerCraft.turtleFuelLimit, 0, Integer.MAX_VALUE );
serverSpec.comment("turtle.normal_fuel_limit", "The fuel limit for Turtles");
serverSpec.defineInRange("turtle.normal_fuel_limit", ComputerCraft.turtleFuelLimit, 0, Integer.MAX_VALUE);
serverSpec.comment( "turtle.advanced_fuel_limit",
"The fuel limit for Advanced Turtles" );
serverSpec.defineInRange( "turtle.advanced_fuel_limit", ComputerCraft.advancedTurtleFuelLimit, 0, Integer.MAX_VALUE );
serverSpec.comment("turtle.advanced_fuel_limit",
"The fuel limit for Advanced Turtles");
serverSpec.defineInRange("turtle.advanced_fuel_limit", ComputerCraft.advancedTurtleFuelLimit, 0, Integer.MAX_VALUE);
serverSpec.comment( "turtle.obey_block_protection",
"If set to true, Turtles will be unable to build, dig, or enter protected areas (such as near the server spawn point)" );
serverSpec.define( "turtle.obey_block_protection", ComputerCraft.turtlesObeyBlockProtection );
serverSpec.comment("turtle.obey_block_protection",
"If set to true, Turtles will be unable to build, dig, or enter protected areas (such as near the server spawn point)");
serverSpec.define("turtle.obey_block_protection", ComputerCraft.turtlesObeyBlockProtection);
serverSpec.comment( "turtle.can_push",
"If set to true, Turtles will push entities out of the way instead of stopping if there is space to do so" );
serverSpec.define( "turtle.can_push", ComputerCraft.turtlesCanPush );
serverSpec.comment("turtle.can_push",
"If set to true, Turtles will push entities out of the way instead of stopping if there is space to do so");
serverSpec.define("turtle.can_push", ComputerCraft.turtlesCanPush);
serverSpec.comment( "turtle.disabled_actions",
"A list of turtle actions which are disabled." );
serverSpec.defineList( "turtle.disabled_actions", Collections.emptyList(), x -> x instanceof String && getAction( (String) x ) != null );
serverSpec.comment("turtle.disabled_actions",
"A list of turtle actions which are disabled.");
serverSpec.defineList("turtle.disabled_actions", Collections.emptyList(), x -> x instanceof String && getAction((String) x) != null);
}
{
serverSpec.comment( "term_sizes", "Configure the size of various computer's terminals.\n" +
"Larger terminals require more bandwidth, so use with care." );
serverSpec.comment("term_sizes", "Configure the size of various computer's terminals.\n" +
"Larger terminals require more bandwidth, so use with care.");
serverSpec.comment( "term_sizes.computer", "Terminal size of computers" );
serverSpec.defineInRange( "term_sizes.computer.width", ComputerCraft.computerTermWidth, 1, 255 );
serverSpec.defineInRange( "term_sizes.computer.height", ComputerCraft.computerTermHeight, 1, 255 );
serverSpec.comment("term_sizes.computer", "Terminal size of computers");
serverSpec.defineInRange("term_sizes.computer.width", ComputerCraft.computerTermWidth, 1, 255);
serverSpec.defineInRange("term_sizes.computer.height", ComputerCraft.computerTermHeight, 1, 255);
serverSpec.comment( "term_sizes.pocket_computer", "Terminal size of pocket computers" );
serverSpec.defineInRange( "term_sizes.pocket_computer.width", ComputerCraft.pocketTermWidth, 1, 255 );
serverSpec.defineInRange( "term_sizes.pocket_computer.height", ComputerCraft.pocketTermHeight, 1, 255 );
serverSpec.comment("term_sizes.pocket_computer", "Terminal size of pocket computers");
serverSpec.defineInRange("term_sizes.pocket_computer.width", ComputerCraft.pocketTermWidth, 1, 255);
serverSpec.defineInRange("term_sizes.pocket_computer.height", ComputerCraft.pocketTermHeight, 1, 255);
serverSpec.comment( "term_sizes.monitor", "Maximum size of monitors (in blocks)" );
serverSpec.defineInRange( "term_sizes.monitor.width", ComputerCraft.monitorWidth, 1, 32 );
serverSpec.defineInRange( "term_sizes.monitor.height", ComputerCraft.monitorHeight, 1, 32 );
serverSpec.comment("term_sizes.monitor", "Maximum size of monitors (in blocks)");
serverSpec.defineInRange("term_sizes.monitor.width", ComputerCraft.monitorWidth, 1, 32);
serverSpec.defineInRange("term_sizes.monitor.height", ComputerCraft.monitorHeight, 1, 32);
}
clientSpec = new CommentedConfigSpec();
clientSpec.comment( "monitor_renderer",
"The renderer to use for monitors. Generally this should be kept at \"best\" - if " +
"monitors have performance issues, you may wish to experiment with alternative renderers." );
clientSpec.defineRestrictedEnum( "monitor_renderer", MonitorRenderer.BEST, EnumSet.allOf( MonitorRenderer.class ), EnumGetMethod.NAME_IGNORECASE );
clientSpec.comment("monitor_renderer",
"The renderer to use for monitors. Generally this should be kept at \"best\" - if " +
"monitors have performance issues, you may wish to experiment with alternative renderers.");
clientSpec.defineRestrictedEnum("monitor_renderer", MonitorRenderer.BEST, EnumSet.allOf(MonitorRenderer.class), EnumGetMethod.NAME_IGNORECASE);
clientSpec.comment( "monitor_distance",
"The maximum distance monitors will render at. This defaults to the standard tile entity limit, " +
clientSpec.comment("monitor_distance",
"The maximum distance monitors will render at. This defaults to the standard tile entity limit, " +
"but may be extended if you wish to build larger monitors." );
clientSpec.defineInRange( "monitor_distance", 64, 16, 1024 );
clientSpec.defineInRange("monitor_distance", 64, 16, 1024);
}
private static final FileNotFoundAction MAKE_DIRECTORIES = ( file, configFormat ) -> {
Files.createDirectories( file.getParent() );
Files.createFile( file );
configFormat.initEmptyFile( file );
private static final FileNotFoundAction MAKE_DIRECTORIES = (file, configFormat) -> {
Files.createDirectories(file.getParent());
Files.createFile(file);
configFormat.initEmptyFile(file);
return false;
};
private static CommentedFileConfig buildFileConfig( Path path )
{
return CommentedFileConfig.builder( path )
.onFileNotFound( MAKE_DIRECTORIES )
.preserveInsertionOrder()
.build();
private static CommentedFileConfig buildFileConfig(Path path) {
return CommentedFileConfig.builder(path)
.onFileNotFound(MAKE_DIRECTORIES)
.preserveInsertionOrder()
.build();
}
public static void serverStarting( MinecraftServer server )
{
Path serverPath = server.getSavePath( serverDir ).resolve( serverFileName );
public static void serverStarting(MinecraftServer server) {
Path serverPath = server.getSavePath(serverDir).resolve(serverFileName);
try( CommentedFileConfig config = buildFileConfig( serverPath ) )
{
try(CommentedFileConfig config = buildFileConfig(serverPath)) {
config.load();
serverSpec.correct( config, Config::correctionListener );
serverSpec.correct(config, Config::correctionListener);
config.save();
serverConfig = config;
sync();
}
}
public static void serverStopping( MinecraftServer server )
{
public static void serverStopping(MinecraftServer server) {
serverConfig = null;
}
public static void clientStarted( MinecraftClient client )
{
try( CommentedFileConfig config = buildFileConfig( clientPath ) )
{
public static void clientStarted(MinecraftClient client) {
try (CommentedFileConfig config = buildFileConfig(clientPath)) {
config.load();
clientSpec.correct( config, Config::correctionListener );
clientSpec.correct(config, Config::correctionListener);
config.save();
clientConfig = config;
sync();
}
}
private static void correctionListener( ConfigSpec.CorrectionAction action, List<String> path, Object incorrectValue, Object correctedValue )
{
String key = String.join( ".", path );
switch( action )
{
private static void correctionListener(ConfigSpec.CorrectionAction action, List<String> path, Object incorrectValue, Object correctedValue) {
String key = String.join(".", path);
switch(action) {
case ADD:
ComputerCraft.log.warn( "Config key {} missing -> added default value.", key );
break;
ComputerCraft.log.warn("Config key {} missing -> added default value.", key); break;
case REMOVE:
ComputerCraft.log.warn( "Config key {} not defined -> removed from config.", key );
break;
ComputerCraft.log.warn("Config key {} not defined -> removed from config.", key); break;
case REPLACE:
ComputerCraft.log.warn( "Config key {} not valid -> replaced with default value.", key );
ComputerCraft.log.warn("Config key {} not valid -> replaced with default value.", key);
}
}
public static void sync()
{
if( serverConfig != null )
{
public static void sync() {
if(serverConfig != null) {
// General
ComputerCraft.computerSpaceLimit = serverConfig.<Integer>get( "computer_space_limit" );
ComputerCraft.floppySpaceLimit = serverConfig.<Integer>get( "floppy_space_limit" );
ComputerCraft.maximumFilesOpen = serverConfig.<Integer>get( "maximum_open_files" );
ComputerCraft.disableLua51Features = serverConfig.<Boolean>get( "disable_lua51_features" );
ComputerCraft.defaultComputerSettings = serverConfig.<String>get( "default_computer_settings" );
ComputerCraft.debugEnable = serverConfig.<Boolean>get( "debug_enabled" );
ComputerCraft.logComputerErrors = serverConfig.<Boolean>get( "log_computer_errors" );
ComputerCraft.commandRequireCreative = serverConfig.<Boolean>get( "command_require_creative" );
ComputerCraft.computerSpaceLimit = serverConfig.<Integer>get("computer_space_limit");
ComputerCraft.floppySpaceLimit = serverConfig.<Integer>get("floppy_space_limit");
ComputerCraft.maximumFilesOpen = serverConfig.<Integer>get("maximum_open_files");
ComputerCraft.disableLua51Features = serverConfig.<Boolean>get("disable_lua51_features");
ComputerCraft.defaultComputerSettings = serverConfig.<String>get("default_computer_settings");
ComputerCraft.debugEnable = serverConfig.<Boolean>get("debug_enabled");
ComputerCraft.logComputerErrors = serverConfig.<Boolean>get("log_computer_errors");
ComputerCraft.commandRequireCreative = serverConfig.<Boolean>get("command_require_creative");
// Execution
ComputerCraft.computerThreads = serverConfig.<Integer>get( "execution.computer_threads" );
ComputerCraft.maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos( serverConfig.<Integer>get( "execution.max_main_global_time" ) );
ComputerCraft.maxMainComputerTime = TimeUnit.MILLISECONDS.toNanos( serverConfig.<Integer>get( "execution.max_main_computer_time" ) );
ComputerCraft.computerThreads = serverConfig.<Integer>get("execution.computer_threads");
ComputerCraft.maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos(serverConfig.<Integer>get("execution.max_main_global_time"));
ComputerCraft.maxMainComputerTime = TimeUnit.MILLISECONDS.toNanos(serverConfig.<Integer>get("execution.max_main_computer_time"));
// HTTP
ComputerCraft.httpEnabled = serverConfig.<Boolean>get( "http.enabled" );
ComputerCraft.httpWebsocketEnabled = serverConfig.<Boolean>get( "http.websocket_enabled" );
ComputerCraft.httpRules = serverConfig.<List<UnmodifiableConfig>>get( "http.rules" ).stream().map( AddressRuleConfig::parseRule )
.filter( Objects::nonNull ).collect( Collectors.toList() );
ComputerCraft.httpMaxRequests = serverConfig.<Integer>get( "http.max_requests" );
ComputerCraft.httpMaxWebsockets = serverConfig.<Integer>get( "http.max_websockets" );
ComputerCraft.httpEnabled = serverConfig.<Boolean>get("http.enabled");
ComputerCraft.httpWebsocketEnabled = serverConfig.<Boolean>get("http.websocket_enabled");
ComputerCraft.httpRules = serverConfig.<List<UnmodifiableConfig>>get("http.rules").stream().map(AddressRuleConfig::parseRule)
.filter(Objects::nonNull).collect(Collectors.toList());
ComputerCraft.httpMaxRequests = serverConfig.<Integer>get("http.max_requests");
ComputerCraft.httpMaxWebsockets = serverConfig.<Integer>get("http.max_websockets");
// Peripherals
ComputerCraft.enableCommandBlock = serverConfig.<Boolean>get( "peripheral.command_block_enabled" );
ComputerCraft.modemRange = serverConfig.<Integer>get( "peripheral.modem_range" );
ComputerCraft.modemHighAltitudeRange = serverConfig.<Integer>get( "peripheral.modem_high_altitude_range" );
ComputerCraft.modemRangeDuringStorm = serverConfig.<Integer>get( "peripheral.modem_range_during_storm" );
ComputerCraft.modemHighAltitudeRangeDuringStorm = serverConfig.<Integer>get( "peripheral.modem_high_altitude_range_during_storm" );
ComputerCraft.maxNotesPerTick = serverConfig.<Integer>get( "peripheral.max_notes_per_tick" );
ComputerCraft.monitorBandwidth = serverConfig.<Integer>get( "peripheral.monitor_bandwidth" );
ComputerCraft.enableCommandBlock = serverConfig.<Boolean>get("peripheral.command_block_enabled");
ComputerCraft.modemRange = serverConfig.<Integer>get("peripheral.modem_range");
ComputerCraft.modemHighAltitudeRange = serverConfig.<Integer>get("peripheral.modem_high_altitude_range");
ComputerCraft.modemRangeDuringStorm = serverConfig.<Integer>get("peripheral.modem_range_during_storm");
ComputerCraft.modemHighAltitudeRangeDuringStorm = serverConfig.<Integer>get("peripheral.modem_high_altitude_range_during_storm");
ComputerCraft.maxNotesPerTick = serverConfig.<Integer>get("peripheral.max_notes_per_tick");
ComputerCraft.monitorBandwidth = serverConfig.<Integer>get("peripheral.monitor_bandwidth");
// Turtles
ComputerCraft.turtlesNeedFuel = serverConfig.<Boolean>get( "turtle.need_fuel" );
ComputerCraft.turtleFuelLimit = serverConfig.<Integer>get( "turtle.normal_fuel_limit" );
ComputerCraft.advancedTurtleFuelLimit = serverConfig.<Integer>get( "turtle.advanced_fuel_limit" );
ComputerCraft.turtlesObeyBlockProtection = serverConfig.<Boolean>get( "turtle.obey_block_protection" );
ComputerCraft.turtlesCanPush = serverConfig.<Boolean>get( "turtle.can_push" );
ComputerCraft.turtlesNeedFuel = serverConfig.<Boolean>get("turtle.need_fuel");
ComputerCraft.turtleFuelLimit = serverConfig.<Integer>get("turtle.normal_fuel_limit");
ComputerCraft.advancedTurtleFuelLimit = serverConfig.<Integer>get("turtle.advanced_fuel_limit");
ComputerCraft.turtlesObeyBlockProtection = serverConfig.<Boolean>get("turtle.obey_block_protection");
ComputerCraft.turtlesCanPush = serverConfig.<Boolean>get("turtle.can_push");
ComputerCraft.turtleDisabledActions.clear();
for( String value : serverConfig.<List<String>>get( "turtle.disabled_actions" ) )
{
ComputerCraft.turtleDisabledActions.add( getAction( value ) );
for(String value : serverConfig.<List<String>>get("turtle.disabled_actions")) {
ComputerCraft.turtleDisabledActions.add(getAction(value));
}
// Terminal Size
ComputerCraft.computerTermWidth = serverConfig.<Integer>get( "term_sizes.computer.width" );
ComputerCraft.computerTermHeight = serverConfig.<Integer>get( "term_sizes.computer.height" );
ComputerCraft.pocketTermWidth = serverConfig.<Integer>get( "term_sizes.pocket_computer.width" );
ComputerCraft.pocketTermHeight = serverConfig.<Integer>get( "term_sizes.pocket_computer.height" );
ComputerCraft.monitorWidth = serverConfig.<Integer>get( "term_sizes.monitor.width" );
ComputerCraft.monitorHeight = serverConfig.<Integer>get( "term_sizes.monitor.height" );
ComputerCraft.computerTermWidth = serverConfig.<Integer>get("term_sizes.computer.width");
ComputerCraft.computerTermHeight = serverConfig.<Integer>get("term_sizes.computer.height");
ComputerCraft.pocketTermWidth = serverConfig.<Integer>get("term_sizes.pocket_computer.width");
ComputerCraft.pocketTermHeight = serverConfig.<Integer>get("term_sizes.pocket_computer.height");
ComputerCraft.monitorWidth = serverConfig.<Integer>get("term_sizes.monitor.width");
ComputerCraft.monitorHeight = serverConfig.<Integer>get("term_sizes.monitor.height");
}
// Client
if( clientConfig != null )
{
ComputerCraft.monitorRenderer = clientConfig.getEnum( "monitor_renderer", MonitorRenderer.class );
int distance = clientConfig.get( "monitor_distance" );
if(clientConfig != null) {
ComputerCraft.monitorRenderer = clientConfig.getEnum("monitor_renderer", MonitorRenderer.class);
int distance = clientConfig.get("monitor_distance");
ComputerCraft.monitorDistanceSq = distance * distance;
}
}
private static final Converter<String, String> converter = CaseFormat.LOWER_CAMEL.converterTo( CaseFormat.UPPER_UNDERSCORE );
private static final Converter<String, String> converter = CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_UNDERSCORE);
private static TurtleAction getAction( String value )
{
try
{
return TurtleAction.valueOf( converter.convert( value ) );
private static TurtleAction getAction(String value) {
try {
return TurtleAction.valueOf(converter.convert(value));
}
catch( IllegalArgumentException e )
{
catch(IllegalArgumentException e) {
return null;
}
}