Merge branch 'master' into mc-1.14.x

This commit is contained in:
SquidDev 2020-05-13 14:04:28 +01:00
commit 4be0b15afa
69 changed files with 1195 additions and 141 deletions

View File

@ -32,6 +32,9 @@ jobs:
name: CC-Tweaked
path: build/libs
- name: Upload Coverage
run: bash <(curl -s https://codecov.io/bash)
lint-lua:
name: Lint Lua
runs-on: ubuntu-latest

View File

@ -17,6 +17,7 @@
plugins {
id "checkstyle"
id "jacoco"
id "com.github.hierynomus.license" version "0.15.0"
id "com.matthewprenger.cursegradle" version "1.3.0"
id "com.github.breadmoirai.github-release" version "2.2.4"
@ -288,6 +289,15 @@ task compressJson(dependsOn: jar) {
}
}
jacocoTestReport {
reports {
xml.enabled true
html.enabled true
}
}
check.dependsOn jacocoTestReport
license {
mapping("java", "SLASHSTAR_STYLE")
strictCheck true

View File

@ -1,6 +1,77 @@
--- Execute a specific command.
--
-- @tparam string command The command to execute.
-- @treturn boolean Whether the command executed successfully.
-- @treturn { string... } The output of this command, as a list of lines.
-- @treturn number|nil The number of "affected" objects, or `nil` if the command
-- failed. The definition of this varies from command to command.
-- @usage Set the block above the command computer to stone.
--
-- commands.exec("setblock ~ ~1 ~ minecraft:stone")
function exec(command) end
--- Asynchronously execute a command.
--
-- Unlike @{exec}, this will immediately return, instead of waiting for the
-- command to execute. This allows you to run multiple commands at the same
-- time.
--
-- When this command has finished executing, it will queue a `task_complete`
-- event containing the result of executing this command (what @{exec} would
-- return).
--
-- @tparam string command The command to execute.
-- @treturn number The "task id". When this command has been executed, it will
-- queue a `task_complete` event with a matching id.
-- @usage Asynchronously sets the block above the computer to stone.
--
-- commands.execAsync("~ ~1 ~ minecraft:stone")
-- @see parallel One may also use the parallel API to run multiple commands at
-- once.
function execAsync(commad) end
--- List all available commands which the computer has permission to execute.
--
-- @treturn { string... } A list of all available commands
function list() end
--- Get the position of the current command computer.
--
-- @treturn number This computer's x position.
-- @treturn number This computer's y position.
-- @treturn number This computer's z position.
-- @see gps.locate To get the position of a non-command computer.
function getBlockPosition() end
function getBlockInfos(min_x, min_y, min_z, max_x, max_y, max_z) end
--- Get some basic information about a block.
--
-- The returned table contains the current name, metadata and block state (as
-- with @{turtle.inspect}). If there is a tile entity for that block, its NBT
-- will also be returned.
--
-- @tparam number x The x position of the block to query.
-- @tparam number y The y position of the block to query.
-- @tparam number z The z position of the block to query.
-- @treturn table The given block's information.
-- @throws If the coordinates are not within the world, or are not currently
-- loaded.
function getBlockInfo(x, y, z) end
--- Get information about a range of blocks.
--
-- This returns the same information as @{getBlockInfo}, just for multiple
-- blocks at once.
--
-- Blocks are traversed by ascending y level, followed by z and x - the returned
-- table may be indexed using `x + z*width + y*depth*depth`.
--
-- @tparam number min_x The start x coordinate of the range to query.
-- @tparam number min_y The start y coordinate of the range to query.
-- @tparam number min_z The start z coordinate of the range to query.
-- @tparam number max_x The end x coordinate of the range to query.
-- @tparam number max_y The end y coordinate of the range to query.
-- @tparam number max_z The end z coordinate of the range to query.
-- @treturn { table... } A list of information about each block.
-- @throws If the coordinates are not within the world.
-- @throws If trying to get information about more than 4096 blocks.
function getBlockInfos(min_x, min_y, min_z, max_x, max_y, max_z) end

View File

@ -19,6 +19,18 @@ function getFreeSpace(path) end
function find(pattern) end
function getDir(path) end
--- 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 exstend this to
-- make other mount types by correctly assigning their return value for getDrive.
--
-- @tparam string path The path to check.
-- @treturn boolean If the path is mounted, rather than a normal file/folder.
-- @throws If the path does not exist.
-- @see getDrive
function isDriveRoot(path) end
--- Get the capacity of the drive at the given path.
--
-- This may be used in conjunction with @{getFreeSpace} to determine what

View File

@ -1,5 +1,5 @@
# Mod properties
mod_version=1.87.1
mod_version=1.88.0
# Minecraft properties (update mods.toml when changing)
mc_version=1.14.4

View File

@ -70,14 +70,12 @@
;; Suppress warnings for currently undocumented modules.
(at
(/doc/stub/commands.lua
/doc/stub/fs.lua
(/doc/stub/fs.lua
/doc/stub/http.lua
/doc/stub/os.lua
/doc/stub/redstone.lua
/doc/stub/term.lua
/doc/stub/turtle.lua
/src/main/resources/*/computercraft/lua/rom/apis/command/commands.lua
/src/main/resources/*/computercraft/lua/rom/apis/io.lua
/src/main/resources/*/computercraft/lua/rom/apis/window.lua
/src/main/resources/*/computercraft/lua/rom/modules/main/cc/shell/completion.lua)

View File

@ -65,7 +65,8 @@ public final class ComputerCraft
public static boolean disable_lua51_features = false;
public static String default_computer_settings = "";
public static boolean debug_enable = true;
public static boolean logPeripheralErrors = false;
public static boolean logPeripheralErrors = true;
public static boolean commandRequireCreative = true;
public static int computer_threads = 1;
public static long maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos( 10 );

View File

@ -50,12 +50,12 @@ private FixedWidthFontRenderer()
{
}
private static float toGreyscale( double[] rgb )
public static float toGreyscale( double[] rgb )
{
return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3);
}
private static int getColour( char c, Colour def )
public static int getColour( char c, Colour def )
{
return 15 - Terminal.getColour( c, def );
}

View File

@ -0,0 +1,174 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.render;
import com.google.common.base.Strings;
import com.mojang.blaze3d.platform.GLX;
import com.mojang.blaze3d.platform.TextureUtil;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.shared.util.Palette;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;
class MonitorTextureBufferShader
{
static final int TEXTURE_INDEX = GL13.GL_TEXTURE3;
private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer( 16 );
private static final FloatBuffer PALETTE_BUFFER = BufferUtils.createFloatBuffer( 16 * 3 );
private static int uniformMv;
private static int uniformP;
private static int uniformFont;
private static int uniformWidth;
private static int uniformHeight;
private static int uniformTbo;
private static int uniformPalette;
private static boolean initialised;
private static boolean ok;
private static int program;
static void setupUniform( int width, int height, Palette palette, boolean greyscale )
{
MATRIX_BUFFER.rewind();
GL11.glGetFloatv( GL11.GL_MODELVIEW_MATRIX, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
GLX.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
GL11.glGetFloatv( GL11.GL_PROJECTION_MATRIX, MATRIX_BUFFER );
MATRIX_BUFFER.rewind();
GLX.glUniformMatrix4( uniformP, false, MATRIX_BUFFER );
GLX.glUniform1i( uniformWidth, width );
GLX.glUniform1i( uniformHeight, height );
PALETTE_BUFFER.rewind();
for( int i = 0; i < 16; i++ )
{
double[] colour = palette.getColour( i );
if( greyscale )
{
float f = FixedWidthFontRenderer.toGreyscale( colour );
PALETTE_BUFFER.put( f ).put( f ).put( f );
}
else
{
PALETTE_BUFFER.put( (float) colour[0] ).put( (float) colour[1] ).put( (float) colour[2] );
}
}
PALETTE_BUFFER.flip();
GLX.glUniform3( uniformPalette, PALETTE_BUFFER );
}
static boolean use()
{
if( initialised )
{
if( ok ) GLX.glUseProgram( program );
return ok;
}
if( ok = load() )
{
GL20.glUseProgram( program );
GLX.glUniform1i( uniformFont, 0 );
GLX.glUniform1i( uniformTbo, TEXTURE_INDEX - GL13.GL_TEXTURE0 );
}
return ok;
}
private static boolean load()
{
initialised = true;
try
{
int vertexShader = loadShader( GL20.GL_VERTEX_SHADER, "assets/computercraft/shaders/monitor.vert" );
int fragmentShader = loadShader( GL20.GL_FRAGMENT_SHADER, "assets/computercraft/shaders/monitor.frag" );
program = GLX.glCreateProgram();
GLX.glAttachShader( program, vertexShader );
GLX.glAttachShader( program, fragmentShader );
GL20.glBindAttribLocation( program, 0, "v_pos" );
GLX.glLinkProgram( program );
boolean ok = GLX.glGetProgrami( program, GL20.GL_LINK_STATUS ) != 0;
String log = GLX.glGetProgramInfoLog( program, Short.MAX_VALUE ).trim();
if( !Strings.isNullOrEmpty( log ) )
{
ComputerCraft.log.warn( "Problems when linking monitor shader: {}", log );
}
GL20.glDetachShader( program, vertexShader );
GL20.glDetachShader( program, fragmentShader );
GLX.glDeleteShader( vertexShader );
GLX.glDeleteShader( fragmentShader );
if( !ok ) return false;
uniformMv = getUniformLocation( program, "u_mv" );
uniformP = getUniformLocation( program, "u_p" );
uniformFont = getUniformLocation( program, "u_font" );
uniformWidth = getUniformLocation( program, "u_width" );
uniformHeight = getUniformLocation( program, "u_height" );
uniformTbo = getUniformLocation( program, "u_tbo" );
uniformPalette = getUniformLocation( program, "u_palette" );
ComputerCraft.log.info( "Loaded monitor shader." );
return true;
}
catch( Exception e )
{
ComputerCraft.log.error( "Cannot load monitor shaders", e );
return false;
}
}
private static int loadShader( int kind, String path ) throws IOException
{
String contents;
try( InputStream stream = TileEntityMonitorRenderer.class.getClassLoader().getResourceAsStream( path ) )
{
if( stream == null ) throw new IllegalArgumentException( "Cannot find " + path );
contents = TextureUtil.readResourceAsString( stream );
}
int shader = GLX.glCreateShader( kind );
GLX.glShaderSource( shader, contents );
GLX.glCompileShader( shader );
boolean ok = GLX.glGetShaderi( shader, GL20.GL_COMPILE_STATUS ) != 0;
String log = GLX.glGetShaderInfoLog( shader, Short.MAX_VALUE ).trim();
if( !Strings.isNullOrEmpty( log ) )
{
ComputerCraft.log.warn( "Problems when loading monitor shader {}: {}", path, log );
}
if( !ok ) throw new IllegalStateException( "Cannot compile shader " + path );
return shader;
}
private static int getUniformLocation( int program, String name )
{
int uniform = GLX.glGetUniformLocation( program, name );
if( uniform == -1 ) throw new IllegalStateException( "Cannot find uniform " + name );
return uniform;
}
}

View File

@ -10,21 +10,30 @@
import dan200.computercraft.client.FrameInfo;
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.shared.util.DirectionUtil;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GLAllocation;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.tileentity.TileEntityRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL31;
import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import static dan200.computercraft.client.gui.FixedWidthFontRenderer.*;
import static dan200.computercraft.shared.peripheral.monitor.TileMonitor.RENDER_MARGIN;
public class TileEntityMonitorRenderer extends TileEntityRenderer<TileMonitor>
@ -92,8 +101,8 @@ public void render( @Nonnull TileMonitor monitor, double posX, double posY, doub
if( terminal != null )
{
// Draw a terminal
double xScale = xSize / (terminal.getWidth() * FixedWidthFontRenderer.FONT_WIDTH);
double yScale = ySize / (terminal.getHeight() * FixedWidthFontRenderer.FONT_HEIGHT);
double xScale = xSize / (terminal.getWidth() * FONT_WIDTH);
double yScale = ySize / (terminal.getHeight() * FONT_HEIGHT);
GlStateManager.pushMatrix();
GlStateManager.scaled( (float) xScale, (float) -yScale, 1.0f );
@ -142,6 +151,52 @@ private static void renderTerminal( ClientMonitor monitor, float xMargin, float
switch( renderer )
{
case TBO:
{
if( !MonitorTextureBufferShader.use() ) return;
Terminal terminal = monitor.getTerminal();
int width = terminal.getWidth(), height = terminal.getHeight();
int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT;
if( redraw )
{
ByteBuffer monitorBuffer = GLAllocation.createDirectByteBuffer( width * height * 3 );
for( int y = 0; y < height; y++ )
{
TextBuffer text = terminal.getLine( y ), textColour = terminal.getTextColourLine( y ), background = terminal.getBackgroundColourLine( y );
for( int x = 0; x < width; x++ )
{
monitorBuffer.put( (byte) (text.charAt( x ) & 0xFF) );
monitorBuffer.put( (byte) getColour( textColour.charAt( x ), Colour.White ) );
monitorBuffer.put( (byte) getColour( background.charAt( x ), Colour.Black ) );
}
}
monitorBuffer.flip();
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, monitor.tboBuffer );
GLX.glBufferData( GL31.GL_TEXTURE_BUFFER, monitorBuffer, GL15.GL_STATIC_DRAW );
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, 0 );
}
// Bind TBO texture and set up the uniforms. We've already set up the main font above.
GlStateManager.activeTexture( MonitorTextureBufferShader.TEXTURE_INDEX );
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, monitor.tboTexture );
GlStateManager.activeTexture( GL13.GL_TEXTURE0 );
MonitorTextureBufferShader.setupUniform( width, height, terminal.getPalette(), !monitor.isColour() );
buffer.begin( GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION );
buffer.pos( -xMargin, -yMargin, 0 ).endVertex();
buffer.pos( -xMargin, pixelHeight + yMargin, 0 ).endVertex();
buffer.pos( pixelWidth + xMargin, -yMargin, 0 ).endVertex();
buffer.pos( pixelWidth + xMargin, pixelHeight + yMargin, 0 ).endVertex();
tessellator.draw();
GLX.glUseProgram( 0 );
break;
}
case VBO:
{
VertexBuffer vbo = monitor.buffer;

View File

@ -13,6 +13,7 @@
import dan200.computercraft.api.turtle.event.TurtleAction;
import dan200.computercraft.core.apis.AddressPredicate;
import dan200.computercraft.core.apis.http.websocket.Websocket;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ModLoadingContext;
@ -74,7 +75,10 @@ public final class Config
private static final ConfigValue<Boolean> turtlesCanPush;
private static final ConfigValue<List<? extends String>> turtleDisabledActions;
private static final ForgeConfigSpec spec;
private static final ConfigValue<MonitorRenderer> monitorRenderer;
private static final ForgeConfigSpec commonSpec;
private static final ForgeConfigSpec clientSpec;
private Config() {}
@ -260,12 +264,20 @@ private Config() {}
builder.pop();
}
spec = builder.build();
commonSpec = builder.build();
Builder clientBuilder = new Builder();
monitorRenderer = clientBuilder
.comment( "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." )
.defineEnum( "monitor_renderer", MonitorRenderer.BEST );
clientSpec = clientBuilder.build();
}
public static void load()
{
ModLoadingContext.get().registerConfig( ModConfig.Type.COMMON, spec );
ModLoadingContext.get().registerConfig( ModConfig.Type.COMMON, commonSpec );
ModLoadingContext.get().registerConfig( ModConfig.Type.CLIENT, clientSpec );
}
public static void sync()
@ -315,6 +327,9 @@ public static void sync()
ComputerCraft.turtleDisabledActions.clear();
for( String value : turtleDisabledActions.get() ) ComputerCraft.turtleDisabledActions.add( getAction( value ) );
// Client
ComputerCraft.monitorRenderer = monitorRenderer.get();
}
@SubscribeEvent

View File

@ -120,6 +120,11 @@ protected ServerComputer createComputer( int instanceID, int id )
@Override
public boolean isUsable( PlayerEntity player, boolean ignoreRange )
{
return isUsable( player ) && super.isUsable( player, ignoreRange );
}
public static boolean isUsable( PlayerEntity player )
{
MinecraftServer server = player.getServer();
if( server == null || !server.isCommandBlockEnabled() )
@ -127,14 +132,12 @@ public boolean isUsable( PlayerEntity player, boolean ignoreRange )
player.sendStatusMessage( new TranslationTextComponent( "advMode.notEnabled" ), true );
return false;
}
else if( !player.canUseCommandBlock() )
else if( ComputerCraft.commandRequireCreative ? !player.canUseCommandBlock() : !server.getPlayerList().canSendCommands( player.getGameProfile() ) )
{
player.sendStatusMessage( new TranslationTextComponent( "advMode.notAllowed" ), true );
return false;
}
else
{
return super.isUsable( player, ignoreRange );
}
return true;
}
}

View File

@ -35,4 +35,3 @@ public String toString()
return name;
}
}

View File

@ -6,6 +6,7 @@
package dan200.computercraft.shared.computer.inventory;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.shared.computer.blocks.TileCommandComputer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.IContainerComputer;
import dan200.computercraft.shared.computer.core.ServerComputer;
@ -14,8 +15,6 @@
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.container.ContainerType;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TranslationTextComponent;
import javax.annotation.Nonnull;
@ -48,18 +47,9 @@ private static boolean canInteractWith( @Nonnull ServerComputer computer, @Nonnu
}
// If we're a command computer then ensure we're in creative
if( computer.getFamily() == ComputerFamily.Command )
if( computer.getFamily() == ComputerFamily.Command && !TileCommandComputer.isUsable( player ) )
{
MinecraftServer server = player.getServer();
if( server == null || !server.isCommandBlockEnabled() )
{
return false;
}
else if( !player.canUseCommandBlock() )
{
player.sendStatusMessage( new TranslationTextComponent( "advMode.notAllowed" ), false );
return false;
}
return false;
}
return true;

View File

@ -5,12 +5,18 @@
*/
package dan200.computercraft.shared.peripheral.monitor;
import com.mojang.blaze3d.platform.GLX;
import com.mojang.blaze3d.platform.GlStateManager;
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.shared.common.ClientTerminal;
import net.minecraft.client.renderer.vertex.VertexBuffer;
import net.minecraft.util.math.BlockPos;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import java.util.HashSet;
import java.util.Iterator;
@ -25,6 +31,8 @@ public final class ClientMonitor extends ClientTerminal
public long lastRenderFrame = -1;
public BlockPos lastRenderPos = null;
public int tboBuffer;
public int tboTexture;
public VertexBuffer buffer;
public ClientMonitor( boolean colour, TileMonitor origin )
@ -50,6 +58,26 @@ public boolean createBuffer( MonitorRenderer renderer )
{
switch( renderer )
{
case TBO:
{
if( tboBuffer != 0 ) return false;
deleteBuffers();
tboBuffer = GLX.glGenBuffers();
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, tboBuffer );
GL15.glBufferData( GL31.GL_TEXTURE_BUFFER, 0, GL15.GL_STATIC_DRAW );
tboTexture = GlStateManager.genTexture();
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, tboTexture );
GL31.glTexBuffer( GL31.GL_TEXTURE_BUFFER, GL30.GL_R8, tboBuffer );
GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, 0 );
GLX.glBindBuffer( GL31.GL_TEXTURE_BUFFER, 0 );
addMonitor();
return true;
}
case VBO:
if( buffer != null ) return false;
@ -73,6 +101,19 @@ private void addMonitor()
private void deleteBuffers()
{
if( tboBuffer != 0 )
{
GLX.glDeleteBuffers( tboBuffer );
tboBuffer = 0;
}
if( tboTexture != 0 )
{
GlStateManager.deleteTexture( tboTexture );
tboTexture = 0;
}
if( buffer != null )
{
buffer.deleteGlBuffers();
@ -83,7 +124,7 @@ private void deleteBuffers()
@OnlyIn( Dist.CLIENT )
public void destroy()
{
if( buffer != null )
if( tboBuffer != 0 || buffer != null )
{
synchronized( allMonitors )
{

View File

@ -6,12 +6,11 @@
package dan200.computercraft.shared.peripheral.monitor;
import com.mojang.blaze3d.platform.GLX;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.render.TileEntityMonitorRenderer;
import org.lwjgl.opengl.GL;
import javax.annotation.Nonnull;
import java.util.Locale;
/**
* The render type to use for monitors.
@ -26,6 +25,13 @@ public enum MonitorRenderer
*/
BEST,
/**
* Render using texture buffer objects.
*
* @see org.lwjgl.opengl.GL31#glTexBuffer(int, int, int)
*/
TBO,
/**
* Render using VBOs.
*
@ -33,37 +39,6 @@ public enum MonitorRenderer
*/
VBO;
private static final MonitorRenderer[] VALUES = values();
public static final String[] NAMES;
private final String displayName = "gui.computercraft:config.peripheral.monitor_renderer." + name().toLowerCase( Locale.ROOT );
static
{
NAMES = new String[VALUES.length];
for( int i = 0; i < VALUES.length; i++ ) NAMES[i] = VALUES[i].displayName();
}
public String displayName()
{
return displayName;
}
@Nonnull
public static MonitorRenderer ofString( String name )
{
for( MonitorRenderer backend : VALUES )
{
if( backend.displayName.equalsIgnoreCase( name ) || backend.name().equalsIgnoreCase( name ) )
{
return backend;
}
}
ComputerCraft.log.warn( "Unknown monitor renderer {}. Falling back to default.", name );
return BEST;
}
/**
* Get the current renderer to use.
*
@ -77,15 +52,16 @@ public static MonitorRenderer current()
{
case BEST:
return best();
case VBO:
if( !GLX.useVbo() )
case TBO:
checkCapabilities();
if( !textureBuffer )
{
ComputerCraft.log.warn( "VBOs are not supported on your graphics card. Falling back to default." );
ComputerCraft.log.warn( "Texture buffers are not supported on your graphics card. Falling back to default." );
ComputerCraft.monitorRenderer = BEST;
return best();
}
return VBO;
return TBO;
default:
return current;
}
@ -93,6 +69,18 @@ public static MonitorRenderer current()
private static MonitorRenderer best()
{
return VBO;
checkCapabilities();
return textureBuffer ? TBO : VBO;
}
private static boolean initialised = false;
private static boolean textureBuffer = false;
private static void checkCapabilities()
{
if( initialised ) return;
textureBuffer = GL.getCapabilities().OpenGL31;
initialised = true;
}
}

View File

@ -154,4 +154,3 @@ private synchronized boolean playSound( ILuaContext context, ResourceLocation na
return true;
}
}

View File

@ -78,4 +78,3 @@ public boolean equals( @Nullable IPeripheral other )
}
}
}

View File

@ -6,4 +6,3 @@
"modem=true,peripheral=true": { "model": "computercraft:block/wired_modem_full_on_peripheral" }
}
}

View File

@ -0,0 +1,40 @@
#version 140
#define FONT_WIDTH 6.0
#define FONT_HEIGHT 9.0
uniform sampler2D u_font;
uniform int u_width;
uniform int u_height;
uniform samplerBuffer u_tbo;
uniform vec3 u_palette[16];
in vec2 f_pos;
out vec4 colour;
vec2 texture_corner(int index) {
float x = 1.0 + float(index % 16) * (FONT_WIDTH + 2.0);
float y = 1.0 + float(index / 16) * (FONT_HEIGHT + 2.0);
return vec2(x, y);
}
void main() {
vec2 term_pos = vec2(f_pos.x / FONT_WIDTH, f_pos.y / FONT_HEIGHT);
vec2 corner = floor(term_pos);
ivec2 cell = ivec2(corner);
int index = 3 * (clamp(cell.x, 0, u_width - 1) + clamp(cell.y, 0, u_height - 1) * u_width);
// 1 if 0 <= x, y < width, height, 0 otherwise
vec2 outside = step(vec2(0.0, 0.0), vec2(cell)) * step(vec2(cell), vec2(float(u_width) - 1.0, float(u_height) - 1.0));
float mult = outside.x * outside.y;
int character = int(texelFetch(u_tbo, index).r * 255.0);
int fg = int(texelFetch(u_tbo, index + 1).r * 255.0);
int bg = int(texelFetch(u_tbo, index + 2).r * 255.0);
vec2 pos = (term_pos - corner) * vec2(FONT_WIDTH, FONT_HEIGHT);
vec4 img = texture2D(u_font, (texture_corner(character) + pos) / 256.0);
colour = vec4(mix(u_palette[bg], img.rgb * u_palette[fg], img.a * mult), 1.0);
}

View File

@ -0,0 +1,13 @@
#version 140
uniform mat4 u_mv;
uniform mat4 u_p;
in vec3 v_pos;
out vec2 f_pos;
void main() {
gl_Position = u_p * u_mv * vec4(v_pos.x, v_pos.y, 0, 1);
f_pos = v_pos.xy;
}

View File

@ -797,6 +797,12 @@ function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
return tEmpty
end
function fs.isDriveRoot(sPath)
expect(1, sPath, "string")
-- Force the root directory to be a mount.
return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath))
end
-- Load APIs
local bAPIError = false
local tApis = fs.list("rom/apis")
@ -932,6 +938,11 @@ settings.define("motd.path", {
description = [[The path to load random messages from. Should be a colon (":") separated string of file paths.]],
type = "string",
})
settings.define("lua.warn_against_use_of_local", {
default = true,
description = [[Print a message when input in the Lua REPL starts with the word 'local'. Local variables defined in the Lua REPL are be inaccessable on the next input.]],
type = "boolean",
})
if term.isColour() then
settings.define("bios.use_multishell", {
default = true,

View File

@ -12,6 +12,9 @@
-- [mc]: https://minecraft.gamepedia.com/Commands
--
-- @module commands
-- @usage Set the block above this computer to stone:
--
-- commands.setblock("~", "~1", "~", "minecraft:stone")
if not commands then
error("Cannot load command API on normal computer", 2)
@ -97,4 +100,13 @@ for _, sCommandName in ipairs(native.list()) do
tAsync[sCommandName] = mk_command({ sCommandName }, bJSONIsNBT, native.execAsync)
end
end
--- A table containing asynchronous wrappers for all commands.
--
-- As with @{commands.execAsync}, this returns the "task id" of the enqueued
-- command.
-- @see execAsync
-- @usage Asynchronously sets the block above the computer to stone.
--
-- commands.async.setblock("~", "~1", "~", "minecraft:stone")
env.async = tAsync

View File

@ -75,7 +75,10 @@ handleMetatable = {
if not handle.read then return nil, "file is not readable" end
local args = table.pack(...)
return function() return checkResult(self, self:read(table.unpack(args, 1, args.n))) end
return function()
if self._closed then error("file is already closed", 2) end
return checkResult(self, self:read(table.unpack(args, 1, args.n)))
end
end,
read = function(self, ...)
@ -259,12 +262,13 @@ end
-- instead. In this case, the handle is not used.
--
-- @tparam[opt] string filename The name of the file to extract lines from
-- @param ... The argument to pass to @{Handle:read} for each line.
-- @treturn function():string|nil The line iterator.
-- @throws If the file cannot be opened for reading
--
-- @see Handle:lines
-- @see io.input
function lines(filename)
function lines(filename, ...)
expect(1, filename, "string", "nil")
if filename then
local ok, err = open(filename, "rb")
@ -273,9 +277,9 @@ function lines(filename)
-- We set this magic flag to mark this file as being opened by io.lines and so should be
-- closed automatically
ok._autoclose = true
return ok:lines()
return ok:lines(...)
else
return currentInput:lines()
return currentInput:lines(...)
end
end
@ -313,7 +317,7 @@ end
-- @throws If the provided filename cannot be opened for writing.
function output(file)
if type_of(file) == "string" then
local res, err = open(file, "w")
local res, err = open(file, "wb")
if not res then error(err, 2) end
currentOutput = res
elseif type_of(file) == "table" and getmetatable(file) == handleMetatable then

View File

@ -1,3 +1,17 @@
# New features in CC: Tweaked 1.88.0
* Computers and turtles now preserve their ID when broken.
* Add `peripheral.getName` - returns the name of a wrapped peripheral.
* Reduce network overhead of monitors and terminals.
* Add a TBO backend for monitors, with a significant performance boost.
* The Lua REPL warns when declaring locals (lupus590, exerro)
* Add config to allow using command computers in survival.
* Add fs.isDriveRoot - checks if a path is the root of a drive.
And several bug fixes:
* Fix io.lines not accepting arguments.
* Fix settings.load using an unknown global (MCJack123).
# New features in CC: Tweaked 1.87.1
* Fix blocks not dropping items in survival.

View File

@ -1,4 +1,3 @@
ComputerCraft was created by Daniel "dan200" Ratcliffe, with additional code by Aaron "Cloudy" Mills.
Thanks to nitrogenfingers, GopherATL and RamiLego for program contributions.
Thanks to Mojang, the Forge team, and the MCP team.

View File

@ -1,5 +1,15 @@
New features in CC: Tweaked 1.87.1
New features in CC: Tweaked 1.88.0
* Fix blocks not dropping items in survival.
* Computers and turtles now preserve their ID when broken.
* Add `peripheral.getName` - returns the name of a wrapped peripheral.
* Reduce network overhead of monitors and terminals.
* Add a TBO backend for monitors, with a significant performance boost.
* The Lua REPL warns when declaring locals (lupus590, exerro)
* Add config to allow using command computers in survival.
* Add fs.isDriveRoot - checks if a path is the root of a drive.
And several bug fixes:
* Fix io.lines not accepting arguments.
* Fix settings.load using an unknown global (MCJack123).
Type "help changelog" to see the full version history.

View File

@ -1,4 +1,3 @@
if not shell.openTab then
printError("Requires multishell")
return

View File

@ -1,4 +1,3 @@
if not shell.openTab then
printError("Requires multishell")
return

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
if #tArgs > 2 then
print("Usage: alias <alias> <program>")

View File

@ -1,4 +1,3 @@
local tApis = {}
for k, v in pairs(_G) do
if type(k) == "string" and type(v) == "table" and k ~= "_G" then

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
if #tArgs < 1 then
print("Usage: cd <path>")

View File

@ -1,4 +1,3 @@
if not commands then
printError("Requires a Command Computer.")
return

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
if not commands then
printError("Requires a Command Computer.")

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
if #tArgs < 2 then
print("Usage: cp <source> <destination>")

View File

@ -9,9 +9,18 @@ for i = 1, args.n do
local files = fs.find(shell.resolve(args[i]))
if #files > 0 then
for _, file in ipairs(files) do
local ok, err = pcall(fs.delete, file)
if not ok then
printError((err:gsub("^pcall: ", "")))
if fs.isReadOnly(file) then
printError("Cannot delete read-only file /" .. file)
elseif fs.isDriveRoot(file) then
printError("Cannot delete mount /" .. file)
if fs.isDir(file) then
print("To delete its contents run rm /" .. fs.combine(file, "*"))
end
else
local ok, err = pcall(fs.delete, file)
if not ok then
printError((err:gsub("^pcall: ", "")))
end
end
end
else

View File

@ -1,4 +1,3 @@
-- Get arguments
local tArgs = { ... }
if #tArgs == 0 then

View File

@ -1,4 +1,3 @@
local tBiomes = {
"in a forest",
"in a pine forest",

View File

@ -1,4 +1,3 @@
-- Display the start screen
local w, h = term.getSize()

View File

@ -1,4 +1,3 @@
local function printUsage()
print("Usages:")
print("gps host")

View File

@ -1,4 +1,3 @@
local function printUsage()
print("Usages:")
print("pastebin put <filename>")

View File

@ -1,4 +1,3 @@
local function printUsage()
print("Usage:")
print("wget <url> [filename]")

View File

@ -1,4 +1,3 @@
local sDrive = nil
local tArgs = { ... }
if #tArgs > 0 then

View File

@ -1,4 +1,3 @@
local function printUsage()
print("Usages:")
print("label get")

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
-- Get all the files in the directory

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
if #tArgs > 0 then
print("This is an interactive Lua prompt.")
@ -67,6 +66,13 @@ while bRunning do
if s:match("%S") and tCommandHistory[#tCommandHistory] ~= s then
table.insert(tCommandHistory, s)
end
if settings.get("lua.warn_against_use_of_local") and s:match("^%s*local%s+") then
if term.isColour() then
term.setTextColour(colours.yellow)
end
print("To access local variables in later inputs, remove the local keyword.")
term.setTextColour(colours.white)
end
local nForcePrint = 0
local func, e = load(s, "=lua", "t", tEnv)

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
if #tArgs < 2 then
print("Usage: mv <source> <destination>")
@ -8,12 +7,35 @@ end
local sSource = shell.resolve(tArgs[1])
local sDest = shell.resolve(tArgs[2])
local tFiles = fs.find(sSource)
local function sanity_checks(source, dest)
if fs.exists(dest) then
printError("Destination exists")
return false
elseif fs.isReadOnly(dest) then
printError("Destination is read-only")
return false
elseif fs.isDriveRoot(source) then
printError("Cannot move mount /" .. source)
return false
elseif fs.isReadOnly(source) then
printError("Cannot move read-only file /" .. source)
return false
end
return true
end
if #tFiles > 0 then
for _, sFile in ipairs(tFiles) do
if fs.isDir(sDest) then
fs.move(sFile, fs.combine(sDest, fs.getName(sFile)))
local dest = fs.combine(sDest, fs.getName(sFile))
if sanity_checks(sFile, dest) then
fs.move(sFile, dest)
end
elseif #tFiles == 1 then
fs.move(sFile, sDest)
if sanity_checks(sFile, sDest) then
fs.move(sFile, sDest)
end
else
printError("Cannot overwrite file multiple times")
return

View File

@ -1,4 +1,3 @@
local bAll = false
local tArgs = { ... }
if #tArgs > 0 and tArgs[1] == "all" then

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
local function printUsage()

View File

@ -1,4 +1,3 @@
-- Find modems
local tModems = {}
for _, sModem in ipairs(peripheral.getNames()) do

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
local function printUsage()

View File

@ -10,6 +10,12 @@ local sDest = shell.resolve(tArgs[2])
if not fs.exists(sSource) then
printError("No matching files")
return
elseif fs.isDriveRoot(sSource) then
printError("Can't rename mounts")
return
elseif fs.isReadOnly(sSource) then
printError("Source is read-only")
return
elseif fs.exists(sDest) then
printError("Destination exists")
return

View File

@ -1,4 +1,3 @@
local tArgs = { ... }
if #tArgs < 1 then
print("Usage: type <path>")
@ -15,4 +14,3 @@ if fs.exists(sPath) then
else
print("No such path")
end

View File

@ -1,4 +1,3 @@
--
-- Lua IDE
-- Made by GravityScore

File diff suppressed because one or more lines are too long

View File

@ -30,12 +30,14 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
@ -58,6 +60,8 @@
*/
public class ComputerTestDelegate
{
private static final File REPORT_PATH = new File( "test-files/luacov.report.out" );
private static final Logger LOG = LogManager.getLogger( ComputerTestDelegate.class );
private static final long TICK_TIME = TimeUnit.MILLISECONDS.toNanos( 50 );
@ -77,12 +81,15 @@ public class ComputerTestDelegate
private final Condition hasFinished = lock.newCondition();
private boolean finished = false;
private Map<String, Map<Double, Double>> finishedWith;
@BeforeEach
public void before() throws IOException
{
ComputerCraft.logPeripheralErrors = true;
if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." );
Terminal term = new Terminal( 78, 20 );
IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 );
@ -264,6 +271,13 @@ else if( !computer.isOn() )
try
{
finished = true;
if( arguments.length > 0 )
{
@SuppressWarnings( "unchecked" )
Map<String, Map<Double, Double>> finished = (Map<String, Map<Double, Double>>) arguments[0];
finishedWith = finished;
}
hasFinished.signal();
}
finally
@ -281,7 +295,7 @@ else if( !computer.isOn() )
}
@AfterEach
public void after() throws InterruptedException
public void after() throws InterruptedException, IOException
{
try
{
@ -316,6 +330,14 @@ public void after() throws InterruptedException
// And shutdown
computer.shutdown();
}
if( finishedWith != null )
{
try( BufferedWriter writer = Files.newBufferedWriter( REPORT_PATH.toPath() ) )
{
new LuaCoverage( finishedWith ).write( writer );
}
}
}
@TestFactory

View File

@ -0,0 +1,158 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2020. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core;
import com.google.common.base.Strings;
import dan200.computercraft.ComputerCraft;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import org.squiddev.cobalt.Prototype;
import org.squiddev.cobalt.compiler.CompileException;
import org.squiddev.cobalt.compiler.LuaC;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class LuaCoverage
{
private static final Path ROOT = new File( "src/main/resources/assets/computercraft/lua" ).toPath();
private static final Path BIOS = ROOT.resolve( "bios.lua" );
private static final Path APIS = ROOT.resolve( "rom/apis" );
private static final Path SHELL = ROOT.resolve( "rom/programs/shell.lua" );
private static final Path MULTISHELL = ROOT.resolve( "rom/programs/advanced/multishell.lua" );
private static final Path TREASURE = ROOT.resolve( "treasure" );
private final Map<String, Map<Double, Double>> coverage;
private final String blank;
private final String zero;
private final String countFormat;
LuaCoverage( Map<String, Map<Double, Double>> coverage )
{
this.coverage = coverage;
int max = (int) coverage.values().stream()
.flatMapToDouble( x -> x.values().stream().mapToDouble( y -> y ) )
.max().orElse( 0 );
int maxLen = Math.max( 1, (int) Math.ceil( Math.log10( max ) ) );
blank = Strings.repeat( " ", maxLen + 1 );
zero = Strings.repeat( "*", maxLen ) + "0";
countFormat = "%" + (maxLen + 1) + "d";
}
void write( Writer out ) throws IOException
{
Files.find( ROOT, Integer.MAX_VALUE, ( path, attr ) -> attr.isRegularFile() && !path.startsWith( TREASURE ) ).forEach( path -> {
Path relative = ROOT.relativize( path );
String full = relative.toString().replace( '\\', '/' );
if( !full.endsWith( ".lua" ) ) return;
Map<Double, Double> files = Stream.of(
coverage.remove( "/" + full ),
path.equals( BIOS ) ? coverage.remove( "bios.lua" ) : null,
path.equals( SHELL ) ? coverage.remove( "shell.lua" ) : null,
path.equals( MULTISHELL ) ? coverage.remove( "multishell.lua" ) : null,
path.startsWith( APIS ) ? coverage.remove( path.getFileName().toString() ) : null
)
.filter( Objects::nonNull )
.flatMap( x -> x.entrySet().stream() )
.collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, Double::sum ) );
try
{
writeCoverageFor( out, path, files );
}
catch( IOException e )
{
throw new UncheckedIOException( e );
}
} );
for( String filename : coverage.keySet() )
{
if( filename.startsWith( "/test-rom/" ) ) continue;
ComputerCraft.log.warn( "Unknown file {}", filename );
}
}
private void writeCoverageFor( Writer out, Path fullName, Map<Double, Double> visitedLines ) throws IOException
{
if( !Files.exists( fullName ) )
{
ComputerCraft.log.error( "Cannot locate file {}", fullName );
return;
}
IntSet activeLines = getActiveLines( fullName.toFile() );
out.write( "==============================================================================\n" );
out.write( fullName.toString().replace( '\\', '/' ) );
out.write( "\n" );
out.write( "==============================================================================\n" );
try( BufferedReader reader = Files.newBufferedReader( fullName ) )
{
String line;
int lineNo = 0;
while( (line = reader.readLine()) != null )
{
lineNo++;
Double count = visitedLines.get( (double) lineNo );
if( count != null )
{
out.write( String.format( countFormat, count.intValue() ) );
}
else if( activeLines.contains( lineNo ) )
{
out.write( zero );
}
else
{
out.write( blank );
}
out.write( ' ' );
out.write( line );
out.write( "\n" );
}
}
}
private static IntSet getActiveLines( File file ) throws IOException
{
IntSet activeLines = new IntOpenHashSet();
try( InputStream stream = new FileInputStream( file ) )
{
Prototype proto = LuaC.compile( stream, "@" + file.getPath() );
Queue<Prototype> queue = new ArrayDeque<>();
queue.add( proto );
while( (proto = queue.poll()) != null )
{
int[] lines = proto.lineinfo;
if( lines != null )
{
for( int line : lines )
{
activeLines.add( line );
}
}
if( proto.p != null ) Collections.addAll( queue, proto.p );
}
}
catch( CompileException e )
{
throw new IllegalStateException( "Cannot compile", e );
}
return activeLines;
}
}

View File

@ -483,6 +483,49 @@ local function pending(name)
test_stack.n = n - 1
end
local native_co_create, native_loadfile = coroutine.create, loadfile
local line_counts = {}
if cct_test then
local string_sub, debug_getinfo = string.sub, debug.getinfo
local function debug_hook(_, line_nr)
local name = debug_getinfo(2, "S").source
if string_sub(name, 1, 1) ~= "@" then return end
name = string_sub(name, 2)
local file = line_counts[name]
if not file then file = {} line_counts[name] = file end
file[line_nr] = (file[line_nr] or 0) + 1
end
coroutine.create = function(...)
local co = native_co_create(...)
debug.sethook(co, debug_hook, "l")
return co
end
local expect = require "cc.expect".expect
_G.native_loadfile = native_loadfile
_G.loadfile = function(filename, mode, env)
-- Support the previous `loadfile(filename, env)` form instead.
if type(mode) == "table" and env == nil then
mode, env = nil, mode
end
expect(1, filename, "string")
expect(2, mode, "string", "nil")
expect(3, env, "table", "nil")
local file = fs.open(filename, "r")
if not file then return nil, "File not found" end
local func, err = load(file.readAll(), "@/" .. fs.combine(filename, ""), mode, env)
file.close()
return func, err
end
debug.sethook(debug_hook, "l")
end
local arg = ...
if arg == "--help" or arg == "-h" then
io.write("Usage: mcfly [DIR]\n")
@ -648,4 +691,11 @@ if test_status.pending > 0 then
end
term.setTextColour(colours.white) io.write(info .. "\n")
-- Restore hook stubs
debug.sethook(nil, "l")
coroutine.create = native_co_create
_G.loadfile = native_loadfile
if cct_test then cct_test.finish(line_counts) end
if howlci then howlci.log("debug", info) sleep(3) end

View File

@ -12,6 +12,21 @@ describe("The fs library", function()
end)
end)
describe("fs.isDriveRoot", function()
it("validates arguments", function()
fs.isDriveRoot("")
expect.error(fs.isDriveRoot, nil):eq("bad argument #1 (expected string, got nil)")
end)
it("correctly identifies drive roots", function()
expect(fs.isDriveRoot("/rom")):eq(true)
expect(fs.isDriveRoot("/")):eq(true)
expect(fs.isDriveRoot("/rom/startup.lua")):eq(false)
expect(fs.isDriveRoot("/rom/programs/delete.lua")):eq(false)
end)
end)
describe("fs.list", function()
it("fails on files", function()
expect.error(fs.list, "rom/startup.lua"):eq("/rom/startup.lua: Not a directory")

View File

@ -1,8 +1,44 @@
--- Tests the io library is (mostly) consistent with PUC Lua.
--
-- These tests are based on the tests for Lua 5.1
-- These tests are based on the tests for Lua 5.1 and 5.3
describe("The io library", function()
local file = "/test-files/tmp.txt"
local otherfile = "/test-files/tmp2.txt"
local t = '0123456789'
for _ = 1, 12 do t = t .. t end
assert(#t == 10 * 2 ^ 12)
local function read_all(f)
local h = fs.open(f, "rb")
local contents = h.readAll()
h.close()
return contents
end
local function write_file(f, contents)
local h = fs.open(f, "wb")
h.write(contents)
h.close()
end
local function setup()
write_file(file, "\"<EFBFBD>lo\"{a}\nsecond line\nthird line \n<EFBFBD>fourth_line\n\n\9\9 3450\n")
end
describe("io.close", function()
it("cannot close stdin", function()
expect{ io.stdin:close() }:same { nil, "attempt to close standard stream" }
end)
it("cannot close stdout", function()
expect{ io.stdout:close() }:same { nil, "attempt to close standard stream" }
end)
it("cannot close stdout", function()
expect{ io.stdout:close() }:same { nil, "attempt to close standard stream" }
end)
end)
it("io.input on a handle returns that handle", function()
expect(io.input(io.stdin)):equals(io.stdin)
end)
@ -11,11 +47,16 @@ describe("The io library", function()
expect(io.output(io.stdout)):equals(io.stdout)
end)
it("defines a __name field", function()
expect(getmetatable(io.input()).__name):eq("FILE*")
end)
describe("io.type", function()
it("returns file on handles", function()
local handle = io.input()
expect(handle):type("table")
expect(io.type(handle)):equals("file")
expect(io.type(io.stdin)):equals("file")
end)
it("returns nil on values", function()
@ -33,6 +74,88 @@ describe("The io library", function()
expect.error(io.lines, ""):eq("/: No such file")
expect.error(io.lines, false):eq("bad argument #1 (expected string, got boolean)")
end)
it("closes the file", function()
setup()
local n = 0
local f = io.lines(file)
while f() do n = n + 1 end
expect(n):eq(6)
expect.error(f):eq("file is already closed")
expect.error(f):eq("file is already closed")
end)
it("can copy a file", function()
setup()
local n = 0
io.output(otherfile)
for l in io.lines(file) do
io.write(l, "\n")
n = n + 1
end
io.close()
expect(n):eq(6)
io.input(file)
local f = io.open(otherfile):lines()
local n = 0
for l in io.lines() do
expect(l):eq(f())
n = n + 1
end
expect(n):eq(6)
end)
it("does not close on a normal file handle", function()
setup()
local f = assert(io.open(file))
local n = 0
for _ in f:lines() do n = n + 1 end
expect(n):eq(6)
expect(tostring(f):sub(1, 5)):eq("file ")
assert(f:close())
expect(tostring(f)):eq("file (closed)")
expect(io.type(f)):eq("closed file")
end)
it("accepts multiple arguments", function()
write_file(file, "0123456789\n")
for a, b in io.lines(file, 1, 1) do
if a == "\n" then
expect(b):eq(nil)
else
expect(tonumber(a)):eq(tonumber(b) - 1)
end
end
for a, b, c in io.lines(file, 1, 2, "a") do
expect(a):eq("0")
expect(b):eq("12")
expect(c):eq("3456789\n")
end
for a, b, c in io.lines(file, "a", 0, 1) do
if a == "" then break end
expect(a):eq("0123456789\n")
expect(b):eq(nil)
expect(c):eq(nil)
end
write_file(file, "00\n10\n20\n30\n40\n")
for a, b in io.lines(file, "n", "n") do
if a == 40 then
expect(b):eq(nil)
else
expect(a):eq(b - 10)
end
end
end)
end)
describe("io.open", function()
@ -44,6 +167,22 @@ describe("The io library", function()
expect.error(io.open, "", false):eq("bad argument #2 (expected string, got boolean)")
end)
it("checks the mode", function()
io.open(file, "w"):close()
-- This really should be invalid mode, but I'll live.
expect.error(io.open, file, "rw"):str_match("Unsupported mode")
-- TODO: expect.error(io.open, file, "rb+"):str_match("Unsupported mode")
expect.error(io.open, file, "r+bk"):str_match("Unsupported mode")
expect.error(io.open, file, ""):str_match("Unsupported mode")
expect.error(io.open, file, "+"):str_match("Unsupported mode")
expect.error(io.open, file, "b"):str_match("Unsupported mode")
assert(io.open(file, "r+b")):close()
assert(io.open(file, "r+")):close()
assert(io.open(file, "rb")):close()
end)
it("returns an error message on non-existent files", function()
local a, b = io.open('xuxu_nao_existe')
expect(a):equals(nil)
@ -51,26 +190,139 @@ describe("The io library", function()
end)
end)
pending("io.output allows redirecting and seeking", function()
fs.delete("/tmp/io_spec.txt")
describe("a readable handle", function()
it("cannot be written to", function()
write_file(file, "")
io.input(file)
expect { io.input():write("xuxu") }:same { nil, "file is not writable" }
io.input(io.stdin)
end)
io.output("/tmp/io_spec.txt")
it("supports various modes", function()
write_file(file, "alo\n " .. t .. " ;end of file\n")
expect(io.output()):not_equals(io.stdout)
io.input(file)
expect(io.read()):eq("alo")
expect(io.read(1)):eq(' ')
expect(io.read(#t)):eq(t)
expect(io.read(1)):eq(' ')
expect(io.read(0))
expect(io.read('*a')):eq(';end of file\n')
expect(io.read(0)):eq(nil)
expect(io.close(io.input())):eq(true)
expect(io.output():seek()):equal(0)
assert(io.write("alo alo"))
expect(io.output():seek()):equal(#"alo alo")
expect(io.output():seek("cur", -3)):equal(#"alo alo" - 3)
assert(io.write("joao"))
expect(io.output():seek("end"):equal(#"alo joao"))
fs.delete(file)
end)
expect(io.output():seek("set")):equal(0)
it("support seeking", function()
setup()
io.input(file)
assert(io.write('"<22>lo"', "{a}\n", "second line\n", "third line \n"))
assert(io.write('<EFBFBD>fourth_line'))
expect(io.read(0)):eq("") -- not eof
expect(io.read(5, '*l')):eq('"<22>lo"')
expect(io.read(0)):eq("")
expect(io.read()):eq("second line")
local x = io.input():seek()
expect(io.read()):eq("third line ")
assert(io.input():seek("set", x))
expect(io.read('*l')):eq("third line ")
expect(io.read(1)):eq("<EFBFBD>")
expect(io.read(#"fourth_line")):eq("fourth_line")
assert(io.input():seek("cur", -#"fourth_line"))
expect(io.read()):eq("fourth_line")
expect(io.read()):eq("") -- empty line
expect(io.read(8)):eq('\9\9 3450') -- FIXME: Not actually supported
expect(io.read(1)):eq('\n')
expect(io.read(0)):eq(nil) -- end of file
expect(io.read(1)):eq(nil) -- end of file
expect(({ io.read(1) })[2]):eq(nil)
expect(io.read()):eq(nil) -- end of file
expect(({ io.read() })[2]):eq(nil)
expect(io.read('*n')):eq(nil) -- end of file
expect(({ io.read('*n') })[2]):eq(nil)
expect(io.read('*a')):eq('') -- end of file (OK for `*a')
expect(io.read('*a')):eq('') -- end of file (OK for `*a')
io.output(io.stdout)
expect(io.output()):equals(io.stdout)
io.close(io.input())
end)
it("supports the 'L' mode", function()
write_file(file, "\n\nline\nother")
io.input(file)
expect(io.read"L"):eq("\n")
expect(io.read"L"):eq("\n")
expect(io.read"L"):eq("line\n")
expect(io.read"L"):eq("other")
expect(io.read"L"):eq(nil)
io.input():close()
local f = assert(io.open(file))
local s = ""
for l in f:lines("L") do s = s .. l end
expect(s):eq("\n\nline\nother")
f:close()
io.input(file)
s = ""
for l in io.lines(nil, "L") do s = s .. l end
expect(s):eq("\n\nline\nother")
io.input():close()
s = ""
for l in io.lines(file, "L") do s = s .. l end
expect(s):eq("\n\nline\nother")
s = ""
for l in io.lines(file, "l") do s = s .. l end
expect(s):eq("lineother")
write_file(file, "a = 10 + 34\na = 2*a\na = -a\n")
local t = {}
load(io.lines(file, "L"), nil, nil, t)()
expect(t.a):eq(-((10 + 34) * 2))
end)
end)
describe("a writable handle", function()
it("supports seeking", function()
fs.delete(file)
io.output(file)
expect(io.output()):not_equals(io.stdout)
expect(io.output():seek()):equal(0)
assert(io.write("alo alo"))
expect(io.output():seek()):equal(#"alo alo")
expect(io.output():seek("cur", -3)):equal(#"alo alo" - 3)
assert(io.write("joao"))
expect(io.output():seek("end")):equal(#"alo joao")
expect(io.output():seek("set")):equal(0)
assert(io.write('"<22>lo"', "{a}\n", "second line\n", "third line \n"))
assert(io.write('<EFBFBD>fourth_line'))
io.output(io.stdout)
expect(io.output()):equals(io.stdout)
end)
it("supports appending", function()
io.output(file)
io.write("alo\n")
io.close()
expect.error(io.write)
local f = io.open(file, "a")
io.output(f)
assert(io.write(' ' .. t .. ' '))
assert(io.write(';', 'end of file\n'))
f:flush()
io.flush()
f:close()
expect(read_all(file)):eq("alo\n " .. t .. " ;end of file\n")
end)
end)
end)

View File

@ -51,7 +51,7 @@ describe("The peripheral library", function()
it_modem("has the correct error location", function()
expect.error(function() peripheral.call("top", "isOpen", false) end)
:str_match("^peripheral_spec.lua:%d+: bad argument #1 %(number expected, got boolean%)$")
:str_match("^[^:]+:%d+: bad argument #1 %(number expected, got boolean%)$")
end)
end)

View File

@ -49,7 +49,7 @@ describe("The textutils library", function()
describe("textutils.empty_json_array", function()
it("is immutable", function()
expect.error(function() textutils.empty_json_array[1] = true end)
:eq("textutils_spec.lua:51: attempt to mutate textutils.empty_json_array")
:str_match("^[^:]+:51: attempt to mutate textutils.empty_json_array$")
end)
end)

View File

@ -28,6 +28,8 @@ describe("The Lua base library", function()
end)
describe("loadfile", function()
local loadfile = _G.native_loadfile or loadfile
local function make_file()
local tmp = fs.open("test-files/out.lua", "w")
tmp.write("return _ENV")

View File

@ -27,7 +27,7 @@ describe("cc.expect", function()
worker()
end
expect.error(trampoline):eq("expect_spec.lua:27: bad argument #1 to 'worker' (expected string, got nil)")
expect.error(trampoline):str_match("^[^:]*expect_spec.lua:27: bad argument #1 to 'worker' %(expected string, got nil%)$")
end)
end)

View File

@ -42,7 +42,15 @@ describe("The rm program", function()
it("errors when trying to delete a read-only file", function()
expect(capture(stub, "rm /rom/startup.lua"))
:matches { ok = true, output = "", error = "/rom/startup.lua: Access denied\n" }
:matches { ok = true, output = "", error = "Cannot delete read-only file /rom/startup.lua\n" }
end)
it("errors when trying to delete the root mount", function()
expect(capture(stub, "rm /")):matches {
ok = true,
output = "To delete its contents run rm /*\n",
error = "Cannot delete mount /\n",
}
end)
it("errors when a glob fails to match", function()

View File

@ -1,11 +1,13 @@
local capture = require "test_helpers".capture_program
describe("The move program", function()
local function cleanup() fs.delete("/test-files/move") end
local function touch(file)
io.open(file, "w"):close()
end
it("move a file", function()
cleanup()
touch("/test-files/move/a.txt")
shell.run("move /test-files/move/a.txt /test-files/move/b.txt")
@ -14,11 +16,57 @@ describe("The move program", function()
expect(fs.exists("/test-files/move/b.txt")):eq(true)
end)
it("try to move a not existing file", function()
it("moves a file to a directory", function()
cleanup()
touch("/test-files/move/a.txt")
fs.makeDir("/test-files/move/a")
expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a"))
:matches { ok = true }
expect(fs.exists("/test-files/move/a.txt")):eq(false)
expect(fs.exists("/test-files/move/a/a.txt")):eq(true)
end)
it("fails when moving a file which doesn't exist", function()
expect(capture(stub, "move nothing destination"))
:matches { ok = true, output = "", error = "No matching files\n" }
end)
it("fails when overwriting an existing file", function()
cleanup()
touch("/test-files/move/a.txt")
expect(capture(stub, "move /test-files/move/a.txt /test-files/move/a.txt"))
:matches { ok = true, output = "", error = "Destination exists\n" }
end)
it("fails when moving to read-only locations", function()
cleanup()
touch("/test-files/move/a.txt")
expect(capture(stub, "move /test-files/move/a.txt /rom/test.txt"))
:matches { ok = true, output = "", error = "Destination is read-only\n" }
end)
it("fails when moving from read-only locations", function()
expect(capture(stub, "move /rom/startup.lua /test-files/move/not-exist.txt"))
:matches { ok = true, output = "", error = "Cannot move read-only file /rom/startup.lua\n" }
end)
it("fails when moving mounts", function()
expect(capture(stub, "move /rom /test-files/move/rom"))
:matches { ok = true, output = "", error = "Cannot move mount /rom\n" }
end)
it("fails when moving a file multiple times", function()
cleanup()
touch("/test-files/move/a.txt")
touch("/test-files/move/b.txt")
expect(capture(stub, "move /test-files/move/*.txt /test-files/move/c.txt"))
:matches { ok = true, output = "", error = "Cannot overwrite file multiple times\n" }
end)
it("displays the usage with no arguments", function()
expect(capture(stub, "move"))
:matches { ok = true, output = "Usage: mv <source> <destination>\n", error = "" }

View File

@ -26,13 +26,23 @@ describe("The rename program", function()
:matches { ok = true, output = "", error = "Destination exists\n" }
end)
it("fails when copying to read-only locations", function()
it("fails when renaming to read-only locations", function()
touch("/test-files/rename/d.txt")
expect(capture(stub, "rename /test-files/rename/d.txt /rom/test.txt"))
:matches { ok = true, output = "", error = "Destination is read-only\n" }
end)
it("fails when renaming from read-only locations", function()
expect(capture(stub, "rename /rom/startup.lua /test-files/rename/d.txt"))
:matches { ok = true, output = "", error = "Source is read-only\n" }
end)
it("fails when renaming mounts", function()
expect(capture(stub, "rename /rom /test-files/rename/rom"))
:matches { ok = true, output = "", error = "Can't rename mounts\n" }
end)
it("displays the usage when given no arguments", function()
expect(capture(stub, "rename"))
:matches { ok = true, output = "Usage: rename <source> <destination>\n", error = "" }

View File

@ -23,4 +23,3 @@ describe("The type program", function()
end)
end)

View File

@ -11,8 +11,13 @@ for path in pathlib.Path("src").glob("**/*"):
continue
with path.open(encoding="utf-8") as file:
has_dos, has_trailing, needs_final = False, False, False
has_dos, has_trailing, first, count = False, False, 0, True
for i, line in enumerate(file):
if first:
first = False
if line.strip() == "":
print("%s has empty first line" % path)
if len(line) >= 2 and line[-2] == "\r" and line[-1] == "\n" and not has_line:
print("%s has contains '\\r\\n' on line %d" % (path, i + 1))
problems = has_dos = True
@ -21,8 +26,15 @@ for path in pathlib.Path("src").glob("**/*"):
print("%s has trailing whitespace on line %d" % (path, i + 1))
problems = has_trailing = True
if line is not None and len(line) >= 1 and line[-1] != "\n":
print("%s should end with '\\n'" % path)
if len(line) == 0 or line[-1] != "\n":
count = 0
elif line.strip() == "":
count += 1
else:
count = 1
if count != 1:
print("%s should have 1 trailing lines, but has %d" % (path, count))
problems = True
if problems: