Merge branch 'mc-1.15.x' into mc-1.16.x
							
								
								
									
										22
									
								
								.gitpod.yml
									
									
									
									
									
										Normal 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
 | 
			
		||||
@@ -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: [](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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						@@ -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)"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						@@ -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
 | 
			
		||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 )
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ public class ClientHooks
 | 
			
		||||
        if( event.getWorld().isClientSide() )
 | 
			
		||||
        {
 | 
			
		||||
            ClientMonitor.destroyAll();
 | 
			
		||||
            SoundManager.reset();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										84
									
								
								src/main/java/dan200/computercraft/client/SoundManager.java
									
									
									
									
									
										Normal 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()
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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 };
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;" );
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								src/main/java/dan200/computercraft/core/lua/LuaContext.java
									
									
									
									
									
										Normal 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" );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 )
 | 
			
		||||
 
 | 
			
		||||
@@ -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 );
 | 
			
		||||
 
 | 
			
		||||
@@ -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.*;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 )
 | 
			
		||||
 
 | 
			
		||||
@@ -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 );
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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 )
 | 
			
		||||
 
 | 
			
		||||
@@ -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 );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ public class TileCable extends TileGeneric
 | 
			
		||||
        @Override
 | 
			
		||||
        public World getWorld()
 | 
			
		||||
        {
 | 
			
		||||
            return TileCable.this.getLevel();
 | 
			
		||||
            return getLevel();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Nonnull
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
        } );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 )
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ) );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ) );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -318,7 +318,7 @@ public class FakeNetHandler extends ServerPlayNetHandler
 | 
			
		||||
        @Override
 | 
			
		||||
        public void disconnect( @Nonnull ITextComponent message )
 | 
			
		||||
        {
 | 
			
		||||
            this.closeReason = message;
 | 
			
		||||
            closeReason = message;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Nonnull
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/main/resources/assets/computercraft/textures/gui/buttons.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 303 B  | 
| 
		 Before Width: | Height: | Size: 352 B After Width: | Height: | Size: 405 B  | 
| 
		 Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 345 B  | 
| 
		 Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 399 B  | 
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 )
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = "" }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||