diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 3ae99ee1e..1d947e320 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -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 diff --git a/build.gradle b/build.gradle index 8371e1ebf..73446fdd6 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ buildscript { 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 @@ test { } } +jacocoTestReport { + reports { + xml.enabled true + html.enabled true + } +} + +check.dependsOn jacocoTestReport + license { mapping("java", "SLASHSTAR_STYLE") strictCheck true diff --git a/doc/stub/commands.lua b/doc/stub/commands.lua index 89b0f604b..e1001230e 100644 --- a/doc/stub/commands.lua +++ b/doc/stub/commands.lua @@ -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 diff --git a/doc/stub/fs.lua b/doc/stub/fs.lua index a9e76a352..41e1db8e1 100644 --- a/doc/stub/fs.lua +++ b/doc/stub/fs.lua @@ -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 diff --git a/gradle.properties b/gradle.properties index 4089f1259..79b9114e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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.15.2 diff --git a/illuaminate.sexp b/illuaminate.sexp index b1bf17f31..631a9eb58 100644 --- a/illuaminate.sexp +++ b/illuaminate.sexp @@ -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) diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index 3af6d527e..681cc8363 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -70,7 +70,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 ); diff --git a/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java b/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java index 720748e8f..e10c0351a 100644 --- a/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java +++ b/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java @@ -41,12 +41,12 @@ public final class 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 ); } diff --git a/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java b/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java new file mode 100644 index 000000000..b96ba7199 --- /dev/null +++ b/src/main/java/dan200/computercraft/client/render/MonitorTextureBufferShader.java @@ -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.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.client.gui.FixedWidthFontRenderer; +import dan200.computercraft.shared.util.Palette; +import net.minecraft.client.renderer.Matrix4f; +import net.minecraft.client.renderer.texture.TextureUtil; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; + +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( Matrix4f transform, int width, int height, Palette palette, boolean greyscale ) + { + MATRIX_BUFFER.rewind(); + transform.write( MATRIX_BUFFER ); + MATRIX_BUFFER.rewind(); + RenderSystem.glUniformMatrix4( uniformMv, false, MATRIX_BUFFER ); + + // TODO: Cache this? + MATRIX_BUFFER.rewind(); + GL11.glGetFloatv( GL11.GL_PROJECTION_MATRIX, MATRIX_BUFFER ); + MATRIX_BUFFER.rewind(); + RenderSystem.glUniformMatrix4( uniformP, false, MATRIX_BUFFER ); + + RenderSystem.glUniform1i( uniformWidth, width ); + RenderSystem.glUniform1i( uniformHeight, height ); + + // TODO: Cache this? Maybe?? + 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(); + RenderSystem.glUniform3( uniformPalette, PALETTE_BUFFER ); + } + + static boolean use() + { + if( initialised ) + { + if( ok ) GlStateManager.useProgram( program ); + return ok; + } + + if( ok = load() ) + { + GL20.glUseProgram( program ); + RenderSystem.glUniform1i( uniformFont, 0 ); + RenderSystem.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 = GlStateManager.createProgram(); + GlStateManager.attachShader( program, vertexShader ); + GlStateManager.attachShader( program, fragmentShader ); + GL20.glBindAttribLocation( program, 0, "v_pos" ); + + GlStateManager.linkProgram( program ); + boolean ok = GlStateManager.getProgram( program, GL20.GL_LINK_STATUS ) != 0; + String log = GlStateManager.getProgramInfoLog( 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 ); + GlStateManager.deleteShader( vertexShader ); + GlStateManager.deleteShader( 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 ) + { + InputStream stream = TileEntityMonitorRenderer.class.getClassLoader().getResourceAsStream( path ); + if( stream == null ) throw new IllegalArgumentException( "Cannot find " + path ); + String contents = TextureUtil.readResourceAsString( stream ); + + int shader = GlStateManager.createShader( kind ); + + GlStateManager.shaderSource( shader, contents ); + GlStateManager.compileShader( shader ); + + boolean ok = GlStateManager.getShader( shader, GL20.GL_COMPILE_STATUS ) != 0; + String log = GlStateManager.getShaderInfoLog( 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 = GlStateManager.getUniformLocation( program, name ); + if( uniform == -1 ) throw new IllegalStateException( "Cannot find uniform " + name ); + return uniform; + } +} diff --git a/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java index 0eff25b14..1a1b29e2b 100644 --- a/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java +++ b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java @@ -6,22 +6,33 @@ package dan200.computercraft.client.render; import com.mojang.blaze3d.matrix.MatrixStack; +import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.vertex.IVertexBuilder; 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.renderer.*; import net.minecraft.client.renderer.tileentity.TileEntityRenderer; import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; +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.GL20; +import org.lwjgl.opengl.GL31; import javax.annotation.Nonnull; +import java.nio.ByteBuffer; + +import static dan200.computercraft.client.gui.FixedWidthFontRenderer.*; public class TileEntityMonitorRenderer extends TileEntityRenderer { @@ -90,50 +101,23 @@ public class TileEntityMonitorRenderer extends TileEntityRenderer Terminal terminal = originTerminal.getTerminal(); if( terminal != null ) { - boolean redraw = originTerminal.pollTerminalChanged(); - if( originTerminal.buffer == null ) - { - originTerminal.createBuffer( MonitorRenderer.VBO ); - redraw = true; - } - VertexBuffer vbo = originTerminal.buffer; - // Draw a terminal - double xScale = xSize / (terminal.getWidth() * FixedWidthFontRenderer.FONT_WIDTH); - double yScale = ySize / (terminal.getHeight() * FixedWidthFontRenderer.FONT_HEIGHT); + int width = terminal.getWidth(), height = terminal.getHeight(); + int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT; + double xScale = xSize / pixelWidth; + double yScale = ySize / pixelHeight; transform.push(); transform.scale( (float) xScale, (float) -yScale, 1.0f ); - float xMargin = (float) (MARGIN / xScale); - float yMargin = (float) (MARGIN / yScale); - Matrix4f matrix = transform.getLast().getMatrix(); - if( redraw ) - { - Tessellator tessellator = Tessellator.getInstance(); - BufferBuilder builder = tessellator.getBuffer(); - builder.begin( FixedWidthFontRenderer.TYPE.getDrawMode(), FixedWidthFontRenderer.TYPE.getVertexFormat() ); - FixedWidthFontRenderer.drawTerminalWithoutCursor( - IDENTITY, builder, 0, 0, - terminal, !originTerminal.isColour(), yMargin, yMargin, xMargin, xMargin - ); - - builder.finishDrawing(); - vbo.upload( builder ); - } - // Sneaky hack here: we get a buffer now in order to flush existing ones and set up the appropriate // render state. I've no clue how well this'll work in future versions of Minecraft, but it does the trick // for now. IVertexBuilder buffer = renderer.getBuffer( FixedWidthFontRenderer.TYPE ); FixedWidthFontRenderer.TYPE.setupRenderState(); - vbo.bindBuffer(); - FixedWidthFontRenderer.TYPE.getVertexFormat().setupBufferState( 0L ); - vbo.draw( matrix, FixedWidthFontRenderer.TYPE.getDrawMode() ); - VertexBuffer.unbindBuffer(); - FixedWidthFontRenderer.TYPE.getVertexFormat().clearBufferState(); + renderTerminal( matrix, originTerminal, (float) (MARGIN / xScale), (float) (MARGIN / yScale) ); // We don't draw the cursor with the VBO, as it's dynamic and so we'll end up refreshing far more than is // reasonable. @@ -158,4 +142,88 @@ public class TileEntityMonitorRenderer extends TileEntityRenderer transform.pop(); } + + private static void renderTerminal( Matrix4f matrix, ClientMonitor monitor, float xMargin, float yMargin ) + { + Terminal terminal = monitor.getTerminal(); + + MonitorRenderer renderType = MonitorRenderer.current(); + boolean redraw = monitor.pollTerminalChanged(); + if( monitor.createBuffer( renderType ) ) redraw = true; + + switch( renderType ) + { + case VBO: + { + VertexBuffer vbo = monitor.buffer; + if( redraw ) + { + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder builder = tessellator.getBuffer(); + builder.begin( FixedWidthFontRenderer.TYPE.getDrawMode(), FixedWidthFontRenderer.TYPE.getVertexFormat() ); + FixedWidthFontRenderer.drawTerminalWithoutCursor( + IDENTITY, builder, 0, 0, + terminal, !monitor.isColour(), yMargin, yMargin, xMargin, xMargin + ); + + builder.finishDrawing(); + vbo.upload( builder ); + } + + vbo.bindBuffer(); + FixedWidthFontRenderer.TYPE.getVertexFormat().setupBufferState( 0L ); + vbo.draw( matrix, FixedWidthFontRenderer.TYPE.getDrawMode() ); + VertexBuffer.unbindBuffer(); + FixedWidthFontRenderer.TYPE.getVertexFormat().clearBufferState(); + break; + } + + case TBO: + { + if( !MonitorTextureBufferShader.use() ) return; + + int width = terminal.getWidth(), height = terminal.getHeight(); + int pixelWidth = width * FONT_WIDTH, pixelHeight = height * FONT_HEIGHT; + + if( redraw ) + { + ByteBuffer buffer = 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++ ) + { + buffer.put( (byte) (text.charAt( x ) & 0xFF) ); + buffer.put( (byte) getColour( textColour.charAt( x ), Colour.WHITE ) ); + buffer.put( (byte) getColour( background.charAt( x ), Colour.BLACK ) ); + } + } + buffer.flip(); + + GlStateManager.bindBuffer( GL31.GL_TEXTURE_BUFFER, monitor.tboBuffer ); + GlStateManager.bufferData( GL31.GL_TEXTURE_BUFFER, buffer, GL20.GL_STATIC_DRAW ); + GlStateManager.bindBuffer( GL31.GL_TEXTURE_BUFFER, 0 ); + } + + // Nobody knows what they're doing! + GlStateManager.activeTexture( MonitorTextureBufferShader.TEXTURE_INDEX ); + GL11.glBindTexture( GL31.GL_TEXTURE_BUFFER, monitor.tboTexture ); + GlStateManager.activeTexture( GL13.GL_TEXTURE0 ); + + MonitorTextureBufferShader.setupUniform( matrix, width, height, terminal.getPalette(), !monitor.isColour() ); + + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder buffer = tessellator.getBuffer(); + 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(); + + GlStateManager.useProgram( 0 ); + break; + } + } + } } diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java index 652d7608a..4004372eb 100644 --- a/src/main/java/dan200/computercraft/shared/Config.java +++ b/src/main/java/dan200/computercraft/shared/Config.java @@ -14,6 +14,7 @@ import dan200.computercraft.ComputerCraft; import dan200.computercraft.api.turtle.event.TurtleAction; import dan200.computercraft.core.apis.http.AddressRule; 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; @@ -75,7 +76,10 @@ public final class Config private static final ConfigValue turtlesCanPush; private static final ConfigValue> turtleDisabledActions; - private static final ForgeConfigSpec spec; + private static final ConfigValue monitorRenderer; + + private static final ForgeConfigSpec commonSpec; + private static final ForgeConfigSpec clientSpec; private Config() {} @@ -261,12 +265,20 @@ public final class 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() @@ -316,6 +328,9 @@ public final class Config ComputerCraft.turtleDisabledActions.clear(); for( String value : turtleDisabledActions.get() ) ComputerCraft.turtleDisabledActions.add( getAction( value ) ); + + // Client + ComputerCraft.monitorRenderer = monitorRenderer.get(); } @SubscribeEvent diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java index 8e4238627..bfd7bfd52 100644 --- a/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java @@ -120,6 +120,11 @@ public class TileCommandComputer extends TileComputer @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 class TileCommandComputer extends TileComputer 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; } } diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java b/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java index 9ad1225da..82f603306 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java @@ -35,4 +35,3 @@ public enum ComputerState implements IStringSerializable return name; } } - diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java index 8b5132950..ef8595d11 100644 --- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java @@ -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 dan200.computercraft.shared.network.container.ViewComputerContainerData; 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 @@ public class ContainerViewComputer extends ContainerComputerBase implements ICon } // 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; diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java index 4db761997..856210908 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java @@ -5,12 +5,18 @@ */ package dan200.computercraft.shared.peripheral.monitor; +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; 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 final class ClientMonitor extends ClientTerminal { switch( renderer ) { + case TBO: + { + if( tboBuffer != 0 ) return false; + + deleteBuffers(); + + tboBuffer = GlStateManager.genBuffers(); + GlStateManager.bindBuffer( 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 ); + + GlStateManager.bindBuffer( GL31.GL_TEXTURE_BUFFER, 0 ); + + addMonitor(); + return true; + } + case VBO: if( buffer != null ) return false; @@ -73,6 +101,19 @@ public final class ClientMonitor extends ClientTerminal private void deleteBuffers() { + + if( tboBuffer != 0 ) + { + RenderSystem.glDeleteBuffers( tboBuffer ); + tboBuffer = 0; + } + + if( tboTexture != 0 ) + { + GlStateManager.deleteTexture( tboTexture ); + tboTexture = 0; + } + if( buffer != null ) { buffer.close(); @@ -83,7 +124,7 @@ public final class ClientMonitor extends ClientTerminal @OnlyIn( Dist.CLIENT ) public void destroy() { - if( buffer != null ) + if( tboBuffer != 0 || buffer != null ) { synchronized( allMonitors ) { diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java index 5effa06c0..0aa86610c 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorRenderer.java @@ -8,9 +8,9 @@ package dan200.computercraft.shared.peripheral.monitor; 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. @@ -25,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. * @@ -32,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. * @@ -72,11 +48,39 @@ public enum MonitorRenderer public static MonitorRenderer current() { MonitorRenderer current = ComputerCraft.monitorRenderer; - return current == MonitorRenderer.BEST ? best() : current; + switch( current ) + { + case BEST: + return best(); + case TBO: + checkCapabilities(); + if( !textureBuffer ) + { + ComputerCraft.log.warn( "Texture buffers are not supported on your graphics card. Falling back to default." ); + ComputerCraft.monitorRenderer = BEST; + return best(); + } + + return TBO; + default: + return 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; } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java index ca7332114..0352c8163 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java @@ -154,4 +154,3 @@ public abstract class SpeakerPeripheral implements IPeripheral return true; } } - diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java index d30d12693..2ca05a11e 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java @@ -78,4 +78,3 @@ public class TileSpeaker extends TileGeneric implements ITickableTileEntity, IPe } } } - diff --git a/src/main/resources/assets/computercraft/blockstates/wired_modem_full.json b/src/main/resources/assets/computercraft/blockstates/wired_modem_full.json index 2bd7372d2..ac068443f 100644 --- a/src/main/resources/assets/computercraft/blockstates/wired_modem_full.json +++ b/src/main/resources/assets/computercraft/blockstates/wired_modem_full.json @@ -6,4 +6,3 @@ "modem=true,peripheral=true": { "model": "computercraft:block/wired_modem_full_on_peripheral" } } } - diff --git a/src/main/resources/assets/computercraft/shaders/monitor.frag b/src/main/resources/assets/computercraft/shaders/monitor.frag new file mode 100644 index 000000000..3e1796932 --- /dev/null +++ b/src/main/resources/assets/computercraft/shaders/monitor.frag @@ -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); +} diff --git a/src/main/resources/assets/computercraft/shaders/monitor.vert b/src/main/resources/assets/computercraft/shaders/monitor.vert new file mode 100644 index 000000000..564f063e2 --- /dev/null +++ b/src/main/resources/assets/computercraft/shaders/monitor.vert @@ -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; +} diff --git a/src/main/resources/data/computercraft/lua/bios.lua b/src/main/resources/data/computercraft/lua/bios.lua index 687c9c798..55688a4f2 100644 --- a/src/main/resources/data/computercraft/lua/bios.lua +++ b/src/main/resources/data/computercraft/lua/bios.lua @@ -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, diff --git a/src/main/resources/data/computercraft/lua/rom/apis/command/commands.lua b/src/main/resources/data/computercraft/lua/rom/apis/command/commands.lua index 5877c1b8e..1c77fa26e 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/command/commands.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/command/commands.lua @@ -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 diff --git a/src/main/resources/data/computercraft/lua/rom/apis/io.lua b/src/main/resources/data/computercraft/lua/rom/apis/io.lua index 8079e022f..917faef7f 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/io.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/io.lua @@ -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 diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt index 5360045ad..1097f3622 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.txt @@ -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. diff --git a/src/main/resources/data/computercraft/lua/rom/help/credits.txt b/src/main/resources/data/computercraft/lua/rom/help/credits.txt index cc01b8eaf..6625cb806 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/credits.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/credits.txt @@ -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. diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt index 02ec15e80..a3cb561de 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt @@ -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. diff --git a/src/main/resources/data/computercraft/lua/rom/programs/advanced/bg.lua b/src/main/resources/data/computercraft/lua/rom/programs/advanced/bg.lua index 907e9f7aa..ccb03ffce 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/advanced/bg.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/advanced/bg.lua @@ -1,4 +1,3 @@ - if not shell.openTab then printError("Requires multishell") return diff --git a/src/main/resources/data/computercraft/lua/rom/programs/advanced/fg.lua b/src/main/resources/data/computercraft/lua/rom/programs/advanced/fg.lua index efc7832eb..0e87ca495 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/advanced/fg.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/advanced/fg.lua @@ -1,4 +1,3 @@ - if not shell.openTab then printError("Requires multishell") return diff --git a/src/main/resources/data/computercraft/lua/rom/programs/alias.lua b/src/main/resources/data/computercraft/lua/rom/programs/alias.lua index d35eb7707..79ebedc1f 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/alias.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/alias.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } if #tArgs > 2 then print("Usage: alias ") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/apis.lua b/src/main/resources/data/computercraft/lua/rom/programs/apis.lua index 801477650..3fd56c80f 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/apis.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/apis.lua @@ -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 diff --git a/src/main/resources/data/computercraft/lua/rom/programs/cd.lua b/src/main/resources/data/computercraft/lua/rom/programs/cd.lua index 6f05a98ac..d1a3e86c1 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/cd.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/cd.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } if #tArgs < 1 then print("Usage: cd ") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/command/commands.lua b/src/main/resources/data/computercraft/lua/rom/programs/command/commands.lua index dacc5626d..fe2b20ec2 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/command/commands.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/command/commands.lua @@ -1,4 +1,3 @@ - if not commands then printError("Requires a Command Computer.") return diff --git a/src/main/resources/data/computercraft/lua/rom/programs/command/exec.lua b/src/main/resources/data/computercraft/lua/rom/programs/command/exec.lua index b50c8ec20..67c9446f9 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/command/exec.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/command/exec.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } if not commands then printError("Requires a Command Computer.") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/copy.lua b/src/main/resources/data/computercraft/lua/rom/programs/copy.lua index d50132f82..49d58a0b9 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/copy.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/copy.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } if #tArgs < 2 then print("Usage: cp ") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/delete.lua b/src/main/resources/data/computercraft/lua/rom/programs/delete.lua index 91f2e0c23..620cdd629 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/delete.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/delete.lua @@ -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 diff --git a/src/main/resources/data/computercraft/lua/rom/programs/eject.lua b/src/main/resources/data/computercraft/lua/rom/programs/eject.lua index 635c2f4e8..b15e2f20f 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/eject.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/eject.lua @@ -1,4 +1,3 @@ - -- Get arguments local tArgs = { ... } if #tArgs == 0 then diff --git a/src/main/resources/data/computercraft/lua/rom/programs/fun/adventure.lua b/src/main/resources/data/computercraft/lua/rom/programs/fun/adventure.lua index 0f08b59be..d13f2cf60 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/fun/adventure.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/fun/adventure.lua @@ -1,4 +1,3 @@ - local tBiomes = { "in a forest", "in a pine forest", diff --git a/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua b/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua index fe753b032..eaaa54869 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/fun/worm.lua @@ -1,4 +1,3 @@ - -- Display the start screen local w, h = term.getSize() diff --git a/src/main/resources/data/computercraft/lua/rom/programs/gps.lua b/src/main/resources/data/computercraft/lua/rom/programs/gps.lua index 9e1ff0538..c0194443b 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/gps.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/gps.lua @@ -1,4 +1,3 @@ - local function printUsage() print("Usages:") print("gps host") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua b/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua index a45ea1a2a..15ad89f36 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua @@ -1,4 +1,3 @@ - local function printUsage() print("Usages:") print("pastebin put ") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua b/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua index dbfa3aeb7..7b5c7654e 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua @@ -1,4 +1,3 @@ - local function printUsage() print("Usage:") print("wget [filename]") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/id.lua b/src/main/resources/data/computercraft/lua/rom/programs/id.lua index dda736612..964503e24 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/id.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/id.lua @@ -1,4 +1,3 @@ - local sDrive = nil local tArgs = { ... } if #tArgs > 0 then diff --git a/src/main/resources/data/computercraft/lua/rom/programs/label.lua b/src/main/resources/data/computercraft/lua/rom/programs/label.lua index d0e712a05..f857dac5c 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/label.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/label.lua @@ -1,4 +1,3 @@ - local function printUsage() print("Usages:") print("label get") diff --git a/src/main/resources/data/computercraft/lua/rom/programs/list.lua b/src/main/resources/data/computercraft/lua/rom/programs/list.lua index 759130bb6..ec281938d 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/list.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/list.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } -- Get all the files in the directory diff --git a/src/main/resources/data/computercraft/lua/rom/programs/lua.lua b/src/main/resources/data/computercraft/lua/rom/programs/lua.lua index 5eea4dd81..7ab9bda42 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/lua.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/lua.lua @@ -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) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/move.lua b/src/main/resources/data/computercraft/lua/rom/programs/move.lua index 0ebc5f7f7..254592200 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/move.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/move.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } if #tArgs < 2 then print("Usage: mv ") @@ -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 diff --git a/src/main/resources/data/computercraft/lua/rom/programs/programs.lua b/src/main/resources/data/computercraft/lua/rom/programs/programs.lua index 015264d94..ca987596a 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/programs.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/programs.lua @@ -1,4 +1,3 @@ - local bAll = false local tArgs = { ... } if #tArgs > 0 and tArgs[1] == "all" then diff --git a/src/main/resources/data/computercraft/lua/rom/programs/rednet/chat.lua b/src/main/resources/data/computercraft/lua/rom/programs/rednet/chat.lua index c632104b1..1bf5582b2 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/rednet/chat.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/rednet/chat.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } local function printUsage() diff --git a/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua b/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua index b9a9bcb94..dfd9ca907 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/rednet/repeat.lua @@ -1,4 +1,3 @@ - -- Find modems local tModems = {} for _, sModem in ipairs(peripheral.getNames()) do diff --git a/src/main/resources/data/computercraft/lua/rom/programs/redstone.lua b/src/main/resources/data/computercraft/lua/rom/programs/redstone.lua index 35fae9f04..65d6c5e1f 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/redstone.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/redstone.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } local function printUsage() diff --git a/src/main/resources/data/computercraft/lua/rom/programs/rename.lua b/src/main/resources/data/computercraft/lua/rom/programs/rename.lua index a90c1f8ad..8b491abcd 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/rename.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/rename.lua @@ -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 diff --git a/src/main/resources/data/computercraft/lua/rom/programs/type.lua b/src/main/resources/data/computercraft/lua/rom/programs/type.lua index ec0ebc0c5..ccbaf0dd8 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/type.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/type.lua @@ -1,4 +1,3 @@ - local tArgs = { ... } if #tArgs < 1 then print("Usage: type ") @@ -15,4 +14,3 @@ if fs.exists(sPath) then else print("No such path") end - diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 2085450bb..301b1c9a8 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -30,12 +30,14 @@ import org.opentest4j.AssertionFailedError; 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 @@ import static dan200.computercraft.api.lua.ArgumentHelper.getType; */ 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> 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 @@ public class ComputerTestDelegate try { finished = true; + if( arguments.length > 0 ) + { + @SuppressWarnings( "unchecked" ) + Map> finished = (Map>) arguments[0]; + finishedWith = finished; + } + hasFinished.signal(); } finally @@ -281,7 +295,7 @@ public class ComputerTestDelegate } @AfterEach - public void after() throws InterruptedException + public void after() throws InterruptedException, IOException { try { @@ -316,6 +330,14 @@ public class ComputerTestDelegate // And shutdown computer.shutdown(); } + + if( finishedWith != null ) + { + try( BufferedWriter writer = Files.newBufferedWriter( REPORT_PATH.toPath() ) ) + { + new LuaCoverage( finishedWith ).write( writer ); + } + } } @TestFactory diff --git a/src/test/java/dan200/computercraft/core/LuaCoverage.java b/src/test/java/dan200/computercraft/core/LuaCoverage.java new file mode 100644 index 000000000..e6e61e8f2 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/LuaCoverage.java @@ -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> coverage; + private final String blank; + private final String zero; + private final String countFormat; + + LuaCoverage( Map> 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 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 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 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; + } +} diff --git a/src/test/resources/test-rom/mcfly.lua b/src/test/resources/test-rom/mcfly.lua index 5081447a6..ddd4f46b3 100644 --- a/src/test/resources/test-rom/mcfly.lua +++ b/src/test/resources/test-rom/mcfly.lua @@ -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 diff --git a/src/test/resources/test-rom/spec/apis/fs_spec.lua b/src/test/resources/test-rom/spec/apis/fs_spec.lua index 5288aa9c8..fd16599de 100644 --- a/src/test/resources/test-rom/spec/apis/fs_spec.lua +++ b/src/test/resources/test-rom/spec/apis/fs_spec.lua @@ -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") diff --git a/src/test/resources/test-rom/spec/apis/io_spec.lua b/src/test/resources/test-rom/spec/apis/io_spec.lua index 484588014..b1746093f 100644 --- a/src/test/resources/test-rom/spec/apis/io_spec.lua +++ b/src/test/resources/test-rom/spec/apis/io_spec.lua @@ -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, "\"�lo\"{a}\nsecond line\nthird line \n�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('"�lo"', "{a}\n", "second line\n", "third line \n")) - assert(io.write('�fourth_line')) + expect(io.read(0)):eq("") -- not eof + expect(io.read(5, '*l')):eq('"�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("�") + 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('"�lo"', "{a}\n", "second line\n", "third line \n")) + assert(io.write('�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) diff --git a/src/test/resources/test-rom/spec/apis/peripheral_spec.lua b/src/test/resources/test-rom/spec/apis/peripheral_spec.lua index 1b11c6552..4fea6ddf0 100644 --- a/src/test/resources/test-rom/spec/apis/peripheral_spec.lua +++ b/src/test/resources/test-rom/spec/apis/peripheral_spec.lua @@ -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) diff --git a/src/test/resources/test-rom/spec/apis/textutils_spec.lua b/src/test/resources/test-rom/spec/apis/textutils_spec.lua index bdef2dbd2..94e243afc 100644 --- a/src/test/resources/test-rom/spec/apis/textutils_spec.lua +++ b/src/test/resources/test-rom/spec/apis/textutils_spec.lua @@ -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) diff --git a/src/test/resources/test-rom/spec/base_spec.lua b/src/test/resources/test-rom/spec/base_spec.lua index 2e557e132..85bbf3f98 100644 --- a/src/test/resources/test-rom/spec/base_spec.lua +++ b/src/test/resources/test-rom/spec/base_spec.lua @@ -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") diff --git a/src/test/resources/test-rom/spec/modules/cc/expect_spec.lua b/src/test/resources/test-rom/spec/modules/cc/expect_spec.lua index 125e43640..1a30a3db7 100644 --- a/src/test/resources/test-rom/spec/modules/cc/expect_spec.lua +++ b/src/test/resources/test-rom/spec/modules/cc/expect_spec.lua @@ -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) diff --git a/src/test/resources/test-rom/spec/programs/delete_spec.lua b/src/test/resources/test-rom/spec/programs/delete_spec.lua index 1eec95cc4..18a01c5c0 100644 --- a/src/test/resources/test-rom/spec/programs/delete_spec.lua +++ b/src/test/resources/test-rom/spec/programs/delete_spec.lua @@ -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() diff --git a/src/test/resources/test-rom/spec/programs/move_spec.lua b/src/test/resources/test-rom/spec/programs/move_spec.lua index de1a36569..664010118 100644 --- a/src/test/resources/test-rom/spec/programs/move_spec.lua +++ b/src/test/resources/test-rom/spec/programs/move_spec.lua @@ -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 \n", error = "" } diff --git a/src/test/resources/test-rom/spec/programs/rename_spec.lua b/src/test/resources/test-rom/spec/programs/rename_spec.lua index 2ab955dab..437e940d3 100644 --- a/src/test/resources/test-rom/spec/programs/rename_spec.lua +++ b/src/test/resources/test-rom/spec/programs/rename_spec.lua @@ -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 \n", error = "" } diff --git a/src/test/resources/test-rom/spec/programs/type_spec.lua b/src/test/resources/test-rom/spec/programs/type_spec.lua index 78009c746..09d59ee4a 100644 --- a/src/test/resources/test-rom/spec/programs/type_spec.lua +++ b/src/test/resources/test-rom/spec/programs/type_spec.lua @@ -23,4 +23,3 @@ describe("The type program", function() end) end) - diff --git a/tools/check-lines.py b/tools/check-lines.py index 956e87d46..3659924ae 100644 --- a/tools/check-lines.py +++ b/tools/check-lines.py @@ -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: