1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-07-03 18:42:53 +00:00

Merge branch 'master' into mc-1.14.x

This commit is contained in:
SquidDev 2019-08-04 10:57:20 +01:00
commit 4b0e5c445c
72 changed files with 1329 additions and 498 deletions

View File

@ -17,7 +17,8 @@ ignore = {
-- are largely unsupported. -- are largely unsupported.
include_files = { include_files = {
'src/main/resources/assets/computercraft/lua/rom', 'src/main/resources/assets/computercraft/lua/rom',
'src/main/resources/assets/computercraft/lua/bios.lua' 'src/main/resources/assets/computercraft/lua/bios.lua',
'src/test/resources/test-rom',
} }
files['src/main/resources/assets/computercraft/lua/bios.lua'] = { files['src/main/resources/assets/computercraft/lua/bios.lua'] = {

View File

@ -11,4 +11,4 @@ cache:
- $HOME/.gradle/wrapper/s - $HOME/.gradle/wrapper/s
jdk: jdk:
- oraclejdk8 - openjdk8

View File

@ -1,5 +1,5 @@
# ![CC: Tweaked](logo.png) # ![CC: Tweaked](logo.png)
[![Current build status](https://travis-ci.org/SquidDev-CC/CC-Tweaked.svg?branch=master)](https://travis-ci.org/SquidDev-CC/CC-Tweaked "Current build status") [![Download CC: Tweaked on CurseForge](https://cf.way2muchnoise.eu/title/cc-tweaked.svg)](https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked on CurseForge") [![Current build status](https://travis-ci.org/SquidDev-CC/CC-Tweaked.svg?branch=master)](https://travis-ci.org/SquidDev-CC/CC-Tweaked "Current build status") [![Download CC: Tweaked on CurseForge](http://cf.way2muchnoise.eu/title/cc-tweaked.svg)](https://minecraft.curseforge.com/projects/cc-tweaked "Download CC: Tweaked on CurseForge")
CC: Tweaked is a fork of [ComputerCraft](https://github.com/dan200/ComputerCraft), adding programmable computers, CC: Tweaked is a fork of [ComputerCraft](https://github.com/dan200/ComputerCraft), adding programmable computers,
turtles and more to Minecraft. turtles and more to Minecraft.

View File

@ -4,7 +4,7 @@ buildscript {
mavenCentral() mavenCentral()
maven { maven {
name = "forge" name = "forge"
url = "http://files.minecraftforge.net/maven" url = "https://files.minecraftforge.net/maven"
} }
} }
dependencies { dependencies {
@ -67,7 +67,7 @@ minecraft {
repositories { repositories {
maven { maven {
name "JEI" name "JEI"
url "http://dvs1.progwml6.com/files/maven" url "https://dvs1.progwml6.com/files/maven"
} }
maven { maven {
name "SquidDev" name "SquidDev"
@ -79,7 +79,7 @@ repositories {
} }
maven { maven {
name "Amadornes" name "Amadornes"
url "http://maven.amadornes.com/" url "https://maven.amadornes.com/"
} }
} }
@ -102,8 +102,8 @@ dependencies {
shade 'org.squiddev:Cobalt:0.5.0-SNAPSHOT' shade 'org.squiddev:Cobalt:0.5.0-SNAPSHOT'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.0' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
deployerJars "org.apache.maven.wagon:wagon-ssh:3.0.0" deployerJars "org.apache.maven.wagon:wagon-ssh:3.0.0"
} }
@ -117,6 +117,8 @@ sourceSets {
} }
} }
// Compile tasks
javadoc { javadoc {
include "dan200/computercraft/api/**/*.java" include "dan200/computercraft/api/**/*.java"
} }
@ -141,6 +143,14 @@ jar {
from configurations.shade.collect { it.isDirectory() ? it : zipTree(it) } from configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
} }
[compileJava, compileTestJava].forEach {
it.configure {
options.compilerArgs << "-Xlint" << "-Xlint:-processing" << "-Werror"
}
}
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.nio.file.* import java.nio.file.*
import java.util.zip.* import java.util.zip.*
@ -276,7 +286,14 @@ task compressJson(dependsOn: jar) {
assemble.dependsOn compressJson assemble.dependsOn compressJson
/* Check tasks */ // Check tasks
test {
useJUnitPlatform()
testLogging {
events "skipped", "failed"
}
}
license { license {
mapping("java", "SLASHSTAR_STYLE") mapping("java", "SLASHSTAR_STYLE")
@ -300,6 +317,13 @@ license {
} }
} }
gradle.projectsEvaluated {
tasks.withType(LicenseFormat) {
outputs.upToDateWhen { false }
}
}
task licenseAPI(type: LicenseCheck); task licenseAPI(type: LicenseCheck);
task licenseFormatAPI(type: LicenseFormat); task licenseFormatAPI(type: LicenseFormat);
[licenseAPI, licenseFormatAPI].forEach { [licenseAPI, licenseFormatAPI].forEach {
@ -310,7 +334,7 @@ task licenseFormatAPI(type: LicenseFormat);
} }
} }
/* Upload tasks */ // Upload tasks
task checkRelease { task checkRelease {
group "upload" group "upload"
@ -441,23 +465,3 @@ task uploadAll(dependsOn: uploadTasks) {
group "upload" group "upload"
description "Uploads to all repositories (Maven, Curse, GitHub release)" description "Uploads to all repositories (Maven, Curse, GitHub release)"
} }
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
gradle.projectsEvaluated {
reobfJar.dependsOn proguardMove
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint" << "-Xlint:-processing" // Causes Forge build to fail << "-Werror"
}
tasks.withType(LicenseFormat) {
outputs.upToDateWhen { false }
}
}

View File

@ -1,5 +1,5 @@
# Mod properties # Mod properties
mod_version=1.83.1 mod_version=1.84.0
# Minecraft properties # Minecraft properties
mc_version=1.14.4 mc_version=1.14.4

View File

@ -144,7 +144,9 @@ public interface ITurtleAccess
GameProfile getOwningPlayer(); GameProfile getOwningPlayer();
/** /**
* Get the inventory of this turtle * Get the inventory of this turtle.
*
* Note: this inventory should only be accessed and modified on the server thread.
* *
* @return This turtle's inventory * @return This turtle's inventory
* @see #getItemHandler() * @see #getItemHandler()
@ -155,6 +157,8 @@ public interface ITurtleAccess
/** /**
* Get the inventory of this turtle as an {@link IItemHandlerModifiable}. * Get the inventory of this turtle as an {@link IItemHandlerModifiable}.
* *
* Note: this inventory should only be accessed and modified on the server thread.
*
* @return This turtle's inventory * @return This turtle's inventory
* @see #getInventory() * @see #getInventory()
* @see IItemHandlerModifiable * @see IItemHandlerModifiable

View File

@ -98,8 +98,8 @@ public interface ITurtleUpgrade
* Will only be called for Tool turtle. Called when turtle.dig() or turtle.attack() is called * Will only be called for Tool turtle. Called when turtle.dig() or turtle.attack() is called
* by the turtle, and the tool is required to do some work. * by the turtle, and the tool is required to do some work.
* *
* Conforming implementations should fire {@link BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig}for digging, * Conforming implementations should fire {@link BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig} for
* {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking. * digging, {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking.
* *
* @param turtle Access to the turtle that the tool resides on. * @param turtle Access to the turtle that the tool resides on.
* @param side Which side of the turtle (left or right) the tool resides on. * @param side Which side of the turtle (left or right) the tool resides on.

View File

@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.MediaProviders;
import dan200.computercraft.shared.media.items.ItemDisk; import dan200.computercraft.shared.media.items.ItemDisk;
import dan200.computercraft.shared.util.StringUtil; import dan200.computercraft.shared.util.StringUtil;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
@ -19,11 +20,11 @@ import javax.annotation.Nonnull;
import static dan200.computercraft.core.apis.ArgumentHelper.optString; import static dan200.computercraft.core.apis.ArgumentHelper.optString;
public class DiskDrivePeripheral implements IPeripheral class DiskDrivePeripheral implements IPeripheral
{ {
private final TileDiskDrive m_diskDrive; private final TileDiskDrive m_diskDrive;
public DiskDrivePeripheral( TileDiskDrive diskDrive ) DiskDrivePeripheral( TileDiskDrive diskDrive )
{ {
m_diskDrive = diskDrive; m_diskDrive = diskDrive;
} }
@ -55,7 +56,7 @@ public class DiskDrivePeripheral implements IPeripheral
} }
@Override @Override
public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException
{ {
switch( method ) switch( method )
{ {
@ -63,21 +64,26 @@ public class DiskDrivePeripheral implements IPeripheral
return new Object[] { !m_diskDrive.getDiskStack().isEmpty() }; return new Object[] { !m_diskDrive.getDiskStack().isEmpty() };
case 1: // getDiskLabel case 1: // getDiskLabel
{ {
IMedia media = m_diskDrive.getDiskMedia(); ItemStack stack = m_diskDrive.getDiskStack();
return media == null ? null : new Object[] { media.getLabel( m_diskDrive.getDiskStack() ) }; IMedia media = MediaProviders.get( stack );
return media == null ? null : new Object[] { media.getLabel( stack ) };
} }
case 2: // setDiskLabel case 2: // setDiskLabel
{ {
String label = optString( arguments, 0, null ); String label = optString( arguments, 0, null );
IMedia media = m_diskDrive.getDiskMedia(); return context.executeMainThreadTask( () -> {
if( media == null ) return null; ItemStack stack = m_diskDrive.getDiskStack();
IMedia media = MediaProviders.get( stack );
if( media == null ) return null;
ItemStack disk = m_diskDrive.getDiskStack(); if( !media.setLabel( stack, StringUtil.normaliseLabel( label ) ) )
label = StringUtil.normaliseLabel( label ); {
if( !media.setLabel( disk, label ) ) throw new LuaException( "Disk label cannot be changed" ); throw new LuaException( "Disk label cannot be changed" );
m_diskDrive.setDiskStack( disk ); }
return null; m_diskDrive.setDiskStack( stack );
return null;
} );
} }
case 3: // hasData case 3: // hasData
return new Object[] { m_diskDrive.getDiskMountPath( computer ) != null }; return new Object[] { m_diskDrive.getDiskMountPath( computer ) != null };
@ -86,14 +92,16 @@ public class DiskDrivePeripheral implements IPeripheral
case 5: case 5:
{ {
// hasAudio // hasAudio
IMedia media = m_diskDrive.getDiskMedia(); ItemStack stack = m_diskDrive.getDiskStack();
return new Object[] { media != null && media.getAudio( m_diskDrive.getDiskStack() ) != null }; IMedia media = MediaProviders.get( stack );
return new Object[] { media != null && media.getAudio( stack ) != null };
} }
case 6: case 6:
{ {
// getAudioTitle // getAudioTitle
IMedia media = m_diskDrive.getDiskMedia(); ItemStack stack = m_diskDrive.getDiskStack();
return new Object[] { media != null ? media.getAudioTitle( m_diskDrive.getDiskStack() ) : false }; IMedia media = MediaProviders.get( stack );
return new Object[] { media != null ? media.getAudioTitle( stack ) : false };
} }
case 7: // playAudio case 7: // playAudio
m_diskDrive.playDiskAudio(); m_diskDrive.playDiskAudio();
@ -129,8 +137,7 @@ public class DiskDrivePeripheral implements IPeripheral
@Override @Override
public boolean equals( IPeripheral other ) public boolean equals( IPeripheral other )
{ {
if( this == other ) return true; return this == other || other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).m_diskDrive == m_diskDrive;
return other instanceof DiskDrivePeripheral && ((DiskDrivePeripheral) other).m_diskDrive == m_diskDrive;
} }
@Nonnull @Nonnull

View File

@ -320,35 +320,31 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
} }
@Nonnull @Nonnull
public ItemStack getDiskStack() ItemStack getDiskStack()
{ {
return getStackInSlot( 0 ); return getStackInSlot( 0 );
} }
public void setDiskStack( @Nonnull ItemStack stack ) void setDiskStack( @Nonnull ItemStack stack )
{ {
setInventorySlotContents( 0, stack ); setInventorySlotContents( 0, stack );
} }
public IMedia getDiskMedia() private IMedia getDiskMedia()
{ {
return MediaProviders.get( getDiskStack() ); return MediaProviders.get( getDiskStack() );
} }
public String getDiskMountPath( IComputerAccess computer ) String getDiskMountPath( IComputerAccess computer )
{ {
synchronized( this ) synchronized( this )
{ {
if( m_computers.containsKey( computer ) ) MountInfo info = m_computers.get( computer );
{ return info != null ? info.mountPath : null;
MountInfo info = m_computers.get( computer );
return info.mountPath;
}
} }
return null;
} }
public void mount( IComputerAccess computer ) void mount( IComputerAccess computer )
{ {
synchronized( this ) synchronized( this )
{ {
@ -357,7 +353,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
} }
} }
public void unmount( IComputerAccess computer ) void unmount( IComputerAccess computer )
{ {
synchronized( this ) synchronized( this )
{ {
@ -366,7 +362,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
} }
} }
public void playDiskAudio() void playDiskAudio()
{ {
synchronized( this ) synchronized( this )
{ {
@ -379,7 +375,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
} }
} }
public void stopDiskAudio() void stopDiskAudio()
{ {
synchronized( this ) synchronized( this )
{ {
@ -388,7 +384,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
} }
} }
public void ejectDisk() void ejectDisk()
{ {
synchronized( this ) synchronized( this )
{ {

View File

@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.peripheral.IComputerAccess; import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.terminal.Terminal; import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.util.StringUtil;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -51,8 +52,13 @@ public class PrinterPeripheral implements IPeripheral
} }
@Override @Override
public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] args ) throws LuaException, InterruptedException
{ {
// FIXME: There's a theoretical race condition here between getCurrentPage and then using the page. Ideally
// we'd lock on the page, consume it, and unlock.
// FIXME: None of our page modification functions actually mark the tile as dirty, so the page may not be
// persisted correctly.
switch( method ) switch( method )
{ {
case 0: // write case 0: // write
@ -89,10 +95,13 @@ public class PrinterPeripheral implements IPeripheral
return new Object[] { width, height }; return new Object[] { width, height };
} }
case 4: // newPage case 4: // newPage
return new Object[] { m_printer.startNewPage() }; return context.executeMainThreadTask( () -> new Object[] { m_printer.startNewPage() } );
case 5: // endPage case 5: // endPage
getCurrentPage(); getCurrentPage();
return new Object[] { m_printer.endCurrentPage() }; return context.executeMainThreadTask( () -> {
getCurrentPage();
return new Object[] { m_printer.endCurrentPage() };
} );
case 6: // getInkLevel case 6: // getInkLevel
return new Object[] { m_printer.getInkLevel() }; return new Object[] { m_printer.getInkLevel() };
case 7: case 7:
@ -100,7 +109,7 @@ public class PrinterPeripheral implements IPeripheral
// setPageTitle // setPageTitle
String title = optString( args, 0, "" ); String title = optString( args, 0, "" );
getCurrentPage(); getCurrentPage();
m_printer.setPageTitle( title ); m_printer.setPageTitle( StringUtil.normaliseLabel( title ) );
return null; return null;
} }
case 8: // getPaperLevel case 8: // getPaperLevel
@ -123,13 +132,11 @@ public class PrinterPeripheral implements IPeripheral
return m_printer; return m_printer;
} }
@Nonnull
private Terminal getCurrentPage() throws LuaException private Terminal getCurrentPage() throws LuaException
{ {
Terminal currentPage = m_printer.getCurrentPage(); Terminal currentPage = m_printer.getCurrentPage();
if( currentPage == null ) if( currentPage == null ) throw new LuaException( "Page not started" );
{
throw new LuaException( "Page not started" );
}
return currentPage; return currentPage;
} }
} }

View File

@ -120,10 +120,7 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
} }
// Read inventory // Read inventory
synchronized( m_inventory ) ItemStackHelper.loadAllItems( nbt, m_inventory );
{
ItemStackHelper.loadAllItems( nbt, m_inventory );
}
} }
@Nonnull @Nonnull
@ -141,15 +138,12 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
} }
// Write inventory // Write inventory
synchronized( m_inventory ) ItemStackHelper.saveAllItems( nbt, m_inventory );
{
ItemStackHelper.saveAllItems( nbt, m_inventory );
}
return super.write( nbt ); return super.write( nbt );
} }
public boolean isPrinting() boolean isPrinting()
{ {
return m_printing; return m_printing;
} }
@ -173,73 +167,59 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
@Nonnull @Nonnull
@Override @Override
public ItemStack getStackInSlot( int i ) public ItemStack getStackInSlot( int slot )
{ {
return m_inventory.get( i ); return m_inventory.get( slot );
} }
@Nonnull @Nonnull
@Override @Override
public ItemStack removeStackFromSlot( int i ) public ItemStack removeStackFromSlot( int slot )
{ {
synchronized( m_inventory ) ItemStack result = m_inventory.get( slot );
{ m_inventory.set( slot, ItemStack.EMPTY );
ItemStack result = m_inventory.get( i ); markDirty();
m_inventory.set( i, ItemStack.EMPTY ); updateBlockState();
markDirty(); return result;
updateBlockState();
return result;
}
} }
@Nonnull @Nonnull
@Override @Override
public ItemStack decrStackSize( int i, int j ) public ItemStack decrStackSize( int slot, int count )
{ {
synchronized( m_inventory ) ItemStack stack = m_inventory.get( slot );
if( stack.isEmpty() ) return ItemStack.EMPTY;
if( stack.getCount() <= count )
{ {
if( m_inventory.get( i ).isEmpty() ) return ItemStack.EMPTY; setInventorySlotContents( slot, ItemStack.EMPTY );
return stack;
if( m_inventory.get( i ).getCount() <= j )
{
ItemStack itemstack = m_inventory.get( i );
m_inventory.set( i, ItemStack.EMPTY );
markDirty();
updateBlockState();
return itemstack;
}
ItemStack part = m_inventory.get( i ).split( j );
if( m_inventory.get( i ).isEmpty() )
{
m_inventory.set( i, ItemStack.EMPTY );
updateBlockState();
}
markDirty();
return part;
} }
ItemStack part = stack.split( count );
if( m_inventory.get( slot ).isEmpty() )
{
m_inventory.set( slot, ItemStack.EMPTY );
updateBlockState();
}
markDirty();
return part;
} }
@Override @Override
public void setInventorySlotContents( int i, @Nonnull ItemStack stack ) public void setInventorySlotContents( int slot, @Nonnull ItemStack stack )
{ {
synchronized( m_inventory ) m_inventory.set( slot, stack );
{ markDirty();
m_inventory.set( i, stack ); updateBlockState();
markDirty();
updateBlockState();
}
} }
@Override @Override
public void clear() public void clear()
{ {
synchronized( m_inventory ) for( int i = 0; i < m_inventory.size(); i++ ) m_inventory.set( i, ItemStack.EMPTY );
{ markDirty();
for( int i = 0; i < m_inventory.size(); i++ ) m_inventory.set( i, ItemStack.EMPTY ); updateBlockState();
markDirty();
updateBlockState();
}
} }
@Override @Override
@ -290,14 +270,18 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
return new PrinterPeripheral( this ); return new PrinterPeripheral( this );
} }
public Terminal getCurrentPage() @Nullable
Terminal getCurrentPage()
{ {
return m_printing ? m_page : null; synchronized( m_page )
{
return m_printing ? m_page : null;
}
} }
public boolean startNewPage() boolean startNewPage()
{ {
synchronized( m_inventory ) synchronized( m_page )
{ {
if( !canInputPage() ) return false; if( !canInputPage() ) return false;
if( m_printing && !outputPage() ) return false; if( m_printing && !outputPage() ) return false;
@ -305,49 +289,36 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
} }
} }
public boolean endCurrentPage() boolean endCurrentPage()
{ {
synchronized( m_inventory ) synchronized( m_page )
{ {
if( m_printing && outputPage() ) return m_printing && outputPage();
{
return true;
}
}
return false;
}
public int getInkLevel()
{
synchronized( m_inventory )
{
ItemStack inkStack = m_inventory.get( 0 );
return isInk( inkStack ) ? inkStack.getCount() : 0;
} }
} }
public int getPaperLevel() int getInkLevel()
{
ItemStack inkStack = m_inventory.get( 0 );
return isInk( inkStack ) ? inkStack.getCount() : 0;
}
int getPaperLevel()
{ {
int count = 0; int count = 0;
synchronized( m_inventory ) for( int i = 1; i < 7; i++ )
{ {
for( int i = 1; i < 7; i++ ) ItemStack paperStack = m_inventory.get( i );
{ if( isPaper( paperStack ) ) count += paperStack.getCount();
ItemStack paperStack = m_inventory.get( i );
if( !paperStack.isEmpty() && isPaper( paperStack ) )
{
count += paperStack.getCount();
}
}
} }
return count; return count;
} }
public void setPageTitle( String title ) void setPageTitle( String title )
{ {
if( m_printing ) synchronized( m_page )
{ {
m_pageTitle = title; if( m_printing ) m_pageTitle = title;
} }
} }
@ -365,116 +336,100 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
private boolean canInputPage() private boolean canInputPage()
{ {
synchronized( m_inventory ) ItemStack inkStack = m_inventory.get( 0 );
{ return !inkStack.isEmpty() && isInk( inkStack ) && getPaperLevel() > 0;
ItemStack inkStack = m_inventory.get( 0 );
return !inkStack.isEmpty() && isInk( inkStack ) && getPaperLevel() > 0;
}
} }
private boolean inputPage() private boolean inputPage()
{ {
synchronized( m_inventory ) ItemStack inkStack = m_inventory.get( 0 );
if( !isInk( inkStack ) ) return false;
for( int i = 1; i < 7; i++ )
{ {
ItemStack inkStack = m_inventory.get( 0 ); ItemStack paperStack = m_inventory.get( i );
if( !isInk( inkStack ) ) return false; if( paperStack.isEmpty() || !isPaper( paperStack ) ) continue;
for( int i = 1; i < 7; i++ ) // Setup the new page
DyeColor dye = ColourUtils.getStackColour( inkStack );
m_page.setTextColour( dye != null ? dye.getId() : 15 );
m_page.clear();
if( paperStack.getItem() instanceof ItemPrintout )
{ {
ItemStack paperStack = m_inventory.get( i ); m_pageTitle = ItemPrintout.getTitle( paperStack );
if( !paperStack.isEmpty() && isPaper( paperStack ) ) String[] text = ItemPrintout.getText( paperStack );
String[] textColour = ItemPrintout.getColours( paperStack );
for( int y = 0; y < m_page.getHeight(); y++ )
{ {
// Setup the new page m_page.setLine( y, text[y], textColour[y], "" );
DyeColor dye = ColourUtils.getStackColour( inkStack );
m_page.setTextColour( dye != null ? dye.getId() : 15 );
m_page.clear();
if( paperStack.getItem() instanceof ItemPrintout )
{
m_pageTitle = ItemPrintout.getTitle( paperStack );
String[] text = ItemPrintout.getText( paperStack );
String[] textColour = ItemPrintout.getColours( paperStack );
for( int y = 0; y < m_page.getHeight(); y++ )
{
m_page.setLine( y, text[y], textColour[y], "" );
}
}
else
{
m_pageTitle = "";
}
m_page.setCursorPos( 0, 0 );
// Decrement ink
inkStack.shrink( 1 );
if( inkStack.isEmpty() ) m_inventory.set( 0, ItemStack.EMPTY );
// Decrement paper
paperStack.shrink( 1 );
if( paperStack.isEmpty() )
{
m_inventory.set( i, ItemStack.EMPTY );
updateBlockState();
}
markDirty();
m_printing = true;
return true;
} }
} }
return false; else
{
m_pageTitle = "";
}
m_page.setCursorPos( 0, 0 );
// Decrement ink
inkStack.shrink( 1 );
if( inkStack.isEmpty() ) m_inventory.set( 0, ItemStack.EMPTY );
// Decrement paper
paperStack.shrink( 1 );
if( paperStack.isEmpty() )
{
m_inventory.set( i, ItemStack.EMPTY );
updateBlockState();
}
markDirty();
m_printing = true;
return true;
} }
return false;
} }
private boolean outputPage() private boolean outputPage()
{ {
synchronized( m_page ) int height = m_page.getHeight();
String[] lines = new String[height];
String[] colours = new String[height];
for( int i = 0; i < height; i++ )
{ {
int height = m_page.getHeight(); lines[i] = m_page.getLine( i ).toString();
String[] lines = new String[height]; colours[i] = m_page.getTextColourLine( i ).toString();
String[] colours = new String[height];
for( int i = 0; i < height; i++ )
{
lines[i] = m_page.getLine( i ).toString();
colours[i] = m_page.getTextColourLine( i ).toString();
}
ItemStack stack = ItemPrintout.createSingleFromTitleAndText( m_pageTitle, lines, colours );
synchronized( m_inventory )
{
for( int slot : BOTTOM_SLOTS )
{
if( m_inventory.get( slot ).isEmpty() )
{
setInventorySlotContents( slot, stack );
m_printing = false;
return true;
}
}
}
return false;
} }
ItemStack stack = ItemPrintout.createSingleFromTitleAndText( m_pageTitle, lines, colours );
for( int slot : BOTTOM_SLOTS )
{
if( m_inventory.get( slot ).isEmpty() )
{
setInventorySlotContents( slot, stack );
m_printing = false;
return true;
}
}
return false;
} }
private void ejectContents() private void ejectContents()
{ {
synchronized( m_inventory ) for( int i = 0; i < 13; i++ )
{ {
for( int i = 0; i < 13; i++ ) ItemStack stack = m_inventory.get( i );
if( !stack.isEmpty() )
{ {
ItemStack stack = m_inventory.get( i ); // Remove the stack from the inventory
if( !stack.isEmpty() ) setInventorySlotContents( i, ItemStack.EMPTY );
{
// Remove the stack from the inventory
setInventorySlotContents( i, ItemStack.EMPTY );
// Spawn the item in the world // Spawn the item in the world
BlockPos pos = getPos(); BlockPos pos = getPos();
double x = pos.getX() + 0.5; double x = pos.getX() + 0.5;
double y = pos.getY() + 0.75; double y = pos.getY() + 0.75;
double z = pos.getZ() + 0.5; double z = pos.getZ() + 0.5;
WorldUtil.dropItemStack( stack, getWorld(), x, y, z ); WorldUtil.dropItemStack( stack, getWorld(), x, y, z );
}
} }
} }
} }
@ -482,25 +437,22 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
private void updateBlockState() private void updateBlockState()
{ {
boolean top = false, bottom = false; boolean top = false, bottom = false;
synchronized( m_inventory ) for( int i = 1; i < 7; i++ )
{ {
for( int i = 1; i < 7; i++ ) ItemStack stack = m_inventory.get( i );
if( !stack.isEmpty() && isPaper( stack ) )
{ {
ItemStack stack = m_inventory.get( i ); top = true;
if( !stack.isEmpty() && isPaper( stack ) ) break;
{
top = true;
break;
}
} }
for( int i = 7; i < 13; i++ ) }
for( int i = 7; i < 13; i++ )
{
ItemStack stack = m_inventory.get( i );
if( !stack.isEmpty() && isPaper( stack ) )
{ {
ItemStack stack = m_inventory.get( i ); bottom = true;
if( !stack.isEmpty() && isPaper( stack ) ) break;
{
bottom = true;
break;
}
} }
} }

View File

@ -31,9 +31,13 @@ public final class FurnaceRefuelHandler implements TurtleRefuelEvent.Handler
@Override @Override
public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStack, int slot, int limit ) public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStack, int slot, int limit )
{ {
ItemStack stack = turtle.getItemHandler().extractItem( slot, limit, false ); int fuelSpaceLeft = turtle.getFuelLimit() - turtle.getFuelLevel();
int fuelToGive = getFuelPerItem( stack ) * stack.getCount(); int fuelPerItem = getFuelPerItem( turtle.getItemHandler().getStackInSlot( slot ) );
int fuelItemLimit = (int) Math.ceil( fuelSpaceLeft / (double) fuelPerItem );
if( limit > fuelItemLimit ) limit = fuelItemLimit;
ItemStack stack = turtle.getItemHandler().extractItem( slot, limit, false );
int fuelToGive = fuelPerItem * stack.getCount();
// Store the replacement item in the inventory // Store the replacement item in the inventory
ItemStack replacementStack = stack.getItem().getContainerItem( stack ); ItemStack replacementStack = stack.getItem().getContainerItem( stack );
if( !replacementStack.isEmpty() ) if( !replacementStack.isEmpty() )

View File

@ -335,9 +335,11 @@ public class TurtleAPI implements ILuaAPI
return tryCommand( context, new TurtleInspectCommand( InteractDirection.Up ) ); return tryCommand( context, new TurtleInspectCommand( InteractDirection.Up ) );
case 40: // inspectDown case 40: // inspectDown
return tryCommand( context, new TurtleInspectCommand( InteractDirection.Down ) ); return tryCommand( context, new TurtleInspectCommand( InteractDirection.Down ) );
case 41: case 41: // getItemDetail
{ {
// getItemDetail // FIXME: There's a race condition here if the stack is being modified (mutating NBT, etc...)
// on another thread. The obvious solution is to move this into a command, but some programs rely
// on this having a 0-tick delay.
int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() ); int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() );
ItemStack stack = m_turtle.getInventory().getStackInSlot( slot ); ItemStack stack = m_turtle.getInventory().getStackInSlot( slot );
if( stack.isEmpty() ) return new Object[] { null }; if( stack.isEmpty() ) return new Object[] { null };

View File

@ -153,7 +153,7 @@ public class BlockTurtle extends BlockComputerBase<TileTurtle> implements IWater
@Override @Override
public float getExplosionResistance( BlockState state, IWorldReader world, BlockPos pos, @Nullable Entity exploder, Explosion explosion ) public float getExplosionResistance( BlockState state, IWorldReader world, BlockPos pos, @Nullable Entity exploder, Explosion explosion )
{ {
if( getFamily() == ComputerFamily.Advanced && (exploder instanceof LivingEntity || exploder instanceof DamagingProjectileEntity) ) if( getFamily() == ComputerFamily.Advanced || exploder instanceof LivingEntity || exploder instanceof DamagingProjectileEntity )
{ {
return 2000; return 2000;
} }

View File

@ -49,13 +49,12 @@ import net.minecraftforge.items.wrapper.InvWrapper;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collections;
import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY; import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY;
public class TileTurtle extends TileComputerBase implements ITurtleTile, DefaultInventory public class TileTurtle extends TileComputerBase implements ITurtleTile, DefaultInventory
{ {
// Statics
public static final int INVENTORY_SIZE = 16; public static final int INVENTORY_SIZE = 16;
public static final int INVENTORY_WIDTH = 4; public static final int INVENTORY_WIDTH = 4;
public static final int INVENTORY_HEIGHT = 4; public static final int INVENTORY_HEIGHT = 4;
@ -70,8 +69,6 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
type -> new TileTurtle( type, ComputerFamily.Advanced ) type -> new TileTurtle( type, ComputerFamily.Advanced )
); );
// Members
enum MoveState enum MoveState
{ {
NOT_MOVED, NOT_MOVED,
@ -79,25 +76,20 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
MOVED MOVED
} }
private NonNullList<ItemStack> m_inventory; private final NonNullList<ItemStack> m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
private NonNullList<ItemStack> m_previousInventory; private final NonNullList<ItemStack> m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
private final IItemHandlerModifiable m_itemHandler = new InvWrapper( this ); private final IItemHandlerModifiable m_itemHandler = new InvWrapper( this );
private LazyOptional<IItemHandlerModifiable> itemHandlerCap; private LazyOptional<IItemHandlerModifiable> itemHandlerCap;
private boolean m_inventoryChanged; private boolean m_inventoryChanged = false;
private TurtleBrain m_brain; private TurtleBrain m_brain = new TurtleBrain( this );
private MoveState m_moveState; private MoveState m_moveState = MoveState.NOT_MOVED;
public TileTurtle( TileEntityType<? extends TileGeneric> type, ComputerFamily family ) public TileTurtle( TileEntityType<? extends TileGeneric> type, ComputerFamily family )
{ {
super( type, family ); super( type, family );
m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
m_inventoryChanged = false;
m_brain = new TurtleBrain( this );
m_moveState = MoveState.NOT_MOVED;
} }
public boolean hasMoved() private boolean hasMoved()
{ {
return m_moveState == MoveState.MOVED; return m_moveState == MoveState.MOVED;
} }
@ -237,18 +229,15 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
{ {
super.tick(); super.tick();
m_brain.update(); m_brain.update();
synchronized( m_inventory ) if( !getWorld().isRemote && m_inventoryChanged )
{ {
if( !getWorld().isRemote && m_inventoryChanged ) ServerComputer computer = getServerComputer();
{ if( computer != null ) computer.queueEvent( "turtle_inventory" );
ServerComputer computer = getServerComputer();
if( computer != null ) computer.queueEvent( "turtle_inventory" );
m_inventoryChanged = false; m_inventoryChanged = false;
for( int n = 0; n < getSizeInventory(); n++ ) for( int n = 0; n < getSizeInventory(); n++ )
{ {
m_previousInventory.set( n, InventoryUtil.copyItem( getStackInSlot( n ) ) ); m_previousInventory.set( n, InventoryUtil.copyItem( getStackInSlot( n ) ) );
}
} }
} }
} }
@ -288,8 +277,8 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
// Read inventory // Read inventory
ListNBT nbttaglist = nbt.getList( "Items", Constants.NBT.TAG_COMPOUND ); ListNBT nbttaglist = nbt.getList( "Items", Constants.NBT.TAG_COMPOUND );
m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); m_inventory.clear();
m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY ); m_previousInventory.clear();
for( int i = 0; i < nbttaglist.size(); i++ ) for( int i = 0; i < nbttaglist.size(); i++ )
{ {
CompoundNBT tag = nbttaglist.getCompound( i ); CompoundNBT tag = nbttaglist.getCompound( i );
@ -396,7 +385,7 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
return m_brain.getToolRenderAngle( side, f ); return m_brain.getToolRenderAngle( side, f );
} }
public void setOwningPlayer( GameProfile player ) void setOwningPlayer( GameProfile player )
{ {
m_brain.setOwningPlayer( player ); m_brain.setOwningPlayer( player );
markDirty(); markDirty();
@ -424,109 +413,76 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
@Override @Override
public ItemStack getStackInSlot( int slot ) public ItemStack getStackInSlot( int slot )
{ {
if( slot >= 0 && slot < INVENTORY_SIZE ) return slot >= 0 && slot < INVENTORY_SIZE ? m_inventory.get( slot ) : ItemStack.EMPTY;
{
synchronized( m_inventory )
{
return m_inventory.get( slot );
}
}
return ItemStack.EMPTY;
} }
@Nonnull @Nonnull
@Override @Override
public ItemStack removeStackFromSlot( int slot ) public ItemStack removeStackFromSlot( int slot )
{ {
synchronized( m_inventory ) ItemStack result = getStackInSlot( slot );
{ setInventorySlotContents( slot, ItemStack.EMPTY );
ItemStack result = getStackInSlot( slot ); return result;
setInventorySlotContents( slot, ItemStack.EMPTY );
return result;
}
} }
@Nonnull @Nonnull
@Override @Override
public ItemStack decrStackSize( int slot, int count ) public ItemStack decrStackSize( int slot, int count )
{ {
if( count == 0 ) if( count == 0 ) return ItemStack.EMPTY;
ItemStack stack = getStackInSlot( slot );
if( stack.isEmpty() ) return ItemStack.EMPTY;
if( stack.getCount() <= count )
{ {
return ItemStack.EMPTY; setInventorySlotContents( slot, ItemStack.EMPTY );
return stack;
} }
synchronized( m_inventory ) ItemStack part = stack.split( count );
{ onInventoryDefinitelyChanged();
ItemStack stack = getStackInSlot( slot ); return part;
if( stack.isEmpty() )
{
return ItemStack.EMPTY;
}
if( stack.getCount() <= count )
{
setInventorySlotContents( slot, ItemStack.EMPTY );
return stack;
}
ItemStack part = stack.split( count );
onInventoryDefinitelyChanged();
return part;
}
} }
@Override @Override
public void setInventorySlotContents( int i, @Nonnull ItemStack stack ) public void setInventorySlotContents( int i, @Nonnull ItemStack stack )
{ {
if( i >= 0 && i < INVENTORY_SIZE ) if( i >= 0 && i < INVENTORY_SIZE && !InventoryUtil.areItemsEqual( stack, m_inventory.get( i ) ) )
{ {
synchronized( m_inventory ) m_inventory.set( i, stack );
{ onInventoryDefinitelyChanged();
if( !InventoryUtil.areItemsEqual( stack, m_inventory.get( i ) ) )
{
m_inventory.set( i, stack );
onInventoryDefinitelyChanged();
}
}
} }
} }
@Override @Override
public void clear() public void clear()
{ {
synchronized( m_inventory ) boolean changed = false;
for( int i = 0; i < INVENTORY_SIZE; i++ )
{ {
boolean changed = false; if( !m_inventory.get( i ).isEmpty() )
for( int i = 0; i < INVENTORY_SIZE; i++ )
{ {
if( !m_inventory.get( i ).isEmpty() ) m_inventory.set( i, ItemStack.EMPTY );
{ changed = true;
m_inventory.set( i, ItemStack.EMPTY );
changed = true;
}
}
if( changed )
{
onInventoryDefinitelyChanged();
} }
} }
if( changed ) onInventoryDefinitelyChanged();
} }
@Override @Override
public void markDirty() public void markDirty()
{ {
super.markDirty(); super.markDirty();
synchronized( m_inventory ) if( !m_inventoryChanged )
{ {
if( !m_inventoryChanged ) for( int n = 0; n < getSizeInventory(); n++ )
{ {
for( int n = 0; n < getSizeInventory(); n++ ) if( !ItemStack.areItemStacksEqual( getStackInSlot( n ), m_previousInventory.get( n ) ) )
{ {
if( !ItemStack.areItemStacksEqual( getStackInSlot( n ), m_previousInventory.get( n ) ) ) m_inventoryChanged = true;
{ break;
m_inventoryChanged = true;
break;
}
} }
} }
} }
@ -587,8 +543,8 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
public void transferStateFrom( TileTurtle copy ) public void transferStateFrom( TileTurtle copy )
{ {
super.transferStateFrom( copy ); super.transferStateFrom( copy );
m_inventory = copy.m_inventory; Collections.copy( m_inventory, copy.m_inventory );
m_previousInventory = copy.m_previousInventory; Collections.copy( m_previousInventory, copy.m_previousInventory );
m_inventoryChanged = copy.m_inventoryChanged; m_inventoryChanged = copy.m_inventoryChanged;
m_brain = copy.m_brain; m_brain = copy.m_brain;
m_brain.setOwner( this ); m_brain.setOwner( this );

View File

@ -248,7 +248,7 @@ public class TurtleTool extends AbstractTurtleUpgrade
boolean canHarvest = state.canHarvestBlock( world, blockPosition, turtlePlayer ); boolean canHarvest = state.canHarvestBlock( world, blockPosition, turtlePlayer );
boolean canBreak = state.removedByPlayer( world, blockPosition, turtlePlayer, canHarvest, fluidState ); boolean canBreak = state.removedByPlayer( world, blockPosition, turtlePlayer, canHarvest, fluidState );
if( canBreak ) state.getBlock().onPlayerDestroy( world, blockPosition, state ); if( canBreak ) state.getBlock().onPlayerDestroy( world, blockPosition, state );
if( canHarvest ) if( canHarvest && canBreak )
{ {
state.getBlock().harvestBlock( world, turtlePlayer, blockPosition, state, tile, turtlePlayer.getHeldItemMainhand() ); state.getBlock().harvestBlock( world, turtlePlayer, blockPosition, state, tile, turtlePlayer.getHeldItemMainhand() );
} }

View File

@ -0,0 +1,46 @@
--- The @{craftos.expect} library provides helper functions for verifying that
-- function arguments are well-formed and of the correct type.
--
-- @module craftos.expect
local native_select, native_type = select, type
--- Expect an argument to have a specific type.
--
-- @tparam int index The 1-based argument index.
-- @param value The argument's value.
-- @tparam string ... The allowed types of the argument.
-- @throws If the value is not one of the allowed types.
local function expect(index, value, ...)
local t = native_type(value)
for i = 1, native_select("#", ...) do
if t == native_select(i, ...) then return true end
end
local types = table.pack(...)
for i = types.n, 1, -1 do
if types[i] == "nil" then table.remove(types, i) end
end
local type_names
if #types <= 1 then
type_names = tostring(...)
else
type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types]
end
-- If we can determine the function name with a high level of confidence, try to include it.
local name
if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then
local ok, info = pcall(debug.getinfo, 3, "nS")
if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end
end
if name then
error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 )
else
error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 )
end
end
return { expect = expect }

View File

@ -1,48 +1,19 @@
local native_select, native_type = select, type -- Load in expect from the module path.
--- Expect an argument to have a specific type.
-- --
-- @tparam int index The 1-based argument index. -- Ideally we'd use require, but that is part of the shell, and so is not
-- @param value The argument's value. -- available to the BIOS or any APIs. All APIs load this using dofile, but that
-- @tparam string ... The allowed types of the argument. -- has not been defined at this point.
-- @throws If the value is not one of the allowed types. local expect
local function expect(index, value, ...)
local t = native_type(value)
for i = 1, native_select("#", ...) do
if t == native_select(i, ...) then return true end
end
local types = table.pack(...) do
for i = types.n, 1, -1 do local h = fs.open("rom/modules/main/cc/expect.lua", "r")
if types[i] == "nil" then table.remove(types, i) end local f, err = loadstring(h.readAll(), "@expect.lua")
end h.close()
local type_names if not f then error(err) end
if #types <= 1 then expect = f().expect
type_names = tostring(...)
else
type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types]
end
-- If we can determine the function name with a high level of confidence, try to include it.
local name
if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then
local ok, info = pcall(debug.getinfo, 3, "nS")
if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end
end
if name then
error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 )
else
error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 )
end
end end
-- We expose expect in the global table as APIs need to access it, but give it
-- a non-identifier name - meaning it does not show up in auto-completion.
-- expect is an internal function, and should not be used by users.
_G["~expect"] = expect
if _VERSION == "Lua 5.1" then if _VERSION == "Lua 5.1" then
-- If we're on Lua 5.1, install parts of the Lua 5.2/5.3 API so that programs can be written against it -- If we're on Lua 5.1, install parts of the Lua 5.2/5.3 API so that programs can be written against it
local type = type local type = type
@ -568,23 +539,28 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault )
return sLine return sLine
end end
function loadfile( _sFile, _tEnv ) function loadfile( filename, mode, env )
expect(1, _sFile, "string") -- Support the previous `loadfile(filename, env)` form instead.
expect(2, _tEnv, "table", "nil") if type(mode) == "table" and env == nil then
mode, env = nil, mode
local file = fs.open( _sFile, "r" )
if file then
local func, err = load( file.readAll(), "@" .. fs.getName( _sFile ), "t", _tEnv )
file.close()
return func, err
end end
return nil, "File not found"
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.getName( filename ), mode, env )
file.close()
return func, err
end end
function dofile( _sFile ) function dofile( _sFile )
expect(1, _sFile, "string") expect(1, _sFile, "string")
local fnFile, e = loadfile( _sFile, _G ) local fnFile, e = loadfile( _sFile, nil, _G )
if fnFile then if fnFile then
return fnFile() return fnFile()
else else
@ -600,7 +576,7 @@ function os.run( _tEnv, _sPath, ... )
local tArgs = table.pack( ... ) local tArgs = table.pack( ... )
local tEnv = _tEnv local tEnv = _tEnv
setmetatable( tEnv, { __index = _G } ) setmetatable( tEnv, { __index = _G } )
local fnFile, err = loadfile( _sPath, tEnv ) local fnFile, err = loadfile( _sPath, nil, tEnv )
if fnFile then if fnFile then
local ok, err = pcall( function() local ok, err = pcall( function()
fnFile( table.unpack( tArgs, 1, tArgs.n ) ) fnFile( table.unpack( tArgs, 1, tArgs.n ) )
@ -634,7 +610,7 @@ function os.loadAPI( _sPath )
local tEnv = {} local tEnv = {}
setmetatable( tEnv, { __index = _G } ) setmetatable( tEnv, { __index = _G } )
local fnAPI, err = loadfile( _sPath, tEnv ) local fnAPI, err = loadfile( _sPath, nil, tEnv )
if fnAPI then if fnAPI then
local ok, err = pcall( fnAPI ) local ok, err = pcall( fnAPI )
if not ok then if not ok then

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
-- Colors -- Colors
white = 1 white = 1

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
CHANNEL_GPS = 65534 CHANNEL_GPS = 65534

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local sPath = "/rom/help" local sPath = "/rom/help"

View File

@ -1,6 +1,6 @@
-- Definition for the IO API -- Definition for the IO API
local expect, typeOf = _G["~expect"], _G.type local expect, typeOf = dofile("rom/modules/main/cc/expect.lua").expect, _G.type
--- If we return nil then close the file, as we've reached the end. --- If we return nil then close the file, as we've reached the end.
-- We use this weird wrapper function as we wish to preserve the varargs -- We use this weird wrapper function as we wish to preserve the varargs

View File

@ -8,7 +8,7 @@
-- taught me anything, it's that emulating LWJGL's weird key handling is nigh-on -- taught me anything, it's that emulating LWJGL's weird key handling is nigh-on
-- impossible. -- impossible.
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local tKeys = {} local tKeys = {}
tKeys[32] = 'space' tKeys[32] = 'space'

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local function drawPixelInternal( xPos, yPos ) local function drawPixelInternal( xPos, yPos )
term.setCursorPos( xPos, yPos ) term.setCursorPos( xPos, yPos )

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local native = peripheral local native = peripheral

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
CHANNEL_BROADCAST = 65535 CHANNEL_BROADCAST = 65535
CHANNEL_REPEAT = 65533 CHANNEL_REPEAT = 65533

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local tSettings = {} local tSettings = {}

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local native = (term.native and term.native()) or term local native = (term.native and term.native()) or term
local redirectTarget = native local redirectTarget = native

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
function slowWrite( sText, nRate ) function slowWrite( sText, nRate )
expect(2, nRate, "number", "nil") expect(2, nRate, "number", "nil")

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local tHex = { local tHex = {
[ colors.white ] = "0", [ colors.white ] = "0",
@ -388,6 +388,16 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible )
return nBackgroundColor return nBackgroundColor
end end
function window.getLine(y)
if type(y) ~= "number" then expect(1, y, "number") end
if y < 1 or y > nHeight then
error("Line is out of range.", 2)
end
return tLines[y].text, tLines[y].textColor, tLines[y].backgroundColor
end
-- Other functions -- Other functions
function window.setVisible( bVis ) function window.setVisible( bVis )
if type(bVis) ~= "boolean" then expect(1, bVis, "boolean") end if type(bVis) ~= "boolean" then expect(1, bVis, "boolean") end

View File

@ -1,3 +1,20 @@
New features in CC: Tweaked 1.84.0
* Improve validation in rename, copy and delete programs
* Add window.getLine - the inverse of blit
* turtle.refuel no longer consumes more fuel than needed
* Add "cc.expect" module, for improved argument type checks
* Mount the ROM from all mod jars, not just CC's
And several bug fixes:
* Ensure file error messages use the absolute correct path
* Fix NPE when closing a file multiple times.
* Do not load chunks when calling writeDescription.
* Fix the signature of loadfile
* Fix turtles harvesting blocks multiple times
* Improve thread-safety of various peripherals
* Prevent printed pages having massive/malformed titles
# New features in CC: Tweaked 1.83.1 # New features in CC: Tweaked 1.83.1
* Add several new MOTD messages (JakobDev) * Add several new MOTD messages (JakobDev)

View File

@ -1,10 +1,18 @@
New features in CC: Tweaked 1.83.1 New features in CC: Tweaked 1.84.0
* Add several new MOTD messages (JakobDev) * Improve validation in rename, copy and delete programs
* Add window.getLine - the inverse of blit
* turtle.refuel no longer consumes more fuel than needed
* Add "cc.expect" module, for improved argument type checks
* Mount the ROM from all mod jars, not just CC's
And several bug fixes: And several bug fixes:
* Fix type check in `rednet.lookup` * Ensure file error messages use the absolute correct path
* Error if turtle and pocket computer programs are run on the wrong system (JakobDev) * Fix NPE when closing a file multiple times.
* Do not discard varargs after a nil. * Do not load chunks when calling writeDescription.
* Fix the signature of loadfile
* Fix turtles harvesting blocks multiple times
* Improve thread-safety of various peripherals
* Prevent printed pages having massive/malformed titles
Type "help changelog" to see the full version history. Type "help changelog" to see the full version history.

View File

@ -23,3 +23,4 @@ getPosition()
reposition( x, y, width, height ) reposition( x, y, width, height )
getPaletteColor( color ) getPaletteColor( color )
setPaletteColor( color, r, g, b ) setPaletteColor( color, r, g, b )
getLine()

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
-- Setup process switching -- Setup process switching
local parentTerm = term.current() local parentTerm = term.current()

View File

@ -329,7 +329,7 @@ local tMenuFuncs = {
printer.setPageTitle( sName.." (page "..nPage..")" ) printer.setPageTitle( sName.." (page "..nPage..")" )
end end
while not printer.newPage() do while not printer.newPage() do
if printer.getInkLevel() < 1 then if printer.getInkLevel() < 1 then
sStatus = "Printer out of ink, please refill" sStatus = "Printer out of ink, please refill"
elseif printer.getPaperLevel() < 1 then elseif printer.getPaperLevel() < 1 then
@ -342,7 +342,6 @@ local tMenuFuncs = {
redrawMenu() redrawMenu()
term.redirect( printerTerminal ) term.redirect( printerTerminal )
local timer = os.startTimer(0.5)
sleep(0.5) sleep(0.5)
end end

View File

@ -1,4 +1,4 @@
local expect = _G["~expect"] local expect = dofile("rom/modules/main/cc/expect.lua").expect
local multishell = multishell local multishell = multishell
local parentShell = shell local parentShell = shell
@ -56,7 +56,7 @@ local function createShellEnv( sDir )
sPath = fs.combine(sDir, sPath) sPath = fs.combine(sDir, sPath)
end end
if fs.exists(sPath) and not fs.isDir(sPath) then if fs.exists(sPath) and not fs.isDir(sPath) then
local fnFile, sError = loadfile( sPath, tEnv ) local fnFile, sError = loadfile( sPath, nil, tEnv )
if fnFile then if fnFile then
return fnFile, sPath return fnFile, sPath
else else

View File

@ -90,7 +90,7 @@ public class ComputerTestDelegate
try( WritableByteChannel channel = mount.openChannelForWrite( "startup.lua" ); try( WritableByteChannel channel = mount.openChannelForWrite( "startup.lua" );
Writer writer = Channels.newWriter( channel, StandardCharsets.UTF_8.newEncoder(), -1 ) ) Writer writer = Channels.newWriter( channel, StandardCharsets.UTF_8.newEncoder(), -1 ) )
{ {
writer.write( "loadfile('test/mcfly.lua', _ENV)('test/spec') cct_test.finish()" ); writer.write( "loadfile('test/mcfly.lua', nil, _ENV)('test/spec') cct_test.finish()" );
} }
computer = new Computer( new BasicEnvironment( mount ), term, 0 ); computer = new Computer( new BasicEnvironment( mount ), term, 0 );

View File

@ -33,7 +33,7 @@ public class ComputerBootstrap
{ {
MemoryMount mount = new MemoryMount() MemoryMount mount = new MemoryMount()
.addFile( "test.lua", program ) .addFile( "test.lua", program )
.addFile( "startup", "assertion.assert(pcall(loadfile('test.lua', _ENV))) os.shutdown()" ); .addFile( "startup", "assertion.assert(pcall(loadfile('test.lua', nil, _ENV))) os.shutdown()" );
run( mount, x -> { } ); run( mount, x -> { } );
} }

View File

@ -27,18 +27,58 @@ local function check(func, arg, ty, val)
end end
end end
--- A stub - wraps a value within a a table,
local stub_mt = {}
stub_mt.__index = stub_mt
--- Revert this stub, restoring the previous value.
--
-- Note, a stub can only be reverted once.
function stub_mt:revert()
if not self.active then return end
self.active = false
rawset(self.stubbed_in, self.key, self.original)
end
local active_stubs = {} local active_stubs = {}
--- Stub a global variable with a specific value local function default_stub() end
--
-- @tparam string var The variable to stub
-- @param value The value to stub it with
local function stub(tbl, var, value)
check('stub', 1, 'table', tbl)
check('stub', 2, 'string', var)
table.insert(active_stubs, { tbl = tbl, var = var, value = tbl[var] }) --- Stub a table entry with a new value.
rawset(tbl, var, value) --
-- @tparam table
-- @tparam string key The variable to stub
-- @param[opt] value The value to stub it with. If this is a function, one can
-- use the various stub expectation methods to determine what it was called
-- with. Defaults to an empty function - pass @{nil} in explicitly to set the
-- value to nil.
-- @treturn Stub The resulting stub
local function stub(tbl, key, ...)
check('stub', 1, 'table', tbl)
check('stub', 2, 'string', key)
local stub = setmetatable({
active = true,
stubbed_in = tbl,
key = key,
original = rawget(tbl, key),
}, stub_mt)
local value = ...
if select('#', ...) == 0 then value = default_stub end
if type(value) == "function" then
local arguments, delegate = {}, value
stub.arguments = arguments
value = function(...)
arguments[#arguments + 1] = table.pack(...)
return delegate(...)
end
end
table.insert(active_stubs, stub)
rawset(tbl, key, value)
return stub
end end
--- Capture the current global state of the computer --- Capture the current global state of the computer
@ -51,16 +91,14 @@ local function push_state()
output = io.output(), output = io.output(),
dir = shell.dir(), dir = shell.dir(),
path = shell.path(), path = shell.path(),
aliases = shell.aliases(),
stubs = stubs, stubs = stubs,
} }
end end
--- Restore the global state of the computer to a previous version --- Restore the global state of the computer to a previous version
local function pop_state(state) local function pop_state(state)
for i = #active_stubs, 1, -1 do for i = #active_stubs, 1, -1 do active_stubs[i]:revert() end
local stub = active_stubs[i]
rawset(stub.tbl, stub.var, stub.value)
end
active_stubs = state.stubs active_stubs = state.stubs
@ -69,6 +107,14 @@ local function pop_state(state)
io.output(state.output) io.output(state.output)
shell.setDir(state.dir) shell.setDir(state.dir)
shell.setPath(state.path) shell.setPath(state.path)
local aliases = shell.aliases()
for k in pairs(aliases) do
if not state.aliases[k] then shell.clearAlias(k) end
end
for k, v in pairs(state.aliases) do
if aliases[k] ~= v then shell.setAlias(k, v) end
end
end end
local error_mt = { __tostring = function(self) return self.message end } local error_mt = { __tostring = function(self) return self.message end }
@ -210,6 +256,16 @@ local function matches(eq, exact, left, right)
return true return true
end end
local function pairwise_equal(left, right)
if left.n ~= right.n then return false end
for i = 1, left.n do
if left[i] ~= right[i] then return false end
end
return true
end
--- Assert that this expectation is structurally equivalent to --- Assert that this expectation is structurally equivalent to
-- the provided object. -- the provided object.
-- --
@ -236,6 +292,70 @@ function expect_mt:matches(value)
return self return self
end end
--- Assert that this stub was called a specific number of times.
--
-- @tparam[opt] number The exact number of times the function must be called.
-- If not given just require the function to be called at least once.
-- @raises If this function was not called the expected number of times.
function expect_mt:called(times)
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
fail(("Expected stubbed function, got %s"):format(type(self.value)))
end
local called = #self.value.arguments
if times == nil then
if called == 0 then
fail("Expected stub to be called\nbut it was not.")
end
else
check('stub', 1, 'number', times)
if called ~= times then
fail(("Expected stub to be called %d times\nbut was called %d times."):format(times, called))
end
end
return self
end
local function called_with_check(eq, self, ...)
if getmetatable(self.value) ~= stub_mt or self.value.arguments == nil then
fail(("Expected stubbed function, got %s"):format(type(self.value)))
end
local exp_args = table.pack(...)
local actual_args = self.value.arguments
for i = 1, #actual_args do
if eq(actual_args[i], exp_args) then return self end
end
local head = ("Expected stub to be called with %s\nbut was"):format(format(exp_args))
if #actual_args == 0 then
fail(head .. " not called at all")
elseif #actual_args == 1 then
fail(("%s called with %s."):format(head, format(actual_args[1])))
else
local lines = { head .. " called with:" }
for i = 1, #actual_args do lines[i + 1] = " - " .. format(actual_args[i]) end
fail(table.concat(lines, "\n"))
end
end
--- Assert that this stub was called with a set of arguments
--
-- Arguments are compared using exact equality.
function expect_mt:called_with(...)
return called_with_check(pairwise_equal, self, ...)
end
--- Assert that this stub was called with a set of arguments
--
-- Arguments are compared using matching.
function expect_mt:called_with_matching(...)
return called_with_check(matches, self, ...)
end
local expect = setmetatable( { local expect = setmetatable( {
--- Construct an expectation on the error message calling this function --- Construct an expectation on the error message calling this function
-- produces -- produces
@ -381,7 +501,7 @@ do
if fs.isDir(file) then if fs.isDir(file) then
run_in(file) run_in(file)
elseif file:sub(-#suffix) == suffix then elseif file:sub(-#suffix) == suffix then
local fun, err = loadfile(file, env) local fun, err = loadfile(file, nil, env)
if not fun then if not fun then
do_test { name = file:sub(#root_dir + 2), error = { message = err } } do_test { name = file:sub(#root_dir + 2), error = { message = err } }
else else

View File

@ -120,4 +120,21 @@ describe("The window library", function()
expect.error(w.reposition, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)") expect.error(w.reposition, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)")
end) end)
end) end)
describe("Window.getLine", function()
it("validates arguments", function()
local w = mk()
w.getLine(1)
local _, y = w.getSize()
expect.error(w.getLine, nil):eq("bad argument #1 (expected number, got nil)")
expect.error(w.getLine, 0):eq("Line is out of range.")
expect.error(w.getLine, y + 1):eq("Line is out of range.")
end)
it("provides a line's contents", function()
local w = mk()
w.blit("test", "aaaa", "4444")
expect({ w.getLine(1) }):same { "test ", "aaaa0", "4444f" }
end)
end)
end) end)

View File

@ -1,36 +1,4 @@
describe("The Lua base library", function() describe("The Lua base library", function()
describe("expect", function()
local e = _G["~expect"]
it("checks a single type", function()
expect(e(1, "test", "string")):eq(true)
expect(e(1, 2, "number")):eq(true)
expect.error(e, 1, nil, "string"):eq("bad argument #1 (expected string, got nil)")
expect.error(e, 2, 1, "nil"):eq("bad argument #2 (expected nil, got number)")
end)
it("checks multiple types", function()
expect(e(1, "test", "string", "number")):eq(true)
expect(e(1, 2, "string", "number")):eq(true)
expect.error(e, 1, nil, "string", "number"):eq("bad argument #1 (expected string or number, got nil)")
expect.error(e, 2, false, "string", "table", "number", "nil")
:eq("bad argument #2 (expected string, table or number, got boolean)")
end)
it("includes the function name", function()
local function worker()
expect(e(1, nil, "string")):eq(true)
end
local function trampoline()
worker()
end
expect.error(trampoline):eq("base_spec.lua:27: bad argument #1 to 'worker' (expected string, got nil)")
end)
end)
describe("sleep", function() describe("sleep", function()
it("validates arguments", function() it("validates arguments", function()
sleep(0) sleep(0)
@ -48,18 +16,43 @@ describe("The Lua base library", function()
end) end)
describe("loadfile", function() describe("loadfile", function()
local function make_file()
local tmp = fs.open("test-files/out.lua", "w")
tmp.write("return _ENV")
tmp.close()
end
it("validates arguments", function() it("validates arguments", function()
loadfile("") loadfile("")
loadfile("", {}) loadfile("", "")
loadfile("", "", {})
expect.error(loadfile, nil):eq("bad argument #1 (expected string, got nil)") expect.error(loadfile, nil):eq("bad argument #1 (expected string, got nil)")
expect.error(loadfile, "", false):eq("bad argument #2 (expected table, got boolean)") expect.error(loadfile, "", false):eq("bad argument #2 (expected string, got boolean)")
expect.error(loadfile, "", "", false):eq("bad argument #3 (expected table, got boolean)")
end) end)
it("prefixes the filename with @", function() it("prefixes the filename with @", function()
local info = debug.getinfo(loadfile("/rom/startup.lua"), "S") local info = debug.getinfo(loadfile("/rom/startup.lua"), "S")
expect(info):matches { short_src = "startup.lua", source = "@startup.lua" } expect(info):matches { short_src = "startup.lua", source = "@startup.lua" }
end) end)
it("loads a file with the global environment", function()
make_file()
expect(loadfile("test-files/out.lua")()):eq(_G)
end)
it("loads a file with a specific environment", function()
make_file()
local env = {}
expect(loadfile("test-files/out.lua", nil, env)()):eq(env)
end)
it("supports the old-style argument form", function()
make_file()
local env = {}
expect(loadfile("test-files/out.lua", env)()):eq(env)
end)
end) end)
describe("dofile", function() describe("dofile", function()

View File

@ -0,0 +1,31 @@
describe("cc.expect", function()
local e = require("cc.expect")
it("checks a single type", function()
expect(e.expect(1, "test", "string")):eq(true)
expect(e.expect(1, 2, "number")):eq(true)
expect.error(e.expect, 1, nil, "string"):eq("bad argument #1 (expected string, got nil)")
expect.error(e.expect, 2, 1, "nil"):eq("bad argument #2 (expected nil, got number)")
end)
it("checks multiple types", function()
expect(e.expect(1, "test", "string", "number")):eq(true)
expect(e.expect(1, 2, "string", "number")):eq(true)
expect.error(e.expect, 1, nil, "string", "number"):eq("bad argument #1 (expected string or number, got nil)")
expect.error(e.expect, 2, false, "string", "table", "number", "nil")
:eq("bad argument #2 (expected string, table or number, got boolean)")
end)
it("includes the function name", function()
local function worker()
expect(e.expect(1, nil, "string")):eq(true)
end
local function trampoline()
worker()
end
expect.error(trampoline):eq("expect_spec.lua:26: bad argument #1 to 'worker' (expected string, got nil)")
end)
end)

View File

@ -0,0 +1,11 @@
local capture = require "test_helpers".capture_program
describe("The bg program", function()
it("opens a tab in the background", function()
local openTab = stub(shell, "openTab", function() return 12 end)
local switchTab = stub(shell, "switchTab")
capture(stub, "bg")
expect(openTab):called_with("shell")
expect(switchTab):called(0)
end)
end)

View File

@ -0,0 +1,11 @@
local capture = require "test_helpers".capture_program
describe("The fg program", function()
it("opens the shell in the foreground", function()
local openTab = stub(shell, "openTab", function() return 12 end)
local switchTab = stub(shell, "switchTab")
capture(stub, "fg")
expect(openTab):called_with("shell")
expect(switchTab):called_with(12)
end)
end)

View File

@ -0,0 +1,28 @@
local capture = require "test_helpers".capture_program
describe("The alias program", function()
it("displays its usage when given too many arguments", function()
expect(capture(stub, "alias a b c"))
:matches { ok = true, output = "Usage: alias <alias> <program>\n", error = "" }
end)
it("lists aliases", function()
local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end)
stub(shell, "aliases", function() return { cp = "copy" } end)
expect(capture(stub, "alias"))
:matches { ok = true, output = "cp:copy\n", error = "" }
expect(pagedTabulate):called_with_matching({ "cp:copy" })
end)
it("sets an alias", function()
local setAlias = stub(shell, "setAlias")
capture(stub, "alias test Hello")
expect(setAlias):called_with("test", "Hello")
end)
it("clears an alias", function()
local clearAlias = stub(shell, "clearAlias")
capture(stub, "alias test")
expect(clearAlias):called_with("test")
end)
end)

View File

@ -1,19 +1,18 @@
local capture = require "test_helpers".capture_program local capture = require "test_helpers".capture_program
describe("The cd program", function() describe("The cd program", function()
it("changes into a directory", function()
it("cd into a directory", function() local setDir = stub(shell, "setDir")
shell.run("cd /rom/programs") capture(stub, "cd /rom/programs")
expect(setDir):called_with("rom/programs")
expect(shell.dir()):eq("rom/programs")
end) end)
it("cd into a not existing directory", function() it("does not move into a non-existent directory", function()
expect(capture(stub, "cd /rom/nothing")) expect(capture(stub, "cd /rom/nothing"))
:matches { ok = true, output = "Not a directory\n", error = "" } :matches { ok = true, output = "Not a directory\n", error = "" }
end) end)
it("displays the usage with no arguments", function() it("displays the usage when given no arguments", function()
expect(capture(stub, "cd")) expect(capture(stub, "cd"))
:matches { ok = true, output = "Usage: cd <path>\n", error = "" } :matches { ok = true, output = "Usage: cd <path>\n", error = "" }
end) end)

View File

@ -0,0 +1,13 @@
local capture = require "test_helpers".capture_program
describe("The clear program", function()
it("clears the screen", function()
local clear = stub(term, "clear")
local setCursorPos = stub(term, "setCursorPos")
capture(stub, "clear")
expect(clear):called(1)
expect(setCursorPos):called_with(1, 1)
end)
end)

View File

@ -0,0 +1,20 @@
local capture = require "test_helpers".capture_program
describe("The commands program", function()
it("displays an error without the commands api", function()
stub(_G, "commands", nil)
expect(capture(stub, "/rom/programs/command/commands.lua"))
:matches { ok = true, output = "", error = "Requires a Command Computer.\n" }
end)
it("lists commands", function()
local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end)
stub(_G, "commands", {
list = function() return { "computercraft" } end
})
expect(capture(stub, "/rom/programs/command/commands.lua"))
:matches { ok = true, output = "Available commands:\ncomputercraft\n", error = "" }
expect(pagedTabulate):called_with_matching({ "computercraft" })
end)
end)

View File

@ -0,0 +1,33 @@
local capture = require "test_helpers".capture_program
describe("The exec program", function()
it("displays an error without the commands api", function()
stub(_G, "commands", nil)
expect(capture(stub, "/rom/programs/command/exec.lua"))
:matches { ok = true, output = "", error = "Requires a Command Computer.\n" }
end)
it("displays its usage when given no argument", function()
stub(_G, "commands", {})
expect(capture(stub, "/rom/programs/command/exec.lua"))
:matches { ok = true, output = "", error = "Usage: exec <command>\n" }
end)
it("runs a command", function()
stub(_G, "commands", {
exec = function() return true, {"Hello World!"} end
})
expect(capture(stub, "/rom/programs/command/exec.lua computercraft"))
:matches { ok = true, output = "Success\nHello World!\n", error = "" }
end)
it("reports command failures", function()
stub(_G,"commands",{
exec = function() return false, {"Hello World!"} end
})
expect(capture(stub, "/rom/programs/command/exec.lua computercraft"))
:matches { ok = true, output = "Hello World!\n", error = "Failed\n" }
end)
end)

View File

@ -0,0 +1,16 @@
local capture = require "test_helpers".capture_program
describe("The drive program", function()
it("run the program", function()
local getFreeSpace = stub(fs, "getFreeSpace", function() return 1234e4 end)
expect(capture(stub, "drive"))
:matches { ok = true, output = "hdd (12.3MB remaining)\n", error = "" }
expect(getFreeSpace):called(1):called_with("")
end)
it("fails on a non-existent path", function()
expect(capture(stub, "drive /rom/nothing"))
:matches { ok = true, output = "No such path\n", error = "" }
end)
end)

View File

@ -1,10 +1,9 @@
local capture = require "test_helpers".capture_program local capture = require "test_helpers".capture_program
local testFile = require "test_helpers".testFile
describe("The edit program", function() describe("The edit program", function()
it("displays its usage when given no argument", function() it("displays its usage when given no argument", function()
multishell = nil
expect(capture(stub, "edit")) expect(capture(stub, "edit"))
:matches { ok = true, output = "Usage: edit <path>\n", error = "" } :matches { ok = true, output = "Usage: edit <path>\n", error = "" }
end) end)

View File

@ -0,0 +1,13 @@
local capture = require "test_helpers".capture_program
describe("The eject program", function()
it("displays its usage when given no argument", function()
expect(capture(stub, "eject"))
:matches { ok = true, output = "Usage: eject <drive>\n", error = "" }
end)
it("fails when trying to eject a non-drive", function()
expect(capture(stub, "eject /rom"))
:matches { ok = true, output = "Nothing in /rom drive\n", error = "" }
end)
end)

View File

@ -0,0 +1,9 @@
local capture = require "test_helpers".capture_program
describe("The exit program", function()
it("exits the shell", function()
local exit = stub(shell, "exit")
expect(capture(stub, "exit")):matches { ok = true, combined = "" }
expect(exit):called(1)
end)
end)

View File

@ -0,0 +1,8 @@
local capture = require "test_helpers".capture_program
describe("The paint program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "paint"))
:matches { ok = true, output = "Usage: paint <path>\n", error = "" }
end)
end)

View File

@ -0,0 +1,13 @@
local capture = require "test_helpers".capture_program
describe("The dj program", function()
it("displays its usage when given too many arguments", function()
expect(capture(stub, "dj a b c"))
:matches { ok = true, output = "Usages:\ndj play\ndj play <drive>\ndj stop\n", error = "" }
end)
it("fails when no disks are present", function()
expect(capture(stub, "dj"))
:matches { ok = true, output = "No Music Discs in attached disk drives\n", error = "" }
end)
end)

View File

@ -0,0 +1,10 @@
local capture = require "test_helpers".capture_program
describe("The hello program", function()
it("says hello", function()
local slowPrint = stub(textutils, "slowPrint", function(...) return print(...) end)
expect(capture(stub, "hello"))
:matches { ok = true, output = "Hello World!\n", error = "" }
expect(slowPrint):called(1)
end)
end)

View File

@ -0,0 +1,23 @@
local capture = require "test_helpers".capture_program
describe("The gps program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "gps"))
:matches { ok = true, output = "Usages:\ngps host\ngps host <x> <y> <z>\ngps locate\n", error = "" }
end)
it("fails on a pocket computer", function()
stub(_G, "pocket", {})
expect(capture(stub, "gps host"))
:matches { ok = true, output = "GPS Hosts must be stationary\n", error = "" }
end)
it("can locate the computer", function()
local locate = stub(gps, "locate", function() print("Some debugging information.") end)
expect(capture(stub, "gps locate"))
:matches { ok = true, output = "Some debugging information.\n", error = "" }
expect(locate):called_with(2, true)
end)
end)

View File

@ -0,0 +1,8 @@
local capture = require "test_helpers".capture_program
describe("The help program", function()
it("errors when there is no such help file", function()
expect(capture(stub, "help nothing"))
:matches { ok = true, output = "No help available\n", error = "" }
end)
end)

View File

@ -0,0 +1,34 @@
local capture = require "test_helpers".capture_program
describe("The label program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "label"))
:matches { ok = true, output = "Usages:\nlabel get\nlabel get <drive>\nlabel set <text>\nlabel set <drive> <text>\nlabel clear\nlabel clear <drive>\n", error = "" }
end)
describe("displays the computer's label", function()
it("when it is not labelled", function()
stub(os, "getComputerLabel", function() return nil end)
expect(capture(stub, "label get"))
:matches { ok = true, output = "No Computer label\n", error = "" }
end)
it("when it is labelled", function()
stub(os, "getComputerLabel", function() return "Test" end)
expect(capture(stub, "label get"))
:matches { ok = true, output = "Computer label is \"Test\"\n", error = "" }
end)
end)
it("sets the computer's label", function()
local setComputerLabel = stub(os, "setComputerLabel")
capture(stub, "label set Test")
expect(setComputerLabel):called_with("Test")
end)
it("clears the computer's label", function()
local setComputerLabel = stub(os, "setComputerLabel")
capture(stub, "label clear")
expect(setComputerLabel):called_with(nil)
end)
end)

View File

@ -0,0 +1,22 @@
local capture = require "test_helpers".capture_program
describe("The list program", function()
it("lists files", function()
local pagedTabulate = stub(textutils, "pagedTabulate")
capture(stub, "list /rom")
expect(pagedTabulate):called_with_matching(
colors.green, { "apis", "autorun", "help", "modules", "programs" },
colors.white, { "motd.txt", "startup.lua" }
)
end)
it("fails on a non-existent directory", function()
expect(capture(stub, "list /rom/nothing"))
:matches { ok = true, output = "", error = "Not a directory\n" }
end)
it("fails on a file", function()
expect(capture(stub, "list /rom/startup.lua"))
:matches { ok = true, output = "", error = "Not a directory\n" }
end)
end)

View File

@ -0,0 +1,8 @@
local capture = require "test_helpers".capture_program
describe("The monitor program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "monitor"))
:matches { ok = true, output = "Usage: monitor <name> <program> <arguments>\n", error = "" }
end)
end)

View File

@ -0,0 +1,8 @@
local capture = require "test_helpers".capture_program
describe("The peripherals program", function()
it("says when there are no peripherals", function()
expect(capture(stub, "peripherals" ))
:matches { ok = true, output = "Attached Peripherals:\nNone\n", error = "" }
end)
end)

View File

@ -0,0 +1,27 @@
local capture = require "test_helpers".capture_program
describe("The pocket equip program", function()
it("errors when not a pocket computer", function()
stub(_G, "pocket", nil)
expect(capture(stub, "/rom/programs/pocket/equip.lua"))
:matches { ok = true, output = "", error = "Requires a Pocket Computer\n" }
end)
it("can equip an upgrade", function()
stub(_G, "pocket", {
equipBack = function() return true end
})
expect(capture(stub, "/rom/programs/pocket/equip.lua"))
:matches { ok = true, output = "Item equipped\n", error = "" }
end)
it("handles when an upgrade cannot be equipped", function()
stub(_G, "pocket", {
equipBack = function() return false, "Cannot equip this item." end
})
expect(capture(stub, "/rom/programs/pocket/equip.lua"))
:matches { ok = true, output = "", error = "Cannot equip this item.\n" }
end)
end)

View File

@ -0,0 +1,27 @@
local capture = require "test_helpers".capture_program
describe("The pocket unequip program", function()
it("errors when not a pocket computer", function()
stub(_G, "pocket", nil)
expect(capture(stub, "/rom/programs/pocket/unequip.lua"))
:matches { ok = true, output = "", error = "Requires a Pocket Computer\n" }
end)
it("unequips an upgrade", function()
stub(_G, "pocket", {
unequipBack = function() return true end
})
expect(capture(stub, "/rom/programs/pocket/unequip.lua"))
:matches { ok = true, output = "Item unequipped\n", error = "" }
end)
it("handles when an upgrade cannot be equipped", function()
stub(_G, "pocket", {
unequipBack = function() return false, "Nothing to remove." end
})
expect(capture(stub, "/rom/programs/pocket/unequip.lua"))
:matches { ok = true, output = "", error = "Nothing to remove.\n" }
end)
end)

View File

@ -0,0 +1,14 @@
local capture = require "test_helpers".capture_program
describe("The programs program", function()
it("list programs", function()
local programs = stub(shell, "programs", function() return { "some", "programs" } end)
local pagedTabulate = stub(textutils, "pagedTabulate", function(x) print(table.unpack(x)) end)
expect(capture(stub, "/rom/programs/programs.lua"))
:matches { ok = true, output = "some programs\n", error = "" }
expect(programs):called_with(false)
expect(pagedTabulate):called_with_matching({ "some", "programs" })
end)
end)

View File

@ -0,0 +1,14 @@
local capture = require "test_helpers".capture_program
describe("The reboot program", function()
it("sleeps and then reboots", function()
local sleep = stub(_G, "sleep")
local reboot = stub(os, "reboot")
expect(capture(stub, "reboot"))
:matches { ok = true, output = "Goodbye\n", error = "" }
expect(sleep):called_with(1)
expect(reboot):called()
end)
end)

View File

@ -0,0 +1,8 @@
local capture = require "test_helpers".capture_program
describe("The redstone program", function()
it("displays its usage when given no arguments", function()
expect(capture(stub, "redstone"))
:matches { ok = true, output = "Usages:\nredstone probe\nredstone set <side> <value>\nredstone set <side> <color> <value>\nredstone pulse <side> <count> <period>\n", error = "" }
end)
end)

View File

@ -0,0 +1,15 @@
local capture = require "test_helpers".capture_program
describe("The shutdown program", function()
it("run the program", function()
local sleep = stub(_G, "sleep")
local shutdown = stub(os, "shutdown")
expect(capture(stub, "shutdown"))
:matches { ok = true, output = "Goodbye\n", error = "" }
expect(sleep):called_with(1)
expect(shutdown):called()
end)
end)

View File

@ -0,0 +1,69 @@
local capture = require "test_helpers".capture_program
describe("The craft program", function()
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/craft.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
it("fails when turtle.craft() is unavailable", function()
stub(_G, "turtle", {})
expect(capture(stub, "/rom/programs/turtle/craft.lua"))
:matches { ok = true, output = "Requires a Crafty Turtle\n", error = "" }
end)
it("displays its usage when given no arguments", function()
stub(_G, "turtle", { craft = function() end })
expect(capture(stub, "/rom/programs/turtle/craft.lua"))
:matches { ok = true, output = "Usage: craft [number]\n", error = "" }
end)
it("crafts multiple items", function()
local item_count = 3
stub(_G, "turtle", {
craft = function()
item_count = 1
return true
end,
getItemCount = function() return item_count end,
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua 2"))
:matches { ok = true, output = "2 items crafted\n", error = "" }
end)
it("craft a single item", function()
local item_count = 2
stub(_G,"turtle",{
craft = function()
item_count = 1
return true
end,
getItemCount = function() return item_count end,
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua 1"))
:matches { ok = true, output = "1 item crafted\n", error = "" }
end)
it("crafts no items", function()
local item_count = 2
stub(_G,"turtle",{
craft = function()
item_count = 1
return false
end,
getItemCount = function() return item_count end,
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua 1"))
:matches { ok = true, output = "No items crafted\n", error = "" }
end)
end)

View File

@ -0,0 +1,89 @@
local capture = require "test_helpers".capture_program
describe("The turtle equip program", function()
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/equip.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
it("displays its usage when given no arguments", function()
stub(_G, "turtle", {})
expect(capture(stub, "/rom/programs/turtle/equip.lua"))
:matches { ok = true, output = "Usage: equip <slot> <side>\n", error = "" }
end)
it("equip nothing", function()
stub(_G, "turtle", {
select = function() end,
getItemCount = function() return 0 end,
})
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Nothing to equip\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Nothing to equip\n", error = "" }
end)
it("swaps existing upgrades", function()
stub(_G,"turtle",{
select = function() end,
getItemCount = function() return 1 end,
equipLeft = function() return true end,
equipRight = function() return true end,
})
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Items swapped\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Items swapped\n", error = "" }
end)
describe("equips a new upgrade", function()
local function setup()
local item_count = 1
stub(_G,"turtle",{
select = function() end,
getItemCount = function() return item_count end,
equipLeft = function()
item_count = 0
return true
end,
equipRight = function()
item_count = 0
return true
end,
})
end
it("on the left", function()
setup()
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Item equipped\n", error = "" }
end)
it("on the right", function()
setup()
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Item equipped\n", error = "" }
end)
end)
it("handles when an upgrade cannot be equipped", function()
stub(_G,"turtle",{
select = function() end,
getItemCount = function() return 1 end,
equipLeft = function() return false end,
equipRight = function() return false end,
})
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 left"))
:matches { ok = true, output = "Item not equippable\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/equip.lua 1 right"))
:matches { ok = true, output = "Item not equippable\n", error = "" }
end)
end)

View File

@ -0,0 +1,62 @@
local capture = require "test_helpers".capture_program
describe("The refuel program", function()
local function setup_turtle(fuel_level, fuel_limit, item_count)
stub(_G, "turtle", {
getFuelLevel = function()
return fuel_level
end,
getItemCount = function()
return item_count
end,
refuel = function(nLimit)
item_count = item_count - nLimit
fuel_level = fuel_level + nLimit
end,
select = function()
end,
getFuelLimit = function()
return fuel_limit
end
})
end
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/refuel.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
it("displays its usage when given too many argument", function()
setup_turtle(0, 5, 0)
expect(capture(stub, "/rom/programs/turtle/refuel.lua a b"))
:matches { ok = true, output = "Usage: refuel [number]\n", error = "" }
end)
it("requires a numeric argument", function()
setup_turtle(0, 0, 0)
expect(capture(stub, "/rom/programs/turtle/refuel.lua nothing"))
:matches { ok = true, output = "Invalid limit, expected a number or \"all\"\n", error = "" }
end)
it("refuels the turtle", function()
setup_turtle(0, 10, 5)
expect(capture(stub, "/rom/programs/turtle/refuel.lua 5"))
:matches { ok = true, output = "Fuel level is 5\n", error = "" }
end)
it("reports when the fuel limit is reached", function()
setup_turtle(0,5,5)
expect(capture(stub, "/rom/programs/turtle/refuel.lua 5"))
:matches { ok = true, output = "Fuel level is 5\nFuel limit reached\n", error = "" }
end)
it("reports when the fuel level is unlimited", function()
setup_turtle("unlimited",5,5)
expect(capture(stub, "/rom/programs/turtle/refuel.lua 5"))
:matches { ok = true, output = "Fuel level is unlimited\n", error = "" }
end)
end)

View File

@ -0,0 +1,69 @@
local capture = require "test_helpers".capture_program
describe("The turtle unequip program", function()
it("errors when not a turtle", function()
stub(_G, "turtle", nil)
expect(capture(stub, "/rom/programs/turtle/unequip.lua"))
:matches { ok = true, output = "", error = "Requires a Turtle\n" }
end)
it("displays its usage when given no arguments", function()
stub(_G, "turtle", {})
expect(capture(stub, "/rom/programs/turtle/unequip.lua"))
:matches { ok = true, output = "Usage: unequip <side>\n", error = "" }
end)
it("says when nothing was unequipped", function()
stub(_G,"turtle",{
select = function() end,
getItemCount = function() return 0 end,
equipRight = function() return true end,
equipLeft = function() return true end
})
expect(capture(stub, "/rom/programs/turtle/unequip.lua left"))
:matches { ok = true, output = "Nothing to unequip\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/unequip.lua right"))
:matches { ok = true, output = "Nothing to unequip\n", error = "" }
end)
it("unequips a upgrade", function()
local item_count = 0
stub(_G,"turtle",{
select = function() end,
getItemCount = function() return item_count end,
equipRight = function()
item_count = 1
return true
end,
equipLeft = function()
item_count = 1
return true
end
})
expect(capture(stub, "/rom/programs/turtle/unequip.lua left"))
:matches { ok = true, output = "Item unequipped\n", error = "" }
item_count = 0
expect(capture(stub, "/rom/programs/turtle/unequip.lua right"))
:matches { ok = true, output = "Item unequipped\n", error = "" }
end)
it("fails when the turtle is full", function()
stub(_G,"turtle",{
select = function() end,
getItemCount = function() return 1 end,
equipRight = function() return true end,
equipLeft = function() return true end
})
expect(capture(stub, "/rom/programs/turtle/unequip.lua left"))
:matches { ok = true, output = "No space to unequip item\n", error = "" }
expect(capture(stub, "/rom/programs/turtle/unequip.lua right"))
:matches { ok = true, output = "No space to unequip item\n", error = "" }
end)
end)