mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-10-31 23:26:19 +00:00
Merge branch 'mc-1.14.x' into mc-1.15.x
This commit is contained in:
commit
abbc641fd4
3
.github/workflows/main-ci.yml
vendored
3
.github/workflows/main-ci.yml
vendored
@ -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
|
||||
|
10
build.gradle
10
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 );
|
||||
|
@ -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 );
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<TileMonitor>
|
||||
{
|
||||
@ -90,50 +101,23 @@ public class TileEntityMonitorRenderer extends TileEntityRenderer<TileMonitor>
|
||||
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<TileMonitor>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<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() {}
|
||||
|
||||
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,3 @@ public enum ComputerState implements IStringSerializable
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 )
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -154,4 +154,3 @@ public abstract class SpeakerPeripheral implements IPeripheral
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,4 +78,3 @@ public class TileSpeaker extends TileGeneric implements ITickableTileEntity, IPe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,4 +6,3 @@
|
||||
"modem=true,peripheral=true": { "model": "computercraft:block/wired_modem_full_on_peripheral" }
|
||||
}
|
||||
}
|
||||
|
||||
|
40
src/main/resources/assets/computercraft/shaders/monitor.frag
Normal file
40
src/main/resources/assets/computercraft/shaders/monitor.frag
Normal 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);
|
||||
}
|
13
src/main/resources/assets/computercraft/shaders/monitor.vert
Normal file
13
src/main/resources/assets/computercraft/shaders/monitor.vert
Normal 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;
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
if not shell.openTab then
|
||||
printError("Requires multishell")
|
||||
return
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
if not shell.openTab then
|
||||
printError("Requires multishell")
|
||||
return
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tArgs = { ... }
|
||||
if #tArgs > 2 then
|
||||
print("Usage: alias <alias> <program>")
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tArgs = { ... }
|
||||
if #tArgs < 1 then
|
||||
print("Usage: cd <path>")
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
if not commands then
|
||||
printError("Requires a Command Computer.")
|
||||
return
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tArgs = { ... }
|
||||
if not commands then
|
||||
printError("Requires a Command Computer.")
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tArgs = { ... }
|
||||
if #tArgs < 2 then
|
||||
print("Usage: cp <source> <destination>")
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
-- Get arguments
|
||||
local tArgs = { ... }
|
||||
if #tArgs == 0 then
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tBiomes = {
|
||||
"in a forest",
|
||||
"in a pine forest",
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
-- Display the start screen
|
||||
local w, h = term.getSize()
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local function printUsage()
|
||||
print("Usages:")
|
||||
print("gps host")
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local function printUsage()
|
||||
print("Usages:")
|
||||
print("pastebin put <filename>")
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local function printUsage()
|
||||
print("Usage:")
|
||||
print("wget <url> [filename]")
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local sDrive = nil
|
||||
local tArgs = { ... }
|
||||
if #tArgs > 0 then
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local function printUsage()
|
||||
print("Usages:")
|
||||
print("label get")
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tArgs = { ... }
|
||||
|
||||
-- Get all the files in the directory
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local bAll = false
|
||||
local tArgs = { ... }
|
||||
if #tArgs > 0 and tArgs[1] == "all" then
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tArgs = { ... }
|
||||
|
||||
local function printUsage()
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
-- Find modems
|
||||
local tModems = {}
|
||||
for _, sModem in ipairs(peripheral.getNames()) do
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
local tArgs = { ... }
|
||||
|
||||
local function printUsage()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<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 @@ public class ComputerTestDelegate
|
||||
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 @@ 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
|
||||
|
158
src/test/java/dan200/computercraft/core/LuaCoverage.java
Normal file
158
src/test/java/dan200/computercraft/core/LuaCoverage.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 = "" }
|
||||
|
@ -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 = "" }
|
||||
|
@ -23,4 +23,3 @@ describe("The type program", function()
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user