1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-12-14 04:00:30 +00:00

Merge branch 'mc-1.15.x' into mc-1.16.x

This commit is contained in:
Jonathan Coates 2021-06-19 12:41:30 +01:00
commit 0bfe960cbd
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
79 changed files with 1320 additions and 472 deletions

22
.gitpod.yml Normal file
View File

@ -0,0 +1,22 @@
image:
file: config/gitpod/Dockerfile
ports:
- port: 25565
onOpen: notify
vscode:
extensions:
- eamodio.gitlens
- github.vscode-pull-request-github
- ms-azuretools.vscode-docker
- redhat.java
- richardwillis.vscode-gradle
- vscjava.vscode-java-debug
- vscode.github
tasks:
- name: Setup pre-commit hool
init: pre-commit install --config config/pre-commit/config.yml --allow-missing-config
- name: Install npm packages
init: npm ci

View File

@ -19,6 +19,10 @@ process. When building on Windows, Use `gradlew.bat` instead of `./gradlew`.
- **Clone the repository:** `git clone https://github.com/SquidDev-CC/CC-Tweaked.git && cd CC-Tweaked`
- **Setup Forge:** `./gradlew build`
- **Run Minecraft:** `./gradlew runClient` (or run the `GradleStart` class from your IDE).
- **Optionally:** For small PRs (especially those only touching Lua code), it may be easier to use GitPod, which
provides a pre-configured environment: [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-2b2b2b?logo=gitpod)](https://gitpod.io/#https://github.com/SquidDev-CC/CC-Tweaked/)
Do note you will need to download the mod after compiling to test.
If you want to run CC:T in a normal Minecraft instance, run `./gradlew build` and copy the `.jar` from `build/libs`.
These commands may take a few minutes to run the first time, as the environment is set up, but should be much faster

View File

@ -1,6 +1,5 @@
buildscript {
repositories {
jcenter()
mavenCentral()
maven {
name = "forge"
@ -14,7 +13,7 @@ buildscript {
}
dependencies {
classpath 'com.google.code.gson:gson:2.8.1'
classpath 'net.minecraftforge.gradle:ForgeGradle:4.1.9'
classpath 'net.minecraftforge.gradle:ForgeGradle:5.0.12'
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
}
}
@ -23,10 +22,11 @@ plugins {
id "checkstyle"
id "jacoco"
id "maven-publish"
id "com.github.hierynomus.license" version "0.15.0"
id "com.github.hierynomus.license" version "0.16.1"
id "com.matthewprenger.cursegradle" version "1.4.0"
id "com.github.breadmoirai.github-release" version "2.2.12"
id "org.jetbrains.kotlin.jvm" version "1.3.72"
id "com.modrinth.minotaur" version "1.2.1"
}
apply plugin: 'net.minecraftforge.gradle'
@ -37,15 +37,30 @@ version = mod_version
group = "org.squiddev"
archivesBaseName = "cc-tweaked-${mc_version}"
def javaVersion = JavaLanguageVersion.of(8)
java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
languageVersion = javaVersion
}
withSourcesJar()
withJavadocJar()
}
// Tragically java.toolchain.languageVersion doesn't apply to ForgeGradle's
// tasks, so we force all launchers to use the right Java version.
tasks.withType(JavaCompile).configureEach {
javaCompiler = javaToolchains.compilerFor {
languageVersion = javaVersion
}
}
tasks.withType(JavaExec).configureEach {
javaLauncher = javaToolchains.launcherFor {
languageVersion = javaVersion
}
}
minecraft {
runs {
client {
@ -219,6 +234,7 @@ processResources {
e.printStackTrace()
}
inputs.property "commithash", hash
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from(sourceSets.main.resources.srcDirs) {
include 'META-INF/mods.toml'
@ -235,6 +251,10 @@ processResources {
}
}
sourcesJar {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
// Web tasks
@ -416,31 +436,31 @@ task checkRelease {
description "Verifies that everything is ready for a release"
inputs.property "version", mod_version
inputs.file("src/main/resources/data/computercraft/lua/rom/help/changelog.txt")
inputs.file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt")
inputs.file("src/main/resources/data/computercraft/lua/rom/help/changelog.md")
inputs.file("src/main/resources/data/computercraft/lua/rom/help/whatsnew.md")
doLast {
def ok = true
// Check we're targetting the current version
def whatsnew = new File(projectDir, "src/main/resources/data/computercraft/lua/rom/help/whatsnew.txt").readLines()
def whatsnew = new File(projectDir, "src/main/resources/data/computercraft/lua/rom/help/whatsnew.md").readLines()
if (whatsnew[0] != "New features in CC: Tweaked $mod_version") {
ok = false
project.logger.error("Expected `whatsnew.txt' to target $mod_version.")
project.logger.error("Expected `whatsnew.md' to target $mod_version.")
}
// Check "read more" exists and trim it
def idx = whatsnew.findIndexOf { it == 'Type "help changelog" to see the full version history.' }
if (idx == -1) {
ok = false
project.logger.error("Must mention the changelog in whatsnew.txt")
project.logger.error("Must mention the changelog in whatsnew.md")
} else {
whatsnew = whatsnew.getAt(0..<idx)
}
// Check whatsnew and changelog match.
def versionChangelog = "# " + whatsnew.join("\n")
def changelog = new File(projectDir, "src/main/resources/data/computercraft/lua/rom/help/changelog.txt").getText()
def changelog = new File(projectDir, "src/main/resources/data/computercraft/lua/rom/help/changelog.md").getText()
if (!changelog.startsWith(versionChangelog)) {
ok = false
project.logger.error("whatsnew and changelog are not in sync")
@ -464,6 +484,21 @@ curseforge {
}
}
import com.modrinth.minotaur.TaskModrinthUpload
tasks.register('publishModrinth', TaskModrinthUpload.class).configure {
dependsOn('assemble', 'reobfJar')
onlyIf {
project.hasProperty('modrinthApiKey')
}
token = project.hasProperty('curseForgeApiKey')
projectId = 'gu7yAYhd'
versionNumber = project.mod_version
uploadFile = jar
addGameVersion(project.mc_version)
addLoader('forge')
}
tasks.withType(GenerateModuleMetadata) {
// We can't generate metadata as that includes Forge as a dependency.
enabled = false
@ -538,10 +573,10 @@ githubRelease {
prerelease false
}
def uploadTasks = ["publish", "curseforge", "githubRelease"]
def uploadTasks = ["publish", "curseforge", "publishModrinth", "githubRelease"]
uploadTasks.forEach { tasks.getByName(it).dependsOn checkRelease }
task uploadAll(dependsOn: uploadTasks) {
group "upload"
description "Uploads to all repositories (Maven, Curse, GitHub release)"
description "Uploads to all repositories (Maven, Curse, Modrinth, GitHub release)"
}

View File

@ -7,9 +7,6 @@
<suppress checks="StaticVariableName" files=".*[\\/]ComputerCraft.java" />
<suppress checks="StaticVariableName" files=".*[\\/]ComputerCraftAPI.java" />
<!-- Do not check for missing package Javadoc. -->
<suppress checks="JavadocStyle" files=".*[\\/]package-info.java" />
<!-- The commands API is documented in Lua. -->
<suppress checks="SummaryJavadocCheck" files=".*[\\/]CommandAPI.java" />
</suppressions>

8
config/gitpod/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM gitpod/workspace-base
USER gitpod
RUN sudo apt-get -q update \
&& sudo apt-get install -yq openjdk-8-jdk openjdk-16-jdk python3-pip npm \
&& sudo pip3 install pre-commit \
&& sudo update-java-alternatives --set java-1.8.0-openjdk-amd64

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -32,7 +32,7 @@ public final class TransformedModel
public TransformedModel( @Nonnull IBakedModel model )
{
this.model = Objects.requireNonNull( model );
this.matrix = TransformationMatrix.identity();
matrix = TransformationMatrix.identity();
}
public static TransformedModel of( @Nonnull ModelResourceLocation location )

View File

@ -30,7 +30,7 @@ public class FileOperationException extends IOException
public FileOperationException( @Nonnull String message )
{
super( Objects.requireNonNull( message, "message cannot be null" ) );
this.filename = null;
filename = null;
}
@Nullable

View File

@ -19,14 +19,14 @@ public class LuaException extends Exception
public LuaException( @Nullable String message )
{
super( message );
this.hasLevel = false;
this.level = 1;
hasLevel = false;
level = 1;
}
public LuaException( @Nullable String message, int level )
{
super( message );
this.hasLevel = true;
hasLevel = true;
this.level = level;
}

View File

@ -30,14 +30,14 @@ public final class MethodResult
private MethodResult( Object[] arguments, ILuaCallback callback )
{
this.result = arguments;
result = arguments;
this.callback = callback;
this.adjust = 0;
adjust = 0;
}
private MethodResult( Object[] arguments, ILuaCallback callback, int adjust )
{
this.result = arguments;
result = arguments;
this.callback = callback;
this.adjust = adjust;
}

View File

@ -22,6 +22,7 @@ public class ClientHooks
if( event.getWorld().isClientSide() )
{
ClientMonitor.destroyAll();
SoundManager.reset();
}
}

View File

@ -0,0 +1,84 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.ISound;
import net.minecraft.client.audio.ITickableSound;
import net.minecraft.client.audio.LocatableSound;
import net.minecraft.client.audio.SoundHandler;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.math.vector.Vector3d;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class SoundManager
{
private static final Map<UUID, MoveableSound> sounds = new HashMap<>();
public static void playSound( UUID source, Vector3d position, SoundEvent event, float volume, float pitch )
{
SoundHandler soundManager = Minecraft.getInstance().getSoundManager();
MoveableSound oldSound = sounds.get( source );
if( oldSound != null ) soundManager.stop( oldSound );
MoveableSound newSound = new MoveableSound( event, position, volume, pitch );
sounds.put( source, newSound );
soundManager.play( newSound );
}
public static void stopSound( UUID source )
{
ISound sound = sounds.remove( source );
if( sound == null ) return;
Minecraft.getInstance().getSoundManager().stop( sound );
}
public static void moveSound( UUID source, Vector3d position )
{
MoveableSound sound = sounds.get( source );
if( sound != null ) sound.setPosition( position );
}
public static void reset()
{
sounds.clear();
}
private static class MoveableSound extends LocatableSound implements ITickableSound
{
protected MoveableSound( SoundEvent sound, Vector3d position, float volume, float pitch )
{
super( sound, SoundCategory.RECORDS );
setPosition( position );
this.volume = volume;
this.pitch = pitch;
}
void setPosition( Vector3d position )
{
x = (float) position.x();
y = (float) position.y();
z = (float) position.z();
}
@Override
public boolean isStopped()
{
return false;
}
@Override
public void tick()
{
}
}
}

View File

@ -8,8 +8,8 @@ package dan200.computercraft.client.gui;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.systems.RenderSystem;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.WidgetTerminal;
import dan200.computercraft.client.gui.widgets.WidgetWrapper;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.shared.computer.core.ClientComputer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
@ -25,7 +25,6 @@ import org.lwjgl.glfw.GLFW;
import javax.annotation.Nonnull;
import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER;
import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN;
public final class GuiComputer<T extends ContainerComputerBase> extends ContainerScreen<T>
{
@ -34,8 +33,7 @@ public final class GuiComputer<T extends ContainerComputerBase> extends Containe
private final int termWidth;
private final int termHeight;
private WidgetTerminal terminal;
private WidgetWrapper terminalWrapper;
private WidgetTerminal terminal = null;
private GuiComputer(
T container, PlayerInventory player, ITextComponent title, int termWidth, int termHeight
@ -46,7 +44,9 @@ public final class GuiComputer<T extends ContainerComputerBase> extends Containe
computer = (ClientComputer) container.getComputer();
this.termWidth = termWidth;
this.termHeight = termHeight;
terminal = null;
imageWidth = WidgetTerminal.getWidth( termWidth ) + BORDER * 2 + ComputerSidebar.WIDTH;
imageHeight = WidgetTerminal.getHeight( termHeight ) + BORDER * 2;
}
public static GuiComputer<ContainerComputer> create( ContainerComputer container, PlayerInventory inventory, ITextComponent component )
@ -77,29 +77,21 @@ public final class GuiComputer<T extends ContainerComputerBase> extends Containe
@Override
protected void init()
{
minecraft.keyboardHandler.setSendRepeatsToGui( true );
int termPxWidth = termWidth * FixedWidthFontRenderer.FONT_WIDTH;
int termPxHeight = termHeight * FixedWidthFontRenderer.FONT_HEIGHT;
imageWidth = termPxWidth + MARGIN * 2 + BORDER * 2;
imageHeight = termPxHeight + MARGIN * 2 + BORDER * 2;
super.init();
terminal = new WidgetTerminal( minecraft, () -> computer, termWidth, termHeight, MARGIN, MARGIN, MARGIN, MARGIN );
terminalWrapper = new WidgetWrapper( terminal, MARGIN + BORDER + leftPos, MARGIN + BORDER + topPos, termPxWidth, termPxHeight );
minecraft.keyboardHandler.setSendRepeatsToGui( true );
children.add( terminalWrapper );
setFocused( terminalWrapper );
terminal = addButton( new WidgetTerminal( computer,
leftPos + ComputerSidebar.WIDTH + BORDER, topPos + BORDER, termWidth, termHeight
) );
ComputerSidebar.addButtons( this, computer, this::addButton, leftPos, topPos + BORDER );
setFocused( terminal );
}
@Override
public void removed()
{
super.removed();
children.remove( terminal );
terminal = null;
minecraft.keyboardHandler.setSendRepeatsToGui( false );
}
@ -114,7 +106,7 @@ public final class GuiComputer<T extends ContainerComputerBase> extends Containe
public boolean keyPressed( int key, int scancode, int modifiers )
{
// Forward the tab key to the terminal, rather than moving between controls.
if( key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminalWrapper )
if( key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminal )
{
return getFocused().keyPressed( key, scancode, modifiers );
}
@ -125,16 +117,11 @@ public final class GuiComputer<T extends ContainerComputerBase> extends Containe
@Override
public void renderBg( @Nonnull MatrixStack stack, float partialTicks, int mouseX, int mouseY )
{
// Draw terminal
terminal.draw( terminalWrapper.getX(), terminalWrapper.getY() );
// Draw a border around the terminal
RenderSystem.color4f( 1, 1, 1, 1 );
minecraft.getTextureManager().bind( ComputerBorderRenderer.getTexture( family ) );
ComputerBorderRenderer.render(
terminalWrapper.getX() - MARGIN, terminalWrapper.getY() - MARGIN, getBlitOffset(),
terminalWrapper.getWidth() + MARGIN * 2, terminalWrapper.getHeight() + MARGIN * 2
);
ComputerBorderRenderer.render( terminal.x, terminal.y, getBlitOffset(), terminal.getWidth(), terminal.getHeight() );
ComputerSidebar.renderBackground( stack, leftPos, topPos + BORDER );
}
@Override

View File

@ -8,8 +8,9 @@ package dan200.computercraft.client.gui;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.systems.RenderSystem;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.WidgetTerminal;
import dan200.computercraft.client.gui.widgets.WidgetWrapper;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.shared.computer.core.ClientComputer;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.turtle.inventory.ContainerTurtle;
@ -21,6 +22,8 @@ import org.lwjgl.glfw.GLFW;
import javax.annotation.Nonnull;
import static dan200.computercraft.shared.turtle.inventory.ContainerTurtle.*;
public class GuiTurtle extends ContainerScreen<ContainerTurtle>
{
private static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation( ComputerCraft.MOD_ID, "textures/gui/turtle_normal.png" );
@ -32,7 +35,6 @@ public class GuiTurtle extends ContainerScreen<ContainerTurtle>
private final ClientComputer computer;
private WidgetTerminal terminal;
private WidgetWrapper terminalWrapper;
public GuiTurtle( ContainerTurtle container, PlayerInventory player, ITextComponent title )
{
@ -52,27 +54,18 @@ public class GuiTurtle extends ContainerScreen<ContainerTurtle>
super.init();
minecraft.keyboardHandler.setSendRepeatsToGui( true );
int termPxWidth = ComputerCraft.turtleTermWidth * FixedWidthFontRenderer.FONT_WIDTH;
int termPxHeight = ComputerCraft.turtleTermHeight * FixedWidthFontRenderer.FONT_HEIGHT;
terminal = new WidgetTerminal(
minecraft, () -> computer,
ComputerCraft.turtleTermWidth,
ComputerCraft.turtleTermHeight,
2, 2, 2, 2
);
terminalWrapper = new WidgetWrapper( terminal, 2 + 8 + leftPos, 2 + 8 + topPos, termPxWidth, termPxHeight );
children.add( terminalWrapper );
setFocused( terminalWrapper );
terminal = addButton( new WidgetTerminal(
computer, leftPos + BORDER + ComputerSidebar.WIDTH, topPos + BORDER,
ComputerCraft.turtleTermWidth, ComputerCraft.turtleTermHeight
) );
ComputerSidebar.addButtons( this, computer, this::addButton, leftPos, topPos + BORDER );
setFocused( terminal );
}
@Override
public void removed()
{
super.removed();
children.remove( terminal );
terminal = null;
minecraft.keyboardHandler.setSendRepeatsToGui( false );
}
@ -87,7 +80,7 @@ public class GuiTurtle extends ContainerScreen<ContainerTurtle>
public boolean keyPressed( int key, int scancode, int modifiers )
{
// Forward the tab key to the terminal, rather than moving between controls.
if( key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminalWrapper )
if( key == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminal )
{
return getFocused().keyPressed( key, scancode, modifiers );
}
@ -98,24 +91,21 @@ public class GuiTurtle extends ContainerScreen<ContainerTurtle>
@Override
protected void renderBg( @Nonnull MatrixStack transform, float partialTicks, int mouseX, int mouseY )
{
// Draw term
ResourceLocation texture = family == ComputerFamily.ADVANCED ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL;
terminal.draw( terminalWrapper.getX(), terminalWrapper.getY() );
boolean advanced = family == ComputerFamily.ADVANCED;
minecraft.getTextureManager().bind( advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL );
blit( transform, leftPos + ComputerSidebar.WIDTH, topPos, 0, 0, imageWidth, imageHeight );
// Draw border/inventory
RenderSystem.color4f( 1.0F, 1.0F, 1.0F, 1.0F );
minecraft.getTextureManager().bind( texture );
blit( transform, leftPos, topPos, 0, 0, imageWidth, imageHeight );
minecraft.getTextureManager().bind( advanced ? ComputerBorderRenderer.BACKGROUND_ADVANCED : ComputerBorderRenderer.BACKGROUND_NORMAL );
ComputerSidebar.renderBackground( transform, leftPos, topPos + BORDER );
// Draw selection slot
int slot = container.getSelectedSlot();
if( slot >= 0 )
{
RenderSystem.color4f( 1.0F, 1.0F, 1.0F, 1.0F );
int slotX = slot % 4;
int slotY = slot / 4;
blit( transform,
leftPos + ContainerTurtle.TURTLE_START_X - 2 + slotX * 18,
topPos + ContainerTurtle.PLAYER_START_Y - 2 + slotY * 18,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18,
0, 217, 24, 24
);
}

View File

@ -0,0 +1,106 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.matrix.MatrixStack;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.client.render.ComputerBorderRenderer;
import dan200.computercraft.shared.computer.core.ClientComputer;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.Widget;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.TranslationTextComponent;
import java.util.Arrays;
import java.util.function.Consumer;
/**
* Registers buttons to interact with a computer.
*/
public final class ComputerSidebar
{
private static final ResourceLocation TEXTURE = new ResourceLocation( ComputerCraft.MOD_ID, "textures/gui/buttons.png" );
private static final int TEX_SIZE = 64;
private static final int ICON_WIDTH = 12;
private static final int ICON_HEIGHT = 12;
private static final int ICON_MARGIN = 2;
private static final int ICON_TEX_Y_DIFF = 14;
private static final int CORNERS_BORDER = 3;
private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN;
private static final int BUTTONS = 2;
private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
public static final int WIDTH = 17;
private ComputerSidebar()
{
}
public static void addButtons( Screen screen, ClientComputer computer, Consumer<Widget> add, int x, int y )
{
x += CORNERS_BORDER + 1;
y += CORNERS_BORDER + ICON_MARGIN;
add.accept( new DynamicImageButton(
screen, x, y, ICON_WIDTH, ICON_HEIGHT, () -> computer.isOn() ? 15 : 1, 1, ICON_TEX_Y_DIFF,
TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer( computer ),
() -> computer.isOn() ? Arrays.asList(
new TranslationTextComponent( "gui.computercraft.tooltip.turn_off" ),
new TranslationTextComponent( "gui.computercraft.tooltip.turn_off.key" ).withStyle( TextFormatting.GRAY )
) : Arrays.asList(
new TranslationTextComponent( "gui.computercraft.tooltip.turn_on" ),
new TranslationTextComponent( "gui.computercraft.tooltip.turn_off.key" ).withStyle( TextFormatting.GRAY )
)
) );
y += ICON_HEIGHT + ICON_MARGIN * 2;
add.accept( new DynamicImageButton(
screen, x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF,
TEXTURE, TEX_SIZE, TEX_SIZE, b -> computer.queueEvent( "terminate" ),
Arrays.asList(
new TranslationTextComponent( "gui.computercraft.tooltip.terminate" ),
new TranslationTextComponent( "gui.computercraft.tooltip.terminate.key" ).withStyle( TextFormatting.GRAY )
)
) );
}
public static void renderBackground( MatrixStack transform, int x, int y )
{
Screen.blit( transform,
x, y, 0, 102, WIDTH, FULL_BORDER,
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
);
Screen.blit( transform,
x, y + FULL_BORDER, WIDTH, HEIGHT - FULL_BORDER * 2,
0, 107, WIDTH, 4,
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
);
Screen.blit( transform,
x, y + HEIGHT - FULL_BORDER, 0, 111, WIDTH, FULL_BORDER,
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
);
}
private static void toggleComputer( ClientComputer computer )
{
if( computer.isOn() )
{
computer.shutdown();
}
else
{
computer.turnOn();
}
}
}

View File

@ -0,0 +1,101 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.button.Button;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.StringTextComponent;
import net.minecraftforge.common.util.NonNullSupplier;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.function.IntSupplier;
/**
* Version of {@link net.minecraft.client.gui.widget.button.ImageButton} which allows changing some properties
* dynamically.
*/
public class DynamicImageButton extends Button
{
private final Screen screen;
private final ResourceLocation texture;
private final IntSupplier xTexStart;
private final int yTexStart;
private final int yDiffTex;
private final int textureWidth;
private final int textureHeight;
private final NonNullSupplier<List<ITextComponent>> tooltip;
public DynamicImageButton(
Screen screen, int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex,
ResourceLocation texture, int textureWidth, int textureHeight,
IPressable onPress, List<ITextComponent> tooltip
)
{
this(
screen, x, y, width, height, () -> xTexStart, yTexStart, yDiffTex,
texture, textureWidth, textureHeight,
onPress, () -> tooltip
);
}
public DynamicImageButton(
Screen screen, int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex,
ResourceLocation texture, int textureWidth, int textureHeight,
IPressable onPress, NonNullSupplier<List<ITextComponent>> tooltip
)
{
super( x, y, width, height, StringTextComponent.EMPTY, onPress );
this.screen = screen;
this.textureWidth = textureWidth;
this.textureHeight = textureHeight;
this.xTexStart = xTexStart;
this.yTexStart = yTexStart;
this.yDiffTex = yDiffTex;
this.texture = texture;
this.tooltip = tooltip;
}
@Override
public void renderButton( @Nonnull MatrixStack stack, int mouseX, int mouseY, float partialTicks )
{
Minecraft minecraft = Minecraft.getInstance();
minecraft.getTextureManager().bind( texture );
RenderSystem.disableDepthTest();
int yTex = yTexStart;
if( isHovered() ) yTex += yDiffTex;
blit( stack, x, y, xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight );
RenderSystem.enableDepthTest();
if( isHovered() ) renderToolTip( stack, mouseX, mouseY );
}
@Nonnull
@Override
public ITextComponent getMessage()
{
List<ITextComponent> tooltip = this.tooltip.get();
return tooltip.isEmpty() ? StringTextComponent.EMPTY : tooltip.get( 0 );
}
@Override
public void renderToolTip( @Nonnull MatrixStack stack, int mouseX, int mouseY )
{
List<ITextComponent> tooltip = this.tooltip.get();
if( !tooltip.isEmpty() )
{
screen.renderWrappedToolTip( stack, tooltip, mouseX, mouseY, screen.getMinecraft().font );
}
}
}

View File

@ -5,32 +5,35 @@
*/
package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.matrix.MatrixStack;
import dan200.computercraft.client.gui.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ClientComputer;
import dan200.computercraft.shared.computer.core.IComputer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.IGuiEventListener;
import net.minecraft.client.gui.widget.Widget;
import net.minecraft.util.SharedConstants;
import net.minecraft.util.math.vector.Matrix4f;
import net.minecraft.util.text.StringTextComponent;
import org.lwjgl.glfw.GLFW;
import javax.annotation.Nonnull;
import java.util.BitSet;
import java.util.function.Supplier;
import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT;
import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_WIDTH;
import static dan200.computercraft.client.render.ComputerBorderRenderer.MARGIN;
public class WidgetTerminal implements IGuiEventListener
public class WidgetTerminal extends Widget
{
private static final float TERMINATE_TIME = 0.5f;
private final Minecraft client;
private final ClientComputer computer;
private boolean focused;
private final Supplier<ClientComputer> computer;
private final int termWidth;
private final int termHeight;
// The positions of the actual terminal
private final int innerX;
private final int innerY;
private final int innerWidth;
private final int innerHeight;
private float terminateTimer = -1;
private float rebootTimer = -1;
@ -40,23 +43,18 @@ public class WidgetTerminal implements IGuiEventListener
private int lastMouseX = -1;
private int lastMouseY = -1;
private final int leftMargin;
private final int rightMargin;
private final int topMargin;
private final int bottomMargin;
private final BitSet keysDown = new BitSet( 256 );
public WidgetTerminal( Minecraft client, Supplier<ClientComputer> computer, int termWidth, int termHeight, int leftMargin, int rightMargin, int topMargin, int bottomMargin )
public WidgetTerminal( @Nonnull ClientComputer computer, int x, int y, int termWidth, int termHeight )
{
this.client = client;
super( x, y, termWidth * FONT_WIDTH + MARGIN * 2, termHeight * FONT_HEIGHT + MARGIN * 2, StringTextComponent.EMPTY );
this.computer = computer;
this.termWidth = termWidth;
this.termHeight = termHeight;
this.leftMargin = leftMargin;
this.rightMargin = rightMargin;
this.topMargin = topMargin;
this.bottomMargin = bottomMargin;
innerX = x + MARGIN;
innerY = y + MARGIN;
innerWidth = termWidth * FONT_WIDTH;
innerHeight = termHeight * FONT_HEIGHT;
}
@Override
@ -65,7 +63,7 @@ public class WidgetTerminal implements IGuiEventListener
if( ch >= 32 && ch <= 126 || ch >= 160 && ch <= 255 ) // printable chars in byte range
{
// Queue the "char" event
queueEvent( "char", Character.toString( ch ) );
computer.queueEvent( "char", new Object[] { Character.toString( ch ) } );
}
return true;
@ -91,7 +89,7 @@ public class WidgetTerminal implements IGuiEventListener
case GLFW.GLFW_KEY_V:
// Ctrl+V for paste
String clipboard = client.keyboardHandler.getClipboard();
String clipboard = Minecraft.getInstance().keyboardHandler.getClipboard();
if( clipboard != null )
{
// Clip to the first occurrence of \r or \n
@ -116,7 +114,7 @@ public class WidgetTerminal implements IGuiEventListener
{
// Clip to 512 characters and queue the event
if( clipboard.length() > 512 ) clipboard = clipboard.substring( 0, 512 );
queueEvent( "paste", clipboard );
computer.queueEvent( "paste", new Object[] { clipboard } );
}
return true;
@ -129,8 +127,7 @@ public class WidgetTerminal implements IGuiEventListener
// Queue the "key" event and add to the down set
boolean repeat = keysDown.get( key );
keysDown.set( key );
IComputer computer = this.computer.get();
if( computer != null ) computer.keyDown( key, repeat );
computer.keyDown( key, repeat );
}
return true;
@ -143,8 +140,7 @@ public class WidgetTerminal implements IGuiEventListener
if( key >= 0 && keysDown.get( key ) )
{
keysDown.set( key, false );
IComputer computer = this.computer.get();
if( computer != null ) computer.keyUp( key );
computer.keyUp( key );
}
switch( key )
@ -170,14 +166,14 @@ public class WidgetTerminal implements IGuiEventListener
@Override
public boolean mouseClicked( double mouseX, double mouseY, int button )
{
ClientComputer computer = this.computer.get();
if( computer == null || !computer.isColour() || button < 0 || button > 2 ) return false;
if( !inTermRegion( mouseX, mouseY ) ) return false;
if( !computer.isColour() || button < 0 || button > 2 ) return false;
Terminal term = computer.getTerminal();
if( term != null )
{
int charX = (int) (mouseX / FONT_WIDTH);
int charY = (int) (mouseY / FONT_HEIGHT);
int charX = (int) ((mouseX - x) / FONT_WIDTH);
int charY = (int) ((mouseY - x) / FONT_HEIGHT);
charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 );
charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 );
@ -194,14 +190,14 @@ public class WidgetTerminal implements IGuiEventListener
@Override
public boolean mouseReleased( double mouseX, double mouseY, int button )
{
ClientComputer computer = this.computer.get();
if( computer == null || !computer.isColour() || button < 0 || button > 2 ) return false;
if( !inTermRegion( mouseX, mouseY ) ) return false;
if( !computer.isColour() || button < 0 || button > 2 ) return false;
Terminal term = computer.getTerminal();
if( term != null )
{
int charX = (int) (mouseX / FONT_WIDTH);
int charY = (int) (mouseY / FONT_HEIGHT);
int charX = (int) ((mouseX - x) / FONT_WIDTH);
int charY = (int) ((mouseY - x) / FONT_HEIGHT);
charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 );
charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 );
@ -221,14 +217,14 @@ public class WidgetTerminal implements IGuiEventListener
@Override
public boolean mouseDragged( double mouseX, double mouseY, int button, double v2, double v3 )
{
ClientComputer computer = this.computer.get();
if( computer == null || !computer.isColour() || button < 0 || button > 2 ) return false;
if( !inTermRegion( mouseX, mouseY ) ) return false;
if( !computer.isColour() || button < 0 || button > 2 ) return false;
Terminal term = computer.getTerminal();
if( term != null )
{
int charX = (int) (mouseX / FONT_WIDTH);
int charY = (int) (mouseY / FONT_HEIGHT);
int charX = (int) ((mouseX - x) / FONT_WIDTH);
int charY = (int) ((mouseY - x) / FONT_HEIGHT);
charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 );
charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 );
@ -246,14 +242,14 @@ public class WidgetTerminal implements IGuiEventListener
@Override
public boolean mouseScrolled( double mouseX, double mouseY, double delta )
{
ClientComputer computer = this.computer.get();
if( computer == null || !computer.isColour() || delta == 0 ) return false;
if( !inTermRegion( mouseX, mouseY ) ) return false;
if( !computer.isColour() || delta == 0 ) return false;
Terminal term = computer.getTerminal();
if( term != null )
{
int charX = (int) (mouseX / FONT_WIDTH);
int charY = (int) (mouseY / FONT_HEIGHT);
int charX = (int) ((mouseX - innerX) / FONT_WIDTH);
int charY = (int) ((mouseY - innerY) / FONT_HEIGHT);
charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 );
charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 );
@ -266,89 +262,74 @@ public class WidgetTerminal implements IGuiEventListener
return true;
}
private boolean inTermRegion( double mouseX, double mouseY )
{
return active && visible && mouseX >= innerX && mouseY >= innerY && mouseX < innerX + innerWidth && mouseY < innerY + innerHeight;
}
public void update()
{
if( terminateTimer >= 0 && terminateTimer < TERMINATE_TIME && (terminateTimer += 0.05f) > TERMINATE_TIME )
{
queueEvent( "terminate" );
computer.queueEvent( "terminate" );
}
if( shutdownTimer >= 0 && shutdownTimer < TERMINATE_TIME && (shutdownTimer += 0.05f) > TERMINATE_TIME )
{
ClientComputer computer = this.computer.get();
if( computer != null ) computer.shutdown();
computer.shutdown();
}
if( rebootTimer >= 0 && rebootTimer < TERMINATE_TIME && (rebootTimer += 0.05f) > TERMINATE_TIME )
{
ClientComputer computer = this.computer.get();
if( computer != null ) computer.reboot();
computer.reboot();
}
}
@Override
public boolean changeFocus( boolean reversed )
public void onFocusedChanged( boolean focused )
{
if( focused )
if( !focused )
{
// When blurring, we should make all keys go up
for( int key = 0; key < keysDown.size(); key++ )
{
if( keysDown.get( key ) ) queueEvent( "key_up", key );
if( keysDown.get( key ) ) computer.keyUp( key );
}
keysDown.clear();
// When blurring, we should make the last mouse button go up
if( lastMouseButton > 0 )
{
IComputer computer = this.computer.get();
if( computer != null ) computer.mouseUp( lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1 );
computer.mouseUp( lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1 );
lastMouseButton = -1;
}
shutdownTimer = terminateTimer = rebootTimer = -1;
}
focused = !focused;
return true;
}
public void draw( int originX, int originY )
{
synchronized( computer )
{
// Draw the screen contents
ClientComputer computer = this.computer.get();
Terminal terminal = computer != null ? computer.getTerminal() : null;
if( terminal != null )
{
FixedWidthFontRenderer.drawTerminal( originX, originY, terminal, !computer.isColour(), topMargin, bottomMargin, leftMargin, rightMargin );
}
else
{
FixedWidthFontRenderer.drawEmptyTerminal(
originX - leftMargin, originY - rightMargin,
termWidth * FONT_WIDTH + leftMargin + rightMargin,
termHeight * FONT_HEIGHT + topMargin + bottomMargin
);
}
}
}
private void queueEvent( String event )
{
ClientComputer computer = this.computer.get();
if( computer != null ) computer.queueEvent( event );
}
private void queueEvent( String event, Object... args )
{
ClientComputer computer = this.computer.get();
if( computer != null ) computer.queueEvent( event, args );
}
@Override
public boolean isMouseOver( double x, double y )
public void render( @Nonnull MatrixStack transform, int mouseX, int mouseY, float partialTicks )
{
return true;
Matrix4f matrix = transform.last().pose();
Terminal terminal = computer.getTerminal();
if( terminal != null )
{
FixedWidthFontRenderer.drawTerminal( matrix, innerX, innerY, terminal, !computer.isColour(), MARGIN, MARGIN, MARGIN, MARGIN );
}
else
{
FixedWidthFontRenderer.drawEmptyTerminal( matrix, x, y, width, height );
}
}
public static int getWidth( int termWidth )
{
return termWidth * FONT_WIDTH + MARGIN * 2;
}
public static int getHeight( int termHeight )
{
return termHeight * FONT_HEIGHT + MARGIN * 2;
}
}

View File

@ -1,105 +0,0 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.client.gui.widgets;
import net.minecraft.client.gui.IGuiEventListener;
public class WidgetWrapper implements IGuiEventListener
{
private final IGuiEventListener listener;
private final int x;
private final int y;
private final int width;
private final int height;
public WidgetWrapper( IGuiEventListener listener, int x, int y, int width, int height )
{
this.listener = listener;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Override
public boolean changeFocus( boolean b )
{
return listener.changeFocus( b );
}
@Override
public boolean mouseClicked( double x, double y, int button )
{
double dx = x - this.x, dy = y - this.y;
return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseClicked( dx, dy, button );
}
@Override
public boolean mouseReleased( double x, double y, int button )
{
double dx = x - this.x, dy = y - this.y;
return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseReleased( dx, dy, button );
}
@Override
public boolean mouseDragged( double x, double y, int button, double deltaX, double deltaY )
{
double dx = x - this.x, dy = y - this.y;
return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseDragged( dx, dy, button, deltaX, deltaY );
}
@Override
public boolean mouseScrolled( double x, double y, double delta )
{
double dx = x - this.x, dy = y - this.y;
return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseScrolled( dx, dy, delta );
}
@Override
public boolean keyPressed( int key, int scancode, int modifiers )
{
return listener.keyPressed( key, scancode, modifiers );
}
@Override
public boolean keyReleased( int key, int scancode, int modifiers )
{
return listener.keyReleased( key, scancode, modifiers );
}
@Override
public boolean charTyped( char character, int modifiers )
{
return listener.charTyped( character, modifiers );
}
public int getX()
{
return x;
}
public int getY()
{
return y;
}
public int getWidth()
{
return width;
}
public int getHeight()
{
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

@ -52,7 +52,8 @@ public class ComputerBorderRenderer
public static final int LIGHT_HEIGHT = 8;
private static final float TEX_SCALE = 1 / 256.0f;
public static final int TEX_SIZE = 256;
private static final float TEX_SCALE = 1 / (float) TEX_SIZE;
private final Matrix4f transform;
private final IVertexBuilder builder;

View File

@ -37,7 +37,7 @@ public class HTTPAPI implements ILuaAPI
{
private final IAPIEnvironment apiEnvironment;
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>();
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>( ResourceGroup.DEFAULT );
private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>( () -> ComputerCraft.httpMaxRequests );
private final ResourceGroup<Websocket> websockets = new ResourceGroup<>( () -> ComputerCraft.httpMaxWebsockets );
@ -127,7 +127,10 @@ public class HTTPAPI implements ILuaAPI
HttpRequest request = new HttpRequest( requests, apiEnvironment, address, postString, headers, binary, redirect );
// Make the request
request.queue( r -> r.request( uri, httpMethod ) );
if( !request.queue( r -> r.request( uri, httpMethod ) ) )
{
throw new LuaException( "Too many ongoing HTTP requests" );
}
return new Object[] { true };
}
@ -138,12 +141,15 @@ public class HTTPAPI implements ILuaAPI
}
@LuaFunction
public final Object[] checkURL( String address )
public final Object[] checkURL( String address ) throws LuaException
{
try
{
URI uri = HttpRequest.checkUri( address );
new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run );
if( !new CheckUrl( checkUrls, apiEnvironment, address, uri ).queue( CheckUrl::run ) )
{
throw new LuaException( "Too many ongoing checkUrl calls" );
}
return new Object[] { true };
}

View File

@ -97,7 +97,7 @@ public abstract class Resource<T extends Resource<T>> implements Closeable
tryClose();
}
public boolean queue( Consumer<T> task )
public final boolean queue( Consumer<T> task )
{
@SuppressWarnings( "unchecked" )
T thisT = (T) this;

View File

@ -18,6 +18,9 @@ import java.util.function.Supplier;
*/
public class ResourceGroup<T extends Resource<T>>
{
public static final int DEFAULT_LIMIT = 512;
public static final IntSupplier DEFAULT = () -> DEFAULT_LIMIT;
private static final IntSupplier ZERO = () -> 0;
final IntSupplier limit;

View File

@ -38,8 +38,10 @@ public class ResourceQueue<T extends Resource<T>> extends ResourceGroup<T>
public synchronized boolean queue( Supplier<T> resource )
{
if( !active ) return false;
if( super.queue( resource ) ) return true;
if( pending.size() > DEFAULT_LIMIT ) return false;
if( !super.queue( resource ) ) pending.add( resource );
pending.add( resource );
return true;
}

View File

@ -11,7 +11,10 @@ import com.google.common.cache.LoadingCache;
import com.google.common.primitives.Primitives;
import com.google.common.reflect.TypeToken;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.MethodResult;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
@ -63,7 +66,7 @@ public final class Generator<T>
{
this.base = base;
this.context = context;
this.interfaces = new String[] { Type.getInternalName( base ) };
interfaces = new String[] { Type.getInternalName( base ) };
this.wrap = wrap;
StringBuilder methodDesc = new StringBuilder().append( "(Ljava/lang/Object;" );

View File

@ -251,6 +251,20 @@ final class ComputerExecutor
* and then schedule a shutdown.
*/
void abort()
{
immediateFail( StateCommand.ABORT );
}
/**
* Abort this whole computer due to an internal error. This will immediately destroy the Lua machine,
* and then schedule a shutdown.
*/
void fastFail()
{
immediateFail( StateCommand.ERROR );
}
private void immediateFail( StateCommand command )
{
ILuaMachine machine = this.machine;
if( machine != null ) machine.close();
@ -258,7 +272,7 @@ final class ComputerExecutor
synchronized( queueLock )
{
if( closed ) return;
command = StateCommand.ABORT;
this.command = command;
if( isOn ) enqueue();
}
}
@ -596,6 +610,12 @@ final class ComputerExecutor
displayFailure( "Error running computer", TimeoutState.ABORT_MESSAGE );
shutdown();
break;
case ERROR:
if( !isOn ) return;
displayFailure( "Error running computer", "An internal error occurred, see logs." );
shutdown();
break;
}
}
else if( event != null )
@ -644,6 +664,7 @@ final class ComputerExecutor
SHUTDOWN,
REBOOT,
ABORT,
ERROR,
}
private static final class Event

View File

@ -506,6 +506,8 @@ public final class ComputerThread
catch( Exception | LinkageError | VirtualMachineError e )
{
ComputerCraft.log.error( "Error running task on computer #" + executor.getComputer().getID(), e );
// Tear down the computer immediately. There's no guarantee it's well behaved from now on.
executor.fastFail();
}
finally
{

View File

@ -10,7 +10,6 @@ import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.MainThread;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingField;
@ -53,7 +52,7 @@ public class CobaltLuaMachine implements ILuaMachine
private final Computer computer;
private final TimeoutState timeout;
private final TimeoutDebugHandler debug;
private final ILuaContext context = new CobaltLuaContext();
private final ILuaContext context;
private LuaState state;
private LuaTable globals;
@ -65,6 +64,7 @@ public class CobaltLuaMachine implements ILuaMachine
{
this.computer = computer;
this.timeout = timeout;
context = new LuaContext( computer );
debug = new TimeoutDebugHandler();
// Create an environment to run in
@ -509,53 +509,6 @@ public class CobaltLuaMachine implements ILuaMachine
}
}
private class CobaltLuaContext implements ILuaContext
{
@Override
public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException
{
// Issue command
final long taskID = MainThread.getUniqueTaskID();
final Runnable iTask = () -> {
try
{
Object[] results = task.execute();
if( results != null )
{
Object[] eventArguments = new Object[results.length + 2];
eventArguments[0] = taskID;
eventArguments[1] = true;
System.arraycopy( results, 0, eventArguments, 2, results.length );
computer.queueEvent( "task_complete", eventArguments );
}
else
{
computer.queueEvent( "task_complete", new Object[] { taskID, true } );
}
}
catch( LuaException e )
{
computer.queueEvent( "task_complete", new Object[] { taskID, false, e.getMessage() } );
}
catch( Throwable t )
{
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t );
computer.queueEvent( "task_complete", new Object[] {
taskID, false, "Java Exception Thrown: " + t,
} );
}
};
if( computer.queueMainThread( iTask ) )
{
return taskID;
}
else
{
throw new LuaException( "Task limit exceeded" );
}
}
}
private static final class HardAbortError extends Error
{
private static final long serialVersionUID = 7954092008586367501L;

View File

@ -0,0 +1,69 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.core.lua;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.ILuaTask;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.MainThread;
import javax.annotation.Nonnull;
class LuaContext implements ILuaContext
{
private final Computer computer;
LuaContext( Computer computer )
{
this.computer = computer;
}
@Override
public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException
{
// Issue command
final long taskID = MainThread.getUniqueTaskID();
final Runnable iTask = () -> {
try
{
Object[] results = task.execute();
if( results != null )
{
Object[] eventArguments = new Object[results.length + 2];
eventArguments[0] = taskID;
eventArguments[1] = true;
System.arraycopy( results, 0, eventArguments, 2, results.length );
computer.queueEvent( "task_complete", eventArguments );
}
else
{
computer.queueEvent( "task_complete", new Object[] { taskID, true } );
}
}
catch( LuaException e )
{
computer.queueEvent( "task_complete", new Object[] { taskID, false, e.getMessage() } );
}
catch( Exception t )
{
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t );
computer.queueEvent( "task_complete", new Object[] {
taskID, false, "Java Exception Thrown: " + t,
} );
}
};
if( computer.queueMainThread( iTask ) )
{
return taskID;
}
else
{
throw new LuaException( "Task limit exceeded" );
}
}
}

View File

@ -44,9 +44,9 @@ public class Terminal
this.height = height;
onChanged = changedCallback;
text = new TextBuffer[this.height];
textColour = new TextBuffer[this.height];
backgroundColour = new TextBuffer[this.height];
text = new TextBuffer[height];
textColour = new TextBuffer[height];
backgroundColour = new TextBuffer[height];
for( int i = 0; i < this.height; i++ )
{
text[i] = new TextBuffer( ' ', this.width );
@ -93,9 +93,9 @@ public class Terminal
this.width = width;
this.height = height;
text = new TextBuffer[this.height];
textColour = new TextBuffer[this.height];
backgroundColour = new TextBuffer[this.height];
text = new TextBuffer[height];
textColour = new TextBuffer[height];
backgroundColour = new TextBuffer[height];
for( int i = 0; i < this.height; i++ )
{
if( i >= oldHeight )

View File

@ -12,7 +12,7 @@ public class TextBuffer
public TextBuffer( char c, int length )
{
text = new char[length];
this.fill( c );
fill( c );
}
public TextBuffer( String text )
@ -79,6 +79,7 @@ public class TextBuffer
}
}
@Override
public String toString()
{
return new String( text );

View File

@ -6,11 +6,11 @@
package dan200.computercraft.data;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.Registry;
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
import dan200.computercraft.shared.data.HasComputerIdLootCondition;
import dan200.computercraft.shared.data.PlayerCreativeLootCondition;
import dan200.computercraft.shared.CommonHooks;
import net.minecraft.block.Block;
import net.minecraft.data.DataGenerator;
import net.minecraft.loot.*;

View File

@ -33,9 +33,9 @@ public final class TurtleUpgrades
Wrapper( ITurtleUpgrade upgrade )
{
this.upgrade = upgrade;
this.id = upgrade.getUpgradeID().toString();
this.modId = ModLoadingContext.get().getActiveNamespace();
this.enabled = true;
id = upgrade.getUpgradeID().toString();
modId = ModLoadingContext.get().getActiveNamespace();
enabled = true;
}
}

View File

@ -61,7 +61,7 @@ public class ContainerHeldItem extends Container
public Factory( ContainerType<ContainerHeldItem> type, ItemStack stack, Hand hand )
{
this.type = type;
this.name = stack.getHoverName();
name = stack.getHoverName();
this.hand = hand;
}

View File

@ -25,14 +25,14 @@ public class ContainerViewComputer extends ContainerComputerBase implements ICon
public ContainerViewComputer( int id, ServerComputer computer )
{
super( Registry.ModContainers.VIEW_COMPUTER.get(), id, player -> canInteractWith( computer, player ), computer, computer.getFamily() );
this.width = this.height = 0;
width = height = 0;
}
public ContainerViewComputer( int id, PlayerInventory player, ViewComputerContainerData data )
{
super( Registry.ModContainers.VIEW_COMPUTER.get(), id, player, data );
this.width = data.getWidth();
this.height = data.getHeight();
width = data.getWidth();
height = data.getHeight();
}
private static boolean canInteractWith( @Nonnull ServerComputer computer, @Nonnull PlayerEntity player )

View File

@ -99,6 +99,7 @@ public class AddTurtleTool implements IUndoableAction
return String.format( "Removing turtle upgrade %s.", id );
}
@Override
public boolean validate( ILogger logger )
{
TrackingLogger trackLog = new TrackingLogger( logger );

View File

@ -41,7 +41,7 @@ public class RemoveTurtleUpgradeByItem implements IUndoableAction
@Override
public void undo()
{
if( this.upgrade != null ) TurtleUpgrades.enable( upgrade );
if( upgrade != null ) TurtleUpgrades.enable( upgrade );
}
@Override

View File

@ -40,7 +40,7 @@ public class RemoveTurtleUpgradeByName implements IUndoableAction
@Override
public void undo()
{
if( this.upgrade != null ) TurtleUpgrades.enable( upgrade );
if( upgrade != null ) TurtleUpgrades.enable( upgrade );
}
@Override

View File

@ -339,17 +339,17 @@ class RecipeResolver implements IRecipeManagerPlugin
UpgradeInfo( ItemStack stack, ITurtleUpgrade turtle )
{
this.stack = stack;
this.ingredient = of( stack );
this.upgrade = this.turtle = turtle;
this.pocket = null;
ingredient = of( stack );
upgrade = this.turtle = turtle;
pocket = null;
}
UpgradeInfo( ItemStack stack, IPocketUpgrade pocket )
{
this.stack = stack;
this.ingredient = of( stack );
this.turtle = null;
this.upgrade = this.pocket = pocket;
ingredient = of( stack );
turtle = null;
upgrade = this.pocket = pocket;
}
List<Shaped> getRecipes()

View File

@ -56,6 +56,9 @@ public final class NetworkHandler
registerMainThread( 13, NetworkDirection.PLAY_TO_CLIENT, ComputerTerminalClientMessage::new );
registerMainThread( 14, NetworkDirection.PLAY_TO_CLIENT, PlayRecordClientMessage.class, PlayRecordClientMessage::new );
registerMainThread( 15, NetworkDirection.PLAY_TO_CLIENT, MonitorClientMessage.class, MonitorClientMessage::new );
registerMainThread( 16, NetworkDirection.PLAY_TO_CLIENT, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new );
registerMainThread( 17, NetworkDirection.PLAY_TO_CLIENT, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new );
registerMainThread( 18, NetworkDirection.PLAY_TO_CLIENT, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new );
}
public static void sendToPlayer( PlayerEntity player, NetworkMessage packet )

View File

@ -0,0 +1,58 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.SoundManager;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.math.vector.Vector3d;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.network.NetworkEvent;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* Starts a sound on the client.
*
* Used by speakers to play sounds.
*
* @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker
*/
public class SpeakerMoveClientMessage implements NetworkMessage
{
private final UUID source;
private final Vector3d pos;
public SpeakerMoveClientMessage( UUID source, Vector3d pos )
{
this.source = source;
this.pos = pos;
}
public SpeakerMoveClientMessage( PacketBuffer buf )
{
source = buf.readUUID();
pos = new Vector3d( buf.readDouble(), buf.readDouble(), buf.readDouble() );
}
@Override
public void toBytes( @Nonnull PacketBuffer buf )
{
buf.writeUUID( source );
buf.writeDouble( pos.x() );
buf.writeDouble( pos.y() );
buf.writeDouble( pos.z() );
}
@Override
@OnlyIn( Dist.CLIENT )
public void handle( NetworkEvent.Context context )
{
SoundManager.moveSound( source, pos );
}
}

View File

@ -0,0 +1,74 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.SoundManager;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.network.PacketBuffer;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.math.vector.Vector3d;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.network.NetworkEvent;
import net.minecraftforge.registries.ForgeRegistries;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* Starts a sound on the client.
*
* Used by speakers to play sounds.
*
* @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker
*/
public class SpeakerPlayClientMessage implements NetworkMessage
{
private final UUID source;
private final Vector3d pos;
private final ResourceLocation sound;
private final float volume;
private final float pitch;
public SpeakerPlayClientMessage( UUID source, Vector3d pos, ResourceLocation event, float volume, float pitch )
{
this.source = source;
this.pos = pos;
sound = event;
this.volume = volume;
this.pitch = pitch;
}
public SpeakerPlayClientMessage( PacketBuffer buf )
{
source = buf.readUUID();
pos = new Vector3d( buf.readDouble(), buf.readDouble(), buf.readDouble() );
sound = buf.readResourceLocation();
volume = buf.readFloat();
pitch = buf.readFloat();
}
@Override
public void toBytes( @Nonnull PacketBuffer buf )
{
buf.writeUUID( source );
buf.writeDouble( pos.x() );
buf.writeDouble( pos.y() );
buf.writeDouble( pos.z() );
buf.writeResourceLocation( sound );
buf.writeFloat( volume );
buf.writeFloat( pitch );
}
@Override
@OnlyIn( Dist.CLIENT )
public void handle( NetworkEvent.Context context )
{
SoundEvent sound = ForgeRegistries.SOUND_EVENTS.getValue( this.sound );
if( sound != null ) SoundManager.playSound( source, pos, sound, volume, pitch );
}
}

View File

@ -0,0 +1,51 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.network.client;
import dan200.computercraft.client.SoundManager;
import dan200.computercraft.shared.network.NetworkMessage;
import net.minecraft.network.PacketBuffer;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.network.NetworkEvent;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* Stops a sound on the client
*
* Called when a speaker is broken.
*
* @see dan200.computercraft.shared.peripheral.speaker.TileSpeaker
*/
public class SpeakerStopClientMessage implements NetworkMessage
{
private final UUID source;
public SpeakerStopClientMessage( UUID source )
{
this.source = source;
}
public SpeakerStopClientMessage( PacketBuffer buf )
{
source = buf.readUUID();
}
@Override
public void toBytes( @Nonnull PacketBuffer buf )
{
buf.writeUUID( source );
}
@Override
@OnlyIn( Dist.CLIENT )
public void handle( NetworkEvent.Context context )
{
SoundManager.stopSound( source );
}
}

View File

@ -54,36 +54,36 @@ public class TerminalState
if( terminal == null )
{
this.width = this.height = 0;
this.buffer = null;
width = height = 0;
buffer = null;
}
else
{
this.width = terminal.getWidth();
this.height = terminal.getHeight();
width = terminal.getWidth();
height = terminal.getHeight();
ByteBuf buf = this.buffer = Unpooled.buffer();
ByteBuf buf = buffer = Unpooled.buffer();
terminal.write( new PacketBuffer( buf ) );
}
}
public TerminalState( PacketBuffer buf )
{
this.colour = buf.readBoolean();
this.compress = buf.readBoolean();
colour = buf.readBoolean();
compress = buf.readBoolean();
if( buf.readBoolean() )
{
this.width = buf.readVarInt();
this.height = buf.readVarInt();
width = buf.readVarInt();
height = buf.readVarInt();
int length = buf.readVarInt();
this.buffer = readCompressed( buf, length, compress );
buffer = readCompressed( buf, length, compress );
}
else
{
this.width = this.height = 0;
this.buffer = null;
width = height = 0;
buffer = null;
}
}

View File

@ -16,14 +16,14 @@ public class ComputerContainerData implements ContainerData
public ComputerContainerData( ServerComputer computer )
{
this.id = computer.getInstanceID();
this.family = computer.getFamily();
id = computer.getInstanceID();
family = computer.getFamily();
}
public ComputerContainerData( PacketBuffer buf )
{
this.id = buf.readInt();
this.family = buf.readEnum( ComputerFamily.class );
id = buf.readInt();
family = buf.readEnum( ComputerFamily.class );
}
@Override

View File

@ -24,7 +24,7 @@ final class SaturatedMethod
SaturatedMethod( Object target, NamedMethod<PeripheralMethod> method )
{
this.target = target;
this.name = method.getName();
name = method.getName();
this.method = method.getMethod();
}

View File

@ -53,7 +53,7 @@ public class TileCable extends TileGeneric
@Override
public World getWorld()
{
return TileCable.this.getLevel();
return getLevel();
}
@Nonnull

View File

@ -87,7 +87,7 @@ public class BlockMonitor extends BlockGeneric
{
TileMonitor monitor = (TileMonitor) entity;
// Defer the block update if we're being placed by another TE. See #691
if ( livingEntity == null || livingEntity instanceof FakePlayer )
if( livingEntity == null || livingEntity instanceof FakePlayer )
{
monitor.updateNeighborsDeferred();
return;

View File

@ -10,17 +10,23 @@ import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.SpeakerMoveClientMessage;
import dan200.computercraft.shared.network.client.SpeakerPlayClientMessage;
import net.minecraft.network.play.server.SPlaySoundPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.state.properties.NoteBlockInstrument;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.ResourceLocationException;
import net.minecraft.util.SoundCategory;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.vector.Vector3d;
import net.minecraft.world.World;
import javax.annotation.Nonnull;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
@ -32,20 +38,44 @@ import static dan200.computercraft.api.lua.LuaValues.checkFinite;
*/
public abstract class SpeakerPeripheral implements IPeripheral
{
private static final int MIN_TICKS_BETWEEN_SOUNDS = 1;
private long clock = 0;
private long lastPlayTime = 0;
private final AtomicInteger notesThisTick = new AtomicInteger();
private long lastPositionTime;
private Vector3d lastPosition;
public void update()
{
clock++;
notesThisTick.set( 0 );
// Push position updates to any speakers which have ever played a note,
// have moved by a non-trivial amount and haven't had a position update
// in the last second.
if( lastPlayTime > 0 && (clock - lastPositionTime) >= 20 )
{
Vector3d position = getPosition();
if( lastPosition == null || lastPosition.distanceToSqr( position ) >= 0.1 )
{
lastPosition = position;
lastPositionTime = clock;
NetworkHandler.sendToAllTracking(
new SpeakerMoveClientMessage( getSource(), position ),
getWorld().getChunkAt( new BlockPos( position ) )
);
}
}
}
public abstract World getWorld();
public abstract Vector3d getPosition();
protected abstract UUID getSource();
public boolean madeSound( long ticks )
{
return clock - lastPlayTime <= ticks;
@ -135,26 +165,37 @@ public abstract class SpeakerPeripheral implements IPeripheral
private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException
{
if( clock - lastPlayTime < TileSpeaker.MIN_TICKS_BETWEEN_SOUNDS &&
(!isNote || clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick) )
if( clock - lastPlayTime < MIN_TICKS_BETWEEN_SOUNDS )
{
// Rate limiting occurs when we've already played a sound within the last tick, or we've
// played more notes than allowable within the current tick.
return false;
// Rate limiting occurs when we've already played a sound within the last tick.
if( !isNote ) return false;
// Or we've played more notes than allowable within the current tick.
if( clock - lastPlayTime != 0 || notesThisTick.get() >= ComputerCraft.maxNotesPerTick ) return false;
}
World world = getWorld();
Vector3d pos = getPosition();
float range = MathHelper.clamp( volume, 1.0f, 3.0f ) * 16;
context.issueMainThreadTask( () -> {
MinecraftServer server = world.getServer();
if( server == null ) return null;
float adjVolume = Math.min( volume, 3.0f );
server.getPlayerList().broadcast(
null, pos.x, pos.y, pos.z, adjVolume > 1.0f ? 16 * adjVolume : 16.0, world.dimension(),
new SPlaySoundPacket( name, SoundCategory.RECORDS, pos, adjVolume, pitch )
);
if( isNote )
{
server.getPlayerList().broadcast(
null, pos.x, pos.y, pos.z, range, world.dimension(),
new SPlaySoundPacket( name, SoundCategory.RECORDS, pos, range, pitch )
);
}
else
{
NetworkHandler.sendToAllAround(
new SpeakerPlayClientMessage( getSource(), pos, name, range, pitch ),
world, pos, range
);
}
return null;
} );

View File

@ -7,6 +7,8 @@ package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.common.TileGeneric;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import dan200.computercraft.shared.util.CapabilityUtil;
import net.minecraft.tileentity.ITickableTileEntity;
import net.minecraft.tileentity.TileEntityType;
@ -19,15 +21,15 @@ import net.minecraftforge.common.util.LazyOptional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.UUID;
import static dan200.computercraft.shared.Capabilities.CAPABILITY_PERIPHERAL;
public class TileSpeaker extends TileGeneric implements ITickableTileEntity
{
public static final int MIN_TICKS_BETWEEN_SOUNDS = 1;
private final SpeakerPeripheral peripheral;
private LazyOptional<IPeripheral> peripheralCap;
private final UUID source = UUID.randomUUID();
public TileSpeaker( TileEntityType<TileSpeaker> type )
{
@ -41,6 +43,13 @@ public class TileSpeaker extends TileGeneric implements ITickableTileEntity
peripheral.update();
}
@Override
public void setRemoved()
{
super.setRemoved();
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
}
@Nonnull
@Override
public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> cap, @Nullable Direction side )
@ -83,6 +92,12 @@ public class TileSpeaker extends TileGeneric implements ITickableTileEntity
return new Vector3d( pos.getX(), pos.getY(), pos.getZ() );
}
@Override
protected UUID getSource()
{
return speaker.source;
}
@Override
public boolean equals( @Nullable IPeripheral other )
{

View File

@ -0,0 +1,33 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.shared.peripheral.speaker;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.shared.network.NetworkHandler;
import dan200.computercraft.shared.network.client.SpeakerStopClientMessage;
import javax.annotation.Nonnull;
import java.util.UUID;
/**
* A speaker peripheral which is used on an upgrade, and so is only attached to one computer.
*/
public abstract class UpgradeSpeakerPeripheral extends SpeakerPeripheral
{
private final UUID source = UUID.randomUUID();
@Override
protected final UUID getSource()
{
return source;
}
@Override
public void detach( @Nonnull IComputerAccess computer )
{
NetworkHandler.sendToAllPlayers( new SpeakerStopClientMessage( source ) );
}
}

View File

@ -46,7 +46,7 @@ public final class ContainerPocketComputer extends ContainerComputerBase
public Factory( ServerComputer computer, ItemStack stack, ItemPocketComputer item, Hand hand )
{
this.computer = computer;
this.name = stack.getHoverName();
name = stack.getHoverName();
this.item = item;
this.hand = hand;
}

View File

@ -6,11 +6,11 @@
package dan200.computercraft.shared.pocket.peripherals;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral;
import net.minecraft.util.math.vector.Vector3d;
import net.minecraft.world.World;
public class PocketSpeakerPeripheral extends SpeakerPeripheral
public class PocketSpeakerPeripheral extends UpgradeSpeakerPeripheral
{
private World world = null;
private Vector3d position = Vector3d.ZERO;

View File

@ -715,7 +715,6 @@ public class TurtleAPI implements ILuaAPI
* more information about the item at the cost of taking longer to run.
* @return The command result.
* @throws LuaException If the slot is out of range.
* @see InventoryMethods#getItemDetail Describes the information returned by a detailed query.
* @cc.treturn nil|table Information about the given slot, or {@code nil} if it is empty.
* @cc.usage Print the current slot, assuming it contains 13 dirt.
*
@ -726,6 +725,7 @@ public class TurtleAPI implements ILuaAPI
* -- count = 13,
* -- }
* }</pre>
* @see InventoryMethods#getItemDetail Describes the information returned by a detailed query.
*/
@LuaFunction
public final MethodResult getItemDetail( ILuaContext context, Optional<Integer> slot, Optional<Boolean> detailed ) throws LuaException

View File

@ -503,20 +503,15 @@ public class TurtleBrain implements ITurtleAccess
setFuelLevel( getFuelLevel() + addition );
}
private int issueCommand( ITurtleCommand command )
{
commandQueue.offer( new TurtleCommandQueueEntry( ++commandsIssued, command ) );
return commandsIssued;
}
@Nonnull
@Override
public MethodResult executeCommand( @Nonnull ITurtleCommand command )
{
if( getWorld().isClientSide ) throw new UnsupportedOperationException( "Cannot run commands on the client" );
if( commandQueue.size() > 16 ) return MethodResult.of( false, "Too many ongoing turtle commands" );
// Issue command
int commandID = issueCommand( command );
commandQueue.offer( new TurtleCommandQueueEntry( ++commandsIssued, command ) );
int commandID = commandsIssued;
return new CommandCallback( commandID ).pull;
}

View File

@ -5,6 +5,7 @@
*/
package dan200.computercraft.shared.turtle.inventory;
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.shared.Registry;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.IComputer;
@ -27,8 +28,10 @@ import java.util.function.Predicate;
public class ContainerTurtle extends ContainerComputerBase
{
public static final int BORDER = 8;
public static final int PLAYER_START_Y = 134;
public static final int TURTLE_START_X = 175;
public static final int TURTLE_START_X = ComputerSidebar.WIDTH + 175;
public static final int PLAYER_START_X = ComputerSidebar.WIDTH + BORDER;
private final IIntArray properties;
@ -56,14 +59,14 @@ public class ContainerTurtle extends ContainerComputerBase
{
for( int x = 0; x < 9; x++ )
{
addSlot( new Slot( playerInventory, x + y * 9 + 9, 8 + x * 18, PLAYER_START_Y + 1 + y * 18 ) );
addSlot( new Slot( playerInventory, x + y * 9 + 9, PLAYER_START_X + x * 18, PLAYER_START_Y + 1 + y * 18 ) );
}
}
// Player hotbar
for( int x = 0; x < 9; x++ )
{
addSlot( new Slot( playerInventory, x, 8 + x * 18, PLAYER_START_Y + 3 * 18 + 5 ) );
addSlot( new Slot( playerInventory, x, PLAYER_START_X + x * 18, PLAYER_START_Y + 3 * 18 + 5 ) );
}
}

View File

@ -12,7 +12,7 @@ import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeType;
import dan200.computercraft.shared.Registry;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
import dan200.computercraft.shared.peripheral.speaker.UpgradeSpeakerPeripheral;
import net.minecraft.client.renderer.model.ModelResourceLocation;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
@ -28,7 +28,7 @@ public class TurtleSpeaker extends AbstractTurtleUpgrade
private static final ModelResourceLocation leftModel = new ModelResourceLocation( "computercraft:turtle_speaker_upgrade_left", "inventory" );
private static final ModelResourceLocation rightModel = new ModelResourceLocation( "computercraft:turtle_speaker_upgrade_right", "inventory" );
private static class Peripheral extends SpeakerPeripheral
private static class Peripheral extends UpgradeSpeakerPeripheral
{
ITurtleAccess turtle;

View File

@ -65,7 +65,7 @@ public class TurtleTool extends AbstractTurtleUpgrade
public TurtleTool( ResourceLocation id, ItemStack craftItem, ItemStack toolItem )
{
super( id, TurtleUpgradeType.TOOL, craftItem );
this.item = toolItem;
item = toolItem;
}
@Override

View File

@ -318,7 +318,7 @@ public class FakeNetHandler extends ServerPlayNetHandler
@Override
public void disconnect( @Nonnull ITextComponent message )
{
this.closeReason = message;
closeReason = message;
}
@Nonnull

View File

@ -110,5 +110,11 @@
"tracking_field.computercraft.coroutines_dead.name": "Coroutines disposed",
"gui.computercraft.tooltip.copy": "Copy to clipboard",
"gui.computercraft.tooltip.computer_id": "Computer ID: %s",
"gui.computercraft.tooltip.disk_id": "Disk ID: %s"
"gui.computercraft.tooltip.disk_id": "Disk ID: %s",
"gui.computercraft.tooltip.turn_on": "Turn this computer on",
"gui.computercraft.tooltip.turn_on.key": "Hold Ctrl+R",
"gui.computercraft.tooltip.turn_off": "Turn this computer off",
"gui.computercraft.tooltip.turn_off.key": "Hold Ctrl+S",
"gui.computercraft.tooltip.terminate": "Stop the currently running code",
"gui.computercraft.tooltip.terminate.key": "Hold Ctrl+T"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 B

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 293 B

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 399 B

View File

@ -27,21 +27,24 @@ function setPath(_sPath)
sPath = _sPath
end
local extensions = { "", ".md", ".txt" }
--- Returns the location of the help file for the given topic.
--
-- @tparam string topic The topic to find
-- @treturn string|nil The path to the given topic's help file, or `nil` if it
-- cannot be found.
-- @usage help.lookup("disk")
function lookup(_sTopic)
expect(1, _sTopic, "string")
function lookup(topic)
expect(1, topic, "string")
-- Look on the path variable
for sPath in string.gmatch(sPath, "[^:]+") do
sPath = fs.combine(sPath, _sTopic)
if fs.exists(sPath) and not fs.isDir(sPath) then
return sPath
elseif fs.exists(sPath .. ".txt") and not fs.isDir(sPath .. ".txt") then
return sPath .. ".txt"
for path in string.gmatch(sPath, "[^:]+") do
path = fs.combine(path, topic)
for _, extension in ipairs(extensions) do
local file = path .. extension
if fs.exists(file) and not fs.isDir(file) then
return file
end
end
end
@ -66,8 +69,11 @@ function topics()
for _, sFile in pairs(tList) do
if string.sub(sFile, 1, 1) ~= "." then
if not fs.isDir(fs.combine(sPath, sFile)) then
if #sFile > 4 and sFile:sub(-4) == ".txt" then
sFile = sFile:sub(1, -5)
for i = 2, #extensions do
local extension = extensions[i]
if #sFile > #extension and sFile:sub(-#extension) == extension then
sFile = sFile:sub(1, -#extension - 1)
end
end
tItems[sFile] = true
end

View File

@ -1,5 +1,5 @@
craft is a program for Crafty Turtles. Craft will craft a stack of items using the current inventory.
ex:
"craft" will craft as many items as possible
"craft all" will craft as many items as possible
"craft 5" will craft at most 5 times

View File

@ -29,8 +29,8 @@ local completion = require "cc.completion"
--- Complete the name of a file relative to the current working directory.
--
-- @tparam table shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @tparam table shell The shell we're completing in.
-- @tparam string text Current text to complete.
-- @treturn { string... } A list of suffixes of matching files.
local function file(shell, text)
return fs.complete(text, shell.dir(), true, false)
@ -38,8 +38,8 @@ end
--- Complete the name of a directory relative to the current working directory.
--
-- @tparam table shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @tparam table shell The shell we're completing in.
-- @tparam string text Current text to complete.
-- @treturn { string... } A list of suffixes of matching directories.
local function dir(shell, text)
return fs.complete(text, shell.dir(), false, true)
@ -48,8 +48,8 @@ end
--- Complete the name of a file or directory relative to the current working
-- directory.
--
-- @tparam table shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @tparam table shell The shell we're completing in.
-- @tparam string text Current text to complete.
-- @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.
@ -74,14 +74,46 @@ end
--- Complete the name of a program.
--
-- @tparam table shell The shell we're completing in
-- @tparam { string... } choices The list of choices to complete from.
-- @tparam table shell The shell we're completing in.
-- @tparam string text Current text to complete.
-- @treturn { string... } A list of suffixes of matching programs.
-- @see shell.completeProgram
local function program(shell, text)
return shell.completeProgram(text)
end
--- Complete arguments of a program.
--
-- @tparam table shell The shell we're completing in.
-- @tparam string text Current text to complete.
-- @tparam { string... } previous The shell arguments before this one.
-- @tparam number starting Which argument index this program and args start at.
-- @treturn { string... } A list of suffixes of matching programs or arguments.
local function programWithArgs(shell, text, previous, starting)
if #previous + 1 == starting then
local tCompletionInfo = shell.getCompletionInfo()
if text:sub(-1) ~= "/" and tCompletionInfo[shell.resolveProgram(text)] then
return { " " }
else
local results = shell.completeProgram(text)
for n = 1, #results do
local sResult = results[n]
if sResult:sub(-1) ~= "/" and tCompletionInfo[shell.resolveProgram(text .. sResult)] then
results[n] = sResult .. " "
end
end
return results
end
else
local program = previous[starting]
local resolved = shell.resolveProgram(program)
if not resolved then return end
local tCompletion = shell.getCompletionInfo()[resolved]
if not tCompletion then return end
return tCompletion.fnComplete(shell, #previous - starting + 1, text, { program, table.unpack(previous, starting + 1, #previous) })
end
end
--[[- A helper function for building shell completion arguments.
This accepts a series of single-argument completion functions, and combines
@ -144,6 +176,7 @@ return {
dir = dir,
dirOrFile = dirOrFile,
program = program,
programWithArgs = programWithArgs,
-- Re-export various other functions
help = wrap(help.completeTopic), --- Wraps @{help.completeTopic} as a @{build} compatible function.

View File

@ -14,16 +14,127 @@ if sTopic == "index" then
end
local strings = require "cc.strings"
local function word_wrap(text, width)
local lines = strings.wrap(text, width)
local function min_of(a, b, default)
if not a and not b then return default end
if not a then return b end
if not b then return a end
return math.min(a, b)
end
--[[- Parse a markdown string, extracting headings and highlighting some basic
constructs.
The implementation of this is horrible. SquidDev shouldn't be allowed to write
parsers, especially ones they think might be "performance critical".
]]
local function parse_markdown(text)
local len = #text
local oob = len + 1
-- Some patterns to match headers and bullets on the start of lines.
-- The `%f[^\n\0]` is some wonderful logic to match the start of a line /or/
-- the start of the document.
local heading = "%f[^\n\0](#+ +)([^\n]*)"
local bullet = "%f[^\n\0]( *)[.*]( +)"
local code = "`([^`]+)`"
local new_text, fg, bg = "", "", ""
local function append(txt, fore, back)
new_text = new_text .. txt
fg = fg .. (fore or "0"):rep(#txt)
bg = bg .. (back or "f"):rep(#txt)
end
local next_header = text:find(heading)
local next_bullet = text:find(bullet)
local next_block = min_of(next_header, next_bullet, oob)
local next_code, next_code_end = text:find(code)
local sections = {}
local start = 1
while start <= len do
if start == next_block then
if start == next_header then
local _, fin, head, content = text:find(heading, start)
sections[#new_text + 1] = content
append(head .. content, "4", "f")
start = fin + 1
next_header = text:find(heading, start)
else
local _, fin, space, content = text:find(bullet, start)
append(space .. "\7" .. content)
start = fin + 1
next_bullet = text:find(bullet, start)
end
next_block = min_of(next_header, next_bullet, oob)
elseif next_code and next_code_end < next_block then
-- Basic inline code blocks
if start < next_code then append(text:sub(start, next_code - 1)) end
local content = text:match(code, next_code)
append(content, "0", "7")
start = next_code_end + 1
next_code, next_code_end = text:find(code, start)
else
-- Normal text
append(text:sub(start, next_block - 1))
start = next_block
-- Rescan for a new code block
if next_code then next_code, next_code_end = text:find(code, start) end
end
end
return new_text, fg, bg, sections
end
local function word_wrap_basic(text, width)
local lines, fg, bg = strings.wrap(text, width), {}, {}
local fg_line, bg_line = ("0"):rep(width), ("f"):rep(width)
-- Normalise the strings suitable for use with blit. We could skip this and
-- just use term.write, but saves us a clearLine call.
for k, line in pairs(lines) do
lines[k] = strings.ensure_width(line, width)
fg[k] = fg_line
bg[k] = bg_line
end
return lines
return lines, fg, bg, {}
end
local function word_wrap_markdown(text, width)
-- Add in styling for Markdown-formatted text.
local text, fg, bg, sections = parse_markdown(text)
local lines = strings.wrap(text, width)
local fglines, bglines, section_list, section_n = {}, {}, {}, 1
-- Normalise the strings suitable for use with blit. We could skip this and
-- just use term.write, but saves us a clearLine call.
local start = 1
for k, line in pairs(lines) do
-- I hate this with a burning passion, but it works!
local pos = text:find(line, start, true)
lines[k], fglines[k], bglines[k] =
strings.ensure_width(line, width),
strings.ensure_width(fg:sub(pos, pos + #line), width),
strings.ensure_width(bg:sub(pos, pos + #line), width)
if sections[pos] then
section_list[section_n], section_n = { content = sections[pos], offset = k - 1 }, section_n + 1
end
start = pos + 1
end
return lines, fglines, bglines, section_list
end
local sFile = help.lookup(sTopic)
@ -33,31 +144,40 @@ if not file then
return
end
local contents = file:read("*a"):gsub("(\n *)[-*]( +)", "%1\7%2")
local contents = file:read("*a")
file:close()
local word_wrap = sFile:sub(-3) == ".md" and word_wrap_markdown or word_wrap_basic
local width, height = term.getSize()
local lines = word_wrap(contents, width)
local lines, fg, bg, sections = word_wrap(contents, width)
local print_height = #lines
-- If we fit within the screen, just display without pagination.
if print_height <= height then
print(contents)
local _, y = term.getCursorPos()
for i = 1, print_height do
if y + i - 1 > height then
term.scroll(1)
term.setCursorPos(1, height)
else
term.setCursorPos(1, y + i - 1)
end
term.blit(lines[i], fg[i], bg[i])
end
return
end
local current_section = nil
local offset = 0
local function draw()
local fg, bg = ("0"):rep(width), ("f"):rep(width)
for y = 1, height - 1 do
term.setCursorPos(1, y)
if y + offset > print_height then
-- Should only happen if we resize the terminal to a larger one
-- than actually needed for the current text.
term.clearLine()
else
term.blit(lines[y + offset], fg, bg)
--- Find the currently visible seciton, or nil if this document has no sections.
--
-- This could potentially be a binary search, but right now it's not worth it.
local function find_section()
for i = #sections, 1, -1 do
if sections[i].offset <= offset then
return i
end
end
end
@ -68,7 +188,10 @@ local function draw_menu()
term.clearLine()
local tag = "Help: " .. sTopic
term.write("Help: " .. sTopic)
if current_section then
tag = tag .. (" (%s)"):format(sections[current_section].content)
end
term.write(tag)
if width >= #tag + 16 then
term.setCursorPos(width - 14, height)
@ -76,11 +199,31 @@ local function draw_menu()
end
end
local function draw()
for y = 1, height - 1 do
term.setCursorPos(1, y)
if y + offset > print_height then
-- Should only happen if we resize the terminal to a larger one
-- than actually needed for the current text.
term.clearLine()
else
term.blit(lines[y + offset], fg[y + offset], bg[y + offset])
end
end
local new_section = find_section()
if new_section ~= current_section then
current_section = new_section
draw_menu()
end
end
draw()
draw_menu()
while true do
local event, param = os.pullEvent()
local event, param = os.pullEventRaw()
if event == "key" then
if param == keys.up and offset > 0 then
offset = offset - 1
@ -97,6 +240,12 @@ while true do
elseif param == keys.home then
offset = 0
draw()
elseif param == keys.left and current_section and current_section > 1 then
offset = sections[current_section - 1].offset
draw()
elseif param == keys.right and current_section and current_section < #sections then
offset = sections[current_section + 1].offset
draw()
elseif param == keys["end"] then
offset = print_height - height
draw()
@ -124,6 +273,8 @@ while true do
offset = math.max(math.min(offset, print_height - height), 0)
draw()
draw_menu()
elseif event == "terminate" then
break
end
end

View File

@ -51,7 +51,7 @@ local function get(sUrl)
local sResponse = response.readAll()
response.close()
return sResponse
return sResponse or ""
end
if run then
@ -79,7 +79,12 @@ else
local res = get(url)
if not res then return end
local file = fs.open(sPath, "wb")
local file, err = fs.open(sPath, "wb")
if not file then
printError("Cannot save file: " .. err)
return
end
file.write(res)
file.close()

View File

@ -7,7 +7,7 @@ local function printUsage()
end
local tArgs = { ... }
if #tArgs < 2 then
if #tArgs < 2 or tArgs[1] == "scale" and #tArgs < 3 then
printUsage()
return
end
@ -21,7 +21,7 @@ if tArgs[1] == "scale" then
local nRes = tonumber(tArgs[3])
if nRes == nil or nRes < 0.5 or nRes > 5 then
print("Invalid scale: " .. nRes)
print("Invalid scale: " .. tArgs[3])
return
end

View File

@ -9,20 +9,19 @@ if not turtle.craft then
end
local tArgs = { ... }
local nLimit = nil
if #tArgs < 1 then
local nLimit = tonumber(tArgs[1])
if not nLimit and tArgs[1] ~= "all" then
local programName = arg[0] or fs.getName(shell.getRunningProgram())
print("Usage: " .. programName .. " [number]")
print("Usage: " .. programName .. " all|<number>")
return
else
nLimit = tonumber(tArgs[1])
end
local nCrafted = 0
local nOldCount = turtle.getItemCount(turtle.getSelectedSlot())
if turtle.craft(nLimit) then
local nNewCount = turtle.getItemCount(turtle.getSelectedSlot())
if nOldCount <= nLimit then
if not nLimit or nOldCount <= nLimit then
nCrafted = nNewCount
else
nCrafted = nOldCount - nNewCount

View File

@ -81,9 +81,17 @@ shell.setCompletionFunction("rom/programs/monitor.lua", completion.build(
if previous[2] == "scale" then
return completion.peripheral(shell, text, previous, true)
else
return completion.program(shell, text, previous)
return completion.programWithArgs(shell, text, previous, 3)
end
end
end,
{
function(shell, text, previous)
if previous[2] ~= "scale" then
return completion.programWithArgs(shell, text, previous, 3)
end
end,
many = true,
}
))
shell.setCompletionFunction("rom/programs/move.lua", completion.build(
@ -98,11 +106,11 @@ 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/shell.lua", completion.build({ completion.programWithArgs, 2, many = true }))
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/advanced/bg.lua", completion.build({ completion.programWithArgs, 2, many = true }))
shell.setCompletionFunction("rom/programs/advanced/fg.lua", completion.build({ completion.programWithArgs, 2, many = true }))
shell.setCompletionFunction("rom/programs/fun/dj.lua", completion.build(
{ completion.choice, { "play", "play ", "stop " } },
completion.peripheral

View File

@ -32,7 +32,7 @@ public class AddressRuleTest
@ParameterizedTest
@ValueSource( strings = {
"0.0.0.0", "[::]",
"localhost", "lvh.me", "127.0.0.1", "[::1]",
"localhost", "127.0.0.1.nip.io", "127.0.0.1", "[::1]",
"172.17.0.1", "192.168.1.114", "[0:0:0:0:0:ffff:c0a8:172]", "10.0.0.1"
} )
public void blocksLocalDomains( String domain )

View File

@ -18,5 +18,10 @@ describe("The help library", function()
help.completeTopic("")
expect.error(help.completeTopic, nil):eq("bad argument #1 (expected string, got nil)")
end)
it("completes topics without extensions", function()
expect(help.completeTopic("changel")):same { "og" }
expect(help.completeTopic("turt")):same { "le" }
end)
end)
end)

View File

@ -17,6 +17,30 @@ describe("cc.shell.completion", function()
end)
end)
describe("program", function()
it("completes programs", function()
expect(c.program(shell, "rom/")):same {
"apis/", "autorun/", "help/", "modules/", "motd.txt", "programs/", "startup.lua",
}
end)
end)
describe("programWithArgs", function()
it("completes program name", function()
shell.setCompletionFunction("rom/motd.txt", function() end)
expect(c.programWithArgs(shell, "rom/", { "rom/programs/shell.lua" }, 2)):same {
"apis/", "autorun/", "help/", "modules/", "motd.txt ", "programs/", "startup.lua",
}
end)
it("completes program arguments", function()
expect(c.programWithArgs(shell, "", { "rom/programs/shell.lua", "pastebin" }, 2)):same {
"put ", "get ", "run ",
}
end)
end)
describe("build", function()
it("completes multiple arguments", function()
local spec = c.build(

View File

@ -1,7 +1,8 @@
local capture = require "test_helpers".capture_program
describe("The wget program", function()
local function setup_request()
local default_contents = [[print("Hello", ...)]]
local function setup_request(contents)
stub(_G, "http", {
checkURL = function()
return true
@ -9,7 +10,7 @@ describe("The wget program", function()
get = function()
return {
readAll = function()
return [[print("Hello", ...)]]
return contents
end,
close = function()
end,
@ -19,28 +20,52 @@ describe("The wget program", function()
end
it("downloads one file", function()
setup_request()
fs.delete("/example.com")
setup_request(default_contents)
capture(stub, "wget", "https://example.com")
expect(fs.exists("/example.com")):eq(true)
end)
it("downloads one file with given filename", function()
setup_request()
fs.delete("/test-files/download")
setup_request(default_contents)
capture(stub, "wget", "https://example.com /test-files/download")
expect(fs.exists("/test-files/download")):eq(true)
end)
it("downloads empty files", function()
fs.delete("/test-files/download")
setup_request(nil)
capture(stub, "wget", "https://example.com", "/test-files/download")
expect(fs.exists("/test-files/download")):eq(true)
expect(fs.getSize("/test-files/download")):eq(0)
end)
it("cannot save to rom", function()
setup_request(default_contents)
expect(capture(stub, "wget", "https://example.com", "/rom/a-file.txt")):matches {
ok = true,
output = "Connecting to https://example.com... Success.\n",
error = "Cannot save file: /rom/a-file.txt: Access denied\n",
}
end)
it("runs a program from the internet", function()
setup_request()
setup_request(default_contents)
expect(capture(stub, "wget", "run", "http://test.com", "a", "b", "c"))
:matches { ok = true, output = "Connecting to http://test.com... Success.\nHello a b c\n", error = "" }
end)
it("displays its usage when given no arguments", function()
setup_request()
setup_request(default_contents)
expect(capture(stub, "wget"))
:matches { ok = true, output = "Usage:\nwget <url> [filename]\nwget run <url>\n", error = "" }

View File

@ -21,4 +21,24 @@ describe("The monitor program", function()
:matches { ok = true, output = "", error = "" }
expect(r):equals(0.5)
end)
it("displays correct error messages", function()
local r = 1
stub(peripheral, "call", function(s, f, t) r = t end)
stub(peripheral, "getType", function(side) return side == "left" and "monitor" or nil end)
expect(capture(stub, "monitor", "scale", "left"))
:matches {
ok = true,
output =
"Usage:\n" ..
" monitor <name> <program> <arguments>\n" ..
" monitor scale <name> <scale>\n",
error = "",
}
expect(capture(stub, "monitor", "scale", "top", "0.5"))
:matches { ok = true, output = "No monitor named top\n", error = "" }
expect(capture(stub, "monitor", "scale", "left", "aaa"))
:matches { ok = true, output = "Invalid scale: aaa\n", error = "" }
expect(r):equals(1)
end)
end)

View File

@ -19,7 +19,14 @@ describe("The craft program", function()
stub(_G, "turtle", { craft = function() end })
expect(capture(stub, "/rom/programs/turtle/craft.lua"))
:matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua [number]\n", error = "" }
:matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua all|<number>\n", error = "" }
end)
it("displays its usage when given incorrect arguments", function()
stub(_G, "turtle", { craft = function() end })
expect(capture(stub, "/rom/programs/turtle/craft.lua a"))
:matches { ok = true, output = "Usage: /rom/programs/turtle/craft.lua all|<number>\n", error = "" }
end)
it("crafts multiple items", function()
@ -52,7 +59,7 @@ describe("The craft program", function()
:matches { ok = true, output = "1 item crafted\n", error = "" }
end)
it("crafts no items", function()
it("crafts no items", function()
local item_count = 2
stub(_G, "turtle", {
craft = function()
@ -66,4 +73,17 @@ describe("The craft program", function()
expect(capture(stub, "/rom/programs/turtle/craft.lua 1"))
:matches { ok = true, output = "No items crafted\n", error = "" }
end)
it("crafts all items", function()
stub(_G, "turtle", {
craft = function()
return true
end,
getItemCount = function() return 17 end,
getSelectedSlot = function() return 1 end,
})
expect(capture(stub, "/rom/programs/turtle/craft.lua all"))
:matches { ok = true, output = "17 items crafted\n", error = "" }
end)
end)