1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-23 09:57:39 +00:00

Compare commits

..

63 Commits

Author SHA1 Message Date
SquidDev
3ea2d6a0a8 Merge branch 'master' into mc-1.14.x 2019-10-04 19:56:49 +01:00
SquidDev
c802290437 Bump version 2019-10-04 19:52:02 +01:00
SquidDev
f7781defe5 Merge branch 'master' into mc-1.14.x 2019-10-04 19:44:34 +01:00
SquidDev
418420523a Proxy the current turtle's inventory
Previously we were just returning the current tile. However, if someone
was holding a reference to this inventory (such as a GUI), then it'd be
outdated and invalid once the turtle had moved.

This caused a couple of issues:
 - turtle_inventory events would not be fired when moving items in the
   turtle GUI.
 - As of 75e2845c01, turtles would no
   longer share their inventory state after moving. Thus, removing items
   from a GUI using an invalid inventory would move them from an old
   tile, duplicating the items.

Fixes #298, fixes #300
2019-10-04 16:53:48 +01:00
SquidDev
d342a1f368 Prevent wired modems dropping on block change
Fixes #303
2019-10-01 20:06:38 +01:00
SquidDev
81f85361d5 Move Lua files to the correct location
I really need to fix this.
2019-10-01 19:43:03 +01:00
SquidDev
f1621b30ec Remove redundant imports 2019-10-01 19:15:13 +01:00
SquidDev
d4f6a594b6 Merge branch 'master' into mc-1.14.x 2019-10-01 18:58:40 +01:00
SquidDev
ff5ba5c131 Update GUI code to be compatible with 1.14.4
Not quite sure when this changed, but I'm fairly sure isMouseOver wasn't
a thing when I wrote this. Or I'm a plonker. Both are possible.

Also fixes mouse dragging not being handled in turtles.

Fixes #299
2019-10-01 18:53:46 +01:00
SquidDev
4243f30308 Bump Forge version 2019-10-01 18:53:38 +01:00
Jonathan Coates
813e91073d Merge pull request #302 from Wendelstein7/master
Fixed turtle property category
2019-09-30 15:59:55 +01:00
Jonathan Coates
7250f22ff6 Update CI to also run on PRs 2019-09-30 15:37:52 +01:00
Wendelstein7
db31a53bba Fixed turtle property category
Likely was a leftover from copy pasting and a tired programmer.
2019-09-30 15:15:22 +02:00
SquidDev
3023f235a4 Goodbye Travis, I'm with GH Actions now 2019-09-27 08:56:53 +01:00
SquidDev
79cd8b4da5 Also return the number of affected entities
Closes #293. Doesn't really solve anything there aside from exposing the
number, but sadly there's not really anything obvious I can do on my end
- the command API just doesn't expose anything else.
2019-09-15 18:48:51 +01:00
Jonathan Coates
8e4d311cd9 Refactor shell completion into a separate module (#281)
- Adds cc.completions module, with a couple of helper functions for
   working with the more general completion functionality (i.e. that
   provided by read).
 - Adds cc.shell.completions module, which provides shell-specific
   completion functions.
 - Add a "program completion builder", which allows you to write stuff
   like this:

       shell.setCompletionFunction( "rom/programs/redstone.lua", 
         completion.build(
           { completion.choice, { "probe", "set ", "pulse " } },
           completion.side) )

Closes #232
2019-09-15 18:48:40 +01:00
SquidDev
9bd8c86a94 Change event priority to HIGHEST
See the comments in a802f25dd6 -
effectively we want to make sure we arrive before any other thing which
may capture items (some magnet mods, etc...).
2019-09-15 16:42:21 +01:00
Jonathan Coates
cbc0c1d0b6 A little experiment with GitHub actions
Let's give this a go.
2019-09-14 09:16:13 +01:00
SquidDev
49c37857d4 Window.reposition now allow changing the redirect buffer
See #270
2019-09-13 20:55:20 +01:00
SquidDev
b1139a4bf6 Bump to Forge RB 2019-09-12 21:09:57 +01:00
Jonathan Coates
7e8559278e Merge pull request #291 from parly/patch-1.14.x-1
Fix os.time() and os.day() behavior on 1.14
2019-08-19 15:34:20 +01:00
parly
1e7f1c98fc Fix os.time() and os.day() behavior 2019-08-19 23:04:57 +09:00
SquidDev
a802f25dd6 Do not listen to block/entity drop events
It appears several mods inject their own drops on the LOWEST priority,
meaning that we capture the existing drops, and the other mod will clear
the (now empty) drop list and add its own, resulting in dupe bugs.

While I'd argue it's somewhat dubious doing this on the LOWEST priority,
it's not a battle I'm prepared to fight. For now, we just remove the
block/entity drop handlers, and handle all drop logic when entities are
spawned.

Fixes #288
2019-08-19 10:33:53 +01:00
SquidDev
f1d6d21d6d Add back texture registration hook
I totally forgot to do this when Forge re-added this functionality.

Fixes #285
2019-08-18 16:12:16 +01:00
Jonathan Coates
a80302c513 Merge pull request #287 from Lignum/patch-1
The pettiest of spelling fixes
2019-08-15 06:59:59 +01:00
Lignum
1c46949da7 Available 2019-08-15 07:37:02 +02:00
SquidDev
07a56454a0 Bumpity bumpity bump 2019-08-09 09:38:30 +01:00
SquidDev
a0e72d02c8 Bump Forge version 2019-08-06 08:33:39 +01:00
SquidDev
455a59ca85 Sure, you can be an actual release now 2019-08-04 17:01:52 +01:00
SquidDev
46d78af068 Fix changelog being out-of-sync 2019-08-04 11:05:44 +01:00
SquidDev
08d22fd3df Fix changelog being out-of-sync 2019-08-04 11:05:14 +01:00
SquidDev
e6c691a8f8 Fix rom's location
This isn't going to get annoying or anything
2019-08-04 11:02:07 +01:00
SquidDev
4b0e5c445c Merge branch 'master' into mc-1.14.x 2019-08-04 10:57:20 +01:00
SquidDev
eb5cff1045 Alright, let's do this one last time 2019-08-04 09:27:48 +01:00
SquidDev
35c7792aa2 Limit the titles of printed pages
Just enforce the same restrictions as we do for computer/disk labels.
2019-08-04 08:59:44 +01:00
Jonathan Coates
521688d630 Merge pull request #183 from SquidDev-CC/feature/thread-safe-inventories 2019-08-01 14:30:32 +01:00
SquidDev
75e2845c01 Remove synchronized from turtle inventory code
These should never be called off the server thread, so this doesn't make
much difference.
2019-08-01 14:09:57 +01:00
SquidDev
2f96283286 Make disk drives thread-safe 2019-08-01 13:48:03 +01:00
SquidDev
cbe6e9b5f5 Make printers thread-safe 2019-08-01 13:48:03 +01:00
SquidDev
2ab79cf474 Version bumps 'n stuff 2019-07-30 15:48:05 +01:00
SquidDev
6ce34aba79 A quick attempt at fixing Travis
Oracle JDK 8 is EOL (I think at least).
2019-07-30 15:25:14 +01:00
SquidDev
5eeb320b60 Include all mods within a resource mount
This is the behaviour on 1.14 already, so it makes sense to backport to
1.12.

Any mod may now insert files into assets/computercraft/lua/rom, and
they'll be automatically added to the default ROM mount. This allows
other mods to easily register new programs or autorun files.

See #242
2019-07-30 15:20:08 +01:00
SquidDev
93310850d2 Use the "cc" module namespace instead of "craftos"
This is what we actually discussed in the issue, and I failed to
remember.
2019-07-27 11:34:59 +01:00
powerboat9
a2880b12ca Do not refuel beyond the turtle limit (#274) 2019-07-24 08:15:02 +01:00
SquidDev
cef2657048 Fix turtles being replaced by leaves
And logs. Well, hopefully at least.

Fixes #278
2019-07-21 10:28:22 +01:00
SquidDev
ccd85eb055 Bump Forge version 2019-07-21 09:41:58 +01:00
powerboat9
303b57779a Fix turtles harvesting blocks when they shouldn't (#276)
harvestBlock should only be called when removedByPlayer and canHarvestBlock
return true, otherwise we run the risk of causing dupe bugs.

See #273.
2019-07-17 09:23:14 +01:00
SquidDev
6279816ecc Try using the HTTP one instead 2019-07-15 08:45:22 +01:00
SquidDev
4ae77261fa Petty changes because I'm petty 2019-07-13 08:29:28 +01:00
liquid
4b7d843b78 Removed term.getLine 2019-07-13 01:45:16 -05:00
liquid
1c28df65c3 Fixed style errors 2019-07-12 23:56:49 -05:00
liquid
85b740f484 Added term.getLine and window.getLine 2019-07-12 22:54:37 -05:00
SquidDev
f9929cb27d Fix the signature of loadfile
Lua 5.2+ uses loadfile(filename, mode, env), not loadfile(filename,
env). While this is a minor incompatibility, it'd be nice to be
consistent as much as possible.

We try to handle the incorrect case too, as obviously we don't want to
break existing programs.
2019-07-12 22:04:28 +01:00
SquidDev
bafab1ac07 Expose expect as a module (#267)
This moves expect from the bios into a new craftos.expect module,
removing the internal _G["~expect"] definition. Apparently people were
using this irrespective of the "don't use this" comment, so we need to
find another solution.

While this does introduce some ugliness (having to load the module in
weird ways for programs, duplicating the expect function in memory), it
does allow people to use the function in a supported way, and removes
the global ugliness.
2019-07-09 08:04:49 +01:00
JakobDev
e05c262468 Add more tests (#253)
I'm not entirely sure how useful all of these will be yet - still
trying to work out what/when to test things, but hopefully this'll
be a useful datapoint.
2019-07-08 09:24:05 +01:00
SquidDev
acfb72246c Bump JEI version 2019-07-07 15:52:52 +01:00
SquidDev
9d51c4c340 Use world-specific collider entity
This is equally an ugly hack, but means we're at least not constructing
entities with null worlds any more.

Ideally we could always use the turtle entity, but this will require a
bit more of a refactor.

Fixes #265
2019-07-05 22:09:42 +01:00
SquidDev
18068effec Use reflection to get the record's sound
getSound is client side only - I could have sworn it became shared at
some point, but that may have been reverted again (or I imagined it).

Fixes #263
2019-07-05 21:51:54 +01:00
SquidDev
7a3f7d3bba I'm a muppet
Even worse, I enabled branch protection for some reason, and so can't
force push and pretend this never happened.
2019-06-29 15:49:14 +01:00
SquidDev
95aa48c456 Allow running expectations against stubbed functions
Co-authored-by: hydraz <urn@semi.works>
2019-06-29 15:37:41 +01:00
SquidDev
904a168d5c Fix incorrect explosion check
We should block explosions if the turtle is advanced /or/ if it's from
a fireball or entity, not if both.

Fixes #257
2019-06-21 18:53:28 +01:00
JakobDev
724441eddc Change URLs in build.gradle to https (#259) 2019-06-21 16:51:55 +01:00
SquidDev
f68ab3edd1 Minor tweaks to build script
Mostly just rearranging. Bump JUnit version in an attempt to fix test
outputs, but it appears this is a mix of gradle/gradle#5975 and
gradle/gradle#4438.
2019-06-15 11:05:45 +01:00
116 changed files with 2066 additions and 916 deletions

18
.github/workflows/main-ci.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build with Gradle
run: ./gradlew build --no-daemon

View File

@@ -17,7 +17,8 @@ ignore = {
-- are largely unsupported.
include_files = {
'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'] = {

View File

@@ -1,14 +0,0 @@
language: java
script: ./gradlew build --no-daemon
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/s
jdk:
- oraclejdk8

View File

@@ -1,5 +1,5 @@
# ![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://github.com/SquidDev-CC/CC-Tweaked/workflows/Build/badge.svg)](https://github.com/SquidDev-CC/CC-Tweaked/actions "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,
turtles and more to Minecraft.

View File

@@ -4,12 +4,12 @@ buildscript {
mavenCentral()
maven {
name = "forge"
url = "http://files.minecraftforge.net/maven"
url = "https://files.minecraftforge.net/maven"
}
}
dependencies {
classpath 'com.google.code.gson:gson:2.8.1'
classpath 'net.minecraftforge.gradle:ForgeGradle:3.0.128'
classpath 'net.minecraftforge.gradle:ForgeGradle:3.0.147'
classpath 'net.sf.proguard:proguard-gradle:6.1.0beta2'
classpath 'org.ajoberstar.grgit:grgit-gradle:3.0.0'
}
@@ -67,7 +67,7 @@ minecraft {
repositories {
maven {
name "JEI"
url "http://dvs1.progwml6.com/files/maven"
url "https://dvs1.progwml6.com/files/maven"
}
maven {
name "SquidDev"
@@ -79,7 +79,7 @@ repositories {
}
maven {
name "Amadornes"
url "http://maven.amadornes.com/"
url "https://maven.amadornes.com/"
}
}
@@ -94,16 +94,16 @@ dependencies {
minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
compileOnly fg.deobf("mezz.jei:jei-1.13.2:5.0.0.20:api")
compileOnly fg.deobf("mezz.jei:jei-1.14.4:6.0.0.10:api")
// deobfProvided "pl.asie:Charset-Lib:0.5.4.6"
// deobfProvided "MCMultiPart2:MCMultiPart:2.5.3"
// runtimeOnly fg.deobf("mezz.jei:jei-1.13.2:5.0.0.20")
runtimeOnly fg.deobf("mezz.jei:jei-1.14.4:6.0.0.10")
shade 'org.squiddev:Cobalt:0.5.0-SNAPSHOT'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2'
deployerJars "org.apache.maven.wagon:wagon-ssh:3.0.0"
}
@@ -117,6 +117,8 @@ sourceSets {
}
}
// Compile tasks
javadoc {
include "dan200/computercraft/api/**/*.java"
}
@@ -127,7 +129,7 @@ jar {
manifest {
attributes(["Specification-Title": "computercraft",
"Specification-Vendor": "SquidDev",
"Specification-Version": "26.0",
"Specification-Version": "1",
"Implementation-Title": "CC: Tweaked",
"Implementation-Version": "${mod_version}",
"Implementation-Vendor" :"SquidDev",
@@ -141,6 +143,14 @@ jar {
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.file.*
import java.util.zip.*
@@ -276,7 +286,14 @@ task compressJson(dependsOn: jar) {
assemble.dependsOn compressJson
/* Check tasks */
// Check tasks
test {
useJUnitPlatform()
testLogging {
events "skipped", "failed"
}
}
license {
mapping("java", "SLASHSTAR_STYLE")
@@ -300,6 +317,13 @@ license {
}
}
gradle.projectsEvaluated {
tasks.withType(LicenseFormat) {
outputs.upToDateWhen { false }
}
}
task licenseAPI(type: LicenseCheck);
task licenseFormatAPI(type: LicenseFormat);
[licenseAPI, licenseFormatAPI].forEach {
@@ -310,7 +334,7 @@ task licenseFormatAPI(type: LicenseFormat);
}
}
/* Upload tasks */
// Upload tasks
task checkRelease {
group "upload"
@@ -355,7 +379,7 @@ curseforge {
apiKey = project.hasProperty('curseForgeApiKey') ? project.curseForgeApiKey : ''
project {
id = '282001'
releaseType = 'beta'
releaseType = 'release'
changelog = "Release notes can be found on the GitHub repository (https://github.com/SquidDev-CC/CC-Tweaked/releases/tag/v${mc_version}-${mod_version})."
relations {
@@ -431,7 +455,7 @@ githubRelease {
.takeWhile { it != 'Type "help changelog" to see the full version history.' }
.join("\n").trim()
}
prerelease true
prerelease false
}
def uploadTasks = ["uploadArchives", "curseforge", "githubRelease"]
@@ -441,23 +465,3 @@ task uploadAll(dependsOn: uploadTasks) {
group "upload"
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,7 +1,7 @@
# Mod properties
mod_version=1.83.1
mod_version=1.85.0
# Minecraft properties
mc_version=1.14.3
forge_version=27.0.3
mappings_version=20190626-1.14.3
mc_version=1.14.4
forge_version=28.1.26
mappings_version=20190912-1.14.3

View File

@@ -144,7 +144,9 @@ public interface ITurtleAccess
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
* @see #getItemHandler()
@@ -155,6 +157,8 @@ public interface ITurtleAccess
/**
* 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
* @see #getInventory()
* @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
* 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,
* {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking.
* Conforming implementations should fire {@link BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig} for
* digging, {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking.
*
* @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.

View File

@@ -12,6 +12,7 @@ import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.media.items.ItemDisk;
import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
import dan200.computercraft.shared.util.Colour;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.model.IBakedModel;
import net.minecraft.client.renderer.model.IUnbakedModel;
import net.minecraft.client.renderer.model.ModelResourceLocation;
@@ -58,8 +59,8 @@ public final class ClientRegistry
};
private static final String[] EXTRA_TEXTURES = new String[] {
// TODO: Gather these automatically from the model. I'm unable to get this working with Forge's current
// model loading code.
// TODO: Gather these automatically from the model. Sadly the model loader isn't available
// when stitching textures.
"block/turtle_colour",
"block/turtle_elf_overlay",
"block/turtle_crafty_face",
@@ -77,13 +78,12 @@ public final class ClientRegistry
@SubscribeEvent
public static void onTextureStitchEvent( TextureStitchEvent.Pre event )
{
/*
IResourceManager manager = Minecraft.getInstance().getResourceManager();
if( event.getMap() != Minecraft.getInstance().getTextureMap() ) return;
for( String extra : EXTRA_TEXTURES )
{
// TODO: event.getMap().registerSprite( manager, new ResourceLocation( ComputerCraft.MOD_ID, extra ) );
event.addSprite( new ResourceLocation( ComputerCraft.MOD_ID, extra ) );
}
*/
}
@SubscribeEvent

View File

@@ -8,9 +8,9 @@ package dan200.computercraft.client;
import dan200.computercraft.ComputerCraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.gameevent.TickEvent;
@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
public final class FrameInfo

View File

@@ -176,11 +176,4 @@ public final class GuiComputer<T extends ContainerComputerBase> extends Containe
return (getFocused() != null && getFocused().mouseDragged( x, y, button, deltaX, deltaY ))
|| super.mouseDragged( x, y, button, deltaX, deltaY );
}
@Override
public boolean mouseReleased( double x, double y, int button )
{
return (getFocused() != null && getFocused().mouseReleased( x, y, button ))
|| super.mouseReleased( x, y, button );
}
}

View File

@@ -129,4 +129,11 @@ public class GuiTurtle extends ContainerScreen<ContainerTurtle>
super.render( mouseX, mouseY, partialTicks );
renderHoveredToolTip( mouseX, mouseY );
}
@Override
public boolean mouseDragged( double x, double y, int button, double deltaX, double deltaY )
{
return (getFocused() != null && getFocused().mouseDragged( x, y, button, deltaX, deltaY ))
|| super.mouseDragged( x, y, button, deltaX, deltaY );
}
}

View File

@@ -241,11 +241,12 @@ public class WidgetTerminal implements IGuiEventListener
charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 );
charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 );
computer.mouseDrag( button + 1, charX + 1, charY + 1 );
lastMouseX = charX;
lastMouseY = charY;
lastMouseButton = button;
if( button == lastMouseButton && (charX != lastMouseX || charY != lastMouseY) )
{
computer.mouseDrag( button + 1, charX + 1, charY + 1 );
lastMouseX = charX;
lastMouseY = charY;
}
}
return false;
@@ -427,4 +428,10 @@ public class WidgetTerminal implements IGuiEventListener
ClientComputer computer = this.computer.get();
if( computer != null ) computer.queueEvent( event, args );
}
@Override
public boolean isMouseOver( double x, double y )
{
return true;
}
}

View File

@@ -95,4 +95,11 @@ public class WidgetWrapper implements IGuiEventListener
{
return height;
}
@Override
public boolean isMouseOver( double x, double y )
{
double dx = x - this.x, dy = y - this.y;
return dx >= 0 && dx < width && dy >= 0 && dy < height;
}
}

View File

@@ -15,7 +15,6 @@ import net.minecraft.block.BlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.ActiveRenderInfo;
import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.util.math.RayTraceResult;
@@ -39,7 +38,7 @@ public final class CableHighlightRenderer
* Draw an outline for a specific part of a cable "Multipart".
*
* @param event The event to observe
* @see WorldRenderer#drawSelectionBox(PlayerEntity, RayTraceResult, int, float)
* @see WorldRenderer#drawSelectionBox(ActiveRenderInfo, RayTraceResult, int)
*/
@SubscribeEvent
public static void drawHighlight( DrawBlockHighlightEvent event )
@@ -48,7 +47,7 @@ public final class CableHighlightRenderer
BlockRayTraceResult hit = (BlockRayTraceResult) event.getTarget();
BlockPos pos = hit.getPos();
World world = event.getInfo().func_216773_g().getEntityWorld();
World world = event.getInfo().getRenderViewEntity().getEntityWorld();
ActiveRenderInfo info = event.getInfo();
BlockState state = world.getBlockState( pos );

View File

@@ -42,12 +42,12 @@ public final class MonitorHighlightRenderer
@SubscribeEvent
public static void drawHighlight( DrawBlockHighlightEvent event )
{
if( event.getTarget().getType() != RayTraceResult.Type.BLOCK || event.getInfo().func_216773_g().isSneaking() )
if( event.getTarget().getType() != RayTraceResult.Type.BLOCK || event.getInfo().getRenderViewEntity().isSneaking() )
{
return;
}
World world = event.getInfo().func_216773_g().getEntityWorld();
World world = event.getInfo().getRenderViewEntity().getEntityWorld();
BlockPos pos = ((BlockRayTraceResult) event.getTarget()).getPos();
TileEntity tile = world.getTileEntity( pos );

View File

@@ -119,6 +119,7 @@ public class TurtleMultiModel implements IBakedModel
@Nonnull
@Override
@Deprecated
public TextureAtlasSprite getParticleTexture()
{
return m_baseModel.getParticleTexture();

View File

@@ -202,6 +202,7 @@ public class TurtleSmartItemModel implements IBakedModel
@Nonnull
@Override
@Deprecated
public TextureAtlasSprite getParticleTexture()
{
return familyModel.getParticleTexture();

View File

@@ -34,8 +34,8 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
import net.minecraft.util.text.TranslationTextComponent;
import net.minecraft.world.ServerWorld;
import net.minecraft.world.World;
import net.minecraft.world.server.ServerWorld;
import javax.annotation.Nonnull;
import java.util.*;

View File

@@ -89,4 +89,10 @@ public abstract class BlockGeneric extends Block
{
return type.create();
}
@Override
public boolean canBeReplacedByLeaves( BlockState state, IWorldReader world, BlockPos pos )
{
return false;
}
}

View File

@@ -8,7 +8,6 @@ package dan200.computercraft.shared.common;
import dan200.computercraft.shared.network.container.ContainerData;
import dan200.computercraft.shared.network.container.HeldItemContainerData;
import dan200.computercraft.shared.util.InventoryUtil;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.container.Container;
@@ -33,7 +32,7 @@ public class ContainerHeldItem extends Container
super( type, id );
this.hand = hand;
stack = InventoryUtil.copyItem( player.getHeldItem( hand ) );
stack = player.getHeldItem( hand ).copy();
}
private static ContainerHeldItem createPrintout( int id, PlayerInventory inventory, HeldItemContainerData data )

View File

@@ -35,7 +35,6 @@ public abstract class TileGeneric extends TileEntity
markDirty();
BlockPos pos = getPos();
BlockState state = getBlockState();
getWorld().markForRerender( pos );
getWorld().notifyBlockUpdate( pos, state, state, 3 );
}

View File

@@ -85,7 +85,7 @@ public class CommandAPI implements ILuaAPI
{
receiver.clearOutput();
int result = commandManager.handleCommand( m_computer.getSource(), command );
return new Object[] { result > 0, receiver.copyOutput() };
return new Object[] { result > 0, receiver.copyOutput(), result };
}
catch( Throwable t )
{

View File

@@ -25,8 +25,8 @@ import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.ServerWorld;
import net.minecraft.world.World;
import net.minecraft.world.server.ServerWorld;
import net.minecraft.world.storage.loot.LootContext;
import net.minecraft.world.storage.loot.LootParameters;

View File

@@ -23,7 +23,7 @@ import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
import net.minecraft.util.text.TranslationTextComponent;
import net.minecraft.world.GameRules;
import net.minecraft.world.ServerWorld;
import net.minecraft.world.server.ServerWorld;
import javax.annotation.Nonnull;
import java.util.HashMap;

View File

@@ -311,13 +311,13 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput
@Override
public double getTimeOfDay()
{
return (m_world.getGameTime() + 6000) % 24000 / 1000.0;
return (m_world.getDayTime() + 6000) % 24000 / 1000.0;
}
@Override
public int getDay()
{
return (int) ((m_world.getGameTime() + 6000) / 24000) + 1;
return (int) ((m_world.getDayTime() + 6000) / 24000) + 1;
}
@Override

View File

@@ -6,11 +6,16 @@
package dan200.computercraft.shared.media.items;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.shared.util.RecordUtil;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.MusicDiscItem;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.text.TranslationTextComponent;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper.UnableToAccessFieldException;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper.UnableToFindFieldException;
import javax.annotation.Nonnull;
@@ -34,12 +39,26 @@ public final class RecordMedia implements IMedia
@Override
public String getAudioTitle( @Nonnull ItemStack stack )
{
return RecordUtil.getRecordInfo( stack );
Item item = stack.getItem();
if( !(item instanceof MusicDiscItem) ) return null;
return new TranslationTextComponent( item.getTranslationKey() + ".desc" ).getString();
}
@Override
public SoundEvent getAudio( @Nonnull ItemStack stack )
{
return ((MusicDiscItem) stack.getItem()).getSound();
Item item = stack.getItem();
if( !(item instanceof MusicDiscItem) ) return null;
try
{
return ObfuscationReflectionHelper.getPrivateValue( MusicDiscItem.class, (MusicDiscItem) item, "field_185076_b" );
}
catch( UnableToAccessFieldException | UnableToFindFieldException e )
{
ComputerCraft.log.error( "Cannot get disk sound", e );
return null;
}
}
}

View File

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

View File

@@ -320,35 +320,31 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
}
@Nonnull
public ItemStack getDiskStack()
ItemStack getDiskStack()
{
return getStackInSlot( 0 );
}
public void setDiskStack( @Nonnull ItemStack stack )
void setDiskStack( @Nonnull ItemStack stack )
{
setInventorySlotContents( 0, stack );
}
public IMedia getDiskMedia()
private IMedia getDiskMedia()
{
return MediaProviders.get( getDiskStack() );
}
public String getDiskMountPath( IComputerAccess computer )
String getDiskMountPath( IComputerAccess computer )
{
synchronized( this )
{
if( m_computers.containsKey( computer ) )
{
MountInfo info = m_computers.get( computer );
return info.mountPath;
}
MountInfo info = m_computers.get( computer );
return info != null ? info.mountPath : null;
}
return null;
}
public void mount( IComputerAccess computer )
void mount( IComputerAccess computer )
{
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 )
{
@@ -366,7 +362,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
}
}
public void playDiskAudio()
void playDiskAudio()
{
synchronized( this )
{
@@ -379,7 +375,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
}
}
public void stopDiskAudio()
void stopDiskAudio()
{
synchronized( this )
{
@@ -388,7 +384,7 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
}
}
public void ejectDisk()
void ejectDisk()
{
synchronized( this )
{
@@ -509,28 +505,6 @@ public final class TileDiskDrive extends TileGeneric implements DefaultInventory
if( !destroyed ) getWorld().playBroadcastSound( 1000, getPos(), 0 );
}
@Override
protected void readDescription( @Nonnull CompoundNBT nbt )
{
super.readDescription( nbt );
customName = nbt.contains( NBT_NAME ) ? ITextComponent.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null;
m_diskStack = nbt.contains( NBT_ITEM ) ? ItemStack.read( nbt.getCompound( NBT_ITEM ) ) : ItemStack.EMPTY;
updateBlock();
}
@Override
protected void writeDescription( @Nonnull CompoundNBT nbt )
{
super.writeDescription( nbt );
if( customName != null ) nbt.putString( NBT_NAME, ITextComponent.Serializer.toJson( customName ) );
if( !m_diskStack.isEmpty() )
{
CompoundNBT item = new CompoundNBT();
m_diskStack.write( item );
nbt.put( NBT_ITEM, item );
}
}
// Private methods
private void playRecord()

View File

@@ -210,7 +210,7 @@ public class BlockCable extends BlockGeneric implements IWaterLoggable
BlockPos offsetPos = pos.offset( facing );
BlockState offsetState = world.getBlockState( offsetPos );
return Block.hasSolidSide( offsetState, world, offsetPos, facing.getOpposite() );
return hasSolidSide( offsetState, world, offsetPos, facing.getOpposite() );
}
@Nullable

View File

@@ -201,8 +201,7 @@ public class TileCable extends TileGeneric implements IPeripheralTile
public void onNeighbourChange( @Nonnull BlockPos neighbour )
{
Direction dir = getDirection();
if( neighbour.equals( getPos().offset( dir ) ) && hasModem()
&& getBlockState().isValidPosition( world, pos ) )
if( neighbour.equals( getPos().offset( dir ) ) && hasModem() && !getBlockState().isValidPosition( getWorld(), getPos() ) )
{
if( hasCable() )
{

View File

@@ -34,7 +34,7 @@ public class ContainerPrinter extends Container
this.properties = properties;
this.inventory = inventory;
func_216961_a( properties );
trackIntArray( properties );
// Ink slot
addSlot( new Slot( inventory, 0, 13, 35 ) );

View File

@@ -11,6 +11,7 @@ import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.util.StringUtil;
import javax.annotation.Nonnull;
@@ -51,8 +52,13 @@ public class PrinterPeripheral implements IPeripheral
}
@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 )
{
case 0: // write
@@ -89,10 +95,13 @@ public class PrinterPeripheral implements IPeripheral
return new Object[] { width, height };
}
case 4: // newPage
return new Object[] { m_printer.startNewPage() };
return context.executeMainThreadTask( () -> new Object[] { m_printer.startNewPage() } );
case 5: // endPage
getCurrentPage();
return new Object[] { m_printer.endCurrentPage() };
return context.executeMainThreadTask( () -> {
getCurrentPage();
return new Object[] { m_printer.endCurrentPage() };
} );
case 6: // getInkLevel
return new Object[] { m_printer.getInkLevel() };
case 7:
@@ -100,7 +109,7 @@ public class PrinterPeripheral implements IPeripheral
// setPageTitle
String title = optString( args, 0, "" );
getCurrentPage();
m_printer.setPageTitle( title );
m_printer.setPageTitle( StringUtil.normaliseLabel( title ) );
return null;
}
case 8: // getPaperLevel
@@ -123,13 +132,11 @@ public class PrinterPeripheral implements IPeripheral
return m_printer;
}
@Nonnull
private Terminal getCurrentPage() throws LuaException
{
Terminal currentPage = m_printer.getCurrentPage();
if( currentPage == null )
{
throw new LuaException( "Page not started" );
}
if( currentPage == null ) throw new LuaException( "Page not started" );
return currentPage;
}
}

View File

@@ -120,10 +120,7 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
}
// Read inventory
synchronized( m_inventory )
{
ItemStackHelper.loadAllItems( nbt, m_inventory );
}
ItemStackHelper.loadAllItems( nbt, m_inventory );
}
@Nonnull
@@ -141,30 +138,12 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
}
// Write inventory
synchronized( m_inventory )
{
ItemStackHelper.saveAllItems( nbt, m_inventory );
}
ItemStackHelper.saveAllItems( nbt, m_inventory );
return super.write( nbt );
}
@Override
protected void writeDescription( @Nonnull CompoundNBT nbt )
{
super.writeDescription( nbt );
if( customName != null ) nbt.putString( NBT_NAME, ITextComponent.Serializer.toJson( customName ) );
}
@Override
public void readDescription( @Nonnull CompoundNBT nbt )
{
super.readDescription( nbt );
customName = nbt.contains( NBT_NAME ) ? ITextComponent.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null;
updateBlock();
}
public boolean isPrinting()
boolean isPrinting()
{
return m_printing;
}
@@ -188,73 +167,59 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
@Nonnull
@Override
public ItemStack getStackInSlot( int i )
public ItemStack getStackInSlot( int slot )
{
return m_inventory.get( i );
return m_inventory.get( slot );
}
@Nonnull
@Override
public ItemStack removeStackFromSlot( int i )
public ItemStack removeStackFromSlot( int slot )
{
synchronized( m_inventory )
{
ItemStack result = m_inventory.get( i );
m_inventory.set( i, ItemStack.EMPTY );
markDirty();
updateBlockState();
return result;
}
ItemStack result = m_inventory.get( slot );
m_inventory.set( slot, ItemStack.EMPTY );
markDirty();
updateBlockState();
return result;
}
@Nonnull
@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;
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;
setInventorySlotContents( slot, ItemStack.EMPTY );
return stack;
}
ItemStack part = stack.split( count );
if( m_inventory.get( slot ).isEmpty() )
{
m_inventory.set( slot, ItemStack.EMPTY );
updateBlockState();
}
markDirty();
return part;
}
@Override
public void setInventorySlotContents( int i, @Nonnull ItemStack stack )
public void setInventorySlotContents( int slot, @Nonnull ItemStack stack )
{
synchronized( m_inventory )
{
m_inventory.set( i, stack );
markDirty();
updateBlockState();
}
m_inventory.set( slot, stack );
markDirty();
updateBlockState();
}
@Override
public void clear()
{
synchronized( m_inventory )
{
for( int i = 0; i < m_inventory.size(); i++ ) m_inventory.set( i, ItemStack.EMPTY );
markDirty();
updateBlockState();
}
for( int i = 0; i < m_inventory.size(); i++ ) m_inventory.set( i, ItemStack.EMPTY );
markDirty();
updateBlockState();
}
@Override
@@ -305,14 +270,18 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
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( m_printing && !outputPage() ) return false;
@@ -320,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 true;
}
}
return false;
}
public int getInkLevel()
{
synchronized( m_inventory )
{
ItemStack inkStack = m_inventory.get( 0 );
return isInk( inkStack ) ? inkStack.getCount() : 0;
return m_printing && outputPage();
}
}
public int getPaperLevel()
int getInkLevel()
{
ItemStack inkStack = m_inventory.get( 0 );
return isInk( inkStack ) ? inkStack.getCount() : 0;
}
int getPaperLevel()
{
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( !paperStack.isEmpty() && isPaper( paperStack ) )
{
count += paperStack.getCount();
}
}
ItemStack paperStack = m_inventory.get( i );
if( isPaper( paperStack ) ) count += paperStack.getCount();
}
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;
}
}
@@ -380,116 +336,100 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
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()
{
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 );
if( !isInk( inkStack ) ) return false;
ItemStack paperStack = m_inventory.get( i );
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 );
if( !paperStack.isEmpty() && isPaper( paperStack ) )
m_pageTitle = ItemPrintout.getTitle( paperStack );
String[] text = ItemPrintout.getText( paperStack );
String[] textColour = ItemPrintout.getColours( paperStack );
for( int y = 0; y < m_page.getHeight(); y++ )
{
// 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 )
{
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;
m_page.setLine( y, text[y], textColour[y], "" );
}
}
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()
{
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();
String[] lines = new String[height];
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;
lines[i] = m_page.getLine( i ).toString();
colours[i] = m_page.getTextColourLine( i ).toString();
}
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()
{
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 );
if( !stack.isEmpty() )
{
// Remove the stack from the inventory
setInventorySlotContents( i, ItemStack.EMPTY );
// Remove the stack from the inventory
setInventorySlotContents( i, ItemStack.EMPTY );
// Spawn the item in the world
BlockPos pos = getPos();
double x = pos.getX() + 0.5;
double y = pos.getY() + 0.75;
double z = pos.getZ() + 0.5;
WorldUtil.dropItemStack( stack, getWorld(), x, y, z );
}
// Spawn the item in the world
BlockPos pos = getPos();
double x = pos.getX() + 0.5;
double y = pos.getY() + 0.75;
double z = pos.getZ() + 0.5;
WorldUtil.dropItemStack( stack, getWorld(), x, y, z );
}
}
}
@@ -497,25 +437,22 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
private void updateBlockState()
{
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 );
if( !stack.isEmpty() && isPaper( stack ) )
{
top = true;
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 );
if( !stack.isEmpty() && isPaper( stack ) )
{
bottom = true;
break;
}
bottom = true;
break;
}
}

View File

@@ -34,11 +34,11 @@ import net.minecraft.tileentity.CommandBlockTileEntity;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.storage.loot.conditions.LootConditionManager;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerContainerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.client.event.ConfigChangedEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.event.server.FMLServerStartedEvent;
import net.minecraftforge.fml.event.server.FMLServerStartingEvent;
@@ -146,7 +146,7 @@ public final class ComputerCraftProxyCommon
IComputer computer = ((IContainerComputer) container).getComputer();
if( computer instanceof ServerComputer )
{
((ServerComputer) computer).sendTerminalState( event.getEntityPlayer() );
((ServerComputer) computer).sendTerminalState( event.getPlayer() );
}
}
}

View File

@@ -12,8 +12,7 @@ import dan200.computercraft.api.turtle.event.TurtleRefuelEvent;
import dan200.computercraft.shared.util.InventoryUtil;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.FurnaceTileEntity;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@@ -31,9 +30,13 @@ public final class FurnaceRefuelHandler implements TurtleRefuelEvent.Handler
@Override
public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStack, int slot, int limit )
{
ItemStack stack = turtle.getItemHandler().extractItem( slot, limit, false );
int fuelToGive = getFuelPerItem( stack ) * stack.getCount();
int fuelSpaceLeft = turtle.getFuelLimit() - turtle.getFuelLevel();
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
ItemStack replacementStack = stack.getItem().getContainerItem( stack );
if( !replacementStack.isEmpty() )
@@ -48,15 +51,9 @@ public final class FurnaceRefuelHandler implements TurtleRefuelEvent.Handler
return fuelToGive;
}
private static int getFuelPerItem( @Nonnull ItemStack stack )
{
int basicBurnTime = stack.getBurnTime();
int burnTime = ForgeEventFactory.getItemBurnTime(
stack,
basicBurnTime == -1 ? FurnaceTileEntity.getBurnTimes().getOrDefault( stack.getItem(), 0 ) : basicBurnTime
);
return (burnTime * 5) / 100;
return (ForgeHooks.getBurnTime( stack ) * 5) / 100;
}
@SubscribeEvent

View File

@@ -335,9 +335,11 @@ public class TurtleAPI implements ILuaAPI
return tryCommand( context, new TurtleInspectCommand( InteractDirection.Up ) );
case 40: // inspectDown
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() );
ItemStack stack = m_turtle.getInventory().getStackInSlot( slot );
if( stack.isEmpty() ) return new Object[] { null };

View File

@@ -153,7 +153,7 @@ public class BlockTurtle extends BlockComputerBase<TileTurtle> implements IWater
@Override
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;
}

View File

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

View File

@@ -23,6 +23,7 @@ import dan200.computercraft.shared.turtle.blocks.TileTurtle;
import dan200.computercraft.shared.util.Colour;
import dan200.computercraft.shared.util.Holiday;
import dan200.computercraft.shared.util.HolidayUtil;
import dan200.computercraft.shared.util.InventoryDelegate;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.entity.Entity;
@@ -43,6 +44,7 @@ import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import net.minecraftforge.common.util.Constants;
import net.minecraftforge.items.IItemHandlerModifiable;
import net.minecraftforge.items.wrapper.InvWrapper;
import javax.annotation.Nonnull;
import java.util.*;
@@ -68,6 +70,9 @@ public class TurtleBrain implements ITurtleAccess
private ComputerProxy m_proxy;
private GameProfile m_owningPlayer;
private final IInventory m_inventory = (InventoryDelegate) () -> m_owner;
private final IItemHandlerModifiable m_inventoryWrapper = new InvWrapper( m_inventory );
private Queue<TurtleCommandQueueEntry> m_commandQueue = new ArrayDeque<>();
private int m_commandsIssued = 0;
@@ -439,14 +444,14 @@ public class TurtleBrain implements ITurtleAccess
@Override
public IInventory getInventory()
{
return m_owner;
return m_inventory;
}
@Nonnull
@Override
public IItemHandlerModifiable getItemHandler()
{
return m_owner.getItemHandler();
return m_inventoryWrapper;
}
@Override

View File

@@ -14,8 +14,8 @@ import net.minecraft.block.BlockState;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.ServerWorld;
import net.minecraft.world.World;
import net.minecraft.world.server.ServerWorld;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import javax.annotation.Nonnull;

View File

@@ -17,7 +17,7 @@ import net.minecraft.item.ItemStack;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.ServerWorld;
import net.minecraft.world.server.ServerWorld;
import net.minecraftforge.common.util.FakePlayer;
import javax.annotation.Nonnull;

View File

@@ -44,7 +44,7 @@ public class ContainerTurtle extends ContainerComputerBase
super( TYPE, id, canUse, computer, family );
this.properties = properties;
func_216961_a( properties );
trackIntArray( properties );
// Turtle inventory
for( int y = 0; y < 4; y++ )

View File

@@ -15,8 +15,8 @@ import net.minecraft.item.ItemStack;
import net.minecraft.item.crafting.IRecipe;
import net.minecraft.item.crafting.IRecipeType;
import net.minecraft.util.NonNullList;
import net.minecraft.world.ServerWorld;
import net.minecraft.world.World;
import net.minecraft.world.server.ServerWorld;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.fml.hooks.BasicEventHooks;

View File

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

View File

@@ -14,8 +14,6 @@ import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
import net.minecraftforge.event.entity.EntityJoinWorldEvent;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import net.minecraftforge.event.world.BlockEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@@ -36,7 +34,6 @@ public final class DropConsumer
private static Function<ItemStack, ItemStack> dropConsumer;
private static List<ItemStack> remainingDrops;
private static WeakReference<World> dropWorld;
private static BlockPos dropPos;
private static AxisAlignedBB dropBounds;
private static WeakReference<Entity> dropEntity;
@@ -46,7 +43,6 @@ public final class DropConsumer
remainingDrops = new ArrayList<>();
dropEntity = new WeakReference<>( entity );
dropWorld = new WeakReference<>( entity.world );
dropPos = null;
dropBounds = new AxisAlignedBB( entity.getPosition() ).grow( 2, 2, 2 );
entity.captureDrops( new ArrayList<>() );
@@ -55,10 +51,9 @@ public final class DropConsumer
public static void set( World world, BlockPos pos, Function<ItemStack, ItemStack> consumer )
{
dropConsumer = consumer;
remainingDrops = new ArrayList<>();
remainingDrops = new ArrayList<>( 2 );
dropEntity = null;
dropWorld = new WeakReference<>( world );
dropPos = pos;
dropBounds = new AxisAlignedBB( pos ).grow( 2, 2, 2 );
}
@@ -83,7 +78,6 @@ public final class DropConsumer
remainingDrops = null;
dropEntity = null;
dropWorld = null;
dropPos = null;
dropBounds = null;
return remainingStacks;
@@ -95,34 +89,7 @@ public final class DropConsumer
if( !remaining.isEmpty() ) remainingDrops.add( remaining );
}
@SubscribeEvent( priority = EventPriority.LOWEST )
public static void onEntityLivingDrops( LivingDropsEvent event )
{
// Capture any mob drops for the current entity
if( dropEntity != null && event.getEntity() == dropEntity.get() )
{
Collection<ItemEntity> drops = event.getDrops();
for( ItemEntity entityItem : drops ) handleDrops( entityItem.getItem() );
drops.clear();
}
}
@SubscribeEvent( priority = EventPriority.LOWEST )
public static void onHarvestDrops( BlockEvent.HarvestDropsEvent event )
{
// Capture block drops for the current entity
if( dropWorld != null && dropWorld.get() == event.getWorld()
&& dropPos != null && dropPos.equals( event.getPos() ) )
{
for( ItemStack item : event.getDrops() )
{
if( event.getWorld().getRandom().nextFloat() < event.getDropChance() ) handleDrops( item );
}
event.getDrops().clear();
}
}
@SubscribeEvent( priority = EventPriority.LOWEST )
@SubscribeEvent( priority = EventPriority.HIGHEST )
public static void onEntitySpawn( EntityJoinWorldEvent event )
{
// Capture any nearby item spawns

View File

@@ -0,0 +1,120 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.util;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import javax.annotation.Nonnull;
import java.util.Set;
/**
* Provides a delegate over inventories.
*
* This may be used both on {@link net.minecraft.tileentity.TileEntity}s to redirect the inventory to another tile,
* and by other interfaces to have inventories which change their backing store.
*/
@FunctionalInterface
public interface InventoryDelegate extends IInventory
{
IInventory getInventory();
@Override
default int getSizeInventory()
{
return getInventory().getSizeInventory();
}
@Override
default boolean isEmpty()
{
return getInventory().isEmpty();
}
@Nonnull
@Override
default ItemStack getStackInSlot( int slot )
{
return getInventory().getStackInSlot( slot );
}
@Nonnull
@Override
default ItemStack decrStackSize( int slot, int count )
{
return getInventory().decrStackSize( slot, count );
}
@Nonnull
@Override
default ItemStack removeStackFromSlot( int slot )
{
return getInventory().removeStackFromSlot( slot );
}
@Override
default void setInventorySlotContents( int slot, ItemStack stack )
{
getInventory().setInventorySlotContents( slot, stack );
}
@Override
default int getInventoryStackLimit()
{
return getInventory().getInventoryStackLimit();
}
@Override
default void markDirty()
{
getInventory().markDirty();
}
@Override
default boolean isUsableByPlayer( @Nonnull PlayerEntity player )
{
return getInventory().isUsableByPlayer( player );
}
@Override
default void openInventory( @Nonnull PlayerEntity player )
{
getInventory().openInventory( player );
}
@Override
default void closeInventory( @Nonnull PlayerEntity player )
{
getInventory().closeInventory( player );
}
@Override
default boolean isItemValidForSlot( int slot, @Nonnull ItemStack stack )
{
return getInventory().isItemValidForSlot( slot, stack );
}
@Override
default void clear()
{
getInventory().clear();
}
@Override
default int count( @Nonnull Item stack )
{
return getInventory().count( stack );
}
@Override
default boolean hasAny( @Nonnull Set<Item> set )
{
return getInventory().hasAny( set );
}
}

View File

@@ -70,12 +70,6 @@ public final class InventoryUtil
return shareTagA.equals( shareTagB );
}
@Nonnull
public static ItemStack copyItem( @Nonnull ItemStack a )
{
return a.copy();
}
// Methods for finding inventories:
public static IItemHandler getInventory( World world, BlockPos pos, Direction side )

View File

@@ -6,16 +6,10 @@
package dan200.computercraft.shared.util;
import com.mojang.datafixers.DataFixUtils;
import com.mojang.datafixers.types.Type;
import dan200.computercraft.ComputerCraft;
import net.minecraft.block.Block;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SharedConstants;
import net.minecraft.util.datafix.DataFixesManager;
import net.minecraft.util.datafix.TypeReferences;
import javax.annotation.Nonnull;
import java.util.Collections;
@@ -30,7 +24,7 @@ public final class NamedTileEntityType<T extends TileEntity> extends TileEntityT
private NamedTileEntityType( ResourceLocation identifier, Supplier<? extends T> supplier )
{
super( supplier, Collections.emptySet(), getDatafixer( identifier ) );
super( supplier, Collections.emptySet(), null );
this.identifier = identifier;
setRegistryName( identifier );
}
@@ -62,22 +56,6 @@ public final class NamedTileEntityType<T extends TileEntity> extends TileEntityT
return identifier;
}
private static Type<?> getDatafixer( ResourceLocation id )
{
try
{
return DataFixesManager.getDataFixer()
.getSchema( DataFixUtils.makeKey( ComputerCraft.DATAFIXER_VERSION ) )
.getChoiceType( TypeReferences.BLOCK_ENTITY, id.toString() );
}
catch( IllegalArgumentException e )
{
if( SharedConstants.developmentMode ) throw e;
ComputerCraft.log.warn( "No data fixer registered for block entity " + id );
return null;
}
}
private static final class FixedPointSupplier<T extends TileEntity> implements Supplier<T>
{
final NamedTileEntityType<T> factory;

View File

@@ -9,17 +9,11 @@ package dan200.computercraft.shared.util;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.PlayRecordClientMessage;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.MusicDiscItem;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.text.TranslationTextComponent;
import net.minecraft.world.World;
import javax.annotation.Nonnull;
public final class RecordUtil
{
private RecordUtil() {}
@@ -29,12 +23,4 @@ public final class RecordUtil
NetworkMessage packet = record != null ? new PlayRecordClientMessage( pos, record, recordInfo ) : new PlayRecordClientMessage( pos );
NetworkHandler.sendToAllAround( packet, world, new Vec3d( pos ), 64 );
}
public static String getRecordInfo( @Nonnull ItemStack recordStack )
{
Item item = recordStack.getItem();
if( !(item instanceof MusicDiscItem) ) return null;
return new TranslationTextComponent( item.getTranslationKey() + ".desc" ).getString();
}
}

View File

@@ -13,9 +13,9 @@ import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.ITickList;
import net.minecraft.world.World;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import java.util.Collections;
import java.util.Iterator;

View File

@@ -7,6 +7,7 @@
package dan200.computercraft.shared.util;
import com.google.common.base.Predicate;
import com.google.common.collect.MapMaker;
import net.minecraft.block.BlockState;
import net.minecraft.entity.*;
import net.minecraft.entity.item.ItemEntity;
@@ -20,25 +21,35 @@ import org.apache.commons.lang3.tuple.Pair;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
public final class WorldUtil
{
@SuppressWarnings( "Guava" )
private static final Predicate<Entity> CAN_COLLIDE = x -> x != null && x.isAlive() && x.canBeCollidedWith();
private static final Entity ENTITY = new ItemEntity( EntityType.ITEM, null )
{
@Override
public EntitySize getSize( Pose pose )
{
return EntitySize.fixed( 0, 0 );
}
};
private static final Map<World, Entity> entityCache = new MapMaker().weakKeys().weakValues().makeMap();
static
private static synchronized Entity getEntity( World world )
{
ENTITY.noClip = true;
ENTITY.recalculateSize();
// TODO: It'd be nice if we could avoid this. Maybe always use the turtle player (if it's available).
Entity entity = entityCache.get( world );
if( entity != null ) return entity;
entity = new ItemEntity( EntityType.ITEM, world )
{
@Nonnull
@Override
public EntitySize getSize( Pose pose )
{
return EntitySize.fixed( 0, 0 );
}
};
entity.noClip = true;
entity.recalculateSize();
entityCache.put( world, entity );
return entity;
}
public static boolean isLiquidBlock( World world, BlockPos pos )
@@ -61,8 +72,9 @@ public final class WorldUtil
Vec3d vecEnd = vecStart.add( vecDir.x * distance, vecDir.y * distance, vecDir.z * distance );
// Raycast for blocks
ENTITY.setPosition( vecStart.x, vecStart.y, vecStart.z );
RayTraceContext context = new RayTraceContext( vecStart, vecEnd, RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.NONE, ENTITY );
Entity collisionEntity = getEntity( world );
collisionEntity.setPosition( vecStart.x, vecStart.y, vecStart.z );
RayTraceContext context = new RayTraceContext( vecStart, vecEnd, RayTraceContext.BlockMode.COLLIDER, RayTraceContext.FluidMode.NONE, collisionEntity );
RayTraceResult result = world.rayTraceBlocks( context );
if( result != null && result.getType() == RayTraceResult.Type.BLOCK )
{

View File

@@ -1,5 +1,5 @@
modLoader="javafml"
loaderVersion="[27,28)"
loaderVersion="[28,29)"
issueTrackerURL="https://github.com/SquidDev-CC/CC-Tweaked/issues"
displayURL="https://github.com/SquidDev-CC/CC-Tweaked"
@@ -19,6 +19,6 @@ CC: Tweaked is a fork of ComputerCraft, adding programmable computers, turtles a
[[dependencies.computercraft]]
modId="forge"
mandatory=true
versionRange="[27,28)"
versionRange="[28,29)"
ordering="NONE"
side="BOTH"

View File

@@ -1,48 +1,19 @@
local native_select, native_type = select, type
--- Expect an argument to have a specific type.
-- Load in expect from the module path.
--
-- @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
-- Ideally we'd use require, but that is part of the shell, and so is not
-- available to the BIOS or any APIs. All APIs load this using dofile, but that
-- has not been defined at this point.
local expect
local types = table.pack(...)
for i = types.n, 1, -1 do
if types[i] == "nil" then table.remove(types, i) end
end
do
local h = fs.open("rom/modules/main/cc/expect.lua", "r")
local f, err = loadstring(h.readAll(), "@expect.lua")
h.close()
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
if not f then error(err) end
expect = f().expect
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 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
@@ -568,23 +539,28 @@ function read( _sReplaceChar, _tHistory, _fnComplete, _sDefault )
return sLine
end
function loadfile( _sFile, _tEnv )
expect(1, _sFile, "string")
expect(2, _tEnv, "table", "nil")
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
function loadfile( filename, mode, env )
-- Support the previous `loadfile(filename, env)` form instead.
if type(mode) == "table" and env == nil then
mode, env = nil, mode
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
function dofile( _sFile )
expect(1, _sFile, "string")
local fnFile, e = loadfile( _sFile, _G )
local fnFile, e = loadfile( _sFile, nil, _G )
if fnFile then
return fnFile()
else
@@ -600,7 +576,7 @@ function os.run( _tEnv, _sPath, ... )
local tArgs = table.pack( ... )
local tEnv = _tEnv
setmetatable( tEnv, { __index = _G } )
local fnFile, err = loadfile( _sPath, tEnv )
local fnFile, err = loadfile( _sPath, nil, tEnv )
if fnFile then
local ok, err = pcall( function()
fnFile( table.unpack( tArgs, 1, tArgs.n ) )
@@ -634,7 +610,7 @@ function os.loadAPI( _sPath )
local tEnv = {}
setmetatable( tEnv, { __index = _G } )
local fnAPI, err = loadfile( _sPath, tEnv )
local fnAPI, err = loadfile( _sPath, nil, tEnv )
if fnAPI then
local ok, err = pcall( fnAPI )
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
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

View File

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

View File

@@ -1,6 +1,6 @@
-- 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.
-- 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
-- impossible.
local expect = _G["~expect"]
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local tKeys = {}
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 )
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

View File

@@ -1,4 +1,4 @@
local expect = _G["~expect"]
local expect = dofile("rom/modules/main/cc/expect.lua").expect
CHANNEL_BROADCAST = 65535
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 = {}

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 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 )
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 = {
[ colors.white ] = "0",
@@ -388,6 +388,16 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible )
return nBackgroundColor
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
function window.setVisible( bVis )
if type(bVis) ~= "boolean" then expect(1, bVis, "boolean") end
@@ -421,16 +431,20 @@ function create( parent, nX, nY, nWidth, nHeight, bStartVisible )
return nX, nY
end
function window.reposition( nNewX, nNewY, nNewWidth, nNewHeight )
function window.reposition( nNewX, nNewY, nNewWidth, nNewHeight, newParent )
if type(nNewX) ~= "number" then expect(1, nNewX, "number") end
if type(nNewY) ~= "number" then expect(2, nNewY, "number") end
if nNewWidth ~= nil or nNewHeight ~= nil then
expect(3, nNewWidth, "number")
expect(4, nNewHeight, "number")
end
if newParent ~= nil and type(newParent) ~= "table" then expect(5, newParent, "table") end
nX = nNewX
nY = nNewY
if newParent then parent = newParent end
if nNewWidth and nNewHeight then
local tNewLines = {}
createEmptyLines( nNewWidth )

View File

@@ -1,3 +1,37 @@
# New features in CC: Tweaked 1.85.0
* Window.reposition now allows changing the redirect buffer
* Add cc.completion and cc.shell.completion modules
* command.exec also returns the number of affected objects, when exposed by the game.
And several bug fixes:
* Change how turtle mining drops are handled, improving compatibility with some mods.
* Fix several GUI desyncs after a turtle moves.
* Fix os.day/os.time using the incorrect world time.
* Prevent wired modems dropping incorrectly.
* Fix mouse events not firing within the computer GUI.
# New features in CC: Tweaked 1.84.1
* Update to latest Forge
# 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
* Add several new MOTD messages (JakobDev)

View File

@@ -1,10 +1,14 @@
New features in CC: Tweaked 1.83.1
New features in CC: Tweaked 1.85.0
* Add several new MOTD messages (JakobDev)
* Window.reposition now allows changing the redirect buffer
* Add cc.completion and cc.shell.completion modules
* command.exec also returns the number of affected objects, when exposed by the game.
And several bug fixes:
* Fix type check in `rednet.lookup`
* Error if turtle and pocket computer programs are run on the wrong system (JakobDev)
* Do not discard varargs after a nil.
* Change how turtle mining drops are handled, improving compatibility with some mods.
* Fix several GUI desyncs after a turtle moves.
* Fix os.day/os.time using the incorrect world time.
* Prevent wired modems dropping incorrectly.
* Fix mouse events not firing within the computer GUI.
Type "help changelog" to see the full version history.

View File

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

View File

@@ -0,0 +1,105 @@
--- A collection of helper methods for working with input completion, such
-- as that require by @{read}.
--
-- @module craftos.completion
-- @see cc.shell.completion For additional helpers to use with
-- @{shell.setCompletionFunction}.
local expect = require "cc.expect".expect
local function choice_impl(text, choices, add_space)
local results = {}
for n = 1, #choices do
local option = choices[n]
if #option + (add_space and 1 or 0) > #text and option:sub(1, #text) == text then
local result = option:sub(#text + 1)
if add_space then
table.insert(results, result .. " ")
else
table.insert(results, result)
end
end
end
return results
end
--- Complete from a choice of one or more strings.
--
-- @tparam string text The input string to complete.
-- @tparam { string... } choices The list of choices to complete from.
-- @tparam[opt] boolean add_space Whether to add a space after the completed item.
-- @treturn { string... } A list of suffixes of matching strings.
-- @usage Call @{read}, completing the names of various animals.
--
-- local animals = { "dog", "cat", "lion", "unicorn" }
-- read(nil, nil, function(text) return choice(text, animals) end)
local function choice(text, choices, add_space)
expect(1, text, "string")
expect(2, choices, "table")
expect(3, add_space, "boolean", "nil")
return choice_impl(text, choices, add_space)
end
--- Complete the name of a currently attached peripheral.
--
-- @tparam string text The input string to complete.
-- @tparam[opt] boolean add_space Whether to add a space after the completed name.
-- @treturn { string... } A list of suffixes of matching peripherals.
-- @usage read(nil, nil, peripheral)
local function peripheral_(text, add_space)
expect(1, text, "string")
expect(2, add_space, "boolean", "nil")
return choice_impl(text, peripheral.getNames(), add_space)
end
local sides = redstone.getSides()
--- Complete the side of a computer.
--
-- @tparam string text The input string to complete.
-- @tparam[opt] boolean add_space Whether to add a space after the completed side.
-- @treturn { string... } A list of suffixes of matching sides.
-- @usage read(nil, nil, side)
local function side(text, add_space)
expect(1, text, "string")
expect(2, add_space, "boolean", "nil")
return choice_impl(text, sides, add_space)
end
--- Complete a @{settings|setting}.
--
-- @tparam string text The input string to complete.
-- @tparam[opt] boolean add_space Whether to add a space after the completed settings.
-- @treturn { string... } A list of suffixes of matching settings.
-- @usage read(nil, nil, setting)
local function setting(text, add_space)
expect(1, text, "string")
expect(2, add_space, "boolean", "nil")
return choice_impl(text, settings.getNames(), add_space)
end
local command_list
--- Complete the name of a Minecraft @{commands|command}.
--
-- @tparam string text The input string to complete.
-- @tparam[opt] boolean add_space Whether to add a space after the completed command.
-- @treturn { string... } A list of suffixes of matching commands.
-- @usage read(nil, nil, command)
local function command(text, add_space)
expect(1, text, "string")
expect(2, add_space, "boolean", "nil")
if command_list == nil then
command_list = commands and commands.list() or {}
end
return choice_impl(text, command_list, add_space)
end
return {
choice = choice,
peripheral = peripheral_,
side = side,
setting = setting,
command = command,
}

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

@@ -0,0 +1,151 @@
--- A collection of helper methods for working with shell completion.
--
-- Most programs may be completed using the @{build} helper method, rather than
-- manually switching on the argument index.
--
-- Note, the helper functions within this module do not accept an argument index,
-- and so are not directly usable with the @{shell.setCompletionFunction}. Instead,
-- wrap them using @{build}, or your own custom function.
--
-- @module craftos.shell.completion
-- @see cc.completion For more general helpers, suitable for use with @{read}.
-- @see shell.setCompletionFunction
local expect = require "cc.expect".expect
local completion = require "cc.completion"
--- Complete the name of a file relative to the current working directory.
--
-- @tparam shell shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @treturn { string... } A list of suffixes of matching files.
local function file(shell, text)
return fs.complete(text, shell.dir(), true, false)
end
--- Complete the name of a directory relative to the current working directory.
--
-- @tparam shell shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @treturn { string... } A list of suffixes of matching directories.
local function dir(shell, text)
return fs.complete(text, shell.dir(), false, true)
end
--- Complete the name of a file or directory relative to the current working
-- directory.
--
-- @tparam shell shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @tparam { string... } previous The shell arguments before this one.
-- @tparam[opt] boolean add_space Whether to add a space after the completed item.
-- @treturn { string... } A list of suffixes of matching files and directories.
local function dirOrFile(shell, text, previous, add_space)
local results = fs.complete(text, shell.dir(), true, true)
if add_space then
for n = 1, #results do
local result = results[n]
if result:sub(-1) ~= "/" then
results[n] = result .. " "
end
end
end
return results
end
local function wrap(func)
return function(shell, text, previous, ...)
return func(text, ...)
end
end
--- Complete the name of a program.
--
-- @tparam shell shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @treturn { string... } A list of suffixes of matching programs.
local function program(shell, text)
return shell.completeProgram(text)
end
--- A helper function for building shell completion arguments.
--
-- This accepts a series of single-argument completion functions, and combines
-- them into a function suitable for use with @{shell.setCompletionFunction}.
--
-- @tparam nil|table|function ... Every argument to @{build} represents an argument
-- to the program you wish to complete. Each argument can be one of three types:
--
-- - `nil`: This argument will not be completed.
--
-- - A function: This argument will be completed with the given function. It is
-- called with the @{shell} object, the string to complete and the arguments
-- before this one.
--
-- - A table: This acts as a more powerful version of the function case. The table
-- must have a function as the first item - this will be called with the shell,
-- string and preceding arguments as above, but also followed by any additional
-- items in the table. This provides a more convenient interface to pass
-- options to your completion functions.
--
-- If this table is the last argument, it may also set the `many` key to true,
-- which states this function should be used to complete any remaining arguments.
--
-- @usage Prompt for a choice of options, followed by a directory, and then multiple
-- files.
--
-- complete.build(
-- { complete.choice, { "get", "put" } },
-- complete.dir,
-- } complete.file, many = true }
-- )
local function build(...)
local arguments = table.pack(...)
for i = 1, arguments.n do
local arg = arguments[i]
if arg ~= nil then
expect(i, arg, "table", "function")
if type(arg) == "function" then
arg = { arg }
arguments[i] = arg
end
if type(arg[1]) ~= "function" then
error(("Bad table entry #1 at argument #%d (expected function, got %s)"):format(i, type(arg[1])), 2)
end
if arg.many and i < arguments.n then
error(("Unexpected 'many' field on argument #%d (should only occur on the last argument)"):format(i), 2)
end
end
end
return function(shell, index, text, previous)
local arg = arguments[index]
if not arg then
if index <= arguments.n then return end
arg = arguments[arguments.n]
if not arg or not arg.many then return end
end
return arg[1](shell, text, previous, table.unpack(arg, 2))
end
end
return {
file = file,
dir = dir,
dirOrFile = dirOrFile,
program = program,
-- Re-export various other functions
help = wrap(help.completeTopic),
choice = wrap(completion.choice),
peripheral = wrap(completion.peripheral),
side = wrap(completion.side),
setting = wrap(completion.setting),
command = wrap(completion.command),
build = build,
}

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ else
end
if sTopic == "index" then
print( "Help topics availiable:" )
print( "Help topics available:" )
local tTopics = help.topics()
textutils.pagedTabulate( tTopics )
return

View File

@@ -1,4 +1,4 @@
local expect = _G["~expect"]
local expect = dofile("rom/modules/main/cc/expect.lua").expect
local multishell = multishell
local parentShell = shell
@@ -56,7 +56,7 @@ local function createShellEnv( sDir )
sPath = fs.combine(sDir, sPath)
end
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
return fnFile, sPath
else

View File

@@ -1,3 +1,4 @@
local completion = require "cc.shell.completion"
-- Setup paths
local sPath = ".:/rom/programs"
@@ -39,217 +40,86 @@ if term.isColor() then
end
-- Setup completion functions
local function completeMultipleChoice( sText, tOptions, bAddSpaces )
local tResults = {}
for n=1,#tOptions do
local sOption = tOptions[n]
if #sOption + (bAddSpaces and 1 or 0) > #sText and string.sub( sOption, 1, #sText ) == sText then
local sResult = string.sub( sOption, #sText + 1 )
if bAddSpaces then
table.insert( tResults, sResult .. " " )
else
table.insert( tResults, sResult )
end
end
end
return tResults
end
local function completePeripheralName( sText, bAddSpaces )
return completeMultipleChoice( sText, peripheral.getNames(), bAddSpaces )
end
local tRedstoneSides = redstone.getSides()
local function completeSide( sText, bAddSpaces )
return completeMultipleChoice( sText, tRedstoneSides, bAddSpaces )
end
local function completeFile( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return fs.complete( sText, shell.dir(), true, false )
local function completePastebinPut(shell, text, previous)
if previous[2] == "put" then
return fs.complete(text, shell.dir(), true, false )
end
end
local function completeFileMany( shell, nIndex, sText, tPreviousText )
return fs.complete( sText, shell.dir(), true, false )
end
local function completeDir( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return fs.complete( sText, shell.dir(), false, true )
end
end
local function completeEither( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return fs.complete( sText, shell.dir(), true, true )
end
end
local function completeEitherMany( shell, nIndex, sText, tPreviousText )
return fs.complete( sText, shell.dir(), true, true )
end
local function completeEitherEither( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
local tResults = fs.complete( sText, shell.dir(), true, true )
for n=1,#tResults do
local sResult = tResults[n]
if string.sub( sResult, #sResult, #sResult ) ~= "/" then
tResults[n] = sResult .. " "
end
end
return tResults
elseif nIndex == 2 then
return fs.complete( sText, shell.dir(), true, true )
end
end
local function completeProgram( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return shell.completeProgram( sText )
end
end
local function completeHelp( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return help.completeTopic( sText )
end
end
local function completeAlias( shell, nIndex, sText, tPreviousText )
if nIndex == 2 then
return shell.completeProgram( sText )
end
end
local function completePeripheral( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completePeripheralName( sText )
end
end
local tGPSOptions = { "host", "host ", "locate" }
local function completeGPS( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, tGPSOptions )
end
end
local tLabelOptions = { "get", "get ", "set ", "clear", "clear " }
local function completeLabel( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, tLabelOptions )
elseif nIndex == 2 then
return completePeripheralName( sText )
end
end
local function completeMonitor( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completePeripheralName( sText, true )
elseif nIndex == 2 then
return shell.completeProgram( sText )
end
end
local tRedstoneOptions = { "probe", "set ", "pulse " }
local function completeRedstone( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, tRedstoneOptions )
elseif nIndex == 2 then
return completeSide( sText )
end
end
local tDJOptions = { "play", "play ", "stop " }
local function completeDJ( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, tDJOptions )
elseif nIndex == 2 then
return completePeripheralName( sText )
end
end
local tPastebinOptions = { "put ", "get ", "run " }
local function completePastebin( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, tPastebinOptions )
elseif nIndex == 2 then
if tPreviousText[2] == "put" then
return fs.complete( sText, shell.dir(), true, false )
end
end
end
local tChatOptions = { "host ", "join " }
local function completeChat( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, tChatOptions )
end
end
local function completeSet( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, settings.getNames(), true )
end
end
local tCommands
if commands then
tCommands = commands.list()
end
local function completeExec( shell, nIndex, sText, tPreviousText )
if nIndex == 1 and commands then
return completeMultipleChoice( sText, tCommands, true )
end
end
local tWgetOptions = { "run" }
local function completeWget( shell, nIndex, sText, tPreviousText )
if nIndex == 1 then
return completeMultipleChoice( sText, tWgetOptions, true )
end
end
shell.setCompletionFunction( "rom/programs/alias.lua", completeAlias )
shell.setCompletionFunction( "rom/programs/cd.lua", completeDir )
shell.setCompletionFunction( "rom/programs/copy.lua", completeEitherEither )
shell.setCompletionFunction( "rom/programs/delete.lua", completeEitherMany )
shell.setCompletionFunction( "rom/programs/drive.lua", completeDir )
shell.setCompletionFunction( "rom/programs/edit.lua", completeFile )
shell.setCompletionFunction( "rom/programs/eject.lua", completePeripheral )
shell.setCompletionFunction( "rom/programs/gps.lua", completeGPS )
shell.setCompletionFunction( "rom/programs/help.lua", completeHelp )
shell.setCompletionFunction( "rom/programs/id.lua", completePeripheral )
shell.setCompletionFunction( "rom/programs/label.lua", completeLabel )
shell.setCompletionFunction( "rom/programs/list.lua", completeDir )
shell.setCompletionFunction( "rom/programs/mkdir.lua", completeFileMany )
shell.setCompletionFunction( "rom/programs/monitor.lua", completeMonitor )
shell.setCompletionFunction( "rom/programs/move.lua", completeEitherEither )
shell.setCompletionFunction( "rom/programs/redstone.lua", completeRedstone )
shell.setCompletionFunction( "rom/programs/rename.lua", completeEitherEither )
shell.setCompletionFunction( "rom/programs/shell.lua", completeProgram )
shell.setCompletionFunction( "rom/programs/type.lua", completeEither )
shell.setCompletionFunction( "rom/programs/set.lua", completeSet )
shell.setCompletionFunction( "rom/programs/advanced/bg.lua", completeProgram )
shell.setCompletionFunction( "rom/programs/advanced/fg.lua", completeProgram )
shell.setCompletionFunction( "rom/programs/fun/dj.lua", completeDJ )
shell.setCompletionFunction( "rom/programs/fun/advanced/paint.lua", completeFile )
shell.setCompletionFunction( "rom/programs/http/pastebin.lua", completePastebin )
shell.setCompletionFunction( "rom/programs/rednet/chat.lua", completeChat )
shell.setCompletionFunction( "rom/programs/command/exec.lua", completeExec )
shell.setCompletionFunction( "rom/programs/http/wget.lua", completeWget )
shell.setCompletionFunction( "rom/programs/alias.lua", completion.build(nil, completion.program) )
shell.setCompletionFunction( "rom/programs/cd.lua", completion.build(completion.dir) )
shell.setCompletionFunction( "rom/programs/copy.lua", completion.build(
{ completion.dirOrFile, true },
completion.dirOrFile
) )
shell.setCompletionFunction( "rom/programs/delete.lua", completion.build({ completion.dirOrFile, many = true }) )
shell.setCompletionFunction( "rom/programs/drive.lua", completion.build(completion.dir) )
shell.setCompletionFunction( "rom/programs/edit.lua", completion.build(completion.file) )
shell.setCompletionFunction( "rom/programs/eject.lua", completion.build(completion.peripheral) )
shell.setCompletionFunction( "rom/programs/gps.lua", completion.build({ completion.choice, { "host", "host ", "locate" } }) )
shell.setCompletionFunction( "rom/programs/help.lua", completion.build(completion.help) )
shell.setCompletionFunction( "rom/programs/id.lua", completion.build(completion.peripheral) )
shell.setCompletionFunction( "rom/programs/label.lua", completion.build(
{ completion.choice, { "get", "get ", "set ", "clear", "clear " } },
completion.peripheral
) )
shell.setCompletionFunction( "rom/programs/list.lua", completion.build(completion.dir) )
shell.setCompletionFunction( "rom/programs/mkdir.lua", completion.build({ completion.dir, many = true }) )
shell.setCompletionFunction( "rom/programs/monitor.lua", completion.build(
{ completion.peripheral, true },
completion.program
) )
shell.setCompletionFunction( "rom/programs/move.lua", completion.build(
{ completion.dirOrFile, true },
completion.dirOrFile
) )
shell.setCompletionFunction( "rom/programs/redstone.lua", completion.build(
{ completion.choice, { "probe", "set ", "pulse " } },
completion.side
) )
shell.setCompletionFunction( "rom/programs/rename.lua", completion.build(
{ completion.dirOrFile, true },
completion.dirOrFile
) )
shell.setCompletionFunction( "rom/programs/shell.lua", completion.build(completion.program) )
shell.setCompletionFunction( "rom/programs/type.lua", completion.build(completion.dirOrFile) )
shell.setCompletionFunction( "rom/programs/set.lua", completion.build({ completion.setting, true }) )
shell.setCompletionFunction( "rom/programs/advanced/bg.lua", completion.build(completion.program) )
shell.setCompletionFunction( "rom/programs/advanced/fg.lua", completion.build(completion.program) )
shell.setCompletionFunction( "rom/programs/fun/dj.lua", completion.build(
{ completion.choice, { "play", "play ", "stop " } },
completion.peripheral
) )
shell.setCompletionFunction( "rom/programs/fun/advanced/paint.lua", completion.build(completion.file) )
shell.setCompletionFunction( "rom/programs/http/pastebin.lua", completion.build(
{ completion.choice, { "put ", "get ", "run " } },
completePastebinPut
) )
shell.setCompletionFunction( "rom/programs/rednet/chat.lua", completion.build({ completion.choice, { "host ", "join " } }) )
shell.setCompletionFunction( "rom/programs/command/exec.lua", completion.build(completion.command) )
shell.setCompletionFunction( "rom/programs/http/wget.lua", completion.build({ completion.choice, { "run " } }) )
if turtle then
local tGoOptions = { "left", "right", "forward", "back", "down", "up" }
local function completeGo( shell, nIndex, sText )
return completeMultipleChoice( sText, tGoOptions, true)
end
local tTurnOptions = { "left", "right" }
local function completeTurn( shell, nIndex, sText )
return completeMultipleChoice( sText, tTurnOptions, true )
end
local tEquipOptions = { "left", "right" }
local function completeEquip( shell, nIndex, sText )
if nIndex == 2 then
return completeMultipleChoice( sText, tEquipOptions )
end
end
local function completeUnequip( shell, nIndex, sText )
if nIndex == 1 then
return completeMultipleChoice( sText, tEquipOptions )
end
end
shell.setCompletionFunction( "rom/programs/turtle/go.lua", completeGo )
shell.setCompletionFunction( "rom/programs/turtle/turn.lua", completeTurn )
shell.setCompletionFunction( "rom/programs/turtle/equip.lua", completeEquip )
shell.setCompletionFunction( "rom/programs/turtle/unequip.lua", completeUnequip )
shell.setCompletionFunction( "rom/programs/turtle/go.lua", completion.build(
{ completion.choice, { "left", "right", "forward", "back", "down", "up" }, true, many = true }
) )
shell.setCompletionFunction( "rom/programs/turtle/turn.lua", completion.build(
{ completion.choice, { "left", "right" }, true, many = true }
))
shell.setCompletionFunction( "rom/programs/turtle/equip.lua", completion.build(
nil,
{ completion.choice, { "left", "right" } }
) )
shell.setCompletionFunction( "rom/programs/turtle/unequip.lua", completion.build(
{ completion.choice, { "left", "right" } }
) )
end
-- Run autorun files
if fs.exists( "/rom/autorun" ) and fs.isDir( "/rom/autorun" ) then
local tFiles = fs.list( "/rom/autorun" )
table.sort( tFiles )
for n, sFile in ipairs( tFiles ) do
for _, sFile in ipairs( tFiles ) do
if string.sub( sFile, 1, 1 ) ~= "." then
local sPath = "/rom/autorun/"..sFile
if not fs.isDir( sPath ) then

View File

@@ -90,7 +90,7 @@ public class ComputerTestDelegate
try( WritableByteChannel channel = mount.openChannelForWrite( "startup.lua" );
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 );

View File

@@ -33,7 +33,7 @@ public class ComputerBootstrap
{
MemoryMount mount = new MemoryMount()
.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 -> { } );
}

View File

@@ -27,18 +27,58 @@ local function check(func, arg, ty, val)
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 = {}
--- Stub a global variable with a specific value
--
-- @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)
local function default_stub() end
table.insert(active_stubs, { tbl = tbl, var = var, value = tbl[var] })
rawset(tbl, var, value)
--- Stub a table entry with a new 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
--- Capture the current global state of the computer
@@ -51,16 +91,14 @@ local function push_state()
output = io.output(),
dir = shell.dir(),
path = shell.path(),
aliases = shell.aliases(),
stubs = stubs,
}
end
--- Restore the global state of the computer to a previous version
local function pop_state(state)
for i = #active_stubs, 1, -1 do
local stub = active_stubs[i]
rawset(stub.tbl, stub.var, stub.value)
end
for i = #active_stubs, 1, -1 do active_stubs[i]:revert() end
active_stubs = state.stubs
@@ -69,6 +107,14 @@ local function pop_state(state)
io.output(state.output)
shell.setDir(state.dir)
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
local error_mt = { __tostring = function(self) return self.message end }
@@ -210,6 +256,16 @@ local function matches(eq, exact, left, right)
return true
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
-- the provided object.
--
@@ -236,6 +292,70 @@ function expect_mt:matches(value)
return self
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( {
--- Construct an expectation on the error message calling this function
-- produces
@@ -381,7 +501,7 @@ do
if fs.isDir(file) then
run_in(file)
elseif file:sub(-#suffix) == suffix then
local fun, err = loadfile(file, env)
local fun, err = loadfile(file, nil, env)
if not fun then
do_test { name = file:sub(#root_dir + 2), error = { message = err } }
else

View File

@@ -118,6 +118,43 @@ describe("The window library", function()
expect.error(w.reposition, 1, 1, false, 1):eq("bad argument #3 (expected number, got boolean)")
expect.error(w.reposition, 1, 1, nil, 1):eq("bad argument #3 (expected number, got nil)")
expect.error(w.reposition, 1, 1, 1, nil):eq("bad argument #4 (expected number, got nil)")
expect.error(w.reposition, 1, 1, 1, 1, true):eq("bad argument #5 (expected table, got boolean)")
end)
it("can change the buffer", function()
local a, b = mk(), mk()
local target = window.create(a, 1, 1, a.getSize())
target.write("Test")
expect((a.getLine(1))):equal("Test ")
expect({ a.getCursorPos() }):same { 5, 1 }
target.reposition(1, 1, nil, nil, b)
target.redraw()
expect((a.getLine(1))):equal("Test ")
expect({ a.getCursorPos() }):same { 5, 1 }
target.setCursorPos(1, 1) target.write("More")
expect((a.getLine(1))):equal("Test ")
expect((b.getLine(1))):equal("More ")
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)

View File

@@ -1,36 +1,4 @@
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()
it("validates arguments", function()
sleep(0)
@@ -48,18 +16,43 @@ describe("The Lua base library", function()
end)
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()
loadfile("")
loadfile("", {})
loadfile("", "")
loadfile("", "", {})
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)
it("prefixes the filename with @", function()
local info = debug.getinfo(loadfile("/rom/startup.lua"), "S")
expect(info):matches { short_src = "startup.lua", source = "@startup.lua" }
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)
describe("dofile", function()

View File

@@ -0,0 +1,57 @@
describe("cc.completion", function()
local c = require("cc.completion")
describe("choice", function()
it("provides all choices", function()
expect(c.choice("", { "some text", "some other", "other" }))
:same { "some text", "some other", "other" }
end)
it("provides a filtered list of choices", function()
expect(c.choice("som", { "some text", "some other", "other" }))
:same { "e text", "e other" }
expect(c.choice("none", { "some text", "some other", "other" }))
:same { }
end)
it("adds text if needed", function()
expect(c.choice("som", { "some text", "some other", "other" }, true))
:same { "e text ", "e other " }
end)
end)
describe("peripheral", function()
it("provides a choice of peripherals", function()
stub(peripheral, "getNames", function() return { "drive_0", "left" } end)
expect(c.peripheral("dri")):same { "ve_0" }
expect(c.peripheral("dri", true)):same { "ve_0 " }
end)
end)
describe("side", function()
it("provides a choice of sides", function()
expect(c.side("le")):same { "ft" }
expect(c.side("le", true)):same { "ft " }
end)
end)
describe("setting", function()
it("provides a choice of setting names", function()
stub(settings, "getNames", function() return { "shell.allow_startup", "list.show_hidden" } end)
expect(c.setting("li")):same { "st.show_hidden" }
expect(c.setting("li", true)):same { "st.show_hidden " }
end)
end)
describe("command", function()
it("provides a choice of command names", function()
stub(_G, "commands", { list = function() return { "list", "say" } end })
expect(c.command("li")):same { "st" }
expect(c.command("li", true)):same { "st " }
end)
end)
end)

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,41 @@
describe("cc.shell.completion", function()
local c = require "cc.shell.completion"
describe("dirOrFile", function()
it("completes both", function()
expect(c.dirOrFile(shell, "rom/")):same {
"apis/", "apis", "autorun/", "autorun", "help/", "help",
"modules/", "modules", "motd.txt", "programs/", "programs", "startup.lua"
}
end)
it("adds a space", function()
expect(c.dirOrFile(shell, "rom/", nil, true)):same {
"apis/", "apis ", "autorun/", "autorun ", "help/", "help ",
"modules/", "modules ", "motd.txt ", "programs/", "programs ", "startup.lua ",
}
end)
end)
describe("build", function()
it("completes multiple arguments", function()
local spec = c.build(
function() return { "a", "b", "c" } end,
nil,
{ c.choice, { "d", "e", "f"} }
)
expect(spec(shell, 1, "")):same { "a", "b", "c" }
expect(spec(shell, 2, "")):same(nil)
expect(spec(shell, 3, "")):same { "d", "e", "f" }
expect(spec(shell, 4, "")):same(nil)
end)
it("supports variadic completions", function()
local spec = c.build({ function() return { "a", "b", "c" } end, many = true })
expect(spec(shell, 1, "")):same({ "a", "b", "c" })
expect(spec(shell, 2, "")):same({ "a", "b", "c" })
end)
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
describe("The cd program", function()
it("cd into a directory", function()
shell.run("cd /rom/programs")
expect(shell.dir()):eq("rom/programs")
it("changes into a directory", function()
local setDir = stub(shell, "setDir")
capture(stub, "cd /rom/programs")
expect(setDir):called_with("rom/programs")
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"))
:matches { ok = true, output = "Not a directory\n", error = "" }
end)
it("displays the usage with no arguments", function()
it("displays the usage when given no arguments", function()
expect(capture(stub, "cd"))
:matches { ok = true, output = "Usage: cd <path>\n", error = "" }
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 testFile = require "test_helpers".testFile
describe("The edit program", function()
it("displays its usage when given no argument", function()
multishell = nil
it("displays its usage when given no argument", function()
expect(capture(stub, "edit"))
:matches { ok = true, output = "Usage: edit <path>\n", error = "" }
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)

Some files were not shown because too many files have changed in this diff Show More