mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-11-03 23:22:59 +00:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			v1.4.7-1.1
			...
			mc-1.4.7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					fc66d15012 | ||
| 
						 | 
					00e57227dc | ||
| 
						 | 
					22c094192b | ||
| 
						 | 
					fbf64a0404 | ||
| 
						 | 
					9f251d7b52 | ||
| 
						 | 
					58d54e2e70 | ||
| 
						 | 
					e62f2630b5 | ||
| 
						 | 
					136fbd2589 | ||
| 
						 | 
					35e227ed02 | ||
| 
						 | 
					687a29de95 | ||
| 
						 | 
					5426d880f0 | 
@@ -6,7 +6,7 @@
 | 
			
		||||
# See https://pre-commit.com/hooks.html for more hooks
 | 
			
		||||
repos:
 | 
			
		||||
- repo: https://github.com/pre-commit/pre-commit-hooks
 | 
			
		||||
  rev: v4.0.1
 | 
			
		||||
  rev: v5.0.0
 | 
			
		||||
  hooks:
 | 
			
		||||
  - id: trailing-whitespace
 | 
			
		||||
  - id: end-of-file-fixer
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								LICENSES/MIT.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								LICENSES/MIT.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
MIT License
 | 
			
		||||
 | 
			
		||||
Copyright (c) <year> <copyright holders>
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 | 
			
		||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
 | 
			
		||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
 | 
			
		||||
following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all copies or substantial
 | 
			
		||||
portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 | 
			
		||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
 | 
			
		||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 | 
			
		||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 | 
			
		||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
plugins {
 | 
			
		||||
	application
 | 
			
		||||
	alias(libs.plugins.kotlin)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
java {
 | 
			
		||||
	toolchain {
 | 
			
		||||
		languageVersion.set(JavaLanguageVersion.of(17))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
	mavenCentral()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
	implementation(libs.bundles.asm)
 | 
			
		||||
	implementation(libs.bundles.kotlin)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.jar {
 | 
			
		||||
	manifest.attributes("Main-Class" to "cc.tweaked.build.MainKt")
 | 
			
		||||
}
 | 
			
		||||
@@ -1,61 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package cc.tweaked.build
 | 
			
		||||
 | 
			
		||||
import org.objectweb.asm.ClassReader
 | 
			
		||||
import org.objectweb.asm.ClassVisitor
 | 
			
		||||
import org.objectweb.asm.ClassWriter
 | 
			
		||||
import org.objectweb.asm.util.CheckClassAdapter
 | 
			
		||||
import java.nio.file.Files
 | 
			
		||||
import java.nio.file.Path
 | 
			
		||||
 | 
			
		||||
/** Generate additional classes which don't exist in the original source set. */
 | 
			
		||||
interface ClassEmitter {
 | 
			
		||||
    /** Emit a class if it does not already exist. */
 | 
			
		||||
    fun generate(name: String, classReader: ClassReader? = null, flags: Int = 0, write: (ClassVisitor) -> Unit)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** An implementation of [ClassEmitter] which writes files to a directory. */
 | 
			
		||||
class FileClassEmitter(private val outputDir: Path) : ClassEmitter {
 | 
			
		||||
    private val emitted = mutableSetOf<String>()
 | 
			
		||||
    override fun generate(name: String, classReader: ClassReader?, flags: Int, write: (ClassVisitor) -> Unit) {
 | 
			
		||||
        if (!emitted.add(name)) return
 | 
			
		||||
 | 
			
		||||
        val cw = NonLoadingClassWriter(classReader, flags)
 | 
			
		||||
        write(CheckClassAdapter(cw))
 | 
			
		||||
 | 
			
		||||
        val outputFile = outputDir.resolve("$name.class")
 | 
			
		||||
        Files.createDirectories(outputFile.parent)
 | 
			
		||||
        Files.write(outputFile, cw.toByteArray())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** A unordered pair, such that (x, y) = (y, x) */
 | 
			
		||||
private class UnorderedPair<T>(private val x: T, private val y: T) {
 | 
			
		||||
    override fun equals(other: Any?): Boolean {
 | 
			
		||||
        if (this === other) return true
 | 
			
		||||
        if (other !is cc.tweaked.build.UnorderedPair<*>) return false
 | 
			
		||||
        return (x == other.x && y == other.y) || (x == other.y && y == other.x)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun hashCode(): Int = x.hashCode() xor y.hashCode()
 | 
			
		||||
    override fun toString(): String = "UnorderedPair($x, $y)"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val subclassRelations = mapOf<UnorderedPair<String>, String>(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
/** A [ClassWriter] extension which avoids loading classes when computing frames. */
 | 
			
		||||
private class NonLoadingClassWriter(reader: ClassReader?, flags: Int) : ClassWriter(reader, flags) {
 | 
			
		||||
    override fun getCommonSuperClass(type1: String, type2: String): String {
 | 
			
		||||
        if (type1 == "java/lang/Object" || type2 == "java/lang/Object") return "java/lang/Object"
 | 
			
		||||
 | 
			
		||||
        val subclass = subclassRelations[UnorderedPair(type1, type2)]
 | 
			
		||||
        if (subclass != null) return subclass
 | 
			
		||||
 | 
			
		||||
        println("[WARN] Guessing the super-class of $type1 and $type2.")
 | 
			
		||||
        return "java/lang/Object"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package cc.tweaked.build
 | 
			
		||||
 | 
			
		||||
import org.objectweb.asm.ClassReader
 | 
			
		||||
import java.nio.file.Files
 | 
			
		||||
import java.nio.file.Paths
 | 
			
		||||
import kotlin.io.path.extension
 | 
			
		||||
import kotlin.system.exitProcess
 | 
			
		||||
 | 
			
		||||
fun main(args: Array<String>) {
 | 
			
		||||
    if (args.size != 2) {
 | 
			
		||||
        System.err.println("Expected: INPUT OUTPUT")
 | 
			
		||||
        exitProcess(1)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val inputDir = Paths.get(args[0])
 | 
			
		||||
    val outputDir = Paths.get(args[1])
 | 
			
		||||
 | 
			
		||||
    val emitter = FileClassEmitter(outputDir)
 | 
			
		||||
    Files.find(inputDir, Int.MAX_VALUE, { path, _ -> path.extension == "class" }).use { files ->
 | 
			
		||||
        files.forEach { inputFile ->
 | 
			
		||||
            val reader = Files.newInputStream(inputFile).use { ClassReader(it) }
 | 
			
		||||
            emitter.generate(reader.className, flags = 0) { cw -> reader.accept(Unlambda(emitter, cw), 0) }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,210 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package cc.tweaked.build
 | 
			
		||||
 | 
			
		||||
import org.objectweb.asm.*
 | 
			
		||||
import org.objectweb.asm.Opcodes.*
 | 
			
		||||
 | 
			
		||||
class Unlambda(private val emitter: ClassEmitter, visitor: ClassVisitor) :
 | 
			
		||||
    ClassVisitor(ASM9, visitor) {
 | 
			
		||||
    internal lateinit var className: String
 | 
			
		||||
    private var isInterface: Boolean = false
 | 
			
		||||
 | 
			
		||||
    private var lambda = 0
 | 
			
		||||
 | 
			
		||||
    override fun visit(version: Int, access: Int, name: String, signature: String?, superName: String, interfaces: Array<out String>?) {
 | 
			
		||||
        super.visit(V1_6, access, name, signature, superName, interfaces)
 | 
			
		||||
 | 
			
		||||
        if (version != V1_8) throw IllegalStateException("Expected Java version 8")
 | 
			
		||||
        className = name
 | 
			
		||||
        isInterface = (access and ACC_INTERFACE) != 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>?): MethodVisitor? {
 | 
			
		||||
        val access = if (access.and(ACC_STATIC) != 0) access.and(ACC_PRIVATE.inv()) else access
 | 
			
		||||
        val mw = super.visitMethod(access, name, descriptor, signature, exceptions) ?: return null
 | 
			
		||||
 | 
			
		||||
        if (isInterface && name != "<clinit>") {
 | 
			
		||||
            if ((access and ACC_STATIC) != 0) println("[WARN] $className.$name is a static method")
 | 
			
		||||
            else if ((access and ACC_ABSTRACT) == 0) println("[WARN] $className.$name is a default method")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return UnlambdaMethodVisitor(this, emitter, mw)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal fun nextLambdaName(): String {
 | 
			
		||||
        val name = "lambda$lambda"
 | 
			
		||||
        lambda++
 | 
			
		||||
        return name
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal class UnlambdaMethodVisitor(
 | 
			
		||||
    private val parent: Unlambda,
 | 
			
		||||
    private val emitter: ClassEmitter,
 | 
			
		||||
    methodVisitor: MethodVisitor,
 | 
			
		||||
) : MethodVisitor(ASM9, methodVisitor) {
 | 
			
		||||
    private class Bridge(val lambda: Handle, val bridgeName: String)
 | 
			
		||||
 | 
			
		||||
    private val bridgeMethods = mutableListOf<Bridge>()
 | 
			
		||||
 | 
			
		||||
    override fun visitMethodInsn(opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean) {
 | 
			
		||||
        if (opcode == INVOKESTATIC && isInterface) println("[WARN] Invoke interface $owner.$name in ${parent.className}")
 | 
			
		||||
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun visitInvokeDynamicInsn(name: String, descriptor: String, handle: Handle, vararg arguments: Any) {
 | 
			
		||||
        if (handle.owner == "java/lang/invoke/LambdaMetafactory" && handle.name == "metafactory" && handle.desc == "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;") {
 | 
			
		||||
            visitLambda(name, descriptor, arguments[0] as Type, arguments[1] as Handle)
 | 
			
		||||
        } else {
 | 
			
		||||
            super.visitInvokeDynamicInsn(name, descriptor, handle, *arguments)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun visitLambda(name: String, descriptor: String, signature: Type, lambda: Handle) {
 | 
			
		||||
        val interfaceTy = Type.getReturnType(descriptor)
 | 
			
		||||
        val fields = Type.getArgumentTypes(descriptor)
 | 
			
		||||
 | 
			
		||||
        val lambdaName = parent.nextLambdaName()
 | 
			
		||||
        val className = "${parent.className}\$$lambdaName"
 | 
			
		||||
        val bridgeName = "${lambdaName}Bridge"
 | 
			
		||||
 | 
			
		||||
        emitter.generate(className, flags = ClassWriter.COMPUTE_MAXS) { cw ->
 | 
			
		||||
            cw.visit(V1_6, ACC_FINAL, className, null, "java/lang/Object", arrayOf(interfaceTy.internalName))
 | 
			
		||||
            for ((i, ty) in fields.withIndex()) {
 | 
			
		||||
                cw.visitField(ACC_PRIVATE or ACC_FINAL, "field$i", ty.descriptor, null, null)
 | 
			
		||||
                    .visitEnd()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cw.visitMethod(ACC_STATIC, "create", Type.getMethodDescriptor(interfaceTy, *fields), null, null).let { mw ->
 | 
			
		||||
                mw.visitCode()
 | 
			
		||||
                mw.visitTypeInsn(NEW, className)
 | 
			
		||||
                mw.visitInsn(DUP)
 | 
			
		||||
                for ((i, ty) in fields.withIndex()) mw.visitVarInsn(ty.getOpcode(ILOAD), i)
 | 
			
		||||
                mw.visitMethodInsn(INVOKESPECIAL, className, "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, *fields), false)
 | 
			
		||||
                mw.visitInsn(ARETURN)
 | 
			
		||||
                mw.visitMaxs(0, 0)
 | 
			
		||||
                mw.visitEnd()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cw.visitMethod(0, "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, *fields), null, null).let { mw ->
 | 
			
		||||
                mw.visitCode()
 | 
			
		||||
                mw.visitVarInsn(ALOAD, 0)
 | 
			
		||||
                mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
 | 
			
		||||
                for ((i, ty) in fields.withIndex()) {
 | 
			
		||||
                    mw.visitVarInsn(ALOAD, 0)
 | 
			
		||||
                    mw.visitVarInsn(ty.getOpcode(ILOAD), i + 1)
 | 
			
		||||
                    mw.visitFieldInsn(PUTFIELD, className, "field$i", ty.descriptor)
 | 
			
		||||
                }
 | 
			
		||||
                mw.visitInsn(RETURN)
 | 
			
		||||
                mw.visitMaxs(0, 0)
 | 
			
		||||
                mw.visitEnd()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cw.visitMethod(ACC_PUBLIC, name, signature.descriptor, null, null).let { mw ->
 | 
			
		||||
                mw.visitCode()
 | 
			
		||||
 | 
			
		||||
                val targetArgs = when (lambda.tag) {
 | 
			
		||||
                    H_INVOKEVIRTUAL, H_INVOKESPECIAL -> arrayOf(
 | 
			
		||||
                        Type.getObjectType(lambda.owner),
 | 
			
		||||
                        *Type.getArgumentTypes(lambda.desc),
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    H_INVOKESTATIC, H_NEWINVOKESPECIAL -> Type.getArgumentTypes(lambda.desc)
 | 
			
		||||
                    else -> throw IllegalStateException("Unhandled opcode")
 | 
			
		||||
                }
 | 
			
		||||
                var targetArgOffset = 0
 | 
			
		||||
 | 
			
		||||
                // If we're a ::new method handle, create the object.
 | 
			
		||||
                if (lambda.tag == H_NEWINVOKESPECIAL) {
 | 
			
		||||
                    mw.visitTypeInsn(NEW, lambda.owner)
 | 
			
		||||
                    mw.visitInsn(DUP)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Load our fields
 | 
			
		||||
                for ((i, ty) in fields.withIndex()) {
 | 
			
		||||
                    mw.visitVarInsn(ALOAD, 0)
 | 
			
		||||
                    mw.visitFieldInsn(GETFIELD, className, "field$i", ty.descriptor)
 | 
			
		||||
 | 
			
		||||
                    val expectedTy = targetArgs[targetArgOffset]
 | 
			
		||||
                    if (ty != expectedTy) println("$ty != $expectedTy")
 | 
			
		||||
                    targetArgOffset++
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Load the additional arguments
 | 
			
		||||
                val arguments = signature.argumentTypes
 | 
			
		||||
                for ((i, ty) in arguments.withIndex()) {
 | 
			
		||||
                    mw.visitVarInsn(ty.getOpcode(ILOAD), i + 1)
 | 
			
		||||
                    val expectedTy = targetArgs[targetArgOffset]
 | 
			
		||||
                    if (ty != expectedTy) {
 | 
			
		||||
                        println("[WARN] $ty != $expectedTy, adding a cast")
 | 
			
		||||
                        mw.visitTypeInsn(CHECKCAST, expectedTy.internalName)
 | 
			
		||||
                    }
 | 
			
		||||
                    targetArgOffset++
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Invoke our init call
 | 
			
		||||
                mw.visitMethodInsn(
 | 
			
		||||
                    when (lambda.tag) {
 | 
			
		||||
                        H_INVOKEVIRTUAL, H_INVOKESPECIAL -> INVOKEVIRTUAL
 | 
			
		||||
                        H_INVOKESTATIC -> INVOKESTATIC
 | 
			
		||||
                        H_NEWINVOKESPECIAL -> INVOKESPECIAL
 | 
			
		||||
                        else -> throw IllegalStateException("Unhandled opcode")
 | 
			
		||||
                    },
 | 
			
		||||
                    lambda.owner, if (lambda.tag == H_INVOKESPECIAL) bridgeName else lambda.name, lambda.desc, false,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                if (lambda.tag != H_NEWINVOKESPECIAL) {
 | 
			
		||||
                    val expectedRetTy = signature.returnType
 | 
			
		||||
                    val retTy = Type.getReturnType(lambda.desc)
 | 
			
		||||
                    if (expectedRetTy != retTy) {
 | 
			
		||||
                        // println("[WARN] $retTy != $expectedRetTy, adding a cast")
 | 
			
		||||
                        if (retTy == Type.INT_TYPE && expectedRetTy.descriptor == "Ljava/lang/Object;") {
 | 
			
		||||
                            mw.visitMethodInsn(INVOKESTATIC, "jav/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // println("[ERROR] Unhandled")
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // A little ugly special handling for ::new
 | 
			
		||||
                mw.visitInsn(
 | 
			
		||||
                    if (lambda.tag == H_NEWINVOKESPECIAL) ARETURN else signature.returnType.getOpcode(IRETURN),
 | 
			
		||||
                )
 | 
			
		||||
                mw.visitMaxs(0, 0)
 | 
			
		||||
                mw.visitEnd()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cw.visitEnd()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If we're a ::new method handle, create the object.
 | 
			
		||||
        if (lambda.tag == H_INVOKESPECIAL) {
 | 
			
		||||
            bridgeMethods.add(Bridge(lambda, bridgeName))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        visitMethodInsn(INVOKESTATIC, className, "create", Type.getMethodDescriptor(interfaceTy, *fields), false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun visitEnd() {
 | 
			
		||||
        super.visitEnd()
 | 
			
		||||
 | 
			
		||||
        for (bridge in bridgeMethods) {
 | 
			
		||||
            println("[INFO] Using bridge method ${bridge.bridgeName} for ${bridge.lambda}")
 | 
			
		||||
            val mw = parent.visitMethod(ACC_PUBLIC, bridge.bridgeName, bridge.lambda.desc, null, null) ?: continue
 | 
			
		||||
            mw.visitCode()
 | 
			
		||||
            mw.visitVarInsn(ALOAD, 0)
 | 
			
		||||
            for ((i, ty) in Type.getArgumentTypes(bridge.lambda.desc)
 | 
			
		||||
                .withIndex()) mw.visitVarInsn(ty.getOpcode(ILOAD), i + 1)
 | 
			
		||||
            mw.visitMethodInsn(INVOKESPECIAL, bridge.lambda.owner, bridge.lambda.name, bridge.lambda.desc, false)
 | 
			
		||||
            mw.visitInsn(Type.getReturnType(bridge.lambda.desc).getOpcode(IRETURN))
 | 
			
		||||
            val size = 1 + Type.getArgumentTypes(bridge.lambda.desc).size
 | 
			
		||||
            mw.visitMaxs(size, size)
 | 
			
		||||
            mw.visitEnd()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,11 +4,13 @@
 | 
			
		||||
 | 
			
		||||
import com.diffplug.gradle.spotless.FormatExtension
 | 
			
		||||
import com.diffplug.spotless.LineEnding
 | 
			
		||||
import net.fabricmc.loom.LoomGradleExtension
 | 
			
		||||
import java.nio.charset.StandardCharsets
 | 
			
		||||
 | 
			
		||||
plugins {
 | 
			
		||||
    alias(libs.plugins.voldeloom)
 | 
			
		||||
    alias(libs.plugins.spotless)
 | 
			
		||||
    id("com.gradleup.shadow") version "8.3.5"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val modVersion: String by extra
 | 
			
		||||
@@ -20,13 +22,16 @@ version = modVersion
 | 
			
		||||
base.archivesName.convention("cc-tweaked-$mcVersion")
 | 
			
		||||
 | 
			
		||||
java {
 | 
			
		||||
    // Last version able to set a --release as low as 6
 | 
			
		||||
    toolchain.languageVersion.set(JavaLanguageVersion.of(11))
 | 
			
		||||
    toolchain {
 | 
			
		||||
        languageVersion.set(JavaLanguageVersion.of(17))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    withSourcesJar()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.compileJava { options.release.set(8) }
 | 
			
		||||
val runtimeToolchain = javaToolchains.launcherFor {
 | 
			
		||||
    languageVersion.set(JavaLanguageVersion.of(8))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
    mavenCentral()
 | 
			
		||||
@@ -38,10 +43,6 @@ repositories {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
volde {
 | 
			
		||||
    forgeCapabilities {
 | 
			
		||||
        setSrgsAsFallback(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    runs {
 | 
			
		||||
        named("client") {
 | 
			
		||||
            programArg("SquidDev")
 | 
			
		||||
@@ -50,6 +51,10 @@ volde {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.withType(net.fabricmc.loom.task.RunTask::class.java).configureEach {
 | 
			
		||||
    javaLauncher.set(runtimeToolchain)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
configurations {
 | 
			
		||||
    val shade by registering
 | 
			
		||||
    compileOnly { extendsFrom(shade.get()) }
 | 
			
		||||
@@ -71,14 +76,18 @@ dependencies {
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    compileOnly("com.google.code.findbugs:jsr305:3.0.2")
 | 
			
		||||
    compileOnly("org.jetbrains:annotations:24.0.1")
 | 
			
		||||
    compileOnly(libs.jspecify)
 | 
			
		||||
    modImplementation("maven.modrinth:computercraft:1.50")
 | 
			
		||||
    "shade"("org.squiddev:Cobalt")
 | 
			
		||||
    "shade"("cc.tweaked:cobalt")
 | 
			
		||||
    "shade"(libs.bundles.netty)
 | 
			
		||||
 | 
			
		||||
    "buildTools"(project(":build-tools"))
 | 
			
		||||
    "buildTools"("cc.tweaked.cobalt:build-tools")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Point compileJava to emit to classes/uninstrumentedJava/main, and then add a task to instrument these classes,
 | 
			
		||||
// saving them back to the the original class directory. This is held together with so much string :(.
 | 
			
		||||
// saving them back to the original class directory. This is held together with so much string :(.
 | 
			
		||||
val mainSource = sourceSets.main.get()
 | 
			
		||||
val javaClassesDir = mainSource.java.classesDirectory.get()
 | 
			
		||||
val untransformedClasses = project.layout.buildDirectory.dir("classes/uninstrumentedJava/main")
 | 
			
		||||
@@ -88,12 +97,9 @@ val instrumentJava = tasks.register(mainSource.getTaskName("Instrument", "Java")
 | 
			
		||||
    inputs.dir(untransformedClasses).withPropertyName("inputDir")
 | 
			
		||||
    outputs.dir(javaClassesDir).withPropertyName("outputDir")
 | 
			
		||||
 | 
			
		||||
    javaLauncher.set(
 | 
			
		||||
        javaToolchains.launcherFor {
 | 
			
		||||
            languageVersion.set(JavaLanguageVersion.of(17))
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    mainClass.set("cc.tweaked.build.MainKt")
 | 
			
		||||
    // Run under Java 8, so we can check compatibility of methods.
 | 
			
		||||
    javaLauncher.set(runtimeToolchain)
 | 
			
		||||
    mainClass.set("cc.tweaked.cobalt.build.MainKt")
 | 
			
		||||
    classpath = buildTools
 | 
			
		||||
 | 
			
		||||
    args = listOf(
 | 
			
		||||
@@ -117,7 +123,16 @@ tasks.withType(AbstractArchiveTask::class.java).configureEach {
 | 
			
		||||
    fileMode = Integer.valueOf("664", 8)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.jar {
 | 
			
		||||
// Override remapJarForRelease, and then manually configure all tasks to have the right classifiers.
 | 
			
		||||
tasks.remapJarForRelease {
 | 
			
		||||
    archiveClassifier = ""
 | 
			
		||||
    input = tasks.shadowJar.flatMap { it.archiveFile }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.jar { archiveClassifier = "dev-slim" }
 | 
			
		||||
 | 
			
		||||
tasks.shadowJar {
 | 
			
		||||
    archiveClassifier = "dev"
 | 
			
		||||
    manifest {
 | 
			
		||||
        attributes(
 | 
			
		||||
            "FMLCorePlugin" to "cc.tweaked.patch.CorePlugin",
 | 
			
		||||
@@ -125,10 +140,22 @@ tasks.jar {
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    from(configurations["shade"].map { if (it.isDirectory) it else zipTree(it) })
 | 
			
		||||
    configurations = listOf(project.configurations["shade"])
 | 
			
		||||
    relocate("io.netty", "cc.tweaked.vendor.netty")
 | 
			
		||||
    minimize()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
project.afterEvaluate {
 | 
			
		||||
    // Remove tasks.jar from the runtime classpath and add shadowJar instead.
 | 
			
		||||
    val field = LoomGradleExtension::class.java.getDeclaredField("unmappedModsBuilt")
 | 
			
		||||
    field.isAccessible = true
 | 
			
		||||
    (field.get(volde) as MutableList<*>).clear()
 | 
			
		||||
    volde.addUnmappedMod(tasks.shadowJar.get().archiveFile.get().asFile.toPath())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.processResources {
 | 
			
		||||
    inputs.property("version", project.version)
 | 
			
		||||
    inputs.property("mcVersion", mcVersion)
 | 
			
		||||
    filesMatching("mcmod.info") {
 | 
			
		||||
        expand("version" to project.version, "mcVersion" to mcVersion)
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,6 @@
 | 
			
		||||
 | 
			
		||||
org.gradle.jvmargs=-Xmx3G
 | 
			
		||||
 | 
			
		||||
modVersion=1.105.0
 | 
			
		||||
modVersion=1.115.1
 | 
			
		||||
 | 
			
		||||
mcVersion=1.4.7
 | 
			
		||||
 
 | 
			
		||||
@@ -5,24 +5,18 @@
 | 
			
		||||
[versions]
 | 
			
		||||
forge = "1.4.7-6.6.2.534"
 | 
			
		||||
 | 
			
		||||
asm = "9.3"
 | 
			
		||||
kotlin = "1.8.10"
 | 
			
		||||
jspecify = "1.0.0"
 | 
			
		||||
netty = "4.1.119.Final"
 | 
			
		||||
 | 
			
		||||
[libraries]
 | 
			
		||||
asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
 | 
			
		||||
asm-analysis = { module = "org.ow2.asm:asm-analysis", version.ref = "asm" }
 | 
			
		||||
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
 | 
			
		||||
asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" }
 | 
			
		||||
asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" }
 | 
			
		||||
jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" }
 | 
			
		||||
 | 
			
		||||
kotlin-platform = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
 | 
			
		||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
 | 
			
		||||
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
 | 
			
		||||
netty-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
 | 
			
		||||
 | 
			
		||||
[bundles]
 | 
			
		||||
asm = ["asm", "asm-analysis", "asm-commons", "asm-tree", "asm-util"]
 | 
			
		||||
kotlin = ["kotlin-stdlib"]
 | 
			
		||||
netty = ["netty-codec", "netty-http"]
 | 
			
		||||
 | 
			
		||||
[plugins]
 | 
			
		||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
 | 
			
		||||
spotless = { id = "com.diffplug.spotless", version = "6.19.0" }
 | 
			
		||||
voldeloom = { id = "agency.highlysuspect.voldeloom", version = "2.4-SNAPSHOT" }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
 | 
			
		||||
networkTimeout=10000
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
zipStorePath=wrapper/dists
 | 
			
		||||
 
 | 
			
		||||
@@ -38,5 +38,3 @@ val mcVersion: String by settings
 | 
			
		||||
rootProject.name = "cc-tweaked-$mcVersion"
 | 
			
		||||
 | 
			
		||||
includeBuild("vendor/Cobalt")
 | 
			
		||||
 | 
			
		||||
include("build-tools")
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ public class ClassTransformer implements IClassTransformer {
 | 
			
		||||
            BasicRemapper.builder()
 | 
			
		||||
                .remapType("dan200/computer/core/apis/FSAPI", "dan200/computercraft/core/apis/FSAPI")
 | 
			
		||||
                .remapType("dan200/computer/core/apis/OSAPI", "dan200/computercraft/core/apis/OSAPI")
 | 
			
		||||
                .remapType("dan200/computer/core/apis/HTTPAPI", "dan200/computercraft/core/apis/HTTPAPI")
 | 
			
		||||
                .remapType("dan200/computer/core/apis/TermAPI", "dan200/computercraft/core/apis/TermAPI")
 | 
			
		||||
                .build().toMethodTransform()
 | 
			
		||||
        )
 | 
			
		||||
@@ -38,6 +39,11 @@ public class ClassTransformer implements IClassTransformer {
 | 
			
		||||
            "dan200.computer.shared.TileEntityMonitor",
 | 
			
		||||
            "cc.tweaked.patch.mixins.TileEntityMonitorMixin"
 | 
			
		||||
        ))
 | 
			
		||||
        // And extend the turtle API
 | 
			
		||||
        .atClass("dan200.turtle.shared.TurtleAPI", new ClassMerger(
 | 
			
		||||
            "dan200.turtle.shared.TurtleAPI",
 | 
			
		||||
            "cc.tweaked.patch.mixins.TurtleAPIMixin"
 | 
			
		||||
        ))
 | 
			
		||||
        // Load from our ROM instead of the CC one. We do this by:
 | 
			
		||||
        // 1. Changing the path of the assets folder.
 | 
			
		||||
        .atMethod(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								src/main/java/cc/tweaked/patch/mixins/TurtleAPIMixin.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/main/java/cc/tweaked/patch/mixins/TurtleAPIMixin.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package cc.tweaked.patch.mixins;
 | 
			
		||||
 | 
			
		||||
import cc.tweaked.patch.framework.transform.MergeVisitor;
 | 
			
		||||
import dan200.CCTurtle;
 | 
			
		||||
import dan200.computer.core.ILuaAPI;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.turtle.shared.ITurtle;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Adds additional methods to {@link dan200.turtle.shared.TurtleAPI}.
 | 
			
		||||
 */
 | 
			
		||||
public abstract class TurtleAPIMixin implements ILuaAPI {
 | 
			
		||||
    @MergeVisitor.Shadow
 | 
			
		||||
    private ITurtle m_turtle;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the currently selected slot.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The current slot.
 | 
			
		||||
     * @cc.since 1.6
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final int getSelectedSlot() {
 | 
			
		||||
        return m_turtle.getSelectedSlot() + 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the maximum amount of fuel this turtle can hold.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * By default, normal turtles have a limit of 20,000 and advanced turtles of 100,000.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The limit, or "unlimited".
 | 
			
		||||
     * @cc.treturn [1] number The maximum amount of fuel a turtle can hold.
 | 
			
		||||
     * @cc.treturn [2] "unlimited" If turtles do not consume fuel when moving.
 | 
			
		||||
     * @cc.since 1.6
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object getFuelLimit() {
 | 
			
		||||
        return CCTurtle.turtlesNeedFuel ? Integer.MAX_VALUE : "unlimited";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,8 +4,6 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A wrapper type for "coerced" values.
 | 
			
		||||
 * <p>
 | 
			
		||||
@@ -20,27 +18,9 @@ import java.util.Objects;
 | 
			
		||||
 * }
 | 
			
		||||
 * }</pre>
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of the underlying value.
 | 
			
		||||
 * @param value The argument value.
 | 
			
		||||
 * @param <T>   The type of the underlying value.
 | 
			
		||||
 * @see IArguments#getStringCoerced(int)
 | 
			
		||||
 */
 | 
			
		||||
public final class Coerced<T> {
 | 
			
		||||
    private final T value;
 | 
			
		||||
 | 
			
		||||
    public Coerced(T value) {
 | 
			
		||||
        this.value = value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public T value() {
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean equals(Object obj) {
 | 
			
		||||
        return obj == this || obj instanceof Coerced && Objects.equals(value, ((Coerced<?>) obj).value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int hashCode() {
 | 
			
		||||
        return Objects.hashCode(value);
 | 
			
		||||
    }
 | 
			
		||||
public record Coerced<T>(T value) {
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,9 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import org.jetbrains.annotations.Contract;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
@@ -39,6 +42,7 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws IllegalStateException If accessing these arguments outside the scope of the original function. See
 | 
			
		||||
     *                               {@link #escapes()}.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public abstract Object get(int index) throws LuaException;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -70,8 +74,8 @@ public abstract class IArguments {
 | 
			
		||||
     * @see #get(int) To get a single argument.
 | 
			
		||||
     */
 | 
			
		||||
    public Object[] getAll() throws LuaException {
 | 
			
		||||
        Object[] result = new Object[count()];
 | 
			
		||||
        for (int i = 0; i < result.length; i++) result[i] = get(i);
 | 
			
		||||
        var result = new Object[count()];
 | 
			
		||||
        for (var i = 0; i < result.length; i++) result[i] = get(i);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -84,9 +88,9 @@ public abstract class IArguments {
 | 
			
		||||
     * @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN).
 | 
			
		||||
     */
 | 
			
		||||
    public double getDouble(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return ((Number) value).doubleValue();
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (!(value instanceof Number number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return number.doubleValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -108,9 +112,9 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a long.
 | 
			
		||||
     */
 | 
			
		||||
    public long getLong(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return LuaValues.checkFiniteNum(index, (Number) value).longValue();
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (!(value instanceof Number number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return LuaValues.checkFiniteNum(index, number).longValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -132,9 +136,9 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a boolean.
 | 
			
		||||
     */
 | 
			
		||||
    public boolean getBoolean(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        if (!(value instanceof Boolean)) throw LuaValues.badArgumentOf(this, index, "boolean");
 | 
			
		||||
        return (Boolean) value;
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (!(value instanceof Boolean bool)) throw LuaValues.badArgumentOf(this, index, "boolean");
 | 
			
		||||
        return bool;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -145,9 +149,9 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a string.
 | 
			
		||||
     */
 | 
			
		||||
    public String getString(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        if (!(value instanceof String)) throw LuaValues.badArgumentOf(this, index, "string");
 | 
			
		||||
        return (String) value;
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (!(value instanceof String string)) throw LuaValues.badArgumentOf(this, index, "string");
 | 
			
		||||
        return string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -167,12 +171,12 @@ public abstract class IArguments {
 | 
			
		||||
     * @see Coerced
 | 
			
		||||
     */
 | 
			
		||||
    public String getStringCoerced(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (value == null) return "nil";
 | 
			
		||||
        if (value instanceof Boolean || value instanceof String) return value.toString();
 | 
			
		||||
        if (value instanceof Number) {
 | 
			
		||||
            double asDouble = ((Number) value).doubleValue();
 | 
			
		||||
            int asInt = (int) asDouble;
 | 
			
		||||
        if (value instanceof Number number) {
 | 
			
		||||
            var asDouble = number.doubleValue();
 | 
			
		||||
            var asInt = (int) asDouble;
 | 
			
		||||
            return asInt == asDouble ? Integer.toString(asInt) : Double.toString(asDouble);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -191,6 +195,19 @@ public abstract class IArguments {
 | 
			
		||||
        return LuaValues.encode(getString(index));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the argument, converting it to the raw-byte representation of its string by following Lua conventions.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is equivalent to {@link #getStringCoerced(int)}, but then
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @return The argument's value. This is a <em>read only</em> buffer.
 | 
			
		||||
     * @throws LuaException If the argument cannot be converted to Java.
 | 
			
		||||
     */
 | 
			
		||||
    public ByteBuffer getBytesCoerced(int index) throws LuaException {
 | 
			
		||||
        return LuaValues.encode(getStringCoerced(index));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a string argument as an enum value.
 | 
			
		||||
     *
 | 
			
		||||
@@ -212,7 +229,7 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     */
 | 
			
		||||
    public Map<?, ?> getTable(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (!(value instanceof Map)) throw LuaValues.badArgumentOf(this, index, "table");
 | 
			
		||||
        return (Map<?, ?>) value;
 | 
			
		||||
    }
 | 
			
		||||
@@ -225,10 +242,10 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Double> optDouble(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return Optional.of(((Number) value).doubleValue());
 | 
			
		||||
        if (!(value instanceof Number number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return Optional.of(number.doubleValue());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -250,10 +267,10 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Long> optLong(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return Optional.of(LuaValues.checkFiniteNum(index, (Number) value).longValue());
 | 
			
		||||
        if (!(value instanceof Number number)) throw LuaValues.badArgumentOf(this, index, "number");
 | 
			
		||||
        return Optional.of(LuaValues.checkFiniteNum(index, number).longValue());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -264,7 +281,7 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not finite.
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Double> optFiniteDouble(int index) throws LuaException {
 | 
			
		||||
        Optional<Double> value = optDouble(index);
 | 
			
		||||
        var value = optDouble(index);
 | 
			
		||||
        if (value.isPresent()) LuaValues.checkFiniteNum(index, value.get());
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
@@ -277,10 +294,10 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a boolean.
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Boolean> optBoolean(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Boolean)) throw LuaValues.badArgumentOf(this, index, "boolean");
 | 
			
		||||
        return Optional.of((Boolean) value);
 | 
			
		||||
        if (!(value instanceof Boolean bool)) throw LuaValues.badArgumentOf(this, index, "boolean");
 | 
			
		||||
        return Optional.of(bool);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -291,10 +308,10 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a string.
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<String> optString(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof String)) throw LuaValues.badArgumentOf(this, index, "string");
 | 
			
		||||
        return Optional.of((String) value);
 | 
			
		||||
        if (!(value instanceof String string)) throw LuaValues.badArgumentOf(this, index, "string");
 | 
			
		||||
        return Optional.of(string);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -318,7 +335,7 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a string or not a valid option for this enum.
 | 
			
		||||
     */
 | 
			
		||||
    public <T extends Enum<T>> Optional<T> optEnum(int index, Class<T> klass) throws LuaException {
 | 
			
		||||
        Optional<String> str = optString(index);
 | 
			
		||||
        var str = optString(index);
 | 
			
		||||
        return str.isPresent() ? Optional.of(LuaValues.checkEnum(index, klass, str.get())) : Optional.empty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -330,7 +347,7 @@ public abstract class IArguments {
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Map<?, ?>> optTable(int index) throws LuaException {
 | 
			
		||||
        Object value = get(index);
 | 
			
		||||
        var value = get(index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Map)) throw LuaValues.badArgumentOf(this, index, "map");
 | 
			
		||||
        return Optional.of((Map<?, ?>) value);
 | 
			
		||||
@@ -340,7 +357,7 @@ public abstract class IArguments {
 | 
			
		||||
     * Get an argument as a double.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @param def   The public value, if this argument is not given.
 | 
			
		||||
     * @param def   The default value, if this argument is not given.
 | 
			
		||||
     * @return The argument's value, or {@code def} if none was provided.
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     */
 | 
			
		||||
@@ -352,7 +369,7 @@ public abstract class IArguments {
 | 
			
		||||
     * Get an argument as an int.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @param def   The public value, if this argument is not given.
 | 
			
		||||
     * @param def   The default value, if this argument is not given.
 | 
			
		||||
     * @return The argument's value, or {@code def} if none was provided.
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     */
 | 
			
		||||
@@ -364,7 +381,7 @@ public abstract class IArguments {
 | 
			
		||||
     * Get an argument as a long.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @param def   The public value, if this argument is not given.
 | 
			
		||||
     * @param def   The default value, if this argument is not given.
 | 
			
		||||
     * @return The argument's value, or {@code def} if none was provided.
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     */
 | 
			
		||||
@@ -376,7 +393,7 @@ public abstract class IArguments {
 | 
			
		||||
     * Get an argument as a finite number (not infinite or NaN).
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @param def   The public value, if this argument is not given.
 | 
			
		||||
     * @param def   The default value, if this argument is not given.
 | 
			
		||||
     * @return The argument's value, or {@code def} if none was provided.
 | 
			
		||||
     * @throws LuaException If the value is not finite.
 | 
			
		||||
     */
 | 
			
		||||
@@ -388,7 +405,7 @@ public abstract class IArguments {
 | 
			
		||||
     * Get an argument as a boolean.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @param def   The public value, if this argument is not given.
 | 
			
		||||
     * @param def   The default value, if this argument is not given.
 | 
			
		||||
     * @return The argument's value, or {@code def} if none was provided.
 | 
			
		||||
     * @throws LuaException If the value is not a boolean.
 | 
			
		||||
     */
 | 
			
		||||
@@ -400,11 +417,13 @@ public abstract class IArguments {
 | 
			
		||||
     * Get an argument as a string.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @param def   The public value, if this argument is not given.
 | 
			
		||||
     * @param def   The default value, if this argument is not given.
 | 
			
		||||
     * @return The argument's value, or {@code def} if none was provided.
 | 
			
		||||
     * @throws LuaException If the value is not a string.
 | 
			
		||||
     */
 | 
			
		||||
    public String optString(int index, String def) throws LuaException {
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Contract("_, !null -> !null")
 | 
			
		||||
    public String optString(int index, @Nullable String def) throws LuaException {
 | 
			
		||||
        return optString(index).orElse(def);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -412,11 +431,13 @@ public abstract class IArguments {
 | 
			
		||||
     * Get an argument as a table.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The argument number.
 | 
			
		||||
     * @param def   The public value, if this argument is not given.
 | 
			
		||||
     * @param def   The default value, if this argument is not given.
 | 
			
		||||
     * @return The argument's value, or {@code def} if none was provided.
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     */
 | 
			
		||||
    public Map<?, ?> optTable(int index, Map<Object, Object> def) throws LuaException {
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Contract("_, !null -> !null")
 | 
			
		||||
    public Map<?, ?> optTable(int index, @Nullable Map<Object, Object> def) throws LuaException {
 | 
			
		||||
        return optTable(index).orElse(def);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -436,9 +457,11 @@ public abstract class IArguments {
 | 
			
		||||
     * yourself.
 | 
			
		||||
     *
 | 
			
		||||
     * @return An {@link IArguments} instance which can escape the current scope. May be {@code this}.
 | 
			
		||||
     * @throws LuaException For the same reasons as {@link #get(int)}.
 | 
			
		||||
     * @throws LuaException          For the same reasons as {@link #get(int)}.
 | 
			
		||||
     * @throws IllegalStateException If marking these arguments as escaping outside the scope of the original function.
 | 
			
		||||
     */
 | 
			
		||||
    public IArguments escapes() throws LuaException {
 | 
			
		||||
        // TODO(1.21.0): Make this return void, require that it mutates this.
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,21 +4,25 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An exception representing an error in Lua, like that raised by the {@code error()} function.
 | 
			
		||||
 */
 | 
			
		||||
public class LuaException extends Exception {
 | 
			
		||||
    @Serial
 | 
			
		||||
    private static final long serialVersionUID = -6136063076818512651L;
 | 
			
		||||
    private final boolean hasLevel;
 | 
			
		||||
    private final int level;
 | 
			
		||||
 | 
			
		||||
    public LuaException(String message) {
 | 
			
		||||
    public LuaException(@Nullable String message) {
 | 
			
		||||
        super(message);
 | 
			
		||||
        hasLevel = false;
 | 
			
		||||
        level = 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public LuaException(String message, int level) {
 | 
			
		||||
    public LuaException(@Nullable String message, int level) {
 | 
			
		||||
        super(message);
 | 
			
		||||
        hasLevel = true;
 | 
			
		||||
        this.level = level;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										461
									
								
								src/main/java/dan200/computercraft/api/lua/LuaTable.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										461
									
								
								src/main/java/dan200/computercraft/api/lua/LuaTable.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,461 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.api.lua.LuaValues.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A view of a Lua table.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * Much like {@link IArguments}, this allows for convenient parsing of fields from a Lua table.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <K> The type of keys in a table, will typically be a wildcard.
 | 
			
		||||
 * @param <V> The type of values in a table, will typically be a wildcard.
 | 
			
		||||
 * @see ObjectArguments
 | 
			
		||||
 */
 | 
			
		||||
public abstract class LuaTable<K, V> implements Map<K, V> {
 | 
			
		||||
    /**
 | 
			
		||||
     * Compute the length of the array part of this table.
 | 
			
		||||
     *
 | 
			
		||||
     * @return This table's length.
 | 
			
		||||
     */
 | 
			
		||||
    public int length() {
 | 
			
		||||
        var size = 0;
 | 
			
		||||
        while (containsKey((double) (size + 1))) size++;
 | 
			
		||||
        return size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a double.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value.
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     * @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN).
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public double getDouble(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value));
 | 
			
		||||
        return number.doubleValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a double.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value.
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     * @see #getFiniteDouble(String) if you require this to be finite (i.e. not infinite or NaN).
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public double getDouble(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (!(value instanceof Number number)) throw badField(key, "number", getType(value));
 | 
			
		||||
        return number.doubleValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    public long getLong(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value));
 | 
			
		||||
        checkFiniteIndex(index, number.doubleValue());
 | 
			
		||||
        return number.longValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    public long getLong(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (!(value instanceof Number number)) throw badField(key, "number", getType(value));
 | 
			
		||||
        checkFiniteField(key, number.doubleValue());
 | 
			
		||||
        return number.longValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    public int getInt(int index) throws LuaException {
 | 
			
		||||
        return (int) getLong(index);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     */
 | 
			
		||||
    public int getInt(String key) throws LuaException {
 | 
			
		||||
        return (int) getLong(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a finite number (not infinite or NaN).
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value.
 | 
			
		||||
     * @throws LuaException If the value is not finite.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public double getFiniteDouble(int index) throws LuaException {
 | 
			
		||||
        return checkFiniteIndex(index, getDouble(index));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a finite number (not infinite or NaN).
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value.
 | 
			
		||||
     * @throws LuaException If the value is not finite.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public double getFiniteDouble(String key) throws LuaException {
 | 
			
		||||
        return checkFiniteField(key, getDouble(key));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a boolean.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value.
 | 
			
		||||
     * @throws LuaException If the value is not a boolean.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public boolean getBoolean(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (!(value instanceof Boolean bool)) throw badTableItem(index, "boolean", getType(value));
 | 
			
		||||
        return bool;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a boolean.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value.
 | 
			
		||||
     * @throws LuaException If the value is not a boolean.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public boolean getBoolean(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (!(value instanceof Boolean bool)) throw badField(key, "boolean", getType(value));
 | 
			
		||||
        return bool;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a string.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value.
 | 
			
		||||
     * @throws LuaException If the value is not a string.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public String getString(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (!(value instanceof String string)) throw badTableItem(index, "string", getType(value));
 | 
			
		||||
        return string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a string.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value.
 | 
			
		||||
     * @throws LuaException If the value is not a string.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public String getString(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (!(value instanceof String string)) throw badField(key, "string", getType(value));
 | 
			
		||||
        return string;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a table.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of
 | 
			
		||||
     * table keys.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value.
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Map<?, ?> getTable(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (!(value instanceof Map<?, ?> table)) throw badTableItem(index, "table", getType(value));
 | 
			
		||||
        return table;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a table.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of
 | 
			
		||||
     * table keys.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value.
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Map<?, ?> getTable(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (!(value instanceof Map<?, ?> table)) throw badField(key, "table", getType(value));
 | 
			
		||||
        return table;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a double.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     * @see #getFiniteDouble(int) if you require this to be finite (i.e. not infinite or NaN).
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Double> optDouble(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value));
 | 
			
		||||
        return Optional.of(number.doubleValue());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a double.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a number.
 | 
			
		||||
     * @see #getFiniteDouble(String) if you require this to be finite (i.e. not infinite or NaN).
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Double> optDouble(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Number number)) throw badField(key, "number", getType(value));
 | 
			
		||||
        return Optional.of(number.doubleValue());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Long> optLong(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Number number)) throw badTableItem(index, "number", getType(value));
 | 
			
		||||
        checkFiniteIndex(index, number.doubleValue());
 | 
			
		||||
        return Optional.of(number.longValue());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Long> optLong(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Number number)) throw badField(key, "number", getType(value));
 | 
			
		||||
        checkFiniteField(key, number.doubleValue());
 | 
			
		||||
        return Optional.of(number.longValue());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Integer> optInt(int index) throws LuaException {
 | 
			
		||||
        return optLong(index).map(Long::intValue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as an integer.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not an integer.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Integer> optInt(String key) throws LuaException {
 | 
			
		||||
        return optLong(key).map(Long::intValue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a finite number (not infinite or NaN).
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not finite.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Double> optFiniteDouble(int index) throws LuaException {
 | 
			
		||||
        var value = optDouble(index);
 | 
			
		||||
        if (value.isPresent()) checkFiniteIndex(index, value.get());
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an argument as a finite number (not infinite or NaN).
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not finite.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Double> optFiniteDouble(String key) throws LuaException {
 | 
			
		||||
        var value = optDouble(key);
 | 
			
		||||
        if (value.isPresent()) checkFiniteField(key, value.get());
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a boolean.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a boolean.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Boolean> optBoolean(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Boolean bool)) throw badTableItem(index, "boolean", getType(value));
 | 
			
		||||
        return Optional.of(bool);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a boolean.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a boolean.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Boolean> optBoolean(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Boolean bool)) throw badField(key, "boolean", getType(value));
 | 
			
		||||
        return Optional.of(bool);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a double.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a string.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<String> optString(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof String string)) throw badTableItem(index, "string", getType(value));
 | 
			
		||||
        return Optional.of(string);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a string.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a string.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<String> optString(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof String string)) throw badField(key, "string", getType(value));
 | 
			
		||||
        return Optional.of(string);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get an array entry as a table.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of
 | 
			
		||||
     * table keys.
 | 
			
		||||
     *
 | 
			
		||||
     * @param index The index in the table, starting at 1.
 | 
			
		||||
     * @return The entry's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Map<?, ?>> optTable(int index) throws LuaException {
 | 
			
		||||
        Object value = get((double) index);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Map<?, ?> table)) throw badTableItem(index, "table", getType(value));
 | 
			
		||||
        return Optional.of(table);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table entry as a table.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * The returned table may be converted into a {@link LuaTable} (using {@link ObjectLuaTable}) for easier parsing of
 | 
			
		||||
     * table keys.
 | 
			
		||||
     *
 | 
			
		||||
     * @param key The name of the field in the table.
 | 
			
		||||
     * @return The field's value, or {@link Optional#empty()} if not present.
 | 
			
		||||
     * @throws LuaException If the value is not a table.
 | 
			
		||||
     * @since 1.116
 | 
			
		||||
     */
 | 
			
		||||
    public Optional<Map<?, ?>> optTable(String key) throws LuaException {
 | 
			
		||||
        Object value = get(key);
 | 
			
		||||
        if (value == null) return Optional.empty();
 | 
			
		||||
        if (!(value instanceof Map<?, ?> table)) throw badField(key, "table", getType(value));
 | 
			
		||||
        return Optional.of(table);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    public V put(K o, V o2) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot modify LuaTable");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public V remove(Object o) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot modify LuaTable");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void putAll(Map<? extends K, ? extends V> map) {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot modify LuaTable");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void clear() {
 | 
			
		||||
        throw new UnsupportedOperationException("Cannot modify LuaTable");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,6 +4,8 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
@@ -23,9 +25,9 @@ public final class LuaValues {
 | 
			
		||||
     * @return The encoded string.
 | 
			
		||||
     */
 | 
			
		||||
    public static ByteBuffer encode(String string) {
 | 
			
		||||
        byte[] chars = new byte[string.length()];
 | 
			
		||||
        for (int i = 0; i < chars.length; i++) {
 | 
			
		||||
            char c = string.charAt(i);
 | 
			
		||||
        var chars = new byte[string.length()];
 | 
			
		||||
        for (var i = 0; i < chars.length; i++) {
 | 
			
		||||
            var c = string.charAt(i);
 | 
			
		||||
            chars[i] = c < 256 ? (byte) c : 63;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +55,7 @@ public final class LuaValues {
 | 
			
		||||
     * @return A string representation of the given value's type, in a similar format to that provided by Lua's
 | 
			
		||||
     * {@code type} function.
 | 
			
		||||
     */
 | 
			
		||||
    public static String getType(Object value) {
 | 
			
		||||
    public static String getType(@Nullable Object value) {
 | 
			
		||||
        if (value == null) return "nil";
 | 
			
		||||
        if (value instanceof String) return "string";
 | 
			
		||||
        if (value instanceof Boolean) return "boolean";
 | 
			
		||||
@@ -95,7 +97,7 @@ public final class LuaValues {
 | 
			
		||||
     * @return The constructed exception, which should be thrown immediately.
 | 
			
		||||
     */
 | 
			
		||||
    public static LuaException badTableItem(int index, String expected, String actual) {
 | 
			
		||||
        return new LuaException("table item #" + index + " is not " + expected + " (got " + actual + ")");
 | 
			
		||||
        return new LuaException("bad item #" + index + " (" + expected + " expected, got " + actual + ")");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -107,7 +109,7 @@ public final class LuaValues {
 | 
			
		||||
     * @return The constructed exception, which should be thrown immediately.
 | 
			
		||||
     */
 | 
			
		||||
    public static LuaException badField(String key, String expected, String actual) {
 | 
			
		||||
        return new LuaException("field " + key + " is not " + expected + " (got " + actual + ")");
 | 
			
		||||
        return new LuaException("bad field '" + key + "' (" + expected + " expected, got " + actual + ")");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -136,6 +138,16 @@ public final class LuaValues {
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static double checkFiniteIndex(int index, double value) throws LuaException {
 | 
			
		||||
        if (!Double.isFinite(value)) throw badTableItem(index, "number", getNumericType(value));
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static double checkFiniteField(String key, double value) throws LuaException {
 | 
			
		||||
        if (!Double.isFinite(value)) throw badField(key, "number", getNumericType(value));
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Ensure a string is a valid enum value.
 | 
			
		||||
     *
 | 
			
		||||
@@ -147,7 +159,7 @@ public final class LuaValues {
 | 
			
		||||
     * @throws LuaException If this is not a known enum value.
 | 
			
		||||
     */
 | 
			
		||||
    public static <T extends Enum<T>> T checkEnum(int index, Class<T> klass, String value) throws LuaException {
 | 
			
		||||
        for (T possibility : klass.getEnumConstants()) {
 | 
			
		||||
        for (var possibility : klass.getEnumConstants()) {
 | 
			
		||||
            if (possibility.name().equalsIgnoreCase(value)) return possibility;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
@@ -44,6 +45,7 @@ public final class ObjectArguments extends IArguments {
 | 
			
		||||
        return new ObjectArguments(args.subList(count, args.size()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    public Object get(int index) {
 | 
			
		||||
        return index >= args.size() ? null : args.get(index);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.api.lua;
 | 
			
		||||
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An implementation of {@link LuaTable} based on a standard Java {@link Map}.
 | 
			
		||||
 */
 | 
			
		||||
public class ObjectLuaTable extends LuaTable<Object, Object> {
 | 
			
		||||
    private final Map<Object, Object> map;
 | 
			
		||||
 | 
			
		||||
    public ObjectLuaTable(Map<?, ?> map) {
 | 
			
		||||
        this.map = Collections.unmodifiableMap(map);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int size() {
 | 
			
		||||
        return map.size();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isEmpty() {
 | 
			
		||||
        return map.isEmpty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean containsKey(Object o) {
 | 
			
		||||
        return map.containsKey(o);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean containsValue(Object o) {
 | 
			
		||||
        return map.containsKey(o);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    public Object get(Object o) {
 | 
			
		||||
        return map.get(o);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Set<Object> keySet() {
 | 
			
		||||
        return map.keySet();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Collection<Object> values() {
 | 
			
		||||
        return map.values();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Set<Entry<Object, Object>> entrySet() {
 | 
			
		||||
        return map.entrySet();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -152,6 +152,6 @@ public class FixedWidthFontRenderer {
 | 
			
		||||
        if (c >= '0' && c <= '9') return c - '0';
 | 
			
		||||
        if (c >= 'a' && c <= 'f') return c - 'a' + 10;
 | 
			
		||||
        if (c >= 'A' && c <= 'F') return c - 'A' + 10;
 | 
			
		||||
        return 15 - def.ordinal();
 | 
			
		||||
        return def.ordinal();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -371,27 +371,36 @@ public class FSAPI implements ILuaAPI {
 | 
			
		||||
    public final Object[] open(String path, String mode) throws LuaException {
 | 
			
		||||
        try {
 | 
			
		||||
            switch (mode) {
 | 
			
		||||
                // Open the file for reading, then create a wrapper around the reader
 | 
			
		||||
                case "r":
 | 
			
		||||
                case "r" -> {
 | 
			
		||||
                    // Open the file for reading, then create a wrapper around the reader
 | 
			
		||||
                    return new Object[]{ new EncodedReadableHandle(getFileSystem().openForRead(path)) };
 | 
			
		||||
                // Open the file for writing, then create a wrapper around the writer
 | 
			
		||||
                case "w":
 | 
			
		||||
                }
 | 
			
		||||
                case "w" -> {
 | 
			
		||||
                    // Open the file for writing, then create a wrapper around the writer
 | 
			
		||||
                    FileSystemExtensions.makeParentDir(fileSystem, path);
 | 
			
		||||
                    return new Object[]{ new EncodedWritableHandle(getFileSystem().openForWrite(path, false)) };
 | 
			
		||||
                // Open the file for appending, then create a wrapper around the writer
 | 
			
		||||
                case "a":
 | 
			
		||||
                }
 | 
			
		||||
                case "a" -> {
 | 
			
		||||
                    // Open the file for appending, then create a wrapper around the writer
 | 
			
		||||
                    FileSystemExtensions.makeParentDir(fileSystem, path);
 | 
			
		||||
                    return new Object[]{ new EncodedWritableHandle(getFileSystem().openForWrite(path, true)) };
 | 
			
		||||
                // Open the file for binary reading, then create a wrapper around the reader
 | 
			
		||||
                case "rb":
 | 
			
		||||
                }
 | 
			
		||||
                case "rb" -> {
 | 
			
		||||
                    // Open the file for binary reading, then create a wrapper around the reader
 | 
			
		||||
                    IMountedFileBinary reader = getFileSystem().openForBinaryRead(path);
 | 
			
		||||
                    return new Object[]{ new BinaryReadableHandle(reader) };
 | 
			
		||||
                // Open the file for binary writing, then create a wrapper around the writer
 | 
			
		||||
                case "wb":
 | 
			
		||||
                }
 | 
			
		||||
                case "wb" -> {
 | 
			
		||||
                    // Open the file for binary writing, then create a wrapper around the writer
 | 
			
		||||
                    FileSystemExtensions.makeParentDir(fileSystem, path);
 | 
			
		||||
                    return new Object[]{ new BinaryWritableHandle(getFileSystem().openForBinaryWrite(path, false)) };
 | 
			
		||||
                // Open the file for binary appending, then create a wrapper around the reader
 | 
			
		||||
                case "ab":
 | 
			
		||||
                }
 | 
			
		||||
                case "ab" -> {
 | 
			
		||||
                    // Open the file for binary appending, then create a wrapper around the reader
 | 
			
		||||
                    FileSystemExtensions.makeParentDir(fileSystem, path);
 | 
			
		||||
                    return new Object[]{ new BinaryWritableHandle(getFileSystem().openForBinaryWrite(path, true)) };
 | 
			
		||||
                default:
 | 
			
		||||
                    throw new LuaException("Unsupported mode");
 | 
			
		||||
                }
 | 
			
		||||
                default -> throw new LuaException("Unsupported mode");
 | 
			
		||||
            }
 | 
			
		||||
        } catch (FileSystemException e) {
 | 
			
		||||
            return new Object[]{ null, e.getMessage() };
 | 
			
		||||
@@ -442,29 +451,6 @@ public class FSAPI implements ILuaAPI {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Searches for files matching a string with wildcards.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This string is formatted like a normal path string, but can include any
 | 
			
		||||
     * number of wildcards ({@code *}) to look for files matching anything.
 | 
			
		||||
     * For example, <code>rom/*/command*</code> will look for any path starting with
 | 
			
		||||
     * {@code command} inside any subdirectory of {@code /rom}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param path The wildcard-qualified path to search for.
 | 
			
		||||
     * @return A list of paths that match the search string.
 | 
			
		||||
     * @throws LuaException If the path doesn't exist.
 | 
			
		||||
     * @cc.since 1.6
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final String[] find(String path) throws LuaException {
 | 
			
		||||
        try {
 | 
			
		||||
            return FileSystemExtensions.find(getFileSystem(), path);
 | 
			
		||||
        } catch (FileSystemException e) {
 | 
			
		||||
            throw new LuaException(e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the capacity of the drive the path is located on.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -7,54 +7,15 @@ import com.google.common.base.Splitter;
 | 
			
		||||
import dan200.computer.core.FileSystem;
 | 
			
		||||
import dan200.computer.core.FileSystemException;
 | 
			
		||||
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.ArrayDeque;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Deque;
 | 
			
		||||
import java.util.regex.Pattern;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Backports additional methods from {@link FileSystem}.
 | 
			
		||||
 */
 | 
			
		||||
final class FileSystemExtensions {
 | 
			
		||||
    private static void findIn(FileSystem fs, String dir, List<String> matches, Pattern wildPattern) throws FileSystemException {
 | 
			
		||||
        String[] list = fs.list(dir);
 | 
			
		||||
        for (String entry : list) {
 | 
			
		||||
            String entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
 | 
			
		||||
            if (wildPattern.matcher(entryPath).matches()) {
 | 
			
		||||
                matches.add(entryPath);
 | 
			
		||||
            }
 | 
			
		||||
            if (fs.isDir(entryPath)) {
 | 
			
		||||
                findIn(fs, entryPath, matches, wildPattern);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static synchronized String[] find(FileSystem fs, String wildPath) throws FileSystemException {
 | 
			
		||||
        // Match all the files on the system
 | 
			
		||||
        wildPath = sanitizePath(wildPath, true);
 | 
			
		||||
 | 
			
		||||
        // If we don't have a wildcard at all just check the file exists
 | 
			
		||||
        int starIndex = wildPath.indexOf('*');
 | 
			
		||||
        if (starIndex == -1) {
 | 
			
		||||
            return fs.exists(wildPath) ? new String[]{ wildPath } : new String[0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
 | 
			
		||||
        int prevDir = wildPath.substring(0, starIndex).lastIndexOf('/');
 | 
			
		||||
        String startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir);
 | 
			
		||||
 | 
			
		||||
        // If this isn't a directory then just abort
 | 
			
		||||
        if (!fs.isDir(startDir)) return new String[0];
 | 
			
		||||
 | 
			
		||||
        // Scan as normal, starting from this directory
 | 
			
		||||
        Pattern wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$");
 | 
			
		||||
        List<String> matches = new ArrayList<>();
 | 
			
		||||
        findIn(fs, startDir, matches, wildPattern);
 | 
			
		||||
 | 
			
		||||
        // Return matches
 | 
			
		||||
        String[] array = matches.toArray(new String[0]);
 | 
			
		||||
        Arrays.sort(array);
 | 
			
		||||
        return array;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String getDirectory(String path) {
 | 
			
		||||
        path = sanitizePath(path, true);
 | 
			
		||||
        if (path.isEmpty()) {
 | 
			
		||||
@@ -69,6 +30,11 @@ final class FileSystemExtensions {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void makeParentDir(FileSystem fileSystem, String path) throws FileSystemException {
 | 
			
		||||
        var parent = getDirectory(path);
 | 
			
		||||
        if (!parent.isEmpty()) fileSystem.makeDir(parent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$");
 | 
			
		||||
 | 
			
		||||
    public static String sanitizePath(String path, boolean allowWildcards) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										217
									
								
								src/main/java/dan200/computercraft/core/apis/HTTPAPI.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								src/main/java/dan200/computercraft/core/apis/HTTPAPI.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,217 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis;
 | 
			
		||||
 | 
			
		||||
import dan200.ComputerCraft;
 | 
			
		||||
import dan200.computer.core.IAPIEnvironment;
 | 
			
		||||
import dan200.computer.core.ILuaAPI;
 | 
			
		||||
import dan200.computercraft.api.lua.*;
 | 
			
		||||
import dan200.computercraft.core.apis.http.*;
 | 
			
		||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
 | 
			
		||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
 | 
			
		||||
import dan200.computercraft.core.apis.http.websocket.WebsocketClient;
 | 
			
		||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaderNames;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaders;
 | 
			
		||||
import io.netty.handler.codec.http.HttpMethod;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.core.util.ArgumentHelpers.assertBetween;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Placeholder description, please ignore.
 | 
			
		||||
 *
 | 
			
		||||
 * @cc.module http
 | 
			
		||||
 * @hidden
 | 
			
		||||
 */
 | 
			
		||||
public class HTTPAPI implements ILuaAPI {
 | 
			
		||||
    private static final double DEFAULT_TIMEOUT = 30;
 | 
			
		||||
    private static final double MAX_TIMEOUT = 60;
 | 
			
		||||
 | 
			
		||||
    private final IAPIEnvironment apiEnvironment;
 | 
			
		||||
 | 
			
		||||
    private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>(() -> ResourceGroup.DEFAULT_LIMIT);
 | 
			
		||||
    private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>(() -> 16);
 | 
			
		||||
    private final ResourceGroup<Websocket> websockets = new ResourceGroup<>(() -> 4);
 | 
			
		||||
 | 
			
		||||
    public HTTPAPI(IAPIEnvironment environment) {
 | 
			
		||||
        apiEnvironment = environment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String[] getNames() {
 | 
			
		||||
        return new String[]{ "http" };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void startup() {
 | 
			
		||||
        checkUrls.startup();
 | 
			
		||||
        requests.startup();
 | 
			
		||||
        websockets.startup();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void shutdown() {
 | 
			
		||||
        checkUrls.shutdown();
 | 
			
		||||
        requests.shutdown();
 | 
			
		||||
        websockets.shutdown();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void advance(double dt) {
 | 
			
		||||
        // It's rather ugly to run this here, but we need to clean up
 | 
			
		||||
        // resources as often as possible to reduce blocking.
 | 
			
		||||
        Resource.cleanup();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object[] request(IArguments args) throws LuaException {
 | 
			
		||||
        String address, requestMethod;
 | 
			
		||||
        ByteBuffer postBody;
 | 
			
		||||
        Map<?, ?> headerTable;
 | 
			
		||||
        boolean binary, redirect;
 | 
			
		||||
        Optional<Double> timeoutArg;
 | 
			
		||||
 | 
			
		||||
        if (args.get(0) instanceof Map) {
 | 
			
		||||
            var options = new ObjectLuaTable(args.getTable(0));
 | 
			
		||||
            address = options.getString("url");
 | 
			
		||||
            postBody = options.optString("body").map(LuaValues::encode).orElse(null);
 | 
			
		||||
            headerTable = options.optTable("headers").orElse(Collections.emptyMap());
 | 
			
		||||
            binary = options.optBoolean("binary").orElse(false);
 | 
			
		||||
            requestMethod = options.optString("method").orElse(null);
 | 
			
		||||
            redirect = options.optBoolean("redirect").orElse(true);
 | 
			
		||||
            timeoutArg = options.optFiniteDouble("timeout");
 | 
			
		||||
        } else {
 | 
			
		||||
            // Get URL and post information
 | 
			
		||||
            address = args.getString(0);
 | 
			
		||||
            postBody = args.optBytes(1).orElse(null);
 | 
			
		||||
            headerTable = args.optTable(2, Collections.emptyMap());
 | 
			
		||||
            binary = args.optBoolean(3, false);
 | 
			
		||||
            requestMethod = null;
 | 
			
		||||
            redirect = true;
 | 
			
		||||
            timeoutArg = Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var headers = getHeaders(headerTable);
 | 
			
		||||
        var timeout = getTimeout(timeoutArg);
 | 
			
		||||
 | 
			
		||||
        HttpMethod httpMethod;
 | 
			
		||||
        if (requestMethod == null) {
 | 
			
		||||
            httpMethod = postBody == null ? HttpMethod.GET : HttpMethod.POST;
 | 
			
		||||
        } else {
 | 
			
		||||
            httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT));
 | 
			
		||||
            if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) {
 | 
			
		||||
                throw new LuaException("Unsupported HTTP method");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            var uri = HttpRequest.checkUri(address);
 | 
			
		||||
            var request = new HttpRequest(requests, apiEnvironment, address, postBody, headers, binary, redirect, timeout);
 | 
			
		||||
 | 
			
		||||
            // Make the request
 | 
			
		||||
            if (!request.queue(r -> r.request(uri, httpMethod))) {
 | 
			
		||||
                throw new LuaException("Too many ongoing HTTP requests");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new Object[]{ true };
 | 
			
		||||
        } catch (HTTPRequestException e) {
 | 
			
		||||
            return new Object[]{ false, e.getMessage() };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object[] checkURL(String address) throws LuaException {
 | 
			
		||||
        try {
 | 
			
		||||
            var uri = HttpRequest.checkUri(address);
 | 
			
		||||
            if (!new CheckUrl(checkUrls, apiEnvironment, address, uri).queue(CheckUrl::run)) {
 | 
			
		||||
                throw new LuaException("Too many ongoing checkUrl calls");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new Object[]{ true };
 | 
			
		||||
        } catch (HTTPRequestException e) {
 | 
			
		||||
            return new Object[]{ false, e.getMessage() };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object[] websocket(IArguments args) throws LuaException {
 | 
			
		||||
        String address;
 | 
			
		||||
        Map<?, ?> headerTable;
 | 
			
		||||
        Optional<Double> timeoutArg;
 | 
			
		||||
 | 
			
		||||
        if (args.get(0) instanceof Map) {
 | 
			
		||||
            var options = new ObjectLuaTable(args.getTable(0));
 | 
			
		||||
            address = options.getString("url");
 | 
			
		||||
            headerTable = options.optTable("headers").orElse(Collections.emptyMap());
 | 
			
		||||
            timeoutArg = options.optFiniteDouble("timeout");
 | 
			
		||||
        } else {
 | 
			
		||||
            address = args.getString(0);
 | 
			
		||||
            headerTable = args.optTable(1, Collections.emptyMap());
 | 
			
		||||
            timeoutArg = Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var headers = getHeaders(headerTable);
 | 
			
		||||
        var timeout = getTimeout(timeoutArg);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            var uri = WebsocketClient.Support.parseUri(address);
 | 
			
		||||
            if (!new Websocket(websockets, apiEnvironment, uri, address, headers, timeout).queue(Websocket::connect)) {
 | 
			
		||||
                throw new LuaException("Too many websockets already open");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new Object[]{ true };
 | 
			
		||||
        } catch (HTTPRequestException e) {
 | 
			
		||||
            return new Object[]{ false, e.getMessage() };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private HttpHeaders getHeaders(Map<?, ?> headerTable) throws LuaException {
 | 
			
		||||
        HttpHeaders headers = new DefaultHttpHeaders();
 | 
			
		||||
        for (Map.Entry<?, ?> entry : headerTable.entrySet()) {
 | 
			
		||||
            var value = entry.getValue();
 | 
			
		||||
            if (entry.getKey() instanceof String && value instanceof String) {
 | 
			
		||||
                try {
 | 
			
		||||
                    headers.add((String) entry.getKey(), value);
 | 
			
		||||
                } catch (IllegalArgumentException e) {
 | 
			
		||||
                    throw new LuaException(e.getMessage());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!headers.contains(HttpHeaderNames.USER_AGENT)) {
 | 
			
		||||
            headers.set(HttpHeaderNames.USER_AGENT, "computercraft/" + ComputerCraft.getVersion());
 | 
			
		||||
        }
 | 
			
		||||
        return headers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Parse the timeout value, asserting it is in range.
 | 
			
		||||
     *
 | 
			
		||||
     * @param timeoutArg The (optional) timeout, in seconds.
 | 
			
		||||
     * @return The parsed timeout value, in milliseconds.
 | 
			
		||||
     * @throws LuaException If the timeout is in-range.
 | 
			
		||||
     */
 | 
			
		||||
    private static int getTimeout(Optional<Double> timeoutArg) throws LuaException {
 | 
			
		||||
        double timeout = timeoutArg.orElse(DEFAULT_TIMEOUT);
 | 
			
		||||
        assertBetween(timeout, 0, MAX_TIMEOUT, "timeout out of range (%s)");
 | 
			
		||||
        return (int) (timeout * 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String[] getMethodNames() {
 | 
			
		||||
        return new String[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Object[] callMethod(int i, Object[] objects) {
 | 
			
		||||
        throw new IllegalStateException();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -203,8 +203,10 @@ public class OSAPI implements ILuaAPI {
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final int startTimer(double timer) throws LuaException {
 | 
			
		||||
        timers.put(nextTimerToken, new Timer((int) Math.round(checkFinite(0, timer) / 0.05)));
 | 
			
		||||
        return nextTimerToken++;
 | 
			
		||||
        synchronized (timers) {
 | 
			
		||||
            timers.put(nextTimerToken, new Timer((int) Math.round(checkFinite(0, timer) / 0.05)));
 | 
			
		||||
            return nextTimerToken++;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -217,7 +219,9 @@ public class OSAPI implements ILuaAPI {
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final void cancelTimer(int token) {
 | 
			
		||||
        timers.remove(token);
 | 
			
		||||
        synchronized (timers) {
 | 
			
		||||
            timers.remove(token);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,327 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.handles;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.Coerced;
 | 
			
		||||
import dan200.computercraft.api.lua.IArguments;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.io.ByteArrayOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.Buffer;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.channels.FileChannel;
 | 
			
		||||
import java.nio.channels.SeekableByteChannel;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The base class for all file handle types.
 | 
			
		||||
 */
 | 
			
		||||
public abstract class AbstractHandle {
 | 
			
		||||
    private static final int BUFFER_SIZE = 8192;
 | 
			
		||||
 | 
			
		||||
    private final SeekableByteChannel channel;
 | 
			
		||||
    private boolean closed;
 | 
			
		||||
    protected final boolean binary;
 | 
			
		||||
 | 
			
		||||
    private final ByteBuffer single = ByteBuffer.allocate(1);
 | 
			
		||||
 | 
			
		||||
    protected AbstractHandle(SeekableByteChannel channel, boolean binary) {
 | 
			
		||||
        this.channel = channel;
 | 
			
		||||
        this.binary = binary;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void checkOpen() throws LuaException {
 | 
			
		||||
        if (closed) throw new LuaException("attempt to use a closed file");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close this file, freeing any resources it uses.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Once a file is closed it may no longer be read or written to.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws LuaException If the file has already been closed.
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final void close() throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        try {
 | 
			
		||||
            closed = true;
 | 
			
		||||
            channel.close();
 | 
			
		||||
        } catch (IOException ignored) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Seek to a new position within the file, changing where bytes are written to. The new position is an offset
 | 
			
		||||
     * given by {@code offset}, relative to a start position determined by {@code whence}:
 | 
			
		||||
     * <p>
 | 
			
		||||
     * - {@code "set"}: {@code offset} is relative to the beginning of the file.
 | 
			
		||||
     * - {@code "cur"}: Relative to the current position. This is the default.
 | 
			
		||||
     * - {@code "end"}: Relative to the end of the file.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * In case of success, {@code seek} returns the new file position from the beginning of the file.
 | 
			
		||||
     *
 | 
			
		||||
     * @param whence Where the offset is relative to.
 | 
			
		||||
     * @param offset The offset to seek to.
 | 
			
		||||
     * @return The new position.
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
     * @cc.treturn [1] number The new position.
 | 
			
		||||
     * @cc.treturn [2] nil If seeking failed.
 | 
			
		||||
     * @cc.treturn string The reason seeking failed.
 | 
			
		||||
     * @cc.since 1.80pr1.9
 | 
			
		||||
     */
 | 
			
		||||
    public Object @Nullable [] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        long actualOffset = offset.orElse(0L);
 | 
			
		||||
        try {
 | 
			
		||||
            switch (whence.orElse("cur")) {
 | 
			
		||||
                case "set" -> channel.position(actualOffset);
 | 
			
		||||
                case "cur" -> channel.position(channel.position() + actualOffset);
 | 
			
		||||
                case "end" -> channel.position(channel.size() + actualOffset);
 | 
			
		||||
                default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return new Object[]{channel.position()};
 | 
			
		||||
        } catch (IllegalArgumentException e) {
 | 
			
		||||
            return new Object[]{null, "Position is negative"};
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Read a number of bytes from this file.
 | 
			
		||||
     *
 | 
			
		||||
     * @param countArg The number of bytes to read. This may be 0 to determine we are at the end of the file. When
 | 
			
		||||
     *                 absent, a single byte will be read.
 | 
			
		||||
     * @return The read bytes.
 | 
			
		||||
     * @throws LuaException When trying to read a negative number of bytes.
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
     * @cc.treturn [1] nil If we are at the end of the file.
 | 
			
		||||
     * @cc.treturn [2] number The value of the byte read. This is returned if the file is opened in binary mode and
 | 
			
		||||
     * {@code count} is absent
 | 
			
		||||
     * @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
 | 
			
		||||
     * @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number.
 | 
			
		||||
     */
 | 
			
		||||
    public Object @Nullable [] read(Optional<Integer> countArg) throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        try {
 | 
			
		||||
            if (binary && !countArg.isPresent()) {
 | 
			
		||||
                clear(single);
 | 
			
		||||
                var b = channel.read(single);
 | 
			
		||||
                return b == -1 ? null : new Object[]{single.get(0) & 0xFF};
 | 
			
		||||
            } else {
 | 
			
		||||
                int count = countArg.orElse(1);
 | 
			
		||||
                if (count < 0) throw new LuaException("Cannot read a negative number of bytes");
 | 
			
		||||
                if (count == 0) return channel.position() >= channel.size() ? null : new Object[]{""};
 | 
			
		||||
 | 
			
		||||
                if (count <= BUFFER_SIZE) {
 | 
			
		||||
                    var buffer = ByteBuffer.allocate(count);
 | 
			
		||||
 | 
			
		||||
                    var read = channel.read(buffer);
 | 
			
		||||
                    if (read < 0) return null;
 | 
			
		||||
                    flip(buffer);
 | 
			
		||||
                    return new Object[]{buffer};
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Read the initial set of characters, failing if none are read.
 | 
			
		||||
                    var buffer = ByteBuffer.allocate(BUFFER_SIZE);
 | 
			
		||||
                    var read = channel.read(buffer);
 | 
			
		||||
                    if (read < 0) return null;
 | 
			
		||||
                    flip(buffer);
 | 
			
		||||
 | 
			
		||||
                    // If we failed to read "enough" here, let's just abort
 | 
			
		||||
                    if (read >= count || read < BUFFER_SIZE) return new Object[]{buffer};
 | 
			
		||||
 | 
			
		||||
                    // Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
 | 
			
		||||
                    // than doubling up the buffer each time.
 | 
			
		||||
                    var totalRead = read;
 | 
			
		||||
                    List<ByteBuffer> parts = new ArrayList<>(4);
 | 
			
		||||
                    parts.add(buffer);
 | 
			
		||||
                    while (read >= BUFFER_SIZE && totalRead < count) {
 | 
			
		||||
                        buffer = ByteBuffer.allocateDirect(Math.min(BUFFER_SIZE, count - totalRead));
 | 
			
		||||
                        read = channel.read(buffer);
 | 
			
		||||
                        if (read < 0) break;
 | 
			
		||||
                        flip(buffer);
 | 
			
		||||
 | 
			
		||||
                        totalRead += read;
 | 
			
		||||
                        assert read == buffer.remaining();
 | 
			
		||||
                        parts.add(buffer);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Now just copy all the bytes across!
 | 
			
		||||
                    var bytes = new byte[totalRead];
 | 
			
		||||
                    var pos = 0;
 | 
			
		||||
                    for (var part : parts) {
 | 
			
		||||
                        var length = part.remaining();
 | 
			
		||||
                        part.get(bytes, pos, length);
 | 
			
		||||
                        pos += length;
 | 
			
		||||
                    }
 | 
			
		||||
                    assert pos == totalRead;
 | 
			
		||||
                    return new Object[]{bytes};
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Read the remainder of the file.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The remaining contents of the file, or {@code null} in the event of an error.
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
     * @cc.treturn string|nil The remaining contents of the file, or {@code nil} in the event of an error.
 | 
			
		||||
     * @cc.since 1.80pr1
 | 
			
		||||
     */
 | 
			
		||||
    public Object @Nullable [] readAll() throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        try {
 | 
			
		||||
            var expected = 32;
 | 
			
		||||
            expected = Math.max(expected, (int) (channel.size() - channel.position()));
 | 
			
		||||
            var stream = new ByteArrayOutputStream(expected);
 | 
			
		||||
 | 
			
		||||
            var buf = ByteBuffer.allocate(8192);
 | 
			
		||||
            while (true) {
 | 
			
		||||
                clear(buf);
 | 
			
		||||
                var r = channel.read(buf);
 | 
			
		||||
                if (r == -1) break;
 | 
			
		||||
 | 
			
		||||
                stream.write(buf.array(), 0, r);
 | 
			
		||||
            }
 | 
			
		||||
            return new Object[]{stream.toByteArray()};
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Read a line from the file.
 | 
			
		||||
     *
 | 
			
		||||
     * @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}.
 | 
			
		||||
     * @return The read string.
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
     * @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
 | 
			
		||||
     * @cc.since 1.80pr1.9
 | 
			
		||||
     * @cc.changed 1.81.0 `\r` is now stripped.
 | 
			
		||||
     */
 | 
			
		||||
    public Object @Nullable [] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        boolean withTrailing = withTrailingArg.orElse(false);
 | 
			
		||||
        try {
 | 
			
		||||
            var stream = new ByteArrayOutputStream();
 | 
			
		||||
 | 
			
		||||
            boolean readAnything = false, readRc = false;
 | 
			
		||||
            while (true) {
 | 
			
		||||
                clear(single);
 | 
			
		||||
                var read = channel.read(single);
 | 
			
		||||
                if (read <= 0) {
 | 
			
		||||
                    // Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
 | 
			
		||||
                    // back.
 | 
			
		||||
                    if (readRc) stream.write('\r');
 | 
			
		||||
                    return readAnything ? new Object[]{stream.toByteArray()} : null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                readAnything = true;
 | 
			
		||||
 | 
			
		||||
                var chr = single.get(0);
 | 
			
		||||
                if (chr == '\n') {
 | 
			
		||||
                    if (withTrailing) {
 | 
			
		||||
                        if (readRc) stream.write('\r');
 | 
			
		||||
                        stream.write(chr);
 | 
			
		||||
                    }
 | 
			
		||||
                    return new Object[]{stream.toByteArray()};
 | 
			
		||||
                } else {
 | 
			
		||||
                    // We want to skip \r\n, but obviously need to include cases where \r is not followed by \n.
 | 
			
		||||
                    // Note, this behaviour is non-standard compliant (strictly speaking we should have no
 | 
			
		||||
                    // special logic for \r), but we preserve compatibility with EncodedReadableHandle and
 | 
			
		||||
                    // previous behaviour of the io library.
 | 
			
		||||
                    if (readRc) stream.write('\r');
 | 
			
		||||
                    readRc = chr == '\r';
 | 
			
		||||
                    if (!readRc) stream.write(chr);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Write a string or byte to the file.
 | 
			
		||||
     *
 | 
			
		||||
     * @param arguments The value to write.
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
     * @cc.tparam [1] string contents The string to write.
 | 
			
		||||
     * @cc.tparam [2] number charcode The byte to write, if the file was opened in binary mode.
 | 
			
		||||
     * @cc.changed 1.80pr1 Now accepts a string to write multiple bytes.
 | 
			
		||||
     */
 | 
			
		||||
    public void write(IArguments arguments) throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        try {
 | 
			
		||||
            var arg = arguments.get(0);
 | 
			
		||||
            if (binary && arg instanceof Number n) {
 | 
			
		||||
                var number = n.intValue();
 | 
			
		||||
                writeSingle((byte) number);
 | 
			
		||||
            } else {
 | 
			
		||||
                channel.write(arguments.getBytesCoerced(0));
 | 
			
		||||
            }
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            throw new LuaException(e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Write a string of characters to the file, following them with a new line character.
 | 
			
		||||
     *
 | 
			
		||||
     * @param text The text to write to the file.
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
     */
 | 
			
		||||
    public void writeLine(Coerced<ByteBuffer> text) throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        try {
 | 
			
		||||
            channel.write(text.value());
 | 
			
		||||
            writeSingle((byte) '\n');
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            throw new LuaException(e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void writeSingle(byte value) throws IOException {
 | 
			
		||||
        clear(single);
 | 
			
		||||
        single.put(value);
 | 
			
		||||
        flip(single);
 | 
			
		||||
        channel.write(single);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save the current file without closing it.
 | 
			
		||||
     *
 | 
			
		||||
     * @throws LuaException If the file has been closed.
 | 
			
		||||
     */
 | 
			
		||||
    public void flush() throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
        try {
 | 
			
		||||
            // Technically this is not needed
 | 
			
		||||
            if (channel instanceof FileChannel channel) channel.force(false);
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            throw new LuaException(e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The ByteBuffer.clear():ByteBuffer overrides don't exist on Java 8, so add some wrapper functions to avoid that.
 | 
			
		||||
 | 
			
		||||
    private static void clear(Buffer buffer) {
 | 
			
		||||
        buffer.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void flip(Buffer buffer) {
 | 
			
		||||
        buffer.flip();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,83 @@
 | 
			
		||||
 | 
			
		||||
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.handles;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.channels.ClosedChannelException;
 | 
			
		||||
import java.nio.channels.NonWritableChannelException;
 | 
			
		||||
import java.nio.channels.SeekableByteChannel;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A seekable, readable byte channel which is backed by a simple byte array.
 | 
			
		||||
 */
 | 
			
		||||
public class ArrayByteChannel implements SeekableByteChannel {
 | 
			
		||||
    private boolean closed = false;
 | 
			
		||||
    private int position = 0;
 | 
			
		||||
 | 
			
		||||
    private final byte[] backing;
 | 
			
		||||
 | 
			
		||||
    public ArrayByteChannel(byte[] backing) {
 | 
			
		||||
        this.backing = backing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int read(ByteBuffer destination) throws ClosedChannelException {
 | 
			
		||||
        if (closed) throw new ClosedChannelException();
 | 
			
		||||
        Objects.requireNonNull(destination, "destination");
 | 
			
		||||
 | 
			
		||||
        if (position >= backing.length) return -1;
 | 
			
		||||
 | 
			
		||||
        var remaining = Math.min(backing.length - position, destination.remaining());
 | 
			
		||||
        destination.put(backing, position, remaining);
 | 
			
		||||
        position += remaining;
 | 
			
		||||
        return remaining;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int write(ByteBuffer src) throws ClosedChannelException {
 | 
			
		||||
        if (closed) throw new ClosedChannelException();
 | 
			
		||||
        throw new NonWritableChannelException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public long position() throws ClosedChannelException {
 | 
			
		||||
        if (closed) throw new ClosedChannelException();
 | 
			
		||||
        return position;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public SeekableByteChannel position(long newPosition) throws ClosedChannelException {
 | 
			
		||||
        if (closed) throw new ClosedChannelException();
 | 
			
		||||
        if (newPosition < 0 || newPosition > Integer.MAX_VALUE) {
 | 
			
		||||
            throw new IllegalArgumentException("Position out of bounds");
 | 
			
		||||
        }
 | 
			
		||||
        position = (int) newPosition;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public long size() throws ClosedChannelException {
 | 
			
		||||
        if (closed) throw new ClosedChannelException();
 | 
			
		||||
        return backing.length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public SeekableByteChannel truncate(long size) throws ClosedChannelException {
 | 
			
		||||
        if (closed) throw new ClosedChannelException();
 | 
			
		||||
        throw new NonWritableChannelException();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isOpen() {
 | 
			
		||||
        return !closed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void close() {
 | 
			
		||||
        closed = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.handles;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.nio.channels.SeekableByteChannel;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A file handle opened for reading with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)}.
 | 
			
		||||
 *
 | 
			
		||||
 * @cc.module fs.ReadHandle
 | 
			
		||||
 */
 | 
			
		||||
public class ReadHandle extends AbstractHandle {
 | 
			
		||||
    public ReadHandle(SeekableByteChannel channel, boolean binary) {
 | 
			
		||||
        super(channel, binary);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object @Nullable [] read(Optional<Integer> countArg) throws LuaException {
 | 
			
		||||
        return super.read(countArg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object @Nullable [] readAll() throws LuaException {
 | 
			
		||||
        return super.readAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object @Nullable [] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
 | 
			
		||||
        return super.readLine(withTrailingArg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritDoc}
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object @Nullable [] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
 | 
			
		||||
        return super.seek(whence, offset);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http;
 | 
			
		||||
 | 
			
		||||
import dan200.computer.core.IAPIEnvironment;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.util.concurrent.Future;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Checks a URL using {@link NetworkUtils#getAddress(String, int, boolean)}}
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This requires a DNS lookup, and so needs to occur off-thread.
 | 
			
		||||
 */
 | 
			
		||||
public class CheckUrl extends Resource<CheckUrl> {
 | 
			
		||||
    private static final String EVENT = "http_check";
 | 
			
		||||
 | 
			
		||||
    private @Nullable Future<?> future;
 | 
			
		||||
 | 
			
		||||
    private final IAPIEnvironment environment;
 | 
			
		||||
    private final String address;
 | 
			
		||||
    private final URI uri;
 | 
			
		||||
 | 
			
		||||
    public CheckUrl(ResourceGroup<CheckUrl> limiter, IAPIEnvironment environment, String address, URI uri) {
 | 
			
		||||
        super(limiter);
 | 
			
		||||
        this.environment = environment;
 | 
			
		||||
        this.address = address;
 | 
			
		||||
        this.uri = uri;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void run() {
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
        future = NetworkUtils.EXECUTOR.submit(this::doRun);
 | 
			
		||||
        checkClosed();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void doRun() {
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            var ssl = uri.getScheme().equalsIgnoreCase("https");
 | 
			
		||||
            var netAddress = NetworkUtils.getAddress(uri, ssl);
 | 
			
		||||
            NetworkUtils.getOptions(uri.getHost(), netAddress);
 | 
			
		||||
 | 
			
		||||
            if (tryClose()) environment.queueEvent(EVENT, new Object[]{ address, true });
 | 
			
		||||
        } catch (HTTPRequestException e) {
 | 
			
		||||
            if (tryClose()) {
 | 
			
		||||
                environment.queueEvent(EVENT, new Object[]{ address, false, NetworkUtils.toFriendlyError(e) });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void dispose() {
 | 
			
		||||
        super.dispose();
 | 
			
		||||
        future = closeFuture(future);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http;
 | 
			
		||||
 | 
			
		||||
import java.io.Serial;
 | 
			
		||||
 | 
			
		||||
public class HTTPRequestException extends Exception {
 | 
			
		||||
    @Serial
 | 
			
		||||
    private static final long serialVersionUID = 7591208619422744652L;
 | 
			
		||||
 | 
			
		||||
    public HTTPRequestException(String s) {
 | 
			
		||||
        super(s);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Throwable fillInStackTrace() {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,196 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.core.apis.http.options.Action;
 | 
			
		||||
import dan200.computercraft.core.apis.http.options.Options;
 | 
			
		||||
import dan200.computercraft.core.util.ThreadUtils;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import io.netty.channel.ConnectTimeoutException;
 | 
			
		||||
import io.netty.channel.EventLoopGroup;
 | 
			
		||||
import io.netty.channel.nio.NioEventLoopGroup;
 | 
			
		||||
import io.netty.channel.socket.SocketChannel;
 | 
			
		||||
import io.netty.handler.codec.DecoderException;
 | 
			
		||||
import io.netty.handler.codec.TooLongFrameException;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
 | 
			
		||||
import io.netty.handler.ssl.SslContext;
 | 
			
		||||
import io.netty.handler.ssl.SslContextBuilder;
 | 
			
		||||
import io.netty.handler.ssl.SslHandler;
 | 
			
		||||
import io.netty.handler.timeout.ReadTimeoutException;
 | 
			
		||||
import io.netty.handler.traffic.AbstractTrafficShapingHandler;
 | 
			
		||||
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import javax.net.ssl.SSLException;
 | 
			
		||||
import javax.net.ssl.SSLHandshakeException;
 | 
			
		||||
import java.net.InetSocketAddress;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import static cc.tweaked.CCTweaked.LOG;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Just a shared object for executing simple HTTP related tasks.
 | 
			
		||||
 */
 | 
			
		||||
public final class NetworkUtils {
 | 
			
		||||
    public static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(4, ThreadUtils.lowPriorityFactory("Network"));
 | 
			
		||||
    public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup(4, ThreadUtils.lowPriorityFactory("Netty"));
 | 
			
		||||
 | 
			
		||||
    private static final AbstractTrafficShapingHandler SHAPING_HANDLER = new GlobalTrafficShapingHandler(
 | 
			
		||||
        EXECUTOR, 32 * 1024 * 1024, 32 * 1024 * 1024
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    static {
 | 
			
		||||
        EXECUTOR.setKeepAliveTime(60, TimeUnit.SECONDS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private NetworkUtils() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final Object sslLock = new Object();
 | 
			
		||||
    private static @Nullable SslContext sslContext;
 | 
			
		||||
    private static boolean triedSslContext = false;
 | 
			
		||||
 | 
			
		||||
    private static @Nullable SslContext makeSslContext() {
 | 
			
		||||
        if (triedSslContext) return sslContext;
 | 
			
		||||
        synchronized (sslLock) {
 | 
			
		||||
            if (triedSslContext) return sslContext;
 | 
			
		||||
 | 
			
		||||
            triedSslContext = true;
 | 
			
		||||
            try {
 | 
			
		||||
                return sslContext = SslContextBuilder.forClient().build();
 | 
			
		||||
            } catch (SSLException e) {
 | 
			
		||||
                LOG.log(Level.SEVERE, "Cannot construct SSL context", e);
 | 
			
		||||
                return sslContext = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static SslContext getSslContext() throws HTTPRequestException {
 | 
			
		||||
        var ssl = makeSslContext();
 | 
			
		||||
        if (ssl == null) throw new HTTPRequestException("Could not create a secure connection");
 | 
			
		||||
        return ssl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void reset() {
 | 
			
		||||
        SHAPING_HANDLER.trafficCounter().resetCumulativeTime();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link InetSocketAddress} from a {@link java.net.URI}.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
 | 
			
		||||
     *
 | 
			
		||||
     * @param uri The URI to fetch.
 | 
			
		||||
     * @param ssl Whether to connect with SSL. This is used to find the default port if not otherwise specified.
 | 
			
		||||
     * @return The resolved address.
 | 
			
		||||
     * @throws HTTPRequestException If the host is not malformed.
 | 
			
		||||
     */
 | 
			
		||||
    public static InetSocketAddress getAddress(URI uri, boolean ssl) throws HTTPRequestException {
 | 
			
		||||
        return getAddress(uri.getHost(), uri.getPort(), ssl);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link InetSocketAddress} from the resolved {@code host} and port.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The host to resolve.
 | 
			
		||||
     * @param port The port, or -1 if not defined.
 | 
			
		||||
     * @param ssl  Whether to connect with SSL. This is used to find the default port if not otherwise specified.
 | 
			
		||||
     * @return The resolved address.
 | 
			
		||||
     * @throws HTTPRequestException If the host is not malformed.
 | 
			
		||||
     */
 | 
			
		||||
    public static InetSocketAddress getAddress(String host, int port, boolean ssl) throws HTTPRequestException {
 | 
			
		||||
        if (port < 0) port = ssl ? 443 : 80;
 | 
			
		||||
        var socketAddress = new InetSocketAddress(host, port);
 | 
			
		||||
        if (socketAddress.isUnresolved()) throw new HTTPRequestException("Unknown host");
 | 
			
		||||
        return socketAddress;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get options for a specific domain.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host    The host to resolve.
 | 
			
		||||
     * @param address The address, resolved by {@link #getAddress(String, int, boolean)}.
 | 
			
		||||
     * @return The options for this host.
 | 
			
		||||
     * @throws HTTPRequestException If the host is not permitted
 | 
			
		||||
     */
 | 
			
		||||
    public static Options getOptions(String host, InetSocketAddress address) throws HTTPRequestException {
 | 
			
		||||
        return new Options(Action.ALLOW, 4 * 1024 * 1024, 16 * 1024 * 1024, 128 * 1024, false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Make an SSL handler for the remote host.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ch         The channel the handler will be added to.
 | 
			
		||||
     * @param sslContext The SSL context, if present.
 | 
			
		||||
     * @param timeout    The timeout on this channel.
 | 
			
		||||
     * @param peerHost   The host to connect to.
 | 
			
		||||
     * @param peerPort   The port to connect to.
 | 
			
		||||
     * @return The SSL handler.
 | 
			
		||||
     * @see io.netty.handler.ssl.SslHandler
 | 
			
		||||
     */
 | 
			
		||||
    private static SslHandler makeSslHandler(SocketChannel ch, SslContext sslContext, int timeout, String peerHost, int peerPort) {
 | 
			
		||||
        var handler = sslContext.newHandler(ch.alloc(), peerHost, peerPort);
 | 
			
		||||
        if (timeout > 0) handler.setHandshakeTimeoutMillis(timeout);
 | 
			
		||||
        return handler;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set up some basic properties of the channel. This adds a timeout, the traffic shaping handler, and the SSL
 | 
			
		||||
     * handler.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ch            The channel to initialise.
 | 
			
		||||
     * @param uri           The URI to connect to.
 | 
			
		||||
     * @param socketAddress The address of the socket to connect to.
 | 
			
		||||
     * @param sslContext    The SSL context, if present.
 | 
			
		||||
     * @param proxy         The proxy handler, if present.
 | 
			
		||||
     * @param timeout       The timeout on this channel.
 | 
			
		||||
     * @see io.netty.channel.ChannelInitializer
 | 
			
		||||
     */
 | 
			
		||||
    public static void initChannel(SocketChannel ch, URI uri, InetSocketAddress socketAddress, @Nullable SslContext sslContext, @Nullable Consumer<SocketChannel> proxy, int timeout) {
 | 
			
		||||
        if (timeout > 0) ch.config().setConnectTimeoutMillis(timeout);
 | 
			
		||||
 | 
			
		||||
        var p = ch.pipeline();
 | 
			
		||||
        p.addLast(SHAPING_HANDLER);
 | 
			
		||||
 | 
			
		||||
        if (proxy != null) proxy.accept(ch);
 | 
			
		||||
 | 
			
		||||
        if (sslContext != null) {
 | 
			
		||||
            p.addLast(makeSslHandler(ch, sslContext, timeout, uri.getHost(), socketAddress.getPort()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Read a {@link ByteBuf} into a byte array.
 | 
			
		||||
     *
 | 
			
		||||
     * @param buffer The buffer to read.
 | 
			
		||||
     * @return The resulting bytes.
 | 
			
		||||
     */
 | 
			
		||||
    public static byte[] toBytes(ByteBuf buffer) {
 | 
			
		||||
        var bytes = new byte[buffer.readableBytes()];
 | 
			
		||||
        buffer.readBytes(bytes);
 | 
			
		||||
        return bytes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String toFriendlyError(Throwable cause) {
 | 
			
		||||
        if (cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException) {
 | 
			
		||||
            var message = cause.getMessage();
 | 
			
		||||
            return message == null ? "Could not connect" : message;
 | 
			
		||||
        } else if (cause instanceof TooLongFrameException) {
 | 
			
		||||
            return "Message is too large";
 | 
			
		||||
        } else if (cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException) {
 | 
			
		||||
            return "Timed out";
 | 
			
		||||
        } else if (cause instanceof SSLHandshakeException || (cause instanceof DecoderException && cause.getCause() instanceof SSLHandshakeException)) {
 | 
			
		||||
            return "Could not create a secure connection";
 | 
			
		||||
        } else {
 | 
			
		||||
            return "Could not connect";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										139
									
								
								src/main/java/dan200/computercraft/core/apis/http/Resource.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/main/java/dan200/computercraft/core/apis/http/Resource.java
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http;
 | 
			
		||||
 | 
			
		||||
import io.netty.channel.ChannelFuture;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.lang.ref.Reference;
 | 
			
		||||
import java.lang.ref.ReferenceQueue;
 | 
			
		||||
import java.lang.ref.WeakReference;
 | 
			
		||||
import java.util.concurrent.Future;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A holder for one or more resources, with a lifetime.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of this resource. Should be the class extending from {@link Resource}.
 | 
			
		||||
 */
 | 
			
		||||
public abstract class Resource<T extends Resource<T>> implements Closeable {
 | 
			
		||||
    private final AtomicBoolean closed = new AtomicBoolean(false);
 | 
			
		||||
    private final ResourceGroup<T> limiter;
 | 
			
		||||
 | 
			
		||||
    protected Resource(ResourceGroup<T> limiter) {
 | 
			
		||||
        this.limiter = limiter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether this resource is closed.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether this resource is closed.
 | 
			
		||||
     */
 | 
			
		||||
    public final boolean isClosed() {
 | 
			
		||||
        return closed.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if this has been cancelled. If so, it'll clean up any existing resources and cancel any pending futures.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether this resource has been closed.
 | 
			
		||||
     */
 | 
			
		||||
    public final boolean checkClosed() {
 | 
			
		||||
        if (!closed.get()) return false;
 | 
			
		||||
        dispose();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Try to close the current resource.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether this was successfully closed, or {@code false} if it has already been closed.
 | 
			
		||||
     */
 | 
			
		||||
    protected final boolean tryClose() {
 | 
			
		||||
        if (closed.getAndSet(true)) return false;
 | 
			
		||||
        dispose();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Clean up any pending resources
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Note, this may be called multiple times, and so should be thread-safe and
 | 
			
		||||
     * avoid any major side effects.
 | 
			
		||||
     */
 | 
			
		||||
    protected void dispose() {
 | 
			
		||||
        @SuppressWarnings("unchecked")
 | 
			
		||||
        var thisT = (T) this;
 | 
			
		||||
        limiter.release(thisT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a {@link WeakReference} which will close {@code this} when collected.
 | 
			
		||||
     *
 | 
			
		||||
     * @param <R>    The object we are wrapping in a reference.
 | 
			
		||||
     * @param object The object to reference to
 | 
			
		||||
     * @return The weak reference.
 | 
			
		||||
     */
 | 
			
		||||
    protected <R> WeakReference<R> createOwnerReference(R object) {
 | 
			
		||||
        return new CloseReference<>(this, object);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void close() {
 | 
			
		||||
        tryClose();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public final boolean queue(Consumer<T> task) {
 | 
			
		||||
        @SuppressWarnings("unchecked")
 | 
			
		||||
        var thisT = (T) this;
 | 
			
		||||
        return limiter.queue(thisT, () -> task.accept(thisT));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    protected static <T extends Closeable> T closeCloseable(@Nullable T closeable) {
 | 
			
		||||
        try {
 | 
			
		||||
            if (closeable != null) closeable.close();
 | 
			
		||||
        } catch (IOException ignored) {
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    protected static ChannelFuture closeChannel(@Nullable ChannelFuture future) {
 | 
			
		||||
        if (future != null) {
 | 
			
		||||
            future.cancel(false);
 | 
			
		||||
            var channel = future.channel();
 | 
			
		||||
            if (channel != null && channel.isOpen()) channel.close();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    protected static <T extends Future<?>> T closeFuture(@Nullable T future) {
 | 
			
		||||
        if (future != null) future.cancel(true);
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<>();
 | 
			
		||||
 | 
			
		||||
    private static class CloseReference<T> extends WeakReference<T> {
 | 
			
		||||
        final Resource<?> resource;
 | 
			
		||||
 | 
			
		||||
        CloseReference(Resource<?> resource, T referent) {
 | 
			
		||||
            super(referent, QUEUE);
 | 
			
		||||
            this.resource = resource;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void cleanup() {
 | 
			
		||||
        Reference<?> reference;
 | 
			
		||||
        while ((reference = QUEUE.poll()) != null) ((CloseReference<?>) reference).resource.close();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http;
 | 
			
		||||
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
import java.util.function.IntSupplier;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A collection of {@link Resource}s, with an upper bound on capacity.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of the resource this group manages.
 | 
			
		||||
 */
 | 
			
		||||
public class ResourceGroup<T extends Resource<T>> {
 | 
			
		||||
    public static final int DEFAULT_LIMIT = 512;
 | 
			
		||||
 | 
			
		||||
    final IntSupplier limit;
 | 
			
		||||
 | 
			
		||||
    boolean active = false;
 | 
			
		||||
 | 
			
		||||
    final Set<T> resources = Collections.newSetFromMap(new ConcurrentHashMap<>());
 | 
			
		||||
 | 
			
		||||
    public ResourceGroup(IntSupplier limit) {
 | 
			
		||||
        this.limit = limit;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void startup() {
 | 
			
		||||
        active = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized void shutdown() {
 | 
			
		||||
        active = false;
 | 
			
		||||
 | 
			
		||||
        for (var resource : resources) resource.close();
 | 
			
		||||
        resources.clear();
 | 
			
		||||
 | 
			
		||||
        Resource.cleanup();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public final boolean queue(T resource, Runnable setup) {
 | 
			
		||||
        return queue(() -> {
 | 
			
		||||
            setup.run();
 | 
			
		||||
            return resource;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized boolean queue(Supplier<T> resource) {
 | 
			
		||||
        Resource.cleanup();
 | 
			
		||||
        if (!active) return false;
 | 
			
		||||
 | 
			
		||||
        var limit = this.limit.getAsInt();
 | 
			
		||||
        if (limit <= 0 || resources.size() < limit) {
 | 
			
		||||
            resources.add(resource.get());
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized void release(T resource) {
 | 
			
		||||
        resources.remove(resource);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayDeque;
 | 
			
		||||
import java.util.function.IntSupplier;
 | 
			
		||||
import java.util.function.Supplier;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A {@link ResourceGroup} which will queue items when the group at capacity.
 | 
			
		||||
 *
 | 
			
		||||
 * @param <T> The type of the resource this queue manages.
 | 
			
		||||
 */
 | 
			
		||||
public class ResourceQueue<T extends Resource<T>> extends ResourceGroup<T> {
 | 
			
		||||
    private final ArrayDeque<Supplier<T>> pending = new ArrayDeque<>();
 | 
			
		||||
 | 
			
		||||
    public ResourceQueue(IntSupplier limit) {
 | 
			
		||||
        super(limit);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public synchronized void shutdown() {
 | 
			
		||||
        super.shutdown();
 | 
			
		||||
        pending.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public synchronized boolean queue(Supplier<T> resource) {
 | 
			
		||||
        if (!active) return false;
 | 
			
		||||
        if (super.queue(resource)) return true;
 | 
			
		||||
        if (pending.size() > DEFAULT_LIMIT) return false;
 | 
			
		||||
 | 
			
		||||
        pending.add(resource);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public synchronized void release(T resource) {
 | 
			
		||||
        super.release(resource);
 | 
			
		||||
 | 
			
		||||
        if (!active) return;
 | 
			
		||||
 | 
			
		||||
        var limit = this.limit.getAsInt();
 | 
			
		||||
        if (limit <= 0 || resources.size() < limit) {
 | 
			
		||||
            var next = pending.poll();
 | 
			
		||||
            if (next != null) resources.add(next.get());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.options;
 | 
			
		||||
 | 
			
		||||
public enum Action {
 | 
			
		||||
    ALLOW,
 | 
			
		||||
    DENY;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.options;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for a given HTTP request or websocket, which control its resource constraints.
 | 
			
		||||
 *
 | 
			
		||||
 * @param action           Whether to {@link Action#ALLOW} or {@link Action#DENY} this request.
 | 
			
		||||
 * @param maxUpload        The maximum size of the HTTP request.
 | 
			
		||||
 * @param maxDownload      The maximum size of the HTTP response.
 | 
			
		||||
 * @param websocketMessage The maximum size of a websocket message (outgoing and incoming).
 | 
			
		||||
 * @param useProxy         Whether to use the configured proxy.
 | 
			
		||||
 */
 | 
			
		||||
public record Options(Action action, long maxUpload, long maxDownload, int websocketMessage, boolean useProxy) {
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,211 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.request;
 | 
			
		||||
 | 
			
		||||
import dan200.computer.core.IAPIEnvironment;
 | 
			
		||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
 | 
			
		||||
import dan200.computercraft.core.apis.http.NetworkUtils;
 | 
			
		||||
import dan200.computercraft.core.apis.http.Resource;
 | 
			
		||||
import dan200.computercraft.core.apis.http.ResourceGroup;
 | 
			
		||||
import io.netty.bootstrap.Bootstrap;
 | 
			
		||||
import io.netty.buffer.ByteBuf;
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import io.netty.channel.ChannelFuture;
 | 
			
		||||
import io.netty.channel.ChannelInitializer;
 | 
			
		||||
import io.netty.channel.socket.SocketChannel;
 | 
			
		||||
import io.netty.channel.socket.nio.NioSocketChannel;
 | 
			
		||||
import io.netty.handler.codec.http.*;
 | 
			
		||||
import io.netty.handler.timeout.ReadTimeoutHandler;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.concurrent.Future;
 | 
			
		||||
import java.util.concurrent.TimeUnit;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import static cc.tweaked.CCTweaked.LOG;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents an in-progress HTTP request.
 | 
			
		||||
 */
 | 
			
		||||
public class HttpRequest extends Resource<HttpRequest> {
 | 
			
		||||
    private static final String SUCCESS_EVENT = "http_success";
 | 
			
		||||
    private static final String FAILURE_EVENT = "http_failure";
 | 
			
		||||
 | 
			
		||||
    private static final int MAX_REDIRECTS = 16;
 | 
			
		||||
 | 
			
		||||
    private @Nullable Future<?> executorFuture;
 | 
			
		||||
    private @Nullable ChannelFuture connectFuture;
 | 
			
		||||
    private @Nullable HttpRequestHandler currentRequest;
 | 
			
		||||
 | 
			
		||||
    private final IAPIEnvironment environment;
 | 
			
		||||
 | 
			
		||||
    private final String address;
 | 
			
		||||
    private final ByteBuf postBuffer;
 | 
			
		||||
    private final HttpHeaders headers;
 | 
			
		||||
    private final boolean binary;
 | 
			
		||||
    private final int timeout;
 | 
			
		||||
 | 
			
		||||
    final AtomicInteger redirects;
 | 
			
		||||
 | 
			
		||||
    public HttpRequest(
 | 
			
		||||
        ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable ByteBuffer postBody,
 | 
			
		||||
        HttpHeaders headers, boolean binary, boolean followRedirects, int timeout
 | 
			
		||||
    ) {
 | 
			
		||||
        super(limiter);
 | 
			
		||||
        this.environment = environment;
 | 
			
		||||
        this.address = address;
 | 
			
		||||
        postBuffer = postBody != null
 | 
			
		||||
            ? Unpooled.wrappedBuffer(postBody)
 | 
			
		||||
            : Unpooled.buffer(0);
 | 
			
		||||
        this.headers = headers;
 | 
			
		||||
        this.binary = binary;
 | 
			
		||||
        redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0);
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
 | 
			
		||||
        if (postBody != null) {
 | 
			
		||||
            if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) {
 | 
			
		||||
                headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
 | 
			
		||||
                headers.set(HttpHeaderNames.CONTENT_LENGTH, postBuffer.readableBytes());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public IAPIEnvironment environment() {
 | 
			
		||||
        return environment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static URI checkUri(String address) throws HTTPRequestException {
 | 
			
		||||
        URI url;
 | 
			
		||||
        try {
 | 
			
		||||
            url = new URI(address);
 | 
			
		||||
        } catch (URISyntaxException e) {
 | 
			
		||||
            throw new HTTPRequestException("URL malformed");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        checkUri(url);
 | 
			
		||||
        return url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void checkUri(URI url) throws HTTPRequestException {
 | 
			
		||||
        // Validate the URL
 | 
			
		||||
        if (url.getScheme() == null) throw new HTTPRequestException("Must specify http or https");
 | 
			
		||||
        if (url.getHost() == null) throw new HTTPRequestException("URL malformed");
 | 
			
		||||
 | 
			
		||||
        var scheme = url.getScheme().toLowerCase(Locale.ROOT);
 | 
			
		||||
        if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
 | 
			
		||||
            throw new HTTPRequestException("Invalid protocol '" + scheme + "'");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void request(URI uri, HttpMethod method) {
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
        executorFuture = NetworkUtils.EXECUTOR.submit(() -> doRequest(uri, method));
 | 
			
		||||
        checkClosed();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void doRequest(URI uri, HttpMethod method) {
 | 
			
		||||
        // If we're cancelled, abort.
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            var ssl = uri.getScheme().equalsIgnoreCase("https");
 | 
			
		||||
            var socketAddress = NetworkUtils.getAddress(uri, ssl);
 | 
			
		||||
            var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
 | 
			
		||||
            var sslContext = ssl ? NetworkUtils.getSslContext() : null;
 | 
			
		||||
 | 
			
		||||
            // getAddress may have a slight delay, so let's perform another cancellation check.
 | 
			
		||||
            if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
            var requestBody = getHeaderSize(headers) + postBuffer.capacity();
 | 
			
		||||
            if (options.maxUpload() != 0 && requestBody > options.maxUpload()) {
 | 
			
		||||
                failure("Request body is too large");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var handler = currentRequest = new HttpRequestHandler(this, uri, method, options);
 | 
			
		||||
            connectFuture = new Bootstrap()
 | 
			
		||||
                .group(NetworkUtils.LOOP_GROUP)
 | 
			
		||||
                .channelFactory(NioSocketChannel::new)
 | 
			
		||||
                .handler(new ChannelInitializer<SocketChannel>() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    protected void initChannel(SocketChannel ch) {
 | 
			
		||||
                        NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, null, timeout);
 | 
			
		||||
 | 
			
		||||
                        var p = ch.pipeline();
 | 
			
		||||
                        if (timeout > 0) p.addLast(new ReadTimeoutHandler(timeout, TimeUnit.MILLISECONDS));
 | 
			
		||||
 | 
			
		||||
                        p.addLast(
 | 
			
		||||
                            new HttpClientCodec(),
 | 
			
		||||
                            new HttpContentDecompressor(),
 | 
			
		||||
                            handler
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .remoteAddress(socketAddress)
 | 
			
		||||
                .connect()
 | 
			
		||||
                .addListener(c -> {
 | 
			
		||||
                    if (!c.isSuccess()) failure(NetworkUtils.toFriendlyError(c.cause()));
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            // Do an additional check for cancellation
 | 
			
		||||
            checkClosed();
 | 
			
		||||
        } catch (HTTPRequestException e) {
 | 
			
		||||
            failure(NetworkUtils.toFriendlyError(e));
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            failure(NetworkUtils.toFriendlyError(e));
 | 
			
		||||
            LOG.log(Level.SEVERE, "Error in HTTP request", e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void failure(String message) {
 | 
			
		||||
        if (tryClose()) environment.queueEvent(FAILURE_EVENT, new Object[]{ address, message });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void failure(String message, HttpResponseHandle object) {
 | 
			
		||||
        if (tryClose()) environment.queueEvent(FAILURE_EVENT, new Object[]{ address, message, object });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void success(HttpResponseHandle object) {
 | 
			
		||||
        if (tryClose()) environment.queueEvent(SUCCESS_EVENT, new Object[]{ address, object });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void dispose() {
 | 
			
		||||
        super.dispose();
 | 
			
		||||
 | 
			
		||||
        executorFuture = closeFuture(executorFuture);
 | 
			
		||||
        connectFuture = closeChannel(connectFuture);
 | 
			
		||||
        currentRequest = closeCloseable(currentRequest);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static long getHeaderSize(HttpHeaders headers) {
 | 
			
		||||
        long size = 0;
 | 
			
		||||
        for (var header : headers) {
 | 
			
		||||
            size += header.getKey() == null ? 0 : header.getKey().length();
 | 
			
		||||
            size += header.getValue() == null ? 0 : header.getValue().length() + 1;
 | 
			
		||||
        }
 | 
			
		||||
        return size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ByteBuf body() {
 | 
			
		||||
        return postBuffer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public HttpHeaders headers() {
 | 
			
		||||
        return headers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isBinary() {
 | 
			
		||||
        return binary;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,215 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.request;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
 | 
			
		||||
import dan200.computercraft.core.apis.http.NetworkUtils;
 | 
			
		||||
import dan200.computercraft.core.apis.http.options.Options;
 | 
			
		||||
import io.netty.buffer.CompositeByteBuf;
 | 
			
		||||
import io.netty.channel.ChannelHandlerContext;
 | 
			
		||||
import io.netty.channel.SimpleChannelInboundHandler;
 | 
			
		||||
import io.netty.handler.codec.http.*;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.charset.Charset;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpObject> implements Closeable {
 | 
			
		||||
    /**
 | 
			
		||||
     * Same as {@link io.netty.handler.codec.MessageAggregator}.
 | 
			
		||||
     */
 | 
			
		||||
    private static final int DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS = 1024;
 | 
			
		||||
 | 
			
		||||
    private static final byte[] EMPTY_BYTES = new byte[0];
 | 
			
		||||
 | 
			
		||||
    private final HttpRequest request;
 | 
			
		||||
    private boolean closed = false;
 | 
			
		||||
 | 
			
		||||
    private final URI uri;
 | 
			
		||||
    private final HttpMethod method;
 | 
			
		||||
    private final Options options;
 | 
			
		||||
 | 
			
		||||
    private @Nullable Charset responseCharset;
 | 
			
		||||
    private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
 | 
			
		||||
    private @Nullable HttpResponseStatus responseStatus;
 | 
			
		||||
    private @Nullable CompositeByteBuf responseBody;
 | 
			
		||||
 | 
			
		||||
    HttpRequestHandler(HttpRequest request, URI uri, HttpMethod method, Options options) {
 | 
			
		||||
        this.request = request;
 | 
			
		||||
 | 
			
		||||
        this.uri = uri;
 | 
			
		||||
        this.method = method;
 | 
			
		||||
        this.options = options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
 | 
			
		||||
        if (request.checkClosed()) return;
 | 
			
		||||
 | 
			
		||||
        var body = request.body();
 | 
			
		||||
        body.resetReaderIndex().retain();
 | 
			
		||||
 | 
			
		||||
        var requestUri = uri.getRawPath();
 | 
			
		||||
        if (uri.getRawQuery() != null) requestUri += "?" + uri.getRawQuery();
 | 
			
		||||
 | 
			
		||||
        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body);
 | 
			
		||||
        request.setMethod(method);
 | 
			
		||||
        request.headers().set(this.request.headers());
 | 
			
		||||
 | 
			
		||||
        // We force some headers to be always applied
 | 
			
		||||
        if (!request.headers().contains(HttpHeaderNames.ACCEPT_CHARSET)) {
 | 
			
		||||
            request.headers().set(HttpHeaderNames.ACCEPT_CHARSET, "UTF-8");
 | 
			
		||||
        }
 | 
			
		||||
        request.headers().set(HttpHeaderNames.HOST, uri.getPort() < 0 ? uri.getHost() : uri.getHost() + ":" + uri.getPort());
 | 
			
		||||
        request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
 | 
			
		||||
 | 
			
		||||
        ctx.channel().writeAndFlush(request);
 | 
			
		||||
 | 
			
		||||
        super.channelActive(ctx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
 | 
			
		||||
        if (!closed) request.failure("Could not connect");
 | 
			
		||||
        super.channelInactive(ctx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void channelRead0(ChannelHandlerContext ctx, HttpObject message) {
 | 
			
		||||
        if (closed || request.checkClosed()) return;
 | 
			
		||||
 | 
			
		||||
        if (message instanceof HttpResponse response) {
 | 
			
		||||
 | 
			
		||||
            if (request.redirects.get() > 0) {
 | 
			
		||||
                var redirect = getRedirect(response.status(), response.headers());
 | 
			
		||||
                if (redirect != null && !uri.equals(redirect) && request.redirects.getAndDecrement() > 0) {
 | 
			
		||||
                    // If we have a redirect, and don't end up at the same place, then follow it.
 | 
			
		||||
 | 
			
		||||
                    // We mark ourselves as disposed first though, to avoid firing events when the channel
 | 
			
		||||
                    // becomes inactive or disposed.
 | 
			
		||||
                    closed = true;
 | 
			
		||||
                    ctx.close();
 | 
			
		||||
 | 
			
		||||
                    try {
 | 
			
		||||
                        HttpRequest.checkUri(redirect);
 | 
			
		||||
                    } catch (HTTPRequestException e) {
 | 
			
		||||
                        // If we cannot visit this uri, then fail.
 | 
			
		||||
                        request.failure(NetworkUtils.toFriendlyError(e));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    request.request(redirect, response.status().code() == 303 ? HttpMethod.GET : method);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            responseCharset = HttpUtil.getCharset(response, StandardCharsets.UTF_8);
 | 
			
		||||
            responseStatus = response.status();
 | 
			
		||||
            responseHeaders.add(response.headers());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (message instanceof HttpContent content) {
 | 
			
		||||
 | 
			
		||||
            if (responseBody == null) {
 | 
			
		||||
                responseBody = ctx.alloc().compositeBuffer(DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var partial = content.content();
 | 
			
		||||
            if (partial.isReadable()) {
 | 
			
		||||
                // If we've read more than we're allowed to handle, abort as soon as possible.
 | 
			
		||||
                if (options.maxDownload() != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload()) {
 | 
			
		||||
                    closed = true;
 | 
			
		||||
                    ctx.close();
 | 
			
		||||
 | 
			
		||||
                    request.failure("Response is too large");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                responseBody.addComponent(true, partial.retain());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (message instanceof LastHttpContent last) {
 | 
			
		||||
                responseHeaders.add(last.trailingHeaders());
 | 
			
		||||
 | 
			
		||||
                // Set the content length, if not already given.
 | 
			
		||||
                if (responseHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)) {
 | 
			
		||||
                    responseHeaders.set(HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                ctx.close();
 | 
			
		||||
                sendResponse();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
 | 
			
		||||
        ctx.close();
 | 
			
		||||
        request.failure(NetworkUtils.toFriendlyError(cause));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void sendResponse() {
 | 
			
		||||
        Objects.requireNonNull(responseStatus, "Status has not been set");
 | 
			
		||||
        Objects.requireNonNull(responseCharset, "Charset has not been set");
 | 
			
		||||
 | 
			
		||||
        // Read the ByteBuf into a channel.
 | 
			
		||||
        var body = responseBody;
 | 
			
		||||
        var bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes(body);
 | 
			
		||||
 | 
			
		||||
        // Decode the headers
 | 
			
		||||
        var status = responseStatus;
 | 
			
		||||
        Map<String, String> headers = new HashMap<>();
 | 
			
		||||
        for (var header : responseHeaders) {
 | 
			
		||||
            var existing = headers.get(header.getKey());
 | 
			
		||||
            headers.put(header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Prepare to queue an event
 | 
			
		||||
        var stream = new HttpResponseHandle(bytes, request.isBinary(), status.code(), status.reasonPhrase(), headers);
 | 
			
		||||
 | 
			
		||||
        if (status.code() >= 200 && status.code() < 400) {
 | 
			
		||||
            request.success(stream);
 | 
			
		||||
        } else {
 | 
			
		||||
            request.failure(status.reasonPhrase(), stream);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine the redirect from this response.
 | 
			
		||||
     *
 | 
			
		||||
     * @param status  The status of the HTTP response.
 | 
			
		||||
     * @param headers The headers of the HTTP response.
 | 
			
		||||
     * @return The URI to redirect to, or {@code null} if no redirect should occur.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private URI getRedirect(HttpResponseStatus status, HttpHeaders headers) {
 | 
			
		||||
        var code = status.code();
 | 
			
		||||
        if (code < 300 || code > 307 || code == 304 || code == 306) return null;
 | 
			
		||||
 | 
			
		||||
        var location = headers.get(HttpHeaderNames.LOCATION);
 | 
			
		||||
        if (location == null) return null;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return uri.resolve(new URI(location));
 | 
			
		||||
        } catch (IllegalArgumentException | URISyntaxException e) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void close() {
 | 
			
		||||
        closed = true;
 | 
			
		||||
        if (responseBody != null) {
 | 
			
		||||
            responseBody.release();
 | 
			
		||||
            responseBody = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.request;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.IArguments;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.computercraft.core.apis.HTTPAPI;
 | 
			
		||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
 | 
			
		||||
import dan200.computercraft.core.apis.handles.ReadHandle;
 | 
			
		||||
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A http response. This provides the same methods as a {@link ReadHandle file}, though provides several request
 | 
			
		||||
 * specific methods.
 | 
			
		||||
 *
 | 
			
		||||
 * @cc.module http.Response
 | 
			
		||||
 * @see HTTPAPI#request(IArguments)  On how to make a http request.
 | 
			
		||||
 */
 | 
			
		||||
public class HttpResponseHandle extends ReadHandle {
 | 
			
		||||
    private final int responseCode;
 | 
			
		||||
    private final String responseStatus;
 | 
			
		||||
    private final Map<String, String> responseHeaders;
 | 
			
		||||
 | 
			
		||||
    public HttpResponseHandle(byte[] buffer, boolean isBinary, int responseCode, String responseStatus, Map<String, String> responseHeaders) {
 | 
			
		||||
        super(new ArrayByteChannel(buffer), isBinary);
 | 
			
		||||
        this.responseCode = responseCode;
 | 
			
		||||
        this.responseStatus = responseStatus;
 | 
			
		||||
        this.responseHeaders = responseHeaders;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the response code and response message returned by the server.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The response code and message.
 | 
			
		||||
     * @cc.treturn number The response code (i.e. 200)
 | 
			
		||||
     * @cc.treturn string The response message (i.e. "OK")
 | 
			
		||||
     * @cc.changed 1.80pr1.13 Added response message return value.
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Object[] getResponseCode() {
 | 
			
		||||
        return new Object[]{responseCode, responseStatus};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a table containing the response's headers, in a format similar to that required by {@link HTTPAPI#request}.
 | 
			
		||||
     * If multiple headers are sent with the same name, they will be combined with a comma.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The response's headers.
 | 
			
		||||
     * @cc.usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), and print the
 | 
			
		||||
     * returned headers.
 | 
			
		||||
     * <pre>{@code
 | 
			
		||||
     * local request = http.get("https://example.tweaked.cc")
 | 
			
		||||
     * print(textutils.serialize(request.getResponseHeaders()))
 | 
			
		||||
     * -- => {
 | 
			
		||||
     * --  [ "Content-Type" ] = "text/plain; charset=utf8",
 | 
			
		||||
     * --  [ "content-length" ] = 17,
 | 
			
		||||
     * --  ...
 | 
			
		||||
     * -- }
 | 
			
		||||
     * request.close()
 | 
			
		||||
     * }</pre>
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final Map<String, String> getResponseHeaders() {
 | 
			
		||||
        return responseHeaders;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.websocket;
 | 
			
		||||
 | 
			
		||||
import io.netty.handler.codec.http.FullHttpRequest;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaderNames;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaders;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
 | 
			
		||||
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the
 | 
			
		||||
 * original HTTP request.
 | 
			
		||||
 */
 | 
			
		||||
class NoOriginWebSocketHandshaker extends WebSocketClientHandshaker13 {
 | 
			
		||||
    NoOriginWebSocketHandshaker(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
 | 
			
		||||
        super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected FullHttpRequest newHandshakeRequest() {
 | 
			
		||||
        var request = super.newHandshakeRequest();
 | 
			
		||||
        var headers = request.headers();
 | 
			
		||||
        if (!customHeaders.contains(HttpHeaderNames.ORIGIN)) headers.remove(HttpHeaderNames.ORIGIN);
 | 
			
		||||
        return request;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,192 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.websocket;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Strings;
 | 
			
		||||
import dan200.computer.core.IAPIEnvironment;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.core.apis.http.*;
 | 
			
		||||
import dan200.computercraft.core.apis.http.options.Options;
 | 
			
		||||
import dan200.computercraft.core.util.AtomicHelpers;
 | 
			
		||||
import io.netty.bootstrap.Bootstrap;
 | 
			
		||||
import io.netty.buffer.Unpooled;
 | 
			
		||||
import io.netty.channel.Channel;
 | 
			
		||||
import io.netty.channel.ChannelFuture;
 | 
			
		||||
import io.netty.channel.ChannelInitializer;
 | 
			
		||||
import io.netty.channel.socket.SocketChannel;
 | 
			
		||||
import io.netty.channel.socket.nio.NioSocketChannel;
 | 
			
		||||
import io.netty.handler.codec.http.HttpClientCodec;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaderNames;
 | 
			
		||||
import io.netty.handler.codec.http.HttpHeaders;
 | 
			
		||||
import io.netty.handler.codec.http.HttpObjectAggregator;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.*;
 | 
			
		||||
import io.netty.util.concurrent.GenericFutureListener;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.concurrent.Future;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import static cc.tweaked.CCTweaked.LOG;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides functionality to verify and connect to a remote websocket.
 | 
			
		||||
 */
 | 
			
		||||
public class Websocket extends Resource<Websocket> implements WebsocketClient {
 | 
			
		||||
    /**
 | 
			
		||||
     * We declare the maximum size to be 2^30 bytes. While messages can be much longer, we set an arbitrary limit as
 | 
			
		||||
     * working with larger messages (especially within a Lua VM) is absurd.
 | 
			
		||||
     */
 | 
			
		||||
    public static final int MAX_MESSAGE_SIZE = 1 << 30;
 | 
			
		||||
 | 
			
		||||
    private @Nullable Future<?> executorFuture;
 | 
			
		||||
    private @Nullable ChannelFuture channelFuture;
 | 
			
		||||
 | 
			
		||||
    private final IAPIEnvironment environment;
 | 
			
		||||
    private final URI uri;
 | 
			
		||||
    private final String address;
 | 
			
		||||
    private final HttpHeaders headers;
 | 
			
		||||
    private final int timeout;
 | 
			
		||||
 | 
			
		||||
    private final AtomicInteger inFlight = new AtomicInteger(0);
 | 
			
		||||
    private final GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Void>> onSend = f -> inFlight.decrementAndGet();
 | 
			
		||||
 | 
			
		||||
    public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) {
 | 
			
		||||
        super(limiter);
 | 
			
		||||
        this.environment = environment;
 | 
			
		||||
        this.uri = uri;
 | 
			
		||||
        this.address = address;
 | 
			
		||||
        this.headers = headers;
 | 
			
		||||
        this.timeout = timeout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void connect() {
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
        executorFuture = NetworkUtils.EXECUTOR.submit(this::doConnect);
 | 
			
		||||
        checkClosed();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void doConnect() {
 | 
			
		||||
        // If we're cancelled, abort.
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            var ssl = uri.getScheme().equalsIgnoreCase("wss");
 | 
			
		||||
            var socketAddress = NetworkUtils.getAddress(uri, ssl);
 | 
			
		||||
            var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
 | 
			
		||||
            var sslContext = ssl ? NetworkUtils.getSslContext() : null;
 | 
			
		||||
 | 
			
		||||
            // getAddress may have a slight delay, so let's perform another cancellation check.
 | 
			
		||||
            if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
            channelFuture = new Bootstrap()
 | 
			
		||||
                .group(NetworkUtils.LOOP_GROUP)
 | 
			
		||||
                .channel(NioSocketChannel.class)
 | 
			
		||||
                .handler(new ChannelInitializer<SocketChannel>() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    protected void initChannel(SocketChannel ch) {
 | 
			
		||||
                        NetworkUtils.initChannel(ch, uri, socketAddress, sslContext, null, timeout);
 | 
			
		||||
 | 
			
		||||
                        var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
 | 
			
		||||
                        var handshaker = new NoOriginWebSocketHandshaker(
 | 
			
		||||
                            uri, WebSocketVersion.V13, subprotocol, true, headers,
 | 
			
		||||
                            options.websocketMessage() <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage()
 | 
			
		||||
                        );
 | 
			
		||||
 | 
			
		||||
                        var p = ch.pipeline();
 | 
			
		||||
                        p.addLast(
 | 
			
		||||
                            new HttpClientCodec(),
 | 
			
		||||
                            new HttpObjectAggregator(8192),
 | 
			
		||||
                            WebsocketCompressionHandler.INSTANCE,
 | 
			
		||||
                            new WebSocketClientProtocolHandler(handshaker, false, timeout),
 | 
			
		||||
                            new WebsocketHandler(Websocket.this, options)
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .remoteAddress(socketAddress)
 | 
			
		||||
                .connect()
 | 
			
		||||
                .addListener(c -> {
 | 
			
		||||
                    if (!c.isSuccess()) failure(NetworkUtils.toFriendlyError(c.cause()));
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            // Do an additional check for cancellation
 | 
			
		||||
            checkClosed();
 | 
			
		||||
        } catch (HTTPRequestException e) {
 | 
			
		||||
            failure(NetworkUtils.toFriendlyError(e));
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            failure(NetworkUtils.toFriendlyError(e));
 | 
			
		||||
            LOG.log(Level.SEVERE, "Error in websocket", e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void success(Options options) {
 | 
			
		||||
        if (isClosed()) return;
 | 
			
		||||
 | 
			
		||||
        var handle = new WebsocketHandle(environment, address, this, options);
 | 
			
		||||
        environment().queueEvent(SUCCESS_EVENT, new Object[]{ address, handle });
 | 
			
		||||
        createOwnerReference(handle);
 | 
			
		||||
 | 
			
		||||
        checkClosed();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void failure(String message) {
 | 
			
		||||
        if (tryClose()) environment.queueEvent(FAILURE_EVENT, new Object[]{ address, message });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void close(int status, String reason) {
 | 
			
		||||
        if (tryClose()) {
 | 
			
		||||
            environment.queueEvent(CLOSE_EVENT, new Object[]{ address,
 | 
			
		||||
                Strings.isNullOrEmpty(reason) ? null : reason,
 | 
			
		||||
                status < 0 ? null : status });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void dispose() {
 | 
			
		||||
        super.dispose();
 | 
			
		||||
 | 
			
		||||
        executorFuture = closeFuture(executorFuture);
 | 
			
		||||
        channelFuture = closeChannel(channelFuture);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    IAPIEnvironment environment() {
 | 
			
		||||
        return environment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    String address() {
 | 
			
		||||
        return address;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private @Nullable Channel channel() {
 | 
			
		||||
        var channel = channelFuture;
 | 
			
		||||
        return channel == null ? null : channel.channel();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendText(String message) throws LuaException {
 | 
			
		||||
        sendMessage(new TextWebSocketFrame(message), message.length());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void sendBinary(ByteBuffer message) throws LuaException {
 | 
			
		||||
        long size = message.remaining();
 | 
			
		||||
        sendMessage(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message)), size);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void sendMessage(WebSocketFrame frame, long size) throws LuaException {
 | 
			
		||||
        var channel = channel();
 | 
			
		||||
        if (channel == null) return;
 | 
			
		||||
 | 
			
		||||
        // Grow the number of in-flight requests, aborting if we've hit the limit. This is then decremented when the
 | 
			
		||||
        // promise finishes.
 | 
			
		||||
        if (!AtomicHelpers.incrementToLimit(inFlight, ResourceQueue.DEFAULT_LIMIT)) {
 | 
			
		||||
            throw new LuaException("Too many ongoing websocket messages");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        channel.writeAndFlush(frame).addListener(onSend);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,95 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.websocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
 | 
			
		||||
 | 
			
		||||
import java.io.Closeable;
 | 
			
		||||
import java.net.URI;
 | 
			
		||||
import java.net.URISyntaxException;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A client-side websocket, which can be used to send messages to a remote server.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * {@link WebsocketHandle} wraps this into a Lua-compatible interface.
 | 
			
		||||
 */
 | 
			
		||||
public interface WebsocketClient extends Closeable {
 | 
			
		||||
    String SUCCESS_EVENT = "websocket_success";
 | 
			
		||||
    String FAILURE_EVENT = "websocket_failure";
 | 
			
		||||
    String CLOSE_EVENT = "websocket_closed";
 | 
			
		||||
    String MESSAGE_EVENT = "websocket_message";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Determine whether this websocket is closed.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Whether this websocket is closed.
 | 
			
		||||
     */
 | 
			
		||||
    boolean isClosed();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close this websocket.
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    void close();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a text websocket frame.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @throws LuaException If the message could not be sent.
 | 
			
		||||
     */
 | 
			
		||||
    void sendText(String message) throws LuaException;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a binary websocket frame.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @throws LuaException If the message could not be sent.
 | 
			
		||||
     */
 | 
			
		||||
    void sendBinary(ByteBuffer message) throws LuaException;
 | 
			
		||||
 | 
			
		||||
    class Support {
 | 
			
		||||
        /**
 | 
			
		||||
         * Parse an address, ensuring it is a valid websocket URI.
 | 
			
		||||
         *
 | 
			
		||||
         * @param address The address to parse.
 | 
			
		||||
         * @return The parsed URI.
 | 
			
		||||
         * @throws HTTPRequestException If the address is not valid.
 | 
			
		||||
         */
 | 
			
		||||
        public static URI parseUri(String address) throws HTTPRequestException {
 | 
			
		||||
            URI uri = null;
 | 
			
		||||
            try {
 | 
			
		||||
                uri = new URI(address);
 | 
			
		||||
            } catch (URISyntaxException ignored) {
 | 
			
		||||
                // Fall through to the case below
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (uri == null || uri.getHost() == null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    uri = new URI("ws://" + address);
 | 
			
		||||
                } catch (URISyntaxException ignored) {
 | 
			
		||||
                    // Fall through to the case below
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
 | 
			
		||||
 | 
			
		||||
            var scheme = uri.getScheme();
 | 
			
		||||
            if (scheme == null) {
 | 
			
		||||
                try {
 | 
			
		||||
                    uri = new URI("ws://" + uri);
 | 
			
		||||
                } catch (URISyntaxException e) {
 | 
			
		||||
                    throw new HTTPRequestException("URL malformed");
 | 
			
		||||
                }
 | 
			
		||||
            } else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
 | 
			
		||||
                throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return uri;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.websocket;
 | 
			
		||||
 | 
			
		||||
import io.netty.channel.ChannelHandler;
 | 
			
		||||
import io.netty.handler.codec.compression.ZlibCodecFactory;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
 | 
			
		||||
 | 
			
		||||
import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An alternative to {@link WebSocketClientCompressionHandler} which supports the {@code client_no_context_takeover}
 | 
			
		||||
 * extension. Makes CC <em>slightly</em> more flexible.
 | 
			
		||||
 */
 | 
			
		||||
@ChannelHandler.Sharable
 | 
			
		||||
final class WebsocketCompressionHandler extends WebSocketClientExtensionHandler {
 | 
			
		||||
    public static final WebsocketCompressionHandler INSTANCE = new WebsocketCompressionHandler();
 | 
			
		||||
 | 
			
		||||
    private WebsocketCompressionHandler() {
 | 
			
		||||
        super(
 | 
			
		||||
            new PerMessageDeflateClientExtensionHandshaker(
 | 
			
		||||
                6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE,
 | 
			
		||||
                true, false
 | 
			
		||||
            ),
 | 
			
		||||
            new DeflateFrameClientExtensionHandshaker(false),
 | 
			
		||||
            new DeflateFrameClientExtensionHandshaker(true)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,85 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.websocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computer.core.IAPIEnvironment;
 | 
			
		||||
import dan200.computercraft.api.lua.Coerced;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaFunction;
 | 
			
		||||
import dan200.computercraft.core.apis.http.options.Options;
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.nio.charset.CharacterCodingException;
 | 
			
		||||
import java.nio.charset.CharsetDecoder;
 | 
			
		||||
import java.nio.charset.CodingErrorAction;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A websocket, which can be used to send and receive messages with a web server.
 | 
			
		||||
 *
 | 
			
		||||
 * @cc.module http.Websocket
 | 
			
		||||
 * @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
 | 
			
		||||
 */
 | 
			
		||||
public class WebsocketHandle {
 | 
			
		||||
    private static final ThreadLocal<CharsetDecoder> DECODER = ThreadLocal.withInitial(() -> StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPLACE));
 | 
			
		||||
 | 
			
		||||
    private final IAPIEnvironment environment;
 | 
			
		||||
    private final String address;
 | 
			
		||||
    private final WebsocketClient websocket;
 | 
			
		||||
    private final Options options;
 | 
			
		||||
 | 
			
		||||
    public WebsocketHandle(IAPIEnvironment environment, String address, WebsocketClient websocket, Options options) {
 | 
			
		||||
        this.environment = environment;
 | 
			
		||||
        this.address = address;
 | 
			
		||||
        this.websocket = websocket;
 | 
			
		||||
        this.options = options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: Can we implement receive()?
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Send a websocket message to the connected server.
 | 
			
		||||
     *
 | 
			
		||||
     * @param message The message to send.
 | 
			
		||||
     * @param binary  Whether this message should be treated as a binary message.
 | 
			
		||||
     * @throws LuaException If the message is too large.
 | 
			
		||||
     * @throws LuaException If the websocket has been closed.
 | 
			
		||||
     * @cc.changed 1.81.0 Added argument for binary mode.
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final void send(Coerced<ByteBuffer> message, Optional<Boolean> binary) throws LuaException {
 | 
			
		||||
        checkOpen();
 | 
			
		||||
 | 
			
		||||
        var text = message.value();
 | 
			
		||||
        if (options.websocketMessage() != 0 && text.remaining() > options.websocketMessage()) {
 | 
			
		||||
            throw new LuaException("Message is too large");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (binary.orElse(false)) {
 | 
			
		||||
            websocket.sendBinary(text);
 | 
			
		||||
        } else {
 | 
			
		||||
            try {
 | 
			
		||||
                websocket.sendText(DECODER.get().decode(text).toString());
 | 
			
		||||
            } catch (CharacterCodingException e) {
 | 
			
		||||
                // This shouldn't happen, but worth mentioning.
 | 
			
		||||
                throw new LuaException("Message is not valid UTF8");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received
 | 
			
		||||
     * along it.
 | 
			
		||||
     */
 | 
			
		||||
    @LuaFunction
 | 
			
		||||
    public final void close() {
 | 
			
		||||
        websocket.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void checkOpen() throws LuaException {
 | 
			
		||||
        if (websocket.isClosed()) throw new LuaException("attempt to use a closed file");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,79 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.apis.http.websocket;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.core.apis.http.NetworkUtils;
 | 
			
		||||
import dan200.computercraft.core.apis.http.options.Options;
 | 
			
		||||
import io.netty.channel.ChannelHandlerContext;
 | 
			
		||||
import io.netty.channel.SimpleChannelInboundHandler;
 | 
			
		||||
import io.netty.handler.codec.http.FullHttpResponse;
 | 
			
		||||
import io.netty.handler.codec.http.websocketx.*;
 | 
			
		||||
import io.netty.util.CharsetUtil;
 | 
			
		||||
 | 
			
		||||
import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESSAGE_EVENT;
 | 
			
		||||
 | 
			
		||||
class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
 | 
			
		||||
    private final Websocket websocket;
 | 
			
		||||
    private final Options options;
 | 
			
		||||
    private boolean handshakeComplete = false;
 | 
			
		||||
 | 
			
		||||
    WebsocketHandler(Websocket websocket, Options options) {
 | 
			
		||||
        this.websocket = websocket;
 | 
			
		||||
        this.options = options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
 | 
			
		||||
        fail("Connection closed");
 | 
			
		||||
        super.channelInactive(ctx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
 | 
			
		||||
        if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) {
 | 
			
		||||
            websocket.success(options);
 | 
			
		||||
            handshakeComplete = true;
 | 
			
		||||
        } else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) {
 | 
			
		||||
            websocket.failure("Timed out");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void channelRead0(ChannelHandlerContext ctx, Object msg) {
 | 
			
		||||
        if (websocket.isClosed()) return;
 | 
			
		||||
 | 
			
		||||
        if (msg instanceof FullHttpResponse response) {
 | 
			
		||||
            throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var frame = (WebSocketFrame) msg;
 | 
			
		||||
        if (frame instanceof TextWebSocketFrame textFrame) {
 | 
			
		||||
            var data = NetworkUtils.toBytes(textFrame.content());
 | 
			
		||||
 | 
			
		||||
            websocket.environment().queueEvent(MESSAGE_EVENT, new Object[]{ websocket.address(), data, false });
 | 
			
		||||
        } else if (frame instanceof BinaryWebSocketFrame) {
 | 
			
		||||
            var data = NetworkUtils.toBytes(frame.content());
 | 
			
		||||
 | 
			
		||||
            websocket.environment().queueEvent(MESSAGE_EVENT, new Object[]{ websocket.address(), data, true });
 | 
			
		||||
        } else if (frame instanceof CloseWebSocketFrame closeFrame) {
 | 
			
		||||
            websocket.close(closeFrame.statusCode(), closeFrame.reasonText());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
 | 
			
		||||
        ctx.close();
 | 
			
		||||
 | 
			
		||||
        fail(NetworkUtils.toFriendlyError(cause));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void fail(String message) {
 | 
			
		||||
        if (handshakeComplete) {
 | 
			
		||||
            websocket.close(-1, message);
 | 
			
		||||
        } else {
 | 
			
		||||
            websocket.failure(message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -20,6 +20,7 @@ import org.objectweb.asm.Type;
 | 
			
		||||
 | 
			
		||||
import java.lang.reflect.Method;
 | 
			
		||||
import java.lang.reflect.Modifier;
 | 
			
		||||
import java.nio.ByteBuffer;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@@ -272,6 +273,14 @@ public final class Generator<T> {
 | 
			
		||||
                mw.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_ARGUMENTS, "getStringCoerced", "(I)Ljava/lang/String;");
 | 
			
		||||
                mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "<init>", "(Ljava/lang/Object;)V");
 | 
			
		||||
                return true;
 | 
			
		||||
            } else if (klass == ByteBuffer.class) {
 | 
			
		||||
                mw.visitTypeInsn(NEW, INTERNAL_COERCED);
 | 
			
		||||
                mw.visitInsn(DUP);
 | 
			
		||||
                mw.visitVarInsn(ALOAD, 2 + context.size());
 | 
			
		||||
                Reflect.loadInt(mw, argIndex);
 | 
			
		||||
                mw.visitMethodInsn(INVOKEVIRTUAL, INTERNAL_ARGUMENTS, "getBytesCoerced", "(I)Ljava/nio/ByteBuffer;");
 | 
			
		||||
                mw.visitMethodInsn(INVOKESPECIAL, INTERNAL_COERCED, "<init>", "(Ljava/lang/Object;)V");
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import dan200.computer.core.ILuaMachine;
 | 
			
		||||
import dan200.computer.core.ILuaObject;
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
import dan200.computercraft.core.asm.Methods;
 | 
			
		||||
import dan200.computercraft.core.lua.errorinfo.ErrorInfoLib;
 | 
			
		||||
import org.squiddev.cobalt.*;
 | 
			
		||||
import org.squiddev.cobalt.compiler.LoadState;
 | 
			
		||||
import org.squiddev.cobalt.function.LuaFunction;
 | 
			
		||||
@@ -54,12 +55,18 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
            .errorReporter((e, m) -> CCTweaked.LOG.log(Level.SEVERE, "Error occurred in Lua VM. Execution will continue:\n" + m.get(), e))
 | 
			
		||||
            .build();
 | 
			
		||||
 | 
			
		||||
        globals = state.getMainThread().getfenv();
 | 
			
		||||
        CoreLibraries.debugGlobals(state);
 | 
			
		||||
        Bit32Lib.add(state, globals);
 | 
			
		||||
        globals = state.globals();
 | 
			
		||||
 | 
			
		||||
        globals.rawset("_HOST", valueOf("ComputerCraft " + ComputerCraft.getVersion() + " (" + Loader.instance().getMCVersionString() + ")"));
 | 
			
		||||
        globals.rawset("_CC_DEFAULT_SETTINGS", valueOf(""));
 | 
			
		||||
        try {
 | 
			
		||||
            CoreLibraries.debugGlobals(state);
 | 
			
		||||
            Bit32Lib.add(state, globals);
 | 
			
		||||
            ErrorInfoLib.add(state);
 | 
			
		||||
 | 
			
		||||
            globals.rawset("_HOST", valueOf("ComputerCraft " + ComputerCraft.getVersion() + " (" + Loader.instance().getMCVersionString() + ")"));
 | 
			
		||||
            globals.rawset("_CC_DEFAULT_SETTINGS", valueOf(""));
 | 
			
		||||
        } catch (LuaError e) {
 | 
			
		||||
            throw new IllegalStateException(e.getMessage());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -79,7 +86,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            LuaFunction value = LoadState.load(state, bios, "@bios.lua", globals);
 | 
			
		||||
            mainRoutine = new LuaThread(state, value, globals);
 | 
			
		||||
            mainRoutine = new LuaThread(state, value);
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            CCTweaked.LOG.log(Level.SEVERE, "Failed to load bios.lua", e);
 | 
			
		||||
            unload();
 | 
			
		||||
@@ -207,7 +214,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
        return table;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private LuaValue toValue(Object object, Map<Object, LuaValue> values) {
 | 
			
		||||
    private LuaValue toValue(Object object, Map<Object, LuaValue> values) throws LuaError {
 | 
			
		||||
        if (object == null) return Constants.NIL;
 | 
			
		||||
        if (object instanceof Number) return valueOf(((Number) object).doubleValue());
 | 
			
		||||
        if (object instanceof Boolean) return valueOf((Boolean) object);
 | 
			
		||||
@@ -260,7 +267,7 @@ public class CobaltLuaMachine implements ILuaMachine {
 | 
			
		||||
        return Constants.NIL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Varargs toValues(Object[] objects) {
 | 
			
		||||
    Varargs toValues(Object[] objects) throws LuaError {
 | 
			
		||||
        if (objects == null || objects.length == 0) return Constants.NONE;
 | 
			
		||||
 | 
			
		||||
        Map<Object, LuaValue> result = new IdentityHashMap<>(0);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2009-2011 Luaj.org, 2015-2020 SquidDev
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.lua.errorinfo;
 | 
			
		||||
 | 
			
		||||
import org.squiddev.cobalt.Prototype;
 | 
			
		||||
 | 
			
		||||
import static org.squiddev.cobalt.Lua.*;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extracted parts of Cobalt's {@link org.squiddev.cobalt.debug.DebugHelpers}.
 | 
			
		||||
 */
 | 
			
		||||
final class DebugHelpers {
 | 
			
		||||
    private DebugHelpers() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int filterPc(int pc, int jumpTarget) {
 | 
			
		||||
        return pc < jumpTarget ? -1 : pc;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find the PC where a register was last set.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This makes some assumptions about the structure of the bytecode, namely that there are no back edges within the
 | 
			
		||||
     * CFG. As a result, this is only valid for temporary values, and not locals.
 | 
			
		||||
     *
 | 
			
		||||
     * @param pt     The function prototype.
 | 
			
		||||
     * @param lastPc The PC to work back from.
 | 
			
		||||
     * @param reg    The register.
 | 
			
		||||
     * @return The last instruction where the register was set, or {@code -1} if not defined.
 | 
			
		||||
     */
 | 
			
		||||
    static int findSetReg(Prototype pt, int lastPc, int reg) {
 | 
			
		||||
        var lastInsn = -1; // Last instruction that changed "reg";
 | 
			
		||||
        var jumpTarget = 0; // Any code before this address is conditional
 | 
			
		||||
 | 
			
		||||
        for (var pc = 0; pc < lastPc; pc++) {
 | 
			
		||||
            var i = pt.code[pc];
 | 
			
		||||
            var op = GET_OPCODE(i);
 | 
			
		||||
            var a = GETARG_A(i);
 | 
			
		||||
            switch (op) {
 | 
			
		||||
                case OP_LOADNIL -> {
 | 
			
		||||
                    var b = GETARG_B(i);
 | 
			
		||||
                    if (a <= reg && reg <= a + b) lastInsn = filterPc(pc, jumpTarget);
 | 
			
		||||
                }
 | 
			
		||||
                case OP_TFORCALL -> {
 | 
			
		||||
                    if (a >= a + 2) lastInsn = filterPc(pc, jumpTarget);
 | 
			
		||||
                }
 | 
			
		||||
                case OP_CALL, OP_TAILCALL -> {
 | 
			
		||||
                    if (reg >= a) lastInsn = filterPc(pc, jumpTarget);
 | 
			
		||||
                }
 | 
			
		||||
                case OP_JMP -> {
 | 
			
		||||
                    var dest = pc + 1 + GETARG_sBx(i);
 | 
			
		||||
                    // If jump is forward and doesn't skip lastPc, update jump target
 | 
			
		||||
                    if (pc < dest && dest <= lastPc && dest > jumpTarget) jumpTarget = dest;
 | 
			
		||||
                }
 | 
			
		||||
                default -> {
 | 
			
		||||
                    if (testAMode(op) && reg == a) lastInsn = filterPc(pc, jumpTarget);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return lastInsn;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,222 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.lua.errorinfo;
 | 
			
		||||
 | 
			
		||||
import com.google.common.annotations.VisibleForTesting;
 | 
			
		||||
import org.jspecify.annotations.Nullable;
 | 
			
		||||
import org.squiddev.cobalt.*;
 | 
			
		||||
import org.squiddev.cobalt.debug.DebugFrame;
 | 
			
		||||
import org.squiddev.cobalt.function.LuaFunction;
 | 
			
		||||
import org.squiddev.cobalt.function.RegisteredFunction;
 | 
			
		||||
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
import static org.squiddev.cobalt.Lua.*;
 | 
			
		||||
import static org.squiddev.cobalt.debug.DebugFrame.FLAG_ANY_HOOK;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides additional info about an error.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This is currently an internal and deeply unstable module. It's not clear if doing this via bytecode (rather than an
 | 
			
		||||
 * AST) is the correct approach and/or, what the correct design is.
 | 
			
		||||
 */
 | 
			
		||||
public class ErrorInfoLib {
 | 
			
		||||
    private static final int MAX_DEPTH = 8;
 | 
			
		||||
 | 
			
		||||
    private static final RegisteredFunction[] functions = new RegisteredFunction[]{
 | 
			
		||||
        RegisteredFunction.ofV("info_for_nil", ErrorInfoLib::getInfoForNil),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public static void add(LuaState state) throws LuaError {
 | 
			
		||||
        state.registry().getSubTable(Constants.LOADED).rawset("cc.internal.error_info", RegisteredFunction.bind(functions));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static Varargs getInfoForNil(LuaState state, Varargs args) throws LuaError {
 | 
			
		||||
        var thread = args.arg(1).checkThread();
 | 
			
		||||
        var level = args.arg(2).checkInteger();
 | 
			
		||||
 | 
			
		||||
        var context = getInfoForNil(state, thread, level);
 | 
			
		||||
        return context == null ? Constants.NIL : ValueFactory.varargsOf(
 | 
			
		||||
            ValueFactory.valueOf(context.op()), ValueFactory.valueOf(context.source().isGlobal()),
 | 
			
		||||
            context.source().table(), context.source().key()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get some additional information about an {@code attempt to $OP (a nil value)} error. This often occurs as a
 | 
			
		||||
     * result of a misspelled local, global or table index, and so we attempt to detect those cases.
 | 
			
		||||
     *
 | 
			
		||||
     * @param state  The current Lua state.
 | 
			
		||||
     * @param thread The thread which has errored.
 | 
			
		||||
     * @param level  The level where the error occurred. We currently expect this to always be 0.
 | 
			
		||||
     * @return Some additional information about the error, where available.
 | 
			
		||||
     */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    static @Nullable NilInfo getInfoForNil(LuaState state, LuaThread thread, int level) {
 | 
			
		||||
        var frame = thread.getDebugState().getFrame(level);
 | 
			
		||||
        if (frame == null || frame.closure == null || (frame.flags & FLAG_ANY_HOOK) != 0) return null;
 | 
			
		||||
 | 
			
		||||
        var prototype = frame.closure.getPrototype();
 | 
			
		||||
        var pc = frame.pc;
 | 
			
		||||
        var insn = prototype.code[pc];
 | 
			
		||||
 | 
			
		||||
        // Find what operation we're doing that errored.
 | 
			
		||||
        return switch (GET_OPCODE(insn)) {
 | 
			
		||||
            case OP_CALL, OP_TAILCALL ->
 | 
			
		||||
                NilInfo.of("call", resolveValueSource(state, frame, prototype, pc, GETARG_A(insn), 0));
 | 
			
		||||
            case OP_GETTABLE, OP_SETTABLE, OP_SELF ->
 | 
			
		||||
                NilInfo.of("index", resolveValueSource(state, frame, prototype, pc, GETARG_A(insn), 0));
 | 
			
		||||
            default -> null;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Information about an {@code attempt to $OP (a nil value)} error.
 | 
			
		||||
     *
 | 
			
		||||
     * @param op     The operation we tried to perform.
 | 
			
		||||
     * @param source The expression that resulted in a nil value.
 | 
			
		||||
     */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    record NilInfo(String op, ValueSource source) {
 | 
			
		||||
        public static @Nullable NilInfo of(String op, @Nullable ValueSource values) {
 | 
			
		||||
            return values == null ? null : new NilInfo(op, values);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A partially-reconstructed Lua expression. This currently only is used for table indexing ({@code table[key]}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param isGlobal Whether this is a global table access. This is a best-effort guess, and does not distinguish between
 | 
			
		||||
     *                 {@code foo} and {@code _ENV.foo}.
 | 
			
		||||
     * @param table    The table being indexed.
 | 
			
		||||
     * @param key      The key we tried to index.
 | 
			
		||||
     */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    record ValueSource(boolean isGlobal, LuaValue table, LuaString key) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Attempt to partially reconstruct a Lua expression from the current debug state.
 | 
			
		||||
     *
 | 
			
		||||
     * @param state     The current Lua state.
 | 
			
		||||
     * @param frame     The current debug frame.
 | 
			
		||||
     * @param prototype The current function.
 | 
			
		||||
     * @param pc        The current program counter.
 | 
			
		||||
     * @param register  The register where this value was stored.
 | 
			
		||||
     * @param depth     The current depth. Starts at 0, and aborts once reaching {@link #MAX_DEPTH}.
 | 
			
		||||
     * @return The reconstructed expression, or {@code null} if not available.
 | 
			
		||||
     */
 | 
			
		||||
    @SuppressWarnings("NullTernary")
 | 
			
		||||
    private static @Nullable ValueSource resolveValueSource(LuaState state, DebugFrame frame, Prototype prototype, int pc, int register, int depth) {
 | 
			
		||||
        if (depth > MAX_DEPTH) return null;
 | 
			
		||||
        if (prototype.getLocalName(register + 1, pc) != null) return null;
 | 
			
		||||
 | 
			
		||||
        // Find where this register was set. If unknown, then abort.
 | 
			
		||||
        pc = DebugHelpers.findSetReg(prototype, pc, register);
 | 
			
		||||
        if (pc == -1) return null;
 | 
			
		||||
 | 
			
		||||
        var insn = prototype.code[pc];
 | 
			
		||||
        return switch (GET_OPCODE(insn)) {
 | 
			
		||||
            case OP_MOVE -> {
 | 
			
		||||
                var a = GETARG_A(insn);
 | 
			
		||||
                var b = GETARG_B(insn); // move from `b' to `a'
 | 
			
		||||
                yield b < a ? resolveValueSource(state, frame, prototype, pc, register, depth + 1) : null; // Resolve 'b' .
 | 
			
		||||
            }
 | 
			
		||||
            case OP_GETTABUP, OP_GETTABLE, OP_SELF -> {
 | 
			
		||||
                var tableIndex = GETARG_B(insn);
 | 
			
		||||
                var keyIndex = GETARG_C(insn);
 | 
			
		||||
                // We're only interested in expressions of the form "foo.bar". Showing a "did you mean" hint for
 | 
			
		||||
                // "foo[i]" isn't very useful!
 | 
			
		||||
                if (!ISK(keyIndex)) yield null;
 | 
			
		||||
 | 
			
		||||
                var key = prototype.constants[INDEXK(keyIndex)];
 | 
			
		||||
                if (key.type() != Constants.TSTRING) yield null;
 | 
			
		||||
 | 
			
		||||
                var table = GET_OPCODE(insn) == OP_GETTABUP
 | 
			
		||||
                    ? frame.closure.getUpvalue(tableIndex).getValue()
 | 
			
		||||
                    : evaluate(state, frame, prototype, pc, tableIndex, depth);
 | 
			
		||||
                if (table == null) yield null;
 | 
			
		||||
 | 
			
		||||
                var isGlobal = GET_OPCODE(insn) == OP_GETTABUP && Objects.equals(prototype.getUpvalueName(tableIndex), Constants.ENV);
 | 
			
		||||
                yield new ValueSource(isGlobal, table, (LuaString) key);
 | 
			
		||||
            }
 | 
			
		||||
            default -> null;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Attempt to reconstruct the value of a register.
 | 
			
		||||
     *
 | 
			
		||||
     * @param state     The current Lua state.
 | 
			
		||||
     * @param frame     The current debug frame.
 | 
			
		||||
     * @param prototype The current function
 | 
			
		||||
     * @param pc        The PC to evaluate at.
 | 
			
		||||
     * @param register  The register to evaluate.
 | 
			
		||||
     * @param depth     The current depth. Starts at 0, and aborts once reaching {@link #MAX_DEPTH}.
 | 
			
		||||
     * @return The reconstructed value, or {@code null} if unavailable.
 | 
			
		||||
     */
 | 
			
		||||
    @SuppressWarnings("NullTernary")
 | 
			
		||||
    private static @Nullable LuaValue evaluate(LuaState state, DebugFrame frame, Prototype prototype, int pc, int register, int depth) {
 | 
			
		||||
        if (depth >= MAX_DEPTH) return null;
 | 
			
		||||
 | 
			
		||||
        // If this is a local, then return its contents.
 | 
			
		||||
        if (prototype.getLocalName(register + 1, pc) != null) return frame.stack[register];
 | 
			
		||||
 | 
			
		||||
        // Otherwise find where this register was set. If unknown, then abort.
 | 
			
		||||
        pc = DebugHelpers.findSetReg(prototype, pc, register);
 | 
			
		||||
        if (pc == -1) return null;
 | 
			
		||||
 | 
			
		||||
        var insn = prototype.code[pc];
 | 
			
		||||
        var opcode = GET_OPCODE(insn);
 | 
			
		||||
        return switch (opcode) {
 | 
			
		||||
            case OP_MOVE -> {
 | 
			
		||||
                var a = GETARG_A(insn);
 | 
			
		||||
                var b = GETARG_B(insn); // move from `b' to `a'
 | 
			
		||||
                yield b < a ? evaluate(state, frame, prototype, pc, register, depth + 1) : null; // Resolve 'b'.
 | 
			
		||||
            }
 | 
			
		||||
            // Load constants
 | 
			
		||||
            case OP_LOADK -> prototype.constants[GETARG_Bx(insn)];
 | 
			
		||||
            case OP_LOADKX -> prototype.constants[GETARG_Ax(prototype.code[pc + 1])];
 | 
			
		||||
            case OP_LOADBOOL -> GETARG_B(insn) == 0 ? Constants.FALSE : Constants.TRUE;
 | 
			
		||||
            case OP_LOADNIL -> Constants.NIL;
 | 
			
		||||
            // Upvalues and tables.
 | 
			
		||||
            case OP_GETUPVAL -> frame.closure.getUpvalue(GETARG_B(insn)).getValue();
 | 
			
		||||
            case OP_GETTABLE, OP_GETTABUP -> {
 | 
			
		||||
                var table = opcode == OP_GETTABUP
 | 
			
		||||
                    ? frame.closure.getUpvalue(GETARG_B(insn)).getValue()
 | 
			
		||||
                    : evaluate(state, frame, prototype, pc, GETARG_B(insn), depth + 1);
 | 
			
		||||
                if (table == null) yield null;
 | 
			
		||||
 | 
			
		||||
                var key = evaluateK(state, frame, prototype, pc, GETARG_C(insn), depth + 1);
 | 
			
		||||
                yield key == null ? null : safeIndex(state, table, key);
 | 
			
		||||
            }
 | 
			
		||||
            default -> null;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static @Nullable LuaValue evaluateK(LuaState state, DebugFrame frame, Prototype prototype, int pc, int registerOrConstant, int depth) {
 | 
			
		||||
        return ISK(registerOrConstant) ? prototype.constants[INDEXK(registerOrConstant)] : evaluate(state, frame, prototype, pc, registerOrConstant, depth + 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static @Nullable LuaValue safeIndex(LuaState state, LuaValue table, LuaValue key) {
 | 
			
		||||
        var loop = 0;
 | 
			
		||||
        do {
 | 
			
		||||
            LuaValue metatable;
 | 
			
		||||
            if (table instanceof LuaTable tbl) {
 | 
			
		||||
                var res = tbl.rawget(key);
 | 
			
		||||
                if (!res.isNil() || (metatable = tbl.metatag(state, CachedMetamethod.INDEX)).isNil()) return res;
 | 
			
		||||
            } else if ((metatable = table.metatag(state, CachedMetamethod.INDEX)).isNil()) {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (metatable instanceof LuaFunction) return null;
 | 
			
		||||
 | 
			
		||||
            table = metatable;
 | 
			
		||||
        }
 | 
			
		||||
        while (++loop < Constants.MAXTAGLOOP);
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,27 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.util;
 | 
			
		||||
 | 
			
		||||
import dan200.computercraft.api.lua.LuaException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A few helpers for working with arguments.
 | 
			
		||||
 * <p>
 | 
			
		||||
 * This should really be moved into the public API. However, until I have settled on a suitable format, we'll keep it
 | 
			
		||||
 * where it is used.
 | 
			
		||||
 */
 | 
			
		||||
public class ArgumentHelpers {
 | 
			
		||||
    public static void assertBetween(double value, double min, double max, String message) throws LuaException {
 | 
			
		||||
        if (value < min || value > max || Double.isNaN(value)) {
 | 
			
		||||
            throw new LuaException(String.format(message, "between " + min + " and " + max));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void assertBetween(int value, int min, int max, String message) throws LuaException {
 | 
			
		||||
        if (value < min || value > max) {
 | 
			
		||||
            throw new LuaException(String.format(message, "between " + min + " and " + max));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.util;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.atomic.AtomicInteger;
 | 
			
		||||
 | 
			
		||||
public final class AtomicHelpers {
 | 
			
		||||
    private AtomicHelpers() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A version of {@link AtomicInteger#getAndIncrement()}, which increments until a limit is reached.
 | 
			
		||||
     *
 | 
			
		||||
     * @param atomic The atomic to increment.
 | 
			
		||||
     * @param limit  The maximum value of {@code value}.
 | 
			
		||||
     * @return Whether the value was sucessfully incremented.
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean incrementToLimit(AtomicInteger atomic, int limit) {
 | 
			
		||||
        int value;
 | 
			
		||||
        do {
 | 
			
		||||
            value = atomic.get();
 | 
			
		||||
            if (value >= limit) return false;
 | 
			
		||||
        } while (!atomic.compareAndSet(value, value + 1));
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -9,10 +9,10 @@ public final class StringUtil {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static String normaliseLabel(String label) {
 | 
			
		||||
        int length = Math.min(32, label.length());
 | 
			
		||||
        StringBuilder builder = new StringBuilder(length);
 | 
			
		||||
        for (int i = 0; i < length; i++) {
 | 
			
		||||
            char c = label.charAt(i);
 | 
			
		||||
        var length = Math.min(32, label.length());
 | 
			
		||||
        var builder = new StringBuilder(length);
 | 
			
		||||
        for (var i = 0; i < length; i++) {
 | 
			
		||||
            var c = label.charAt(i);
 | 
			
		||||
            if ((c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255)) {
 | 
			
		||||
                builder.append(c);
 | 
			
		||||
            } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
 | 
			
		||||
//
 | 
			
		||||
// SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
package dan200.computercraft.core.util;
 | 
			
		||||
 | 
			
		||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.ThreadFactory;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
 | 
			
		||||
import static cc.tweaked.CCTweaked.LOG;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides some utilities to create thread groups.
 | 
			
		||||
 */
 | 
			
		||||
public final class ThreadUtils {
 | 
			
		||||
    private static final ThreadGroup baseGroup = new ThreadGroup("ComputerCraft");
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A lower thread priority (though not the minimum), used for most of ComputerCraft's threads.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * The Minecraft thread typically runs on a higher priority thread anyway, but this ensures we don't dominate other,
 | 
			
		||||
     * more critical work.
 | 
			
		||||
     *
 | 
			
		||||
     * @see Thread#setPriority(int)
 | 
			
		||||
     */
 | 
			
		||||
    public static final int LOWER_PRIORITY = (Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2;
 | 
			
		||||
 | 
			
		||||
    private ThreadUtils() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the base thread group, that all off-thread ComputerCraft activities are run on.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The ComputerCraft group.
 | 
			
		||||
     */
 | 
			
		||||
    public static ThreadGroup group() {
 | 
			
		||||
        return baseGroup;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new {@link ThreadFactoryBuilder}, which constructs threads under a group of the given {@code name}.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Each thread will be of the format {@code ComputerCraft-<name>-<number>}, and belong to a group
 | 
			
		||||
     * called {@code ComputerCraft-<name>} (which in turn will be a child group of the main {@code ComputerCraft} group.
 | 
			
		||||
     *
 | 
			
		||||
     * @param name The name for the thread group and child threads.
 | 
			
		||||
     * @return The constructed thread factory builder, which may be extended with other properties.
 | 
			
		||||
     * @see #factory(String)
 | 
			
		||||
     */
 | 
			
		||||
    public static ThreadFactoryBuilder builder(String name) {
 | 
			
		||||
        var group = new ThreadGroup(baseGroup, baseGroup.getName() + "-" + name);
 | 
			
		||||
        return new ThreadFactoryBuilder()
 | 
			
		||||
            .setDaemon(true)
 | 
			
		||||
            .setNameFormat(group.getName().replace("%", "%%") + "-%d")
 | 
			
		||||
            .setUncaughtExceptionHandler((t, e) -> LOG.log(Level.SEVERE, "Exception in thread " + t.getName(), e))
 | 
			
		||||
            .setThreadFactory(x -> new Thread(group, x));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new {@link ThreadFactory}, which constructs threads under a group of the given {@code name}.
 | 
			
		||||
     * <p>
 | 
			
		||||
     * Each thread will be of the format {@code ComputerCraft-<name>-<number>}, and belong to a group
 | 
			
		||||
     * called {@code ComputerCraft-<name>} (which in turn will be a child group of the main {@code ComputerCraft} group.
 | 
			
		||||
     *
 | 
			
		||||
     * @param name The name for the thread group and child threads.
 | 
			
		||||
     * @return The constructed thread factory.
 | 
			
		||||
     * @see #builder(String)
 | 
			
		||||
     */
 | 
			
		||||
    public static ThreadFactory factory(String name) {
 | 
			
		||||
        return builder(name).build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new {@link ThreadFactory}, which constructs threads under a group of the given {@code name}. This is the
 | 
			
		||||
     * same as {@link #factory(String)}, but threads will be created with a {@linkplain #LOWER_PRIORITY lower priority}.
 | 
			
		||||
     *
 | 
			
		||||
     * @param name The name for the thread group and child threads.
 | 
			
		||||
     * @return The constructed thread factory.
 | 
			
		||||
     * @see #builder(String)
 | 
			
		||||
     */
 | 
			
		||||
    public static ThreadFactory lowPriorityFactory(String name) {
 | 
			
		||||
        return builder(name).setPriority(LOWER_PRIORITY).build();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,58 +18,20 @@ do
 | 
			
		||||
    expect = f().expect
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
if _VERSION == "Lua 5.1" then
 | 
			
		||||
    -- If we're on Lua 5.1, install parts of the Lua 5.2/5.3 API so that programs can be written against it
 | 
			
		||||
    local nativeload = load
 | 
			
		||||
 | 
			
		||||
    function load(x, name, mode, env)
 | 
			
		||||
        expect(1, x, "function", "string")
 | 
			
		||||
        expect(2, name, "string", "nil")
 | 
			
		||||
        expect(3, mode, "string", "nil")
 | 
			
		||||
        expect(4, env, "table", "nil")
 | 
			
		||||
 | 
			
		||||
        local ok, p1, p2 = pcall(function()
 | 
			
		||||
            local result, err = nativeload(x, name, mode, env)
 | 
			
		||||
            if result and env then
 | 
			
		||||
                env._ENV = env
 | 
			
		||||
            end
 | 
			
		||||
            return result, err
 | 
			
		||||
        end)
 | 
			
		||||
        if ok then
 | 
			
		||||
            return p1, p2
 | 
			
		||||
        else
 | 
			
		||||
            error(p1, 2)
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if _CC_DISABLE_LUA51_FEATURES then
 | 
			
		||||
        -- Remove the Lua 5.1 features that will be removed when we update to Lua 5.2, for compatibility testing.
 | 
			
		||||
        -- See "disable_lua51_functions" in ComputerCraft.cfg
 | 
			
		||||
        setfenv = nil
 | 
			
		||||
        getfenv = nil
 | 
			
		||||
        loadstring = nil
 | 
			
		||||
        unpack = nil
 | 
			
		||||
        math.log10 = nil
 | 
			
		||||
        table.maxn = nil
 | 
			
		||||
    else
 | 
			
		||||
        loadstring = function(string, chunkname) return nativeload(string, chunkname) end
 | 
			
		||||
 | 
			
		||||
        -- Inject a stub for the old bit library
 | 
			
		||||
        _G.bit = {
 | 
			
		||||
            bnot = bit32.bnot,
 | 
			
		||||
            band = bit32.band,
 | 
			
		||||
            bor = bit32.bor,
 | 
			
		||||
            bxor = bit32.bxor,
 | 
			
		||||
            brshift = bit32.arshift,
 | 
			
		||||
            blshift = bit32.lshift,
 | 
			
		||||
            blogic_rshift = bit32.rshift,
 | 
			
		||||
        }
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
-- Inject a stub for the old bit library
 | 
			
		||||
_G.bit = {
 | 
			
		||||
    bnot = bit32.bnot,
 | 
			
		||||
    band = bit32.band,
 | 
			
		||||
    bor = bit32.bor,
 | 
			
		||||
    bxor = bit32.bxor,
 | 
			
		||||
    brshift = bit32.arshift,
 | 
			
		||||
    blshift = bit32.lshift,
 | 
			
		||||
    blogic_rshift = bit32.rshift,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
-- Install lua parts of the os api
 | 
			
		||||
function os.version()
 | 
			
		||||
    return "CraftOS 1.8"
 | 
			
		||||
    return "CraftOS 1.9"
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
function os.pullEventRaw(sFilter)
 | 
			
		||||
@@ -687,7 +649,7 @@ settings.define("paint.default_extension", {
 | 
			
		||||
 | 
			
		||||
settings.define("list.show_hidden", {
 | 
			
		||||
    default = false,
 | 
			
		||||
    description = [[Show hidden files (those starting with "." in the Lua REPL).]],
 | 
			
		||||
    description = [[Whether the list program show  hidden files (those starting with ".").]],
 | 
			
		||||
    type = "boolean",
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,20 +3,20 @@
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--[[- Constants and functions for colour values, suitable for working with
 | 
			
		||||
@{term} and @{redstone}.
 | 
			
		||||
[`term`] and [`redstone`].
 | 
			
		||||
 | 
			
		||||
This is useful in conjunction with @{redstone.setBundledOutput|Bundled Cables}
 | 
			
		||||
from mods like Project Red, and @{term.setTextColour|colors on Advanced
 | 
			
		||||
Computers and Advanced Monitors}.
 | 
			
		||||
This is useful in conjunction with [Bundled Cables][`redstone.setBundledOutput`]
 | 
			
		||||
from mods like Project Red, and [colors on Advanced Computers and Advanced
 | 
			
		||||
Monitors][`term.setTextColour`].
 | 
			
		||||
 | 
			
		||||
For the non-American English version just replace @{colors} with @{colours}.
 | 
			
		||||
For the non-American English version just replace [`colors`] with [`colours`].
 | 
			
		||||
This alternative API is exactly the same, except the colours use British English
 | 
			
		||||
(e.g. @{colors.gray} is spelt @{colours.grey}).
 | 
			
		||||
(e.g. [`colors.gray`] is spelt [`colours.grey`]).
 | 
			
		||||
 | 
			
		||||
On basic terminals (such as the Computer and Monitor), all the colors are
 | 
			
		||||
converted to grayscale. This means you can still use all 16 colors on the
 | 
			
		||||
screen, but they will appear as the nearest tint of gray. You can check if a
 | 
			
		||||
terminal supports color by using the function @{term.isColor}.
 | 
			
		||||
terminal supports color by using the function [`term.isColor`].
 | 
			
		||||
 | 
			
		||||
Grayscale colors are calculated by taking the average of the three components,
 | 
			
		||||
i.e. `(red + green + blue) / 3`.
 | 
			
		||||
@@ -140,67 +140,67 @@ i.e. `(red + green + blue) / 3`.
 | 
			
		||||
 | 
			
		||||
local expect = dofile("rom/modules/main/cc/expect.lua").expect
 | 
			
		||||
 | 
			
		||||
--- White: Written as `0` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- White: Written as `0` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #F0F0F0.
 | 
			
		||||
white = 0x1
 | 
			
		||||
 | 
			
		||||
--- Orange: Written as `1` in paint files and @{term.blit}, has a
 | 
			
		||||
--- Orange: Written as `1` in paint files and [`term.blit`], has a
 | 
			
		||||
-- default terminal colour of #F2B233.
 | 
			
		||||
orange = 0x2
 | 
			
		||||
 | 
			
		||||
--- Magenta: Written as `2` in paint files and @{term.blit}, has a
 | 
			
		||||
--- Magenta: Written as `2` in paint files and [`term.blit`], has a
 | 
			
		||||
-- default terminal colour of #E57FD8.
 | 
			
		||||
magenta = 0x4
 | 
			
		||||
 | 
			
		||||
--- Light blue: Written as `3` in paint files and @{term.blit}, has a
 | 
			
		||||
--- Light blue: Written as `3` in paint files and [`term.blit`], has a
 | 
			
		||||
-- default terminal colour of #99B2F2.
 | 
			
		||||
lightBlue = 0x8
 | 
			
		||||
 | 
			
		||||
--- Yellow: Written as `4` in paint files and @{term.blit}, has a
 | 
			
		||||
--- Yellow: Written as `4` in paint files and [`term.blit`], has a
 | 
			
		||||
-- default terminal colour of #DEDE6C.
 | 
			
		||||
yellow = 0x10
 | 
			
		||||
 | 
			
		||||
--- Lime: Written as `5` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Lime: Written as `5` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #7FCC19.
 | 
			
		||||
lime = 0x20
 | 
			
		||||
 | 
			
		||||
--- Pink: Written as `6` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Pink: Written as `6` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #F2B2CC.
 | 
			
		||||
pink = 0x40
 | 
			
		||||
 | 
			
		||||
--- Gray: Written as `7` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Gray: Written as `7` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #4C4C4C.
 | 
			
		||||
gray = 0x80
 | 
			
		||||
 | 
			
		||||
--- Light gray: Written as `8` in paint files and @{term.blit}, has a
 | 
			
		||||
--- Light gray: Written as `8` in paint files and [`term.blit`], has a
 | 
			
		||||
-- default terminal colour of #999999.
 | 
			
		||||
lightGray = 0x100
 | 
			
		||||
 | 
			
		||||
--- Cyan: Written as `9` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Cyan: Written as `9` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #4C99B2.
 | 
			
		||||
cyan = 0x200
 | 
			
		||||
 | 
			
		||||
--- Purple: Written as `a` in paint files and @{term.blit}, has a
 | 
			
		||||
--- Purple: Written as `a` in paint files and [`term.blit`], has a
 | 
			
		||||
-- default terminal colour of #B266E5.
 | 
			
		||||
purple = 0x400
 | 
			
		||||
 | 
			
		||||
--- Blue: Written as `b` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Blue: Written as `b` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #3366CC.
 | 
			
		||||
blue = 0x800
 | 
			
		||||
 | 
			
		||||
--- Brown: Written as `c` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Brown: Written as `c` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #7F664C.
 | 
			
		||||
brown = 0x1000
 | 
			
		||||
 | 
			
		||||
--- Green: Written as `d` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Green: Written as `d` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #57A64E.
 | 
			
		||||
green = 0x2000
 | 
			
		||||
 | 
			
		||||
--- Red: Written as `e` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Red: Written as `e` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #CC4C4C.
 | 
			
		||||
red = 0x4000
 | 
			
		||||
 | 
			
		||||
--- Black: Written as `f` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Black: Written as `f` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #111111.
 | 
			
		||||
black = 0x8000
 | 
			
		||||
 | 
			
		||||
@@ -313,18 +313,18 @@ function unpackRGB(rgb)
 | 
			
		||||
        bit32.band(rgb, 0xFF) / 255
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Either calls @{colors.packRGB} or @{colors.unpackRGB}, depending on how many
 | 
			
		||||
--- Either calls [`colors.packRGB`] or [`colors.unpackRGB`], depending on how many
 | 
			
		||||
-- arguments it receives.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam[1] number r The red channel, as an argument to @{colors.packRGB}.
 | 
			
		||||
-- @tparam[1] number g The green channel, as an argument to @{colors.packRGB}.
 | 
			
		||||
-- @tparam[1] number b The blue channel, as an argument to @{colors.packRGB}.
 | 
			
		||||
-- @tparam[2] number rgb The combined hexadecimal color, as an argument to @{colors.unpackRGB}.
 | 
			
		||||
-- @treturn[1] number The combined hexadecimal colour, as returned by @{colors.packRGB}.
 | 
			
		||||
-- @treturn[2] number The red channel, as returned by @{colors.unpackRGB}
 | 
			
		||||
-- @treturn[2] number The green channel, as returned by @{colors.unpackRGB}
 | 
			
		||||
-- @treturn[2] number The blue channel, as returned by @{colors.unpackRGB}
 | 
			
		||||
-- @deprecated Use @{packRGB} or @{unpackRGB} directly.
 | 
			
		||||
-- @tparam[1] number r The red channel, as an argument to [`colors.packRGB`].
 | 
			
		||||
-- @tparam[1] number g The green channel, as an argument to [`colors.packRGB`].
 | 
			
		||||
-- @tparam[1] number b The blue channel, as an argument to [`colors.packRGB`].
 | 
			
		||||
-- @tparam[2] number rgb The combined hexadecimal color, as an argument to [`colors.unpackRGB`].
 | 
			
		||||
-- @treturn[1] number The combined hexadecimal colour, as returned by [`colors.packRGB`].
 | 
			
		||||
-- @treturn[2] number The red channel, as returned by [`colors.unpackRGB`]
 | 
			
		||||
-- @treturn[2] number The green channel, as returned by [`colors.unpackRGB`]
 | 
			
		||||
-- @treturn[2] number The blue channel, as returned by [`colors.unpackRGB`]
 | 
			
		||||
-- @deprecated Use [`packRGB`] or [`unpackRGB`] directly.
 | 
			
		||||
-- @usage
 | 
			
		||||
-- ```lua
 | 
			
		||||
-- colors.rgb8(0xb23399)
 | 
			
		||||
@@ -353,7 +353,8 @@ end
 | 
			
		||||
 | 
			
		||||
--[[- Converts the given color to a paint/blit hex character (0-9a-f).
 | 
			
		||||
 | 
			
		||||
This is equivalent to converting floor(log_2(color)) to hexadecimal.
 | 
			
		||||
This is equivalent to converting `floor(log_2(color))` to hexadecimal. Values
 | 
			
		||||
outside the range of a valid colour will error.
 | 
			
		||||
 | 
			
		||||
@tparam number color The color to convert.
 | 
			
		||||
@treturn string The blit hex code of the color.
 | 
			
		||||
@@ -367,7 +368,11 @@ colors.toBlit(colors.red)
 | 
			
		||||
]]
 | 
			
		||||
function toBlit(color)
 | 
			
		||||
    expect(1, color, "number")
 | 
			
		||||
    return color_hex_lookup[color] or string.format("%x", math.floor(math.log(color, 2)))
 | 
			
		||||
    local hex = color_hex_lookup[color]
 | 
			
		||||
    if hex then return hex end
 | 
			
		||||
 | 
			
		||||
    if color < 0 or color > 0xffff then error("Colour out of range", 2) end
 | 
			
		||||
    return string.format("%x", math.floor(math.log(color, 2)))
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Converts the given paint/blit hex character (0-9a-f) to a color.
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--- An alternative version of @{colors} for lovers of British spelling.
 | 
			
		||||
--- An alternative version of [`colors`] for lovers of British spelling.
 | 
			
		||||
--
 | 
			
		||||
-- @see colors
 | 
			
		||||
-- @module colours
 | 
			
		||||
@@ -13,14 +13,14 @@ for k, v in pairs(colors) do
 | 
			
		||||
    colours[k] = v
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Grey. Written as `7` in paint files and @{term.blit}, has a default
 | 
			
		||||
--- Grey. Written as `7` in paint files and [`term.blit`], has a default
 | 
			
		||||
-- terminal colour of #4C4C4C.
 | 
			
		||||
--
 | 
			
		||||
-- @see colors.gray
 | 
			
		||||
colours.grey = colors.gray
 | 
			
		||||
colours.gray = nil --- @local
 | 
			
		||||
 | 
			
		||||
--- Light grey. Written as `8` in paint files and @{term.blit}, has a
 | 
			
		||||
--- Light grey. Written as `8` in paint files and [`term.blit`], has a
 | 
			
		||||
-- default terminal colour of #999999.
 | 
			
		||||
--
 | 
			
		||||
-- @see colors.lightGray
 | 
			
		||||
 
 | 
			
		||||
@@ -5,20 +5,19 @@
 | 
			
		||||
--[[- Execute [Minecraft commands][mc] and gather data from the results from
 | 
			
		||||
a command computer.
 | 
			
		||||
 | 
			
		||||
:::note
 | 
			
		||||
This API is only available on Command computers. It is not accessible to normal
 | 
			
		||||
players.
 | 
			
		||||
:::
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> This API is only available on Command computers. It is not accessible to normal
 | 
			
		||||
> players.
 | 
			
		||||
 | 
			
		||||
While one may use @{commands.exec} directly to execute a command, the
 | 
			
		||||
While one may use [`commands.exec`] directly to execute a command, the
 | 
			
		||||
commands API also provides helper methods to execute every command. For
 | 
			
		||||
instance, `commands.say("Hi!")` is equivalent to `commands.exec("say Hi!")`.
 | 
			
		||||
 | 
			
		||||
@{commands.async} provides a similar interface to execute asynchronous
 | 
			
		||||
[`commands.async`] provides a similar interface to execute asynchronous
 | 
			
		||||
commands. `commands.async.say("Hi!")` is equivalent to
 | 
			
		||||
`commands.execAsync("say Hi!")`.
 | 
			
		||||
 | 
			
		||||
[mc]: https://minecraft.gamepedia.com/Commands
 | 
			
		||||
[mc]: https://minecraft.wiki/w/Commands
 | 
			
		||||
 | 
			
		||||
@module commands
 | 
			
		||||
@usage Set the block above this computer to stone:
 | 
			
		||||
@@ -31,7 +30,7 @@ end
 | 
			
		||||
 | 
			
		||||
--- The builtin commands API, without any generated command helper functions
 | 
			
		||||
--
 | 
			
		||||
-- This may be useful if a built-in function (such as @{commands.list}) has been
 | 
			
		||||
-- This may be useful if a built-in function (such as [`commands.list`]) has been
 | 
			
		||||
-- overwritten by a command.
 | 
			
		||||
local native = commands.native or commands
 | 
			
		||||
 | 
			
		||||
@@ -112,7 +111,7 @@ end
 | 
			
		||||
 | 
			
		||||
--- A table containing asynchronous wrappers for all commands.
 | 
			
		||||
--
 | 
			
		||||
-- As with @{commands.execAsync}, this returns the "task id" of the enqueued
 | 
			
		||||
-- As with [`commands.execAsync`], this returns the "task id" of the enqueued
 | 
			
		||||
-- command.
 | 
			
		||||
-- @see execAsync
 | 
			
		||||
-- @usage Asynchronously sets the block above the computer to stone.
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,9 @@ locally attached drive, specify “side” as one of the six sides (e.g. `left`)
 | 
			
		||||
use a remote disk drive, specify its name as printed when enabling its modem
 | 
			
		||||
(e.g. `drive_0`).
 | 
			
		||||
 | 
			
		||||
:::tip
 | 
			
		||||
All computers (except command computers), turtles and pocket computers can be
 | 
			
		||||
placed within a disk drive to access it's internal storage like a disk.
 | 
			
		||||
:::
 | 
			
		||||
> [!TIP]
 | 
			
		||||
> All computers (except command computers), turtles and pocket computers can be
 | 
			
		||||
> placed within a disk drive to access it's internal storage like a disk.
 | 
			
		||||
 | 
			
		||||
@module disk
 | 
			
		||||
@since 1.2
 | 
			
		||||
@@ -95,9 +94,9 @@ end
 | 
			
		||||
--- Whether the current disk is a [music disk][disk] as opposed to a floppy disk
 | 
			
		||||
-- or other item.
 | 
			
		||||
--
 | 
			
		||||
-- If this returns true, you will can @{disk.playAudio|play} the record.
 | 
			
		||||
-- If this returns true, you will can [play][`disk.playAudio`] the record.
 | 
			
		||||
--
 | 
			
		||||
-- [disk]: https://minecraft.gamepedia.com/Music_Disc
 | 
			
		||||
-- [disk]: https://minecraft.wiki/w/Music_Disc
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the disk drive.
 | 
			
		||||
-- @treturn boolean If the disk is present and has audio saved on it.
 | 
			
		||||
@@ -110,10 +109,10 @@ end
 | 
			
		||||
 | 
			
		||||
--- Get the title of the audio track from the music record in the drive.
 | 
			
		||||
--
 | 
			
		||||
-- This generally returns the same as @{disk.getLabel} for records.
 | 
			
		||||
-- This generally returns the same as [`disk.getLabel`] for records.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the disk drive.
 | 
			
		||||
-- @treturn string|false|nil The track title, @{false} if there is not a music
 | 
			
		||||
-- @treturn string|false|nil The track title, [`false`] if there is not a music
 | 
			
		||||
-- record in the drive or `nil` if no drive is present.
 | 
			
		||||
function getAudioTitle(name)
 | 
			
		||||
    if isDrive(name) then
 | 
			
		||||
@@ -126,7 +125,7 @@ end
 | 
			
		||||
--
 | 
			
		||||
-- If any record is already playing on any disk drive, it stops before the
 | 
			
		||||
-- target drive starts playing. The record stops when it reaches the end of the
 | 
			
		||||
-- track, when it is removed from the drive, when @{disk.stopAudio} is called, or
 | 
			
		||||
-- track, when it is removed from the drive, when [`disk.stopAudio`] is called, or
 | 
			
		||||
-- when another record is started.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the disk drive.
 | 
			
		||||
@@ -138,7 +137,7 @@ function playAudio(name)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Stops the music record in the drive from playing, if it was started with
 | 
			
		||||
-- @{disk.playAudio}.
 | 
			
		||||
-- [`disk.playAudio`].
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name o the disk drive.
 | 
			
		||||
function stopAudio(name)
 | 
			
		||||
@@ -165,7 +164,7 @@ end
 | 
			
		||||
 | 
			
		||||
--- Returns a number which uniquely identifies the disk in the drive.
 | 
			
		||||
--
 | 
			
		||||
-- Note, unlike @{disk.getLabel}, this does not return anything for other media,
 | 
			
		||||
-- Note, unlike [`disk.getLabel`], this does not return anything for other media,
 | 
			
		||||
-- such as computers or turtles.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the disk drive.
 | 
			
		||||
 
 | 
			
		||||
@@ -13,19 +13,19 @@ local fs = _ENV
 | 
			
		||||
for k, v in pairs(native) do fs[k] = v end
 | 
			
		||||
 | 
			
		||||
--[[- Provides completion for a file or directory name, suitable for use with
 | 
			
		||||
@{_G.read}.
 | 
			
		||||
[`_G.read`].
 | 
			
		||||
 | 
			
		||||
When a directory is a possible candidate for completion, two entries are
 | 
			
		||||
included - one with a trailing slash (indicating that entries within this
 | 
			
		||||
directory exist) and one without it (meaning this entry is an immediate
 | 
			
		||||
completion candidate). `include_dirs` can be set to @{false} to only include
 | 
			
		||||
completion candidate). `include_dirs` can be set to [`false`] to only include
 | 
			
		||||
those with a trailing slash.
 | 
			
		||||
 | 
			
		||||
@tparam[1] string path The path to complete.
 | 
			
		||||
@tparam[1] string location The location where paths are resolved from.
 | 
			
		||||
@tparam[1,opt=true] boolean include_files When @{false}, only directories will
 | 
			
		||||
@tparam[1,opt=true] boolean include_files When [`false`], only directories will
 | 
			
		||||
be included in the returned list.
 | 
			
		||||
@tparam[1,opt=true] boolean include_dirs When @{false}, "raw" directories will
 | 
			
		||||
@tparam[1,opt=true] boolean include_dirs When [`false`], "raw" directories will
 | 
			
		||||
not be included in the returned list.
 | 
			
		||||
 | 
			
		||||
@tparam[2] string path The path to complete.
 | 
			
		||||
@@ -133,19 +133,111 @@ function fs.complete(sPath, sLocation, bIncludeFiles, bIncludeDirs)
 | 
			
		||||
    return {}
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function find_aux(path, parts, i, out)
 | 
			
		||||
    local part = parts[i]
 | 
			
		||||
    if not part then
 | 
			
		||||
        -- If we're at the end of the pattern, ensure our path exists and append it.
 | 
			
		||||
        if fs.exists(path) then out[#out + 1] = path end
 | 
			
		||||
    elseif part.exact then
 | 
			
		||||
        -- If we're an exact match, just recurse into this directory.
 | 
			
		||||
        return find_aux(fs.combine(path, part.contents), parts, i + 1, out)
 | 
			
		||||
    else
 | 
			
		||||
        -- Otherwise we're a pattern. Check we're a directory, then recurse into each
 | 
			
		||||
        -- matching file.
 | 
			
		||||
        if not fs.isDir(path) then return end
 | 
			
		||||
 | 
			
		||||
        local files = fs.list(path)
 | 
			
		||||
        for j = 1, #files do
 | 
			
		||||
            local file = files[j]
 | 
			
		||||
            if file:find(part.contents) then find_aux(fs.combine(path, file), parts, i + 1, out) end
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local find_escape = {
 | 
			
		||||
    -- Escape standard Lua pattern characters
 | 
			
		||||
    ["^"] = "%^", ["$"] = "%$", ["("] = "%(", [")"] = "%)", ["%"] = "%%",
 | 
			
		||||
    ["."] = "%.", ["["] = "%[", ["]"] = "%]", ["+"] = "%+", ["-"] = "%-",
 | 
			
		||||
    -- Aside from our wildcards.
 | 
			
		||||
    ["*"] = ".*",
 | 
			
		||||
    ["?"] = ".",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
--[[- Searches for files matching a string with wildcards.
 | 
			
		||||
 | 
			
		||||
This string looks like a normal path string, but can include wildcards, which
 | 
			
		||||
can match multiple paths:
 | 
			
		||||
 | 
			
		||||
 - "?" matches any single character in a file name.
 | 
			
		||||
 - "*" matches any number of characters.
 | 
			
		||||
 | 
			
		||||
For example, `rom/*/command*` will look for any path starting with `command`
 | 
			
		||||
inside any subdirectory of `/rom`.
 | 
			
		||||
 | 
			
		||||
Note that these wildcards match a single segment of the path. For instance
 | 
			
		||||
`rom/*.lua` will include `rom/startup.lua` but _not_ include `rom/programs/list.lua`.
 | 
			
		||||
 | 
			
		||||
@tparam string path The wildcard-qualified path to search for.
 | 
			
		||||
@treturn { string... } A list of paths that match the search string.
 | 
			
		||||
@throws If the supplied path was invalid.
 | 
			
		||||
@since 1.6
 | 
			
		||||
@changed 1.106.0 Added support for the `?` wildcard.
 | 
			
		||||
 | 
			
		||||
@usage List all Markdown files in the help folder
 | 
			
		||||
 | 
			
		||||
    fs.find("rom/help/*.md")
 | 
			
		||||
]]
 | 
			
		||||
function fs.find(pattern)
 | 
			
		||||
    expect(1, pattern, "string")
 | 
			
		||||
 | 
			
		||||
    pattern = fs.combine(pattern) -- Normalise the path, removing ".."s.
 | 
			
		||||
 | 
			
		||||
    -- If the pattern is trying to search outside the computer root, just abort.
 | 
			
		||||
    -- This will fail later on anyway.
 | 
			
		||||
    if pattern == ".." or pattern:sub(1, 3) == "../" then
 | 
			
		||||
        error("/" .. pattern .. ": Invalid Path", 2)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    -- If we've no wildcards, just check the file exists.
 | 
			
		||||
    if not pattern:find("[*?]") then
 | 
			
		||||
        if fs.exists(pattern) then return { pattern } else return {} end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local parts = {}
 | 
			
		||||
    for part in pattern:gmatch("[^/]+") do
 | 
			
		||||
        if part:find("[*?]") then
 | 
			
		||||
            parts[#parts + 1] = {
 | 
			
		||||
                exact = false,
 | 
			
		||||
                contents = "^" .. part:gsub(".", find_escape) .. "$",
 | 
			
		||||
            }
 | 
			
		||||
        else
 | 
			
		||||
            parts[#parts + 1] = { exact = true, contents = part }
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local out = {}
 | 
			
		||||
    find_aux("", parts, 1, out)
 | 
			
		||||
    return out
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Returns true if a path is mounted to the parent filesystem.
 | 
			
		||||
--
 | 
			
		||||
-- The root filesystem "/" is considered a mount, along with disk folders and
 | 
			
		||||
-- the rom folder. Other programs (such as network shares) can exstend this to
 | 
			
		||||
-- make other mount types by correctly assigning their return value for getDrive.
 | 
			
		||||
-- the rom folder.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string path The path to check.
 | 
			
		||||
-- @treturn boolean If the path is mounted, rather than a normal file/folder.
 | 
			
		||||
-- @throws If the path does not exist.
 | 
			
		||||
-- @see getDrive
 | 
			
		||||
-- @since 1.87.0
 | 
			
		||||
function fs.isDriveRoot(sPath)
 | 
			
		||||
    expect(1, sPath, "string")
 | 
			
		||||
function fs.isDriveRoot(path)
 | 
			
		||||
    expect(1, path, "string")
 | 
			
		||||
 | 
			
		||||
    local parent = fs.getDir(path)
 | 
			
		||||
 | 
			
		||||
    -- Force the root directory to be a mount.
 | 
			
		||||
    return fs.getDir(sPath) == ".." or fs.getDrive(sPath) ~= fs.getDrive(fs.getDir(sPath))
 | 
			
		||||
    if parent == ".." then return true end
 | 
			
		||||
 | 
			
		||||
    local drive = fs.getDrive(path)
 | 
			
		||||
    return drive ~= nil and drive ~= fs.getDrive(parent)
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -2,21 +2,20 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--[[- Use @{modem|modems} to locate the position of the current turtle or
 | 
			
		||||
--[[- Use [modems][`modem`] to locate the position of the current turtle or
 | 
			
		||||
computers.
 | 
			
		||||
 | 
			
		||||
It broadcasts a PING message over @{rednet} and wait for responses. In order for
 | 
			
		||||
It broadcasts a PING message over [`rednet`] and wait for responses. In order for
 | 
			
		||||
this system to work, there must be at least 4 computers used as gps hosts which
 | 
			
		||||
will respond and allow trilateration. Three of these hosts should be in a plane,
 | 
			
		||||
and the fourth should be either above or below the other three. The three in a
 | 
			
		||||
plane should not be in a line with each other. You can set up hosts using the
 | 
			
		||||
gps program.
 | 
			
		||||
 | 
			
		||||
:::note
 | 
			
		||||
When entering in the coordinates for the host you need to put in the `x`, `y`,
 | 
			
		||||
and `z` coordinates of the block that the modem is connected to, not the modem.
 | 
			
		||||
All modem distances are measured from the block that the modem is placed on.
 | 
			
		||||
:::
 | 
			
		||||
> [!NOTE]
 | 
			
		||||
> When entering in the coordinates for the host you need to put in the `x`, `y`,
 | 
			
		||||
> and `z` coordinates of the block that the modem is connected to, not the modem.
 | 
			
		||||
> All modem distances are measured from the block that the modem is placed on.
 | 
			
		||||
 | 
			
		||||
Also note that you may choose which axes x, y, or z refers to - so long as your
 | 
			
		||||
systems have the same definition as any GPS servers that're in range, it works
 | 
			
		||||
@@ -24,7 +23,7 @@ just the same. For example, you might build a GPS cluster according to [this
 | 
			
		||||
tutorial][1], using z to account for height, or you might use y to account for
 | 
			
		||||
height in the way that Minecraft's debug screen displays.
 | 
			
		||||
 | 
			
		||||
[1]: http://www.computercraft.info/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/
 | 
			
		||||
[1]: https://ccf.squiddev.cc/forums2/index.php?/topic/3088-how-to-guide-gps-global-position-system/
 | 
			
		||||
 | 
			
		||||
@module gps
 | 
			
		||||
@since 1.31
 | 
			
		||||
@@ -110,7 +109,7 @@ function locate(_nTimeout, _bDebug)
 | 
			
		||||
    -- Find a modem
 | 
			
		||||
    local sModemSide = nil
 | 
			
		||||
    for _, sSide in ipairs(rs.getSides()) do
 | 
			
		||||
        if peripheral.getType(sSide) == "modem" and peripheral.call(sSide, "isWireless") then
 | 
			
		||||
        if peripheral.getType(sSide) == "modem" then
 | 
			
		||||
            sModemSide = sSide
 | 
			
		||||
            break
 | 
			
		||||
        end
 | 
			
		||||
@@ -197,6 +196,8 @@ function locate(_nTimeout, _bDebug)
 | 
			
		||||
        modem.close(CHANNEL_GPS)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    os.cancelTimer(timeout)
 | 
			
		||||
 | 
			
		||||
    -- Return the response
 | 
			
		||||
    if pos1 and pos2 then
 | 
			
		||||
        if _bDebug then
 | 
			
		||||
 
 | 
			
		||||
@@ -66,15 +66,14 @@ end
 | 
			
		||||
@tparam string url   The url to request
 | 
			
		||||
@tparam[opt] { [string] = string } headers Additional headers to send as part
 | 
			
		||||
of this request.
 | 
			
		||||
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
 | 
			
		||||
the body will not be UTF-8 encoded, and the received response will not be
 | 
			
		||||
decoded.
 | 
			
		||||
@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`]
 | 
			
		||||
should be opened in binary mode.
 | 
			
		||||
 | 
			
		||||
@tparam[2] {
 | 
			
		||||
  url = string, headers? = { [string] = string },
 | 
			
		||||
  binary? = boolean, method? = string, redirect? = boolean,
 | 
			
		||||
  timeout? = number,
 | 
			
		||||
} request Options for the request. See @{http.request} for details on how
 | 
			
		||||
} request Options for the request. See [`http.request`] for details on how
 | 
			
		||||
these options behave.
 | 
			
		||||
 | 
			
		||||
@treturn Response The resulting http response, which can be read from.
 | 
			
		||||
@@ -89,6 +88,8 @@ error or connection timeout.
 | 
			
		||||
@changed 1.80pr1.6 Added support for table argument.
 | 
			
		||||
@changed 1.86.0 Added PATCH and TRACE methods.
 | 
			
		||||
@changed 1.105.0 Added support for custom timeouts.
 | 
			
		||||
@changed 1.109.0 The returned response now reads the body as raw bytes, rather
 | 
			
		||||
                 than decoding from UTF-8.
 | 
			
		||||
 | 
			
		||||
@usage Make a request to [example.tweaked.cc](https://example.tweaked.cc),
 | 
			
		||||
and print the returned page.
 | 
			
		||||
@@ -118,15 +119,14 @@ end
 | 
			
		||||
@tparam string body  The body of the POST request.
 | 
			
		||||
@tparam[opt] { [string] = string } headers Additional headers to send as part
 | 
			
		||||
of this request.
 | 
			
		||||
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
 | 
			
		||||
the body will not be UTF-8 encoded, and the received response will not be
 | 
			
		||||
decoded.
 | 
			
		||||
@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`]
 | 
			
		||||
should be opened in binary mode.
 | 
			
		||||
 | 
			
		||||
@tparam[2] {
 | 
			
		||||
  url = string, body? = string, headers? = { [string] = string },
 | 
			
		||||
  binary? = boolean, method? = string, redirect? = boolean,
 | 
			
		||||
  timeout? = number,
 | 
			
		||||
} request Options for the request. See @{http.request} for details on how
 | 
			
		||||
} request Options for the request. See [`http.request`] for details on how
 | 
			
		||||
these options behave.
 | 
			
		||||
 | 
			
		||||
@treturn Response The resulting http response, which can be read from.
 | 
			
		||||
@@ -142,6 +142,8 @@ error or connection timeout.
 | 
			
		||||
@changed 1.80pr1.6 Added support for table argument.
 | 
			
		||||
@changed 1.86.0 Added PATCH and TRACE methods.
 | 
			
		||||
@changed 1.105.0 Added support for custom timeouts.
 | 
			
		||||
@changed 1.109.0 The returned response now reads the body as raw bytes, rather
 | 
			
		||||
                 than decoding from UTF-8.
 | 
			
		||||
]]
 | 
			
		||||
function post(_url, _post, _headers, _binary)
 | 
			
		||||
    if type(_url) == "table" then
 | 
			
		||||
@@ -158,7 +160,7 @@ end
 | 
			
		||||
 | 
			
		||||
--[[- Asynchronously make a HTTP request to the given url.
 | 
			
		||||
 | 
			
		||||
This returns immediately, a @{http_success} or @{http_failure} will be queued
 | 
			
		||||
This returns immediately, a [`http_success`] or [`http_failure`] will be queued
 | 
			
		||||
once the request has completed.
 | 
			
		||||
 | 
			
		||||
@tparam      string url   The url to request
 | 
			
		||||
@@ -166,9 +168,8 @@ once the request has completed.
 | 
			
		||||
request. If specified, a `POST` request will be made instead.
 | 
			
		||||
@tparam[opt] { [string] = string } headers Additional headers to send as part
 | 
			
		||||
of this request.
 | 
			
		||||
@tparam[opt] boolean binary Whether to make a binary HTTP request. If true,
 | 
			
		||||
the body will not be UTF-8 encoded, and the received response will not be
 | 
			
		||||
decoded.
 | 
			
		||||
@tparam[opt=false] boolean binary Whether the [response handle][`fs.ReadHandle`]
 | 
			
		||||
should be opened in binary mode.
 | 
			
		||||
 | 
			
		||||
@tparam[2] {
 | 
			
		||||
  url = string, body? = string, headers? = { [string] = string },
 | 
			
		||||
@@ -194,6 +195,8 @@ from above are passed in as fields instead (for instance,
 | 
			
		||||
@changed 1.80pr1.6 Added support for table argument.
 | 
			
		||||
@changed 1.86.0 Added PATCH and TRACE methods.
 | 
			
		||||
@changed 1.105.0 Added support for custom timeouts.
 | 
			
		||||
@changed 1.109.0 The returned response now reads the body as raw bytes, rather
 | 
			
		||||
                 than decoding from UTF-8.
 | 
			
		||||
]]
 | 
			
		||||
function request(_url, _post, _headers, _binary)
 | 
			
		||||
    local url
 | 
			
		||||
@@ -221,7 +224,7 @@ local nativeCheckURL = native.checkURL
 | 
			
		||||
 | 
			
		||||
--[[- Asynchronously determine whether a URL can be requested.
 | 
			
		||||
 | 
			
		||||
If this returns `true`, one should also listen for @{http_check} which will
 | 
			
		||||
If this returns `true`, one should also listen for [`http_check`] which will
 | 
			
		||||
container further information about whether the URL is allowed or not.
 | 
			
		||||
 | 
			
		||||
@tparam string url The URL to check.
 | 
			
		||||
@@ -237,11 +240,11 @@ checkURLAsync = nativeCheckURL
 | 
			
		||||
 | 
			
		||||
--[[- Determine whether a URL can be requested.
 | 
			
		||||
 | 
			
		||||
If this returns `true`, one should also listen for @{http_check} which will
 | 
			
		||||
If this returns `true`, one should also listen for [`http_check`] which will
 | 
			
		||||
container further information about whether the URL is allowed or not.
 | 
			
		||||
 | 
			
		||||
@tparam string url The URL to check.
 | 
			
		||||
@treturn true When this url is valid and can be requested via @{http.request}.
 | 
			
		||||
@treturn true When this url is valid and can be requested via [`http.request`].
 | 
			
		||||
@treturn[2] false When this url is invalid.
 | 
			
		||||
@treturn string A reason why this URL is not valid (for instance, if it is
 | 
			
		||||
malformed, or blocked).
 | 
			
		||||
@@ -280,7 +283,7 @@ end
 | 
			
		||||
 | 
			
		||||
--[[- Asynchronously open a websocket.
 | 
			
		||||
 | 
			
		||||
This returns immediately, a @{websocket_success} or @{websocket_failure}
 | 
			
		||||
This returns immediately, a [`websocket_success`] or [`websocket_failure`]
 | 
			
		||||
will be queued once the request has completed.
 | 
			
		||||
 | 
			
		||||
@tparam[1] string url The websocket url to connect to. This should have the
 | 
			
		||||
@@ -290,12 +293,14 @@ of the initial websocket connection.
 | 
			
		||||
 | 
			
		||||
@tparam[2] {
 | 
			
		||||
  url = string, headers? = { [string] = string }, timeout ?= number,
 | 
			
		||||
} request Options for the websocket.  See @{http.websocket} for details on how
 | 
			
		||||
} request Options for the websocket.  See [`http.websocket`] for details on how
 | 
			
		||||
these options behave.
 | 
			
		||||
 | 
			
		||||
@since 1.80pr1.3
 | 
			
		||||
@changed 1.95.3 Added User-Agent to default headers.
 | 
			
		||||
@changed 1.105.0 Added support for table argument and custom timeout.
 | 
			
		||||
@changed 1.109.0 Non-binary websocket messages now use the raw bytes rather than
 | 
			
		||||
                 using UTF-8.
 | 
			
		||||
@see websocket_success
 | 
			
		||||
@see websocket_failure
 | 
			
		||||
]]
 | 
			
		||||
@@ -346,6 +351,8 @@ from above are passed in as fields instead (for instance,
 | 
			
		||||
@changed 1.80pr1.3 No longer asynchronous.
 | 
			
		||||
@changed 1.95.3 Added User-Agent to default headers.
 | 
			
		||||
@changed 1.105.0 Added support for table argument and custom timeout.
 | 
			
		||||
@changed 1.109.0 Non-binary websocket messages now use the raw bytes rather than
 | 
			
		||||
                 using UTF-8.
 | 
			
		||||
 | 
			
		||||
@usage Connect to an echo websocket and send a message.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -74,10 +74,10 @@ handleMetatable = {
 | 
			
		||||
 | 
			
		||||
        This can be used in a for loop to iterate over all lines of a file
 | 
			
		||||
 | 
			
		||||
        Once the end of the file has been reached, @{nil} will be returned. The file is
 | 
			
		||||
        Once the end of the file has been reached, [`nil`] will be returned. The file is
 | 
			
		||||
        *not* automatically closed.
 | 
			
		||||
 | 
			
		||||
        @param ... The argument to pass to @{Handle:read} for each line.
 | 
			
		||||
        @param ... The argument to pass to [`Handle:read`] for each line.
 | 
			
		||||
        @treturn function():string|nil The line iterator.
 | 
			
		||||
        @throws If the file cannot be opened for reading
 | 
			
		||||
        @since 1.3
 | 
			
		||||
@@ -324,14 +324,14 @@ each time it is called, returns a new line from the file.
 | 
			
		||||
 | 
			
		||||
This can be used in a for loop to iterate over all lines of a file
 | 
			
		||||
 | 
			
		||||
Once the end of the file has been reached, @{nil} will be returned. The file is
 | 
			
		||||
Once the end of the file has been reached, [`nil`] will be returned. The file is
 | 
			
		||||
automatically closed.
 | 
			
		||||
 | 
			
		||||
If no file name is given, the @{io.input|current input} will be used instead.
 | 
			
		||||
If no file name is given, the [current input][`io.input`] will be used instead.
 | 
			
		||||
In this case, the handle is not used.
 | 
			
		||||
 | 
			
		||||
@tparam[opt] string filename The name of the file to extract lines from
 | 
			
		||||
@param ... The argument to pass to @{Handle:read} for each line.
 | 
			
		||||
@param ... The argument to pass to [`Handle:read`] for each line.
 | 
			
		||||
@treturn function():string|nil The line iterator.
 | 
			
		||||
@throws If the file cannot be opened for reading
 | 
			
		||||
 | 
			
		||||
@@ -362,7 +362,7 @@ function lines(filename, ...)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Open a file with the given mode, either returning a new file handle
 | 
			
		||||
-- or @{nil}, plus an error message.
 | 
			
		||||
-- or [`nil`], plus an error message.
 | 
			
		||||
--
 | 
			
		||||
-- The `mode` string can be any of the following:
 | 
			
		||||
--  - **"r"**: Read mode
 | 
			
		||||
@@ -410,11 +410,11 @@ end
 | 
			
		||||
 | 
			
		||||
--- Read from the currently opened input file.
 | 
			
		||||
--
 | 
			
		||||
-- This is equivalent to `io.input():read(...)`. See @{Handle:read|the
 | 
			
		||||
-- documentation} there for full details.
 | 
			
		||||
-- This is equivalent to `io.input():read(...)`. See [the documentation][`Handle:read`]
 | 
			
		||||
-- there for full details.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string ... The formats to read, defaulting to a whole line.
 | 
			
		||||
-- @treturn (string|nil)... The data read, or @{nil} if nothing can be read.
 | 
			
		||||
-- @treturn (string|nil)... The data read, or [`nil`] if nothing can be read.
 | 
			
		||||
function read(...)
 | 
			
		||||
    return currentInput:read(...)
 | 
			
		||||
end
 | 
			
		||||
@@ -438,8 +438,8 @@ end
 | 
			
		||||
 | 
			
		||||
--- Write to the currently opened output file.
 | 
			
		||||
--
 | 
			
		||||
-- This is equivalent to `io.output():write(...)`. See @{Handle:write|the
 | 
			
		||||
-- documentation} there for full details.
 | 
			
		||||
-- This is equivalent to `io.output():write(...)`. See [the documentation][`Handle:write`]
 | 
			
		||||
-- there for full details.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string ... The strings to write
 | 
			
		||||
-- @changed 1.81.0 Multiple arguments are now allowed.
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--- Constants for all keyboard "key codes", as queued by the @{key} event.
 | 
			
		||||
--- Constants for all keyboard "key codes", as queued by the [`key`] event.
 | 
			
		||||
--
 | 
			
		||||
-- These values are not guaranteed to remain the same between versions. It is
 | 
			
		||||
-- recommended that you use the constants provided by this file, rather than
 | 
			
		||||
 
 | 
			
		||||
@@ -47,12 +47,22 @@ local function sortCoords(startX, startY, endX, endY)
 | 
			
		||||
    return minX, maxX, minY, maxY
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Parses an image from a multi-line string
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string image The string containing the raw-image data.
 | 
			
		||||
-- @treturn table The parsed image data, suitable for use with
 | 
			
		||||
-- @{paintutils.drawImage}.
 | 
			
		||||
-- @since 1.80pr1
 | 
			
		||||
--[=[- Parses an image from a multi-line string
 | 
			
		||||
 | 
			
		||||
@tparam string image The string containing the raw-image data.
 | 
			
		||||
@treturn table The parsed image data, suitable for use with [`paintutils.drawImage`].
 | 
			
		||||
@usage Parse an image from a string, and draw it.
 | 
			
		||||
 | 
			
		||||
    local image = paintutils.parseImage([[
 | 
			
		||||
     e  e
 | 
			
		||||
 | 
			
		||||
    e    e
 | 
			
		||||
     eeee
 | 
			
		||||
    ]])
 | 
			
		||||
    paintutils.drawImage(image, term.getCursorPos())
 | 
			
		||||
 | 
			
		||||
@since 1.80pr1
 | 
			
		||||
]=]
 | 
			
		||||
function parseImage(image)
 | 
			
		||||
    expect(1, image, "string")
 | 
			
		||||
    local tImage = {}
 | 
			
		||||
@@ -69,7 +79,7 @@ end
 | 
			
		||||
-- @tparam string path The file to load.
 | 
			
		||||
--
 | 
			
		||||
-- @treturn table|nil The parsed image data, suitable for use with
 | 
			
		||||
-- @{paintutils.drawImage}, or `nil` if the file does not exist.
 | 
			
		||||
-- [`paintutils.drawImage`], or `nil` if the file does not exist.
 | 
			
		||||
-- @usage Load an image and draw it.
 | 
			
		||||
--
 | 
			
		||||
--     local image = paintutils.loadImage("data/example.nfp")
 | 
			
		||||
@@ -93,7 +103,7 @@ end
 | 
			
		||||
--
 | 
			
		||||
-- @tparam number xPos The x position to draw at, where 1 is the far left.
 | 
			
		||||
-- @tparam number yPos The y position to draw at, where 1 is the very top.
 | 
			
		||||
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
 | 
			
		||||
-- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be
 | 
			
		||||
-- the current background colour if not specified.
 | 
			
		||||
function drawPixel(xPos, yPos, colour)
 | 
			
		||||
    expect(1, xPos, "number")
 | 
			
		||||
@@ -115,7 +125,7 @@ end
 | 
			
		||||
-- @tparam number startY The starting y position of the line.
 | 
			
		||||
-- @tparam number endX The end x position of the line.
 | 
			
		||||
-- @tparam number endY The end y position of the line.
 | 
			
		||||
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
 | 
			
		||||
-- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be
 | 
			
		||||
-- the current background colour if not specified.
 | 
			
		||||
-- @usage paintutils.drawLine(2, 3, 30, 7, colors.red)
 | 
			
		||||
function drawLine(startX, startY, endX, endY, colour)
 | 
			
		||||
@@ -189,7 +199,7 @@ end
 | 
			
		||||
-- @tparam number startY The starting y position of the line.
 | 
			
		||||
-- @tparam number endX The end x position of the line.
 | 
			
		||||
-- @tparam number endY The end y position of the line.
 | 
			
		||||
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
 | 
			
		||||
-- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be
 | 
			
		||||
-- the current background colour if not specified.
 | 
			
		||||
-- @usage paintutils.drawBox(2, 3, 30, 7, colors.red)
 | 
			
		||||
function drawBox(startX, startY, endX, endY, nColour)
 | 
			
		||||
@@ -242,7 +252,7 @@ end
 | 
			
		||||
-- @tparam number startY The starting y position of the line.
 | 
			
		||||
-- @tparam number endX The end x position of the line.
 | 
			
		||||
-- @tparam number endY The end y position of the line.
 | 
			
		||||
-- @tparam[opt] number colour The @{colors|color} of this pixel. This will be
 | 
			
		||||
-- @tparam[opt] number colour The [color][`colors`] of this pixel. This will be
 | 
			
		||||
-- the current background colour if not specified.
 | 
			
		||||
-- @usage paintutils.drawFilledBox(2, 3, 30, 7, colors.red)
 | 
			
		||||
function drawFilledBox(startX, startY, endX, endY, nColour)
 | 
			
		||||
@@ -278,7 +288,7 @@ function drawFilledBox(startX, startY, endX, endY, nColour)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Draw an image loaded by @{paintutils.parseImage} or @{paintutils.loadImage}.
 | 
			
		||||
--- Draw an image loaded by [`paintutils.parseImage`] or [`paintutils.loadImage`].
 | 
			
		||||
--
 | 
			
		||||
-- @tparam table image The parsed image data.
 | 
			
		||||
-- @tparam number xPos The x position to start drawing at.
 | 
			
		||||
 
 | 
			
		||||
@@ -6,101 +6,94 @@
 | 
			
		||||
 | 
			
		||||
Functions are not actually executed simultaneously, but rather this API will
 | 
			
		||||
automatically switch between them whenever they yield (e.g. whenever they call
 | 
			
		||||
@{coroutine.yield}, or functions that call that - such as @{os.pullEvent} - or
 | 
			
		||||
[`coroutine.yield`], or functions that call that - such as [`os.pullEvent`] - or
 | 
			
		||||
functions that call that, etc - basically, anything that causes the function
 | 
			
		||||
to "pause").
 | 
			
		||||
 | 
			
		||||
Each function executed in "parallel" gets its own copy of the event queue,
 | 
			
		||||
and so "event consuming" functions (again, mostly anything that causes the
 | 
			
		||||
script to pause - eg @{os.sleep}, @{rednet.receive}, most of the @{turtle} API,
 | 
			
		||||
script to pause - eg [`os.sleep`], [`rednet.receive`], most of the [`turtle`] API,
 | 
			
		||||
etc) can safely be used in one without affecting the event queue accessed by
 | 
			
		||||
the other.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
:::caution
 | 
			
		||||
When using this API, be careful to pass the functions you want to run in
 | 
			
		||||
parallel, and _not_ the result of calling those functions.
 | 
			
		||||
 | 
			
		||||
For instance, the following is correct:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local function do_sleep() sleep(1) end
 | 
			
		||||
parallel.waitForAny(do_sleep, rednet.receive)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
but the following is **NOT**:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local function do_sleep() sleep(1) end
 | 
			
		||||
parallel.waitForAny(do_sleep(), rednet.receive)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
:::
 | 
			
		||||
> [!WARNING]
 | 
			
		||||
> When using this API, be careful to pass the functions you want to run in
 | 
			
		||||
> parallel, and _not_ the result of calling those functions.
 | 
			
		||||
>
 | 
			
		||||
> For instance, the following is correct:
 | 
			
		||||
>
 | 
			
		||||
> ```lua
 | 
			
		||||
> local function do_sleep() sleep(1) end
 | 
			
		||||
> parallel.waitForAny(do_sleep, rednet.receive)
 | 
			
		||||
> ```
 | 
			
		||||
>
 | 
			
		||||
> but the following is **NOT**:
 | 
			
		||||
>
 | 
			
		||||
> ```lua
 | 
			
		||||
> local function do_sleep() sleep(1) end
 | 
			
		||||
> parallel.waitForAny(do_sleep(), rednet.receive)
 | 
			
		||||
> ```
 | 
			
		||||
 | 
			
		||||
@module parallel
 | 
			
		||||
@since 1.2
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
local exception = dofile("rom/modules/main/cc/internal/tiny_require.lua")("cc.internal.exception")
 | 
			
		||||
 | 
			
		||||
local function create(...)
 | 
			
		||||
    local tFns = table.pack(...)
 | 
			
		||||
    local tCos = {}
 | 
			
		||||
    for i = 1, tFns.n, 1 do
 | 
			
		||||
        local fn = tFns[i]
 | 
			
		||||
    local barrier_ctx = { co = coroutine.running() }
 | 
			
		||||
 | 
			
		||||
    local functions = table.pack(...)
 | 
			
		||||
    local threads = {}
 | 
			
		||||
    for i = 1, functions.n, 1 do
 | 
			
		||||
        local fn = functions[i]
 | 
			
		||||
        if type(fn) ~= "function" then
 | 
			
		||||
            error("bad argument #" .. i .. " (function expected, got " .. type(fn) .. ")", 3)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        tCos[i] = coroutine.create(fn)
 | 
			
		||||
        threads[i] = { co = coroutine.create(function() return exception.try_barrier(barrier_ctx, fn) end), filter = nil }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return tCos
 | 
			
		||||
    return threads
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function runUntilLimit(_routines, _limit)
 | 
			
		||||
    local count = #_routines
 | 
			
		||||
local function runUntilLimit(threads, limit)
 | 
			
		||||
    local count = #threads
 | 
			
		||||
    if count < 1 then return 0 end
 | 
			
		||||
    local living = count
 | 
			
		||||
 | 
			
		||||
    local tFilters = {}
 | 
			
		||||
    local eventData = { n = 0 }
 | 
			
		||||
    local event = { n = 0 }
 | 
			
		||||
    while true do
 | 
			
		||||
        for n = 1, count do
 | 
			
		||||
            local r = _routines[n]
 | 
			
		||||
            if r then
 | 
			
		||||
                if tFilters[r] == nil or tFilters[r] == eventData[1] or eventData[1] == "terminate" then
 | 
			
		||||
                    local ok, param = coroutine.resume(r, table.unpack(eventData, 1, eventData.n))
 | 
			
		||||
                    if not ok then
 | 
			
		||||
                        error(param, 0)
 | 
			
		||||
                    else
 | 
			
		||||
                        tFilters[r] = param
 | 
			
		||||
                    end
 | 
			
		||||
                    if coroutine.status(r) == "dead" then
 | 
			
		||||
                        _routines[n] = nil
 | 
			
		||||
                        living = living - 1
 | 
			
		||||
                        if living <= _limit then
 | 
			
		||||
                            return n
 | 
			
		||||
                        end
 | 
			
		||||
        for i = 1, count do
 | 
			
		||||
            local thread = threads[i]
 | 
			
		||||
            if thread and (thread.filter == nil or thread.filter == event[1] or event[1] == "terminate") then
 | 
			
		||||
                local ok, param = coroutine.resume(thread.co, table.unpack(event, 1, event.n))
 | 
			
		||||
                if ok then
 | 
			
		||||
                    thread.filter = param
 | 
			
		||||
                elseif type(param) == "string" and exception.can_wrap_errors() then
 | 
			
		||||
                    error(exception.make_exception(param, thread.co))
 | 
			
		||||
                else
 | 
			
		||||
                    error(param, 0)
 | 
			
		||||
                end
 | 
			
		||||
 | 
			
		||||
                if coroutine.status(thread.co) == "dead" then
 | 
			
		||||
                    threads[i] = false
 | 
			
		||||
                    living = living - 1
 | 
			
		||||
                    if living <= limit then
 | 
			
		||||
                        return i
 | 
			
		||||
                    end
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
        for n = 1, count do
 | 
			
		||||
            local r = _routines[n]
 | 
			
		||||
            if r and coroutine.status(r) == "dead" then
 | 
			
		||||
                _routines[n] = nil
 | 
			
		||||
                living = living - 1
 | 
			
		||||
                if living <= _limit then
 | 
			
		||||
                    return n
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
        eventData = table.pack(os.pullEventRaw())
 | 
			
		||||
 | 
			
		||||
        event = table.pack(os.pullEventRaw())
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Switches between execution of the functions, until any of them
 | 
			
		||||
finishes. If any of the functions errors, the message is propagated upwards
 | 
			
		||||
from the @{parallel.waitForAny} call.
 | 
			
		||||
from the [`parallel.waitForAny`] call.
 | 
			
		||||
 | 
			
		||||
@tparam function ... The functions this task will run
 | 
			
		||||
@usage Print a message every second until the `q` key is pressed.
 | 
			
		||||
@@ -122,13 +115,13 @@ from the @{parallel.waitForAny} call.
 | 
			
		||||
    print("Everything done!")
 | 
			
		||||
]]
 | 
			
		||||
function waitForAny(...)
 | 
			
		||||
    local routines = create(...)
 | 
			
		||||
    return runUntilLimit(routines, #routines - 1)
 | 
			
		||||
    local threads = create(...)
 | 
			
		||||
    return runUntilLimit(threads, #threads - 1)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Switches between execution of the functions, until all of them are
 | 
			
		||||
finished. If any of the functions errors, the message is propagated upwards
 | 
			
		||||
from the @{parallel.waitForAll} call.
 | 
			
		||||
from the [`parallel.waitForAll`] call.
 | 
			
		||||
 | 
			
		||||
@tparam function ... The functions this task will run
 | 
			
		||||
@usage Start off two timers and wait for them both to run.
 | 
			
		||||
@@ -146,6 +139,6 @@ from the @{parallel.waitForAll} call.
 | 
			
		||||
    print("Everything done!")
 | 
			
		||||
]]
 | 
			
		||||
function waitForAll(...)
 | 
			
		||||
    local routines = create(...)
 | 
			
		||||
    return runUntilLimit(routines, 0)
 | 
			
		||||
    local threads = create(...)
 | 
			
		||||
    return runUntilLimit(threads, 0)
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,8 @@
 | 
			
		||||
--[[- Find and control peripherals attached to this computer.
 | 
			
		||||
 | 
			
		||||
Peripherals are blocks (or turtle and pocket computer upgrades) which can
 | 
			
		||||
be controlled by a computer. For instance, the @{speaker} peripheral allows a
 | 
			
		||||
computer to play music and the @{monitor} peripheral allows you to display text
 | 
			
		||||
be controlled by a computer. For instance, the [`speaker`] peripheral allows a
 | 
			
		||||
computer to play music and the [`monitor`] peripheral allows you to display text
 | 
			
		||||
in the world.
 | 
			
		||||
 | 
			
		||||
## Referencing peripherals
 | 
			
		||||
@@ -18,10 +18,10 @@ computer will be called `"bottom"` in your Lua code, one to the left called
 | 
			
		||||
`"right"`, `"front"`, `"back"`).
 | 
			
		||||
 | 
			
		||||
You can list the names of all peripherals with the `peripherals` program, or the
 | 
			
		||||
@{peripheral.getNames} function.
 | 
			
		||||
[`peripheral.getNames`] function.
 | 
			
		||||
 | 
			
		||||
It's also possible to use peripherals which are further away from your computer
 | 
			
		||||
through the use of @{modem|Wired Modems}. Place one modem against your computer
 | 
			
		||||
through the use of [Wired Modems][`modem`]. Place one modem against your computer
 | 
			
		||||
(you may need to sneak and right click), run Networking Cable to your
 | 
			
		||||
peripheral, and then place another modem against that block. You can then right
 | 
			
		||||
click the modem to use (or *attach*) the peripheral. This will print a
 | 
			
		||||
@@ -32,24 +32,23 @@ clipboard.
 | 
			
		||||
## Using peripherals
 | 
			
		||||
 | 
			
		||||
Once you have the name of a peripheral, you can call functions on it using the
 | 
			
		||||
@{peripheral.call} function. This takes the name of our peripheral, the name of
 | 
			
		||||
[`peripheral.call`] function. This takes the name of our peripheral, the name of
 | 
			
		||||
the function we want to call, and then its arguments.
 | 
			
		||||
 | 
			
		||||
:::info
 | 
			
		||||
Some bits of the peripheral API call peripheral functions *methods* instead
 | 
			
		||||
(for example, the @{peripheral.getMethods} function). Don't worry, they're the
 | 
			
		||||
same thing!
 | 
			
		||||
:::
 | 
			
		||||
> [!INFO]
 | 
			
		||||
> Some bits of the peripheral API call peripheral functions *methods* instead
 | 
			
		||||
> (for example, the [`peripheral.getMethods`] function). Don't worry, they're the
 | 
			
		||||
> same thing!
 | 
			
		||||
 | 
			
		||||
Let's say we have a monitor above our computer (and so "top") and want to
 | 
			
		||||
@{monitor.write|write some text to it}. We'd write the following:
 | 
			
		||||
[write some text to it][`monitor.write`]. We'd write the following:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
peripheral.call("top", "write", "This is displayed on a monitor!")
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Once you start calling making a couple of peripheral calls this can get very
 | 
			
		||||
repetitive, and so we can @{peripheral.wrap|wrap} a peripheral. This builds a
 | 
			
		||||
repetitive, and so we can [wrap][`peripheral.wrap`] a peripheral. This builds a
 | 
			
		||||
table of all the peripheral's functions so you can use it like an API or module.
 | 
			
		||||
 | 
			
		||||
For instance, we could have written the above example as follows:
 | 
			
		||||
@@ -66,7 +65,7 @@ called, you just need to know it's there. For instance, if you're writing a
 | 
			
		||||
music player, you just need a speaker - it doesn't matter if it's above or below
 | 
			
		||||
the computer.
 | 
			
		||||
 | 
			
		||||
Thankfully there's a quick way to do this: @{peripheral.find}. This takes a
 | 
			
		||||
Thankfully there's a quick way to do this: [`peripheral.find`]. This takes a
 | 
			
		||||
*peripheral type* and returns all the attached peripherals which are of this
 | 
			
		||||
type.
 | 
			
		||||
 | 
			
		||||
@@ -76,10 +75,10 @@ are just called `"speaker"`, and monitors `"monitor"`. Some peripherals might
 | 
			
		||||
have more than one type - a Minecraft chest is both a `"minecraft:chest"` and
 | 
			
		||||
`"inventory"`.
 | 
			
		||||
 | 
			
		||||
You can get all the types a peripheral has with @{peripheral.getType}, and check
 | 
			
		||||
a peripheral is a specific type with @{peripheral.hasType}.
 | 
			
		||||
You can get all the types a peripheral has with [`peripheral.getType`], and check
 | 
			
		||||
a peripheral is a specific type with [`peripheral.hasType`].
 | 
			
		||||
 | 
			
		||||
To return to our original example, let's use @{peripheral.find} to find an
 | 
			
		||||
To return to our original example, let's use [`peripheral.find`] to find an
 | 
			
		||||
attached speaker:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
@@ -100,7 +99,7 @@ local expect = dofile("rom/modules/main/cc/expect.lua").expect
 | 
			
		||||
local native = peripheral
 | 
			
		||||
 | 
			
		||||
-- Stub in peripheral.hasType
 | 
			
		||||
function native.hasType(p, type) return peripheral.getType(p) == type end
 | 
			
		||||
function native.hasType(p, ty) return native.getType(p) == ty end
 | 
			
		||||
 | 
			
		||||
local sides = rs.getSides()
 | 
			
		||||
 | 
			
		||||
@@ -233,7 +232,7 @@ function getMethods(name)
 | 
			
		||||
    return nil
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Get the name of a peripheral wrapped with @{peripheral.wrap}.
 | 
			
		||||
--- Get the name of a peripheral wrapped with [`peripheral.wrap`].
 | 
			
		||||
--
 | 
			
		||||
-- @tparam table peripheral The peripheral to get the name of.
 | 
			
		||||
-- @treturn string The name of the given peripheral.
 | 
			
		||||
@@ -274,7 +273,7 @@ function call(name, method, ...)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Get a table containing all functions available on a peripheral. These can
 | 
			
		||||
-- then be called instead of using @{peripheral.call} every time.
 | 
			
		||||
-- then be called instead of using [`peripheral.call`] every time.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the peripheral to wrap.
 | 
			
		||||
-- @treturn table|nil The table containing the peripheral's methods, or `nil` if
 | 
			
		||||
@@ -309,7 +308,7 @@ function wrap(name)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Find all peripherals of a specific type, and return the
 | 
			
		||||
@{peripheral.wrap|wrapped} peripherals.
 | 
			
		||||
[wrapped][`peripheral.wrap`] peripherals.
 | 
			
		||||
 | 
			
		||||
@tparam string ty The type of peripheral to look for.
 | 
			
		||||
@tparam[opt] function(name:string, wrapped:table):boolean filter A
 | 
			
		||||
@@ -329,7 +328,7 @@ and returns if it should be included in the result.
 | 
			
		||||
        return modem.isWireless() -- Check this modem is wireless.
 | 
			
		||||
    end) }
 | 
			
		||||
 | 
			
		||||
@usage This abuses the `filter` argument to call @{rednet.open} on every modem.
 | 
			
		||||
@usage This abuses the `filter` argument to call [`rednet.open`] on every modem.
 | 
			
		||||
 | 
			
		||||
    peripheral.find("modem", rednet.open)
 | 
			
		||||
@since 1.6
 | 
			
		||||
 
 | 
			
		||||
@@ -2,42 +2,41 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--[[- Communicate with other computers by using @{modem|modems}. @{rednet}
 | 
			
		||||
provides a layer of abstraction on top of the main @{modem} peripheral, making
 | 
			
		||||
--[[- Communicate with other computers by using [modems][`modem`]. [`rednet`]
 | 
			
		||||
provides a layer of abstraction on top of the main [`modem`] peripheral, making
 | 
			
		||||
it slightly easier to use.
 | 
			
		||||
 | 
			
		||||
## Basic usage
 | 
			
		||||
In order to send a message between two computers, each computer must have a
 | 
			
		||||
modem on one of its sides (or in the case of pocket computers and turtles, the
 | 
			
		||||
modem must be equipped as an upgrade). The two computers should then call
 | 
			
		||||
@{rednet.open}, which sets up the modems ready to send and receive messages.
 | 
			
		||||
[`rednet.open`], which sets up the modems ready to send and receive messages.
 | 
			
		||||
 | 
			
		||||
Once rednet is opened, you can send messages using @{rednet.send} and receive
 | 
			
		||||
them using @{rednet.receive}. It's also possible to send a message to _every_
 | 
			
		||||
rednet-using computer using @{rednet.broadcast}.
 | 
			
		||||
Once rednet is opened, you can send messages using [`rednet.send`] and receive
 | 
			
		||||
them using [`rednet.receive`]. It's also possible to send a message to _every_
 | 
			
		||||
rednet-using computer using [`rednet.broadcast`].
 | 
			
		||||
 | 
			
		||||
:::caution Network security
 | 
			
		||||
 | 
			
		||||
While rednet provides a friendly way to send messages to specific computers, it
 | 
			
		||||
doesn't provide any guarantees about security. Other computers could be
 | 
			
		||||
listening in to your messages, or even pretending to send messages from other computers!
 | 
			
		||||
 | 
			
		||||
If you're playing on a multi-player server (or at least one where you don't
 | 
			
		||||
trust other players), it's worth encrypting or signing your rednet messages.
 | 
			
		||||
:::
 | 
			
		||||
> [Network security][!WARNING]
 | 
			
		||||
>
 | 
			
		||||
> While rednet provides a friendly way to send messages to specific computers, it
 | 
			
		||||
> doesn't provide any guarantees about security. Other computers could be
 | 
			
		||||
> listening in to your messages, or even pretending to send messages from other computers!
 | 
			
		||||
>
 | 
			
		||||
> If you're playing on a multi-player server (or at least one where you don't
 | 
			
		||||
> trust other players), it's worth encrypting or signing your rednet messages.
 | 
			
		||||
 | 
			
		||||
## Protocols and hostnames
 | 
			
		||||
Several rednet messages accept "protocol"s - simple string names describing what
 | 
			
		||||
a message is about. When sending messages using @{rednet.send} and
 | 
			
		||||
@{rednet.broadcast}, you can optionally specify a protocol for the message. This
 | 
			
		||||
same protocol can then be given to @{rednet.receive}, to ignore all messages not
 | 
			
		||||
a message is about. When sending messages using [`rednet.send`] and
 | 
			
		||||
[`rednet.broadcast`], you can optionally specify a protocol for the message. This
 | 
			
		||||
same protocol can then be given to [`rednet.receive`], to ignore all messages not
 | 
			
		||||
using this protocol.
 | 
			
		||||
 | 
			
		||||
It's also possible to look-up computers based on protocols, providing a basic
 | 
			
		||||
system for service discovery and [DNS]. A computer can advertise that it
 | 
			
		||||
supports a particular protocol with @{rednet.host}, also providing a friendly
 | 
			
		||||
supports a particular protocol with [`rednet.host`], also providing a friendly
 | 
			
		||||
"hostname". Other computers may then find all computers which support this
 | 
			
		||||
protocol using @{rednet.lookup}.
 | 
			
		||||
protocol using [`rednet.lookup`].
 | 
			
		||||
 | 
			
		||||
[DNS]: https://en.wikipedia.org/wiki/Domain_Name_System "Domain Name System"
 | 
			
		||||
 | 
			
		||||
@@ -50,7 +49,7 @@ bare-bones but flexible interface.
 | 
			
		||||
 | 
			
		||||
local expect = dofile("rom/modules/main/cc/expect.lua").expect
 | 
			
		||||
 | 
			
		||||
--- The channel used by the Rednet API to @{broadcast} messages.
 | 
			
		||||
--- The channel used by the Rednet API to [`broadcast`] messages.
 | 
			
		||||
CHANNEL_BROADCAST = 65535
 | 
			
		||||
 | 
			
		||||
--- The channel used by the Rednet API to repeat messages.
 | 
			
		||||
@@ -68,12 +67,12 @@ local function id_as_channel(id)
 | 
			
		||||
    return (id or os.getComputerID()) % MAX_ID_CHANNELS
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Opens a modem with the given @{peripheral} name, allowing it to send and
 | 
			
		||||
--[[- Opens a modem with the given [`peripheral`] name, allowing it to send and
 | 
			
		||||
receive messages over rednet.
 | 
			
		||||
 | 
			
		||||
This will open the modem on two channels: one which has the same
 | 
			
		||||
@{os.getComputerID|ID} as the computer, and another on
 | 
			
		||||
@{CHANNEL_BROADCAST|the broadcast channel}.
 | 
			
		||||
[ID][`os.getComputerID`] as the computer, and another on
 | 
			
		||||
[the broadcast channel][`CHANNEL_BROADCAST`].
 | 
			
		||||
 | 
			
		||||
@tparam string modem The name of the modem to open.
 | 
			
		||||
@throws If there is no such modem with the given name
 | 
			
		||||
@@ -83,7 +82,7 @@ rednet messages using it.
 | 
			
		||||
    rednet.open("back")
 | 
			
		||||
 | 
			
		||||
@usage Open rednet on all attached modems. This abuses the "filter" argument to
 | 
			
		||||
@{peripheral.find}.
 | 
			
		||||
[`peripheral.find`].
 | 
			
		||||
 | 
			
		||||
    peripheral.find("modem", rednet.open)
 | 
			
		||||
@see rednet.close
 | 
			
		||||
@@ -98,7 +97,7 @@ function open(modem)
 | 
			
		||||
    peripheral.call(modem, "open", CHANNEL_BROADCAST)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Close a modem with the given @{peripheral} name, meaning it can no longer
 | 
			
		||||
--- Close a modem with the given [`peripheral`] name, meaning it can no longer
 | 
			
		||||
-- send and receive rednet messages.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam[opt] string modem The side the modem exists on. If not given, all
 | 
			
		||||
@@ -150,22 +149,22 @@ function isOpen(modem)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Allows a computer or turtle with an attached modem to send a message
 | 
			
		||||
intended for a sycomputer with a specific ID. At least one such modem must first
 | 
			
		||||
be @{rednet.open|opened} before sending is possible.
 | 
			
		||||
intended for a computer with a specific ID. At least one such modem must first
 | 
			
		||||
be [opened][`rednet.open`] before sending is possible.
 | 
			
		||||
 | 
			
		||||
Assuming the target was in range and also had a correctly opened modem, the
 | 
			
		||||
target computer may then use @{rednet.receive} to collect the message.
 | 
			
		||||
target computer may then use [`rednet.receive`] to collect the message.
 | 
			
		||||
 | 
			
		||||
@tparam number recipient The ID of the receiving computer.
 | 
			
		||||
@param message The message to send. Like with @{modem.transmit}, this can
 | 
			
		||||
@param message The message to send. Like with [`modem.transmit`], this can
 | 
			
		||||
contain any primitive type (numbers, booleans and strings) as well as
 | 
			
		||||
tables. Other types (like functions), as well as metatables, will not be
 | 
			
		||||
transmitted.
 | 
			
		||||
@tparam[opt] string protocol The "protocol" to send this message under. When
 | 
			
		||||
using @{rednet.receive} one can filter to only receive messages sent under a
 | 
			
		||||
using [`rednet.receive`] one can filter to only receive messages sent under a
 | 
			
		||||
particular protocol.
 | 
			
		||||
@treturn boolean If this message was successfully sent (i.e. if rednet is
 | 
			
		||||
currently @{rednet.open|open}). Note, this does not guarantee the message was
 | 
			
		||||
currently [open][`rednet.open`]). Note, this does not guarantee the message was
 | 
			
		||||
actually _received_.
 | 
			
		||||
@changed 1.6 Added protocol parameter.
 | 
			
		||||
@changed 1.82.0 Now returns whether the message was successfully sent.
 | 
			
		||||
@@ -217,13 +216,13 @@ function send(recipient, message, protocol)
 | 
			
		||||
    return sent
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Broadcasts a string message over the predefined @{CHANNEL_BROADCAST}
 | 
			
		||||
--[[- Broadcasts a string message over the predefined [`CHANNEL_BROADCAST`]
 | 
			
		||||
channel. The message will be received by every device listening to rednet.
 | 
			
		||||
 | 
			
		||||
@param message The message to send. This should not contain coroutines or
 | 
			
		||||
functions, as they will be converted to @{nil}.
 | 
			
		||||
functions, as they will be converted to [`nil`].
 | 
			
		||||
@tparam[opt] string protocol The "protocol" to send this message under. When
 | 
			
		||||
using @{rednet.receive} one can filter to only receive messages sent under a
 | 
			
		||||
using [`rednet.receive`] one can filter to only receive messages sent under a
 | 
			
		||||
particular protocol.
 | 
			
		||||
@see rednet.receive
 | 
			
		||||
@changed 1.6 Added protocol parameter.
 | 
			
		||||
@@ -299,6 +298,7 @@ function receive(protocol_filter, timeout)
 | 
			
		||||
            -- Return the first matching rednet_message
 | 
			
		||||
            local sender_id, message, protocol = p1, p2, p3
 | 
			
		||||
            if protocol_filter == nil or protocol == protocol_filter then
 | 
			
		||||
                if timer then os.cancelTimer(timer) end
 | 
			
		||||
                return sender_id, message, protocol
 | 
			
		||||
            end
 | 
			
		||||
        elseif event == "timer" then
 | 
			
		||||
@@ -311,7 +311,7 @@ function receive(protocol_filter, timeout)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Register the system as "hosting" the desired protocol under the specified
 | 
			
		||||
name. If a rednet @{rednet.lookup|lookup} is performed for that protocol (and
 | 
			
		||||
name. If a rednet [lookup][`rednet.lookup`] is performed for that protocol (and
 | 
			
		||||
maybe name) on the same network, the registered system will automatically
 | 
			
		||||
respond via a background process, hence providing the system performing the
 | 
			
		||||
lookup with its ID number.
 | 
			
		||||
@@ -343,8 +343,8 @@ function host(protocol, hostname)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Stop @{rednet.host|hosting} a specific protocol, meaning it will no longer
 | 
			
		||||
-- respond to @{rednet.lookup} requests.
 | 
			
		||||
--- Stop [hosting][`rednet.host`] a specific protocol, meaning it will no longer
 | 
			
		||||
-- respond to [`rednet.lookup`] requests.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string protocol The protocol to unregister your self from.
 | 
			
		||||
-- @since 1.6
 | 
			
		||||
@@ -353,7 +353,7 @@ function unhost(protocol)
 | 
			
		||||
    hostnames[protocol] = nil
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Search the local rednet network for systems @{rednet.host|hosting} the
 | 
			
		||||
--[[- Search the local rednet network for systems [hosting][`rednet.host`] the
 | 
			
		||||
desired protocol and returns any computer IDs that respond as "registered"
 | 
			
		||||
against it.
 | 
			
		||||
 | 
			
		||||
@@ -365,7 +365,7 @@ match is found).
 | 
			
		||||
 | 
			
		||||
@treturn[1] number... A list of computer IDs hosting the given protocol.
 | 
			
		||||
@treturn[2] number|nil The computer ID with the provided hostname and protocol,
 | 
			
		||||
or @{nil} if none exists.
 | 
			
		||||
or [`nil`] if none exists.
 | 
			
		||||
@since 1.6
 | 
			
		||||
@usage Find all computers which are hosting the `"chat"` protocol.
 | 
			
		||||
 | 
			
		||||
@@ -432,6 +432,7 @@ function lookup(protocol, hostname)
 | 
			
		||||
                    if hostname == nil then
 | 
			
		||||
                        table.insert(results, sender_id)
 | 
			
		||||
                    elseif message.sHostname == hostname then
 | 
			
		||||
                        os.cancelTimer(timer)
 | 
			
		||||
                        return sender_id
 | 
			
		||||
                    end
 | 
			
		||||
                end
 | 
			
		||||
@@ -441,6 +442,9 @@ function lookup(protocol, hostname)
 | 
			
		||||
            break
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    os.cancelTimer(timer)
 | 
			
		||||
 | 
			
		||||
    if results then
 | 
			
		||||
        return table.unpack(results)
 | 
			
		||||
    end
 | 
			
		||||
@@ -450,7 +454,7 @@ end
 | 
			
		||||
local started = false
 | 
			
		||||
 | 
			
		||||
--- Listen for modem messages and converts them into rednet messages, which may
 | 
			
		||||
-- then be @{receive|received}.
 | 
			
		||||
-- then be [received][`receive`].
 | 
			
		||||
--
 | 
			
		||||
-- This is automatically started in the background on computer startup, and
 | 
			
		||||
-- should not be called manually.
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,31 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--- Read and write configuration options for CraftOS and your programs.
 | 
			
		||||
--
 | 
			
		||||
-- By default, the settings API will load its configuration from the
 | 
			
		||||
-- `/.settings` file. One can then use @{settings.save} to update the file.
 | 
			
		||||
--
 | 
			
		||||
-- @module settings
 | 
			
		||||
-- @since 1.78
 | 
			
		||||
--[[- Read and write configuration options for CraftOS and your programs.
 | 
			
		||||
 | 
			
		||||
When a computer starts, it reads the current value of settings from the
 | 
			
		||||
`/.settings` file. These values then may be [read][`settings.get`] or
 | 
			
		||||
[modified][`settings.set`].
 | 
			
		||||
 | 
			
		||||
> [!WARNING]
 | 
			
		||||
> Calling [`settings.set`] does _not_ update the settings file by default. You
 | 
			
		||||
> _must_ call [`settings.save`] to persist values.
 | 
			
		||||
 | 
			
		||||
@module settings
 | 
			
		||||
@since 1.78
 | 
			
		||||
@usage Define an basic setting `123` and read its value.
 | 
			
		||||
 | 
			
		||||
    settings.define("my.setting", {
 | 
			
		||||
        description = "An example setting",
 | 
			
		||||
        default = 123,
 | 
			
		||||
        type = "number",
 | 
			
		||||
    })
 | 
			
		||||
    print("my.setting = " .. settings.get("my.setting")) -- 123
 | 
			
		||||
 | 
			
		||||
You can then use the `set` program to change its value (e.g. `set my.setting 456`),
 | 
			
		||||
and then re-run the `example` program to check it has changed.
 | 
			
		||||
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
local expect = dofile("rom/modules/main/cc/expect.lua")
 | 
			
		||||
local type, expect, field = type, expect.expect, expect.field
 | 
			
		||||
@@ -40,10 +58,10 @@ for _, v in ipairs(valid_types) do valid_types[v] = true end
 | 
			
		||||
-- Options for this setting. This table accepts the following fields:
 | 
			
		||||
--
 | 
			
		||||
--  - `description`: A description which may be printed when running the `set` program.
 | 
			
		||||
--  - `default`: A default value, which is returned by @{settings.get} if the
 | 
			
		||||
--  - `default`: A default value, which is returned by [`settings.get`] if the
 | 
			
		||||
--    setting has not been changed.
 | 
			
		||||
--  - `type`: Require values to be of this type. @{set|Setting} the value to another type
 | 
			
		||||
--    will error.
 | 
			
		||||
--  - `type`: Require values to be of this type. [Setting][`set`] the value to another type
 | 
			
		||||
--    will error. Must be one of: `"number"`, `"string"`, `"boolean"`, or `"table"`.
 | 
			
		||||
-- @since 1.87.0
 | 
			
		||||
function define(name, options)
 | 
			
		||||
    expect(1, name, "string")
 | 
			
		||||
@@ -66,9 +84,9 @@ function define(name, options)
 | 
			
		||||
    details[name] = options
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Remove a @{define|definition} of a setting.
 | 
			
		||||
--- Remove a [definition][`define`] of a setting.
 | 
			
		||||
--
 | 
			
		||||
-- If a setting has been changed, this does not remove its value. Use @{settings.unset}
 | 
			
		||||
-- If a setting has been changed, this does not remove its value. Use [`settings.unset`]
 | 
			
		||||
-- for that.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of this option
 | 
			
		||||
@@ -92,13 +110,18 @@ local function set_value(name, new)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Set the value of a setting.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the setting to set
 | 
			
		||||
-- @param value The setting's value. This cannot be `nil`, and must be
 | 
			
		||||
-- serialisable by @{textutils.serialize}.
 | 
			
		||||
-- @throws If this value cannot be serialised
 | 
			
		||||
-- @see settings.unset
 | 
			
		||||
--[[- Set the value of a setting.
 | 
			
		||||
 | 
			
		||||
> [!WARNING]
 | 
			
		||||
> Calling [`settings.set`] does _not_ update the settings file by default. You
 | 
			
		||||
> _must_ call [`settings.save`] to persist values.
 | 
			
		||||
 | 
			
		||||
@tparam string name The name of the setting to set
 | 
			
		||||
@param value The setting's value. This cannot be `nil`, and must be
 | 
			
		||||
serialisable by [`textutils.serialize`].
 | 
			
		||||
@throws If this value cannot be serialised
 | 
			
		||||
@see settings.unset
 | 
			
		||||
]]
 | 
			
		||||
function set(name, value)
 | 
			
		||||
    expect(1, name, "string")
 | 
			
		||||
    expect(2, value, "number", "string", "boolean", "table")
 | 
			
		||||
@@ -134,7 +157,7 @@ end
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the setting to get.
 | 
			
		||||
-- @treturn { description? = string, default? = any, type? = string, value? = any }
 | 
			
		||||
-- Information about this setting. This includes all information from @{settings.define},
 | 
			
		||||
-- Information about this setting. This includes all information from [`settings.define`],
 | 
			
		||||
-- as well as this setting's value.
 | 
			
		||||
-- @since 1.87.0
 | 
			
		||||
function getDetails(name)
 | 
			
		||||
@@ -148,8 +171,8 @@ end
 | 
			
		||||
 | 
			
		||||
--- Remove the value of a setting, setting it to the default.
 | 
			
		||||
--
 | 
			
		||||
-- @{settings.get} will return the default value until the setting's value is
 | 
			
		||||
-- @{settings.set|set}, or the computer is rebooted.
 | 
			
		||||
-- [`settings.get`] will return the default value until the setting's value is
 | 
			
		||||
-- [set][`settings.set`], or the computer is rebooted.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string name The name of the setting to unset.
 | 
			
		||||
-- @see settings.set
 | 
			
		||||
@@ -159,8 +182,8 @@ function unset(name)
 | 
			
		||||
    set_value(name, nil)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Resets the value of all settings. Equivalent to calling @{settings.unset}
 | 
			
		||||
--- on every setting.
 | 
			
		||||
--- Resets the value of all settings. Equivalent to calling [`settings.unset`]
 | 
			
		||||
-- on every setting.
 | 
			
		||||
--
 | 
			
		||||
-- @see settings.unset
 | 
			
		||||
function clear()
 | 
			
		||||
@@ -190,16 +213,16 @@ end
 | 
			
		||||
-- Existing settings will be merged with any pre-existing ones. Conflicting
 | 
			
		||||
-- entries will be overwritten, but any others will be preserved.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam[opt] string sPath The file to load from, defaulting to `.settings`.
 | 
			
		||||
-- @tparam[opt=".settings"] string path The file to load from.
 | 
			
		||||
-- @treturn boolean Whether settings were successfully read from this
 | 
			
		||||
-- file. Reasons for failure may include the file not existing or being
 | 
			
		||||
-- corrupted.
 | 
			
		||||
--
 | 
			
		||||
-- @see settings.save
 | 
			
		||||
-- @changed 1.87.0 `sPath` is now optional.
 | 
			
		||||
function load(sPath)
 | 
			
		||||
    expect(1, sPath, "string", "nil")
 | 
			
		||||
    local file = fs.open(sPath or ".settings", "r")
 | 
			
		||||
-- @changed 1.87.0 `path` is now optional.
 | 
			
		||||
function load(path)
 | 
			
		||||
    expect(1, path, "string", "nil")
 | 
			
		||||
    local file = fs.open(path or ".settings", "r")
 | 
			
		||||
    if not file then
 | 
			
		||||
        return false
 | 
			
		||||
    end
 | 
			
		||||
@@ -232,14 +255,14 @@ end
 | 
			
		||||
-- This will entirely overwrite the pre-existing file. Settings defined in the
 | 
			
		||||
-- file, but not currently loaded will be removed.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam[opt] string sPath The path to save settings to, defaulting to `.settings`.
 | 
			
		||||
-- @tparam[opt=".settings"] string path The path to save settings to.
 | 
			
		||||
-- @treturn boolean If the settings were successfully saved.
 | 
			
		||||
--
 | 
			
		||||
-- @see settings.load
 | 
			
		||||
-- @changed 1.87.0 `sPath` is now optional.
 | 
			
		||||
function save(sPath)
 | 
			
		||||
    expect(1, sPath, "string", "nil")
 | 
			
		||||
    local file = fs.open(sPath or ".settings", "w")
 | 
			
		||||
-- @changed 1.87.0 `path` is now optional.
 | 
			
		||||
function save(path)
 | 
			
		||||
    expect(1, path, "string", "nil")
 | 
			
		||||
    local file = fs.open(path or ".settings", "w")
 | 
			
		||||
    if not file then
 | 
			
		||||
        return false
 | 
			
		||||
    end
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@ end
 | 
			
		||||
 | 
			
		||||
local term = _ENV
 | 
			
		||||
 | 
			
		||||
--- Redirects terminal output to a monitor, a @{window}, or any other custom
 | 
			
		||||
--- Redirects terminal output to a monitor, a [`window`], or any other custom
 | 
			
		||||
-- terminal object. Once the redirect is performed, any calls to a "term"
 | 
			
		||||
-- function - or to a function that makes use of a term function, as @{print} -
 | 
			
		||||
-- function - or to a function that makes use of a term function, as [`print`] -
 | 
			
		||||
-- will instead operate with the new terminal object.
 | 
			
		||||
--
 | 
			
		||||
-- A "terminal object" is simply a table that contains functions with the same
 | 
			
		||||
@@ -29,12 +29,13 @@ local term = _ENV
 | 
			
		||||
-- The redirect can be undone by pointing back to the previous terminal object
 | 
			
		||||
-- (which this function returns whenever you switch).
 | 
			
		||||
--
 | 
			
		||||
-- @tparam Redirect target The terminal redirect the @{term} API will draw to.
 | 
			
		||||
-- @tparam Redirect target The terminal redirect the [`term`] API will draw to.
 | 
			
		||||
-- @treturn Redirect The previous redirect object, as returned by
 | 
			
		||||
-- @{term.current}.
 | 
			
		||||
-- [`term.current`].
 | 
			
		||||
-- @since 1.31
 | 
			
		||||
-- @usage
 | 
			
		||||
-- Redirect to a monitor on the right of the computer.
 | 
			
		||||
--
 | 
			
		||||
--     term.redirect(peripheral.wrap("right"))
 | 
			
		||||
term.redirect = function(target)
 | 
			
		||||
    expect(1, target, "table")
 | 
			
		||||
@@ -60,7 +61,7 @@ end
 | 
			
		||||
-- @treturn Redirect The current terminal redirect
 | 
			
		||||
-- @since 1.6
 | 
			
		||||
-- @usage
 | 
			
		||||
-- Create a new @{window} which draws to the current redirect target.
 | 
			
		||||
-- Create a new [`window`] which draws to the current redirect target.
 | 
			
		||||
--
 | 
			
		||||
--     window.create(term.current(), 1, 1, 10, 10)
 | 
			
		||||
term.current = function()
 | 
			
		||||
@@ -70,7 +71,7 @@ end
 | 
			
		||||
--- Get the native terminal object of the current computer.
 | 
			
		||||
--
 | 
			
		||||
-- It is recommended you do not use this function unless you absolutely have
 | 
			
		||||
-- to. In a multitasked environment, @{term.native} will _not_ be the current
 | 
			
		||||
-- to. In a multitasked environment, [`term.native`] will _not_ be the current
 | 
			
		||||
-- terminal object, and so drawing may interfere with other programs.
 | 
			
		||||
--
 | 
			
		||||
-- @treturn Redirect The native terminal redirect.
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,16 @@
 | 
			
		||||
-- @module textutils
 | 
			
		||||
-- @since 1.2
 | 
			
		||||
 | 
			
		||||
local expect = dofile("rom/modules/main/cc/expect.lua")
 | 
			
		||||
local require = dofile("rom/modules/main/cc/internal/tiny_require.lua")
 | 
			
		||||
 | 
			
		||||
local expect = require("cc.expect")
 | 
			
		||||
local expect, field = expect.expect, expect.field
 | 
			
		||||
local wrap = dofile("rom/modules/main/cc/strings.lua").wrap
 | 
			
		||||
local wrap = require("cc.strings").wrap
 | 
			
		||||
 | 
			
		||||
--- Slowly writes string text at current cursor position,
 | 
			
		||||
-- character-by-character.
 | 
			
		||||
--
 | 
			
		||||
-- Like @{_G.write}, this does not insert a newline at the end.
 | 
			
		||||
-- Like [`_G.write`], this does not insert a newline at the end.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string text The the text to write to the screen
 | 
			
		||||
-- @tparam[opt] number rate The number of characters to write each second,
 | 
			
		||||
@@ -42,7 +44,7 @@ end
 | 
			
		||||
--- Slowly prints string text at current cursor position,
 | 
			
		||||
-- character-by-character.
 | 
			
		||||
--
 | 
			
		||||
-- Like @{print}, this inserts a newline after printing.
 | 
			
		||||
-- Like [`print`], this inserts a newline after printing.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string sText The the text to write to the screen
 | 
			
		||||
-- @tparam[opt] number nRate The number of characters to write each second,
 | 
			
		||||
@@ -56,7 +58,7 @@ end
 | 
			
		||||
 | 
			
		||||
--- Takes input time and formats it in a more readable format such as `6:30 PM`.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam number nTime The time to format, as provided by @{os.time}.
 | 
			
		||||
-- @tparam number nTime The time to format, as provided by [`os.time`].
 | 
			
		||||
-- @tparam[opt] boolean bTwentyFourHour Whether to format this as a 24-hour
 | 
			
		||||
-- clock (`18:30`) rather than a 12-hour one (`6:30 AM`)
 | 
			
		||||
-- @treturn string The formatted time
 | 
			
		||||
@@ -114,7 +116,7 @@ end
 | 
			
		||||
--[[- Prints a given string to the display.
 | 
			
		||||
 | 
			
		||||
If the action can be completed without scrolling, it acts much the same as
 | 
			
		||||
@{print}; otherwise, it will throw up a "Press any key to continue" prompt at
 | 
			
		||||
[`print`]; otherwise, it will throw up a "Press any key to continue" prompt at
 | 
			
		||||
the bottom of the display. Each press will cause it to scroll down and write a
 | 
			
		||||
single line more before prompting again, if need be.
 | 
			
		||||
 | 
			
		||||
@@ -253,7 +255,7 @@ end
 | 
			
		||||
--[[- Prints tables in a structured form, stopping and prompting for input should
 | 
			
		||||
the result not fit on the terminal.
 | 
			
		||||
 | 
			
		||||
This functions identically to @{textutils.tabulate}, but will prompt for user
 | 
			
		||||
This functions identically to [`textutils.tabulate`], but will prompt for user
 | 
			
		||||
input should the whole output not fit on the display.
 | 
			
		||||
 | 
			
		||||
@tparam {string...}|number ... The rows and text colors to display.
 | 
			
		||||
@@ -424,25 +426,49 @@ do
 | 
			
		||||
        if map[c] == nil then map[c] = hexify(c) end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    serializeJSONString = function(s)
 | 
			
		||||
        return ('"%s"'):format(s:gsub("[\0-\x1f\"\\]", map):gsub("[\x7f-\xff]", hexify))
 | 
			
		||||
    serializeJSONString = function(s, options)
 | 
			
		||||
        if options and options.unicode_strings and s:find("[\x80-\xff]") then
 | 
			
		||||
            local retval = '"'
 | 
			
		||||
            for _, code in utf8.codes(s) do
 | 
			
		||||
                if code > 0xFFFF then
 | 
			
		||||
                    -- Encode the codepoint as a UTF-16 surrogate pair
 | 
			
		||||
                    code = code - 0x10000
 | 
			
		||||
                    local high, low = bit32.extract(code, 10, 10) + 0xD800, bit32.extract(code, 0, 10) + 0xDC00
 | 
			
		||||
                    retval = retval .. ("\\u%04X\\u%04X"):format(high, low)
 | 
			
		||||
                elseif code <= 0x5C and map[string.char(code)] then -- 0x5C = `\`, don't run `string.char` if we don't need to
 | 
			
		||||
                    retval = retval .. map[string.char(code)]
 | 
			
		||||
                elseif code < 0x20 or code >= 0x7F then
 | 
			
		||||
                    retval = retval .. ("\\u%04X"):format(code)
 | 
			
		||||
                else
 | 
			
		||||
                    retval = retval .. string.char(code)
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
            return retval .. '"'
 | 
			
		||||
        else
 | 
			
		||||
            return ('"%s"'):format(s:gsub("[\0-\x1f\"\\]", map):gsub("[\x7f-\xff]", hexify))
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function serializeJSONImpl(t, tTracking, bNBTStyle)
 | 
			
		||||
local function serializeJSONImpl(t, tracking, options)
 | 
			
		||||
    local sType = type(t)
 | 
			
		||||
    if t == empty_json_array then return "[]"
 | 
			
		||||
    elseif t == json_null then return "null"
 | 
			
		||||
 | 
			
		||||
    elseif sType == "table" then
 | 
			
		||||
        if tTracking[t] ~= nil then
 | 
			
		||||
            error("Cannot serialize table with recursive entries", 0)
 | 
			
		||||
        if tracking[t] ~= nil then
 | 
			
		||||
            if tracking[t] == false then
 | 
			
		||||
                error("Cannot serialize table with repeated entries", 0)
 | 
			
		||||
            else
 | 
			
		||||
                error("Cannot serialize table with recursive entries", 0)
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
        tTracking[t] = true
 | 
			
		||||
        tracking[t] = true
 | 
			
		||||
 | 
			
		||||
        local result
 | 
			
		||||
        if next(t) == nil then
 | 
			
		||||
            -- Empty tables are simple
 | 
			
		||||
            return "{}"
 | 
			
		||||
            result = "{}"
 | 
			
		||||
        else
 | 
			
		||||
            -- Other tables take more work
 | 
			
		||||
            local sObjectResult = "{"
 | 
			
		||||
@@ -450,13 +476,14 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
 | 
			
		||||
            local nObjectSize = 0
 | 
			
		||||
            local nArraySize = 0
 | 
			
		||||
            local largestArrayIndex = 0
 | 
			
		||||
            local bNBTStyle = options.nbt_style
 | 
			
		||||
            for k, v in pairs(t) do
 | 
			
		||||
                if type(k) == "string" then
 | 
			
		||||
                    local sEntry
 | 
			
		||||
                    if bNBTStyle then
 | 
			
		||||
                        sEntry = tostring(k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle)
 | 
			
		||||
                        sEntry = tostring(k) .. ":" .. serializeJSONImpl(v, tracking, options)
 | 
			
		||||
                    else
 | 
			
		||||
                        sEntry = serializeJSONString(k) .. ":" .. serializeJSONImpl(v, tTracking, bNBTStyle)
 | 
			
		||||
                        sEntry = serializeJSONString(k, options) .. ":" .. serializeJSONImpl(v, tracking, options)
 | 
			
		||||
                    end
 | 
			
		||||
                    if nObjectSize == 0 then
 | 
			
		||||
                        sObjectResult = sObjectResult .. sEntry
 | 
			
		||||
@@ -473,7 +500,7 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
 | 
			
		||||
                if t[k] == nil then --if the array is nil at index k the value is "null" as to keep the unused indexes in between used ones.
 | 
			
		||||
                    sEntry = "null"
 | 
			
		||||
                else -- if the array index does not point to a nil we serialise it's content.
 | 
			
		||||
                    sEntry = serializeJSONImpl(t[k], tTracking, bNBTStyle)
 | 
			
		||||
                    sEntry = serializeJSONImpl(t[k], tracking, options)
 | 
			
		||||
                end
 | 
			
		||||
                if nArraySize == 0 then
 | 
			
		||||
                    sArrayResult = sArrayResult .. sEntry
 | 
			
		||||
@@ -485,14 +512,21 @@ local function serializeJSONImpl(t, tTracking, bNBTStyle)
 | 
			
		||||
            sObjectResult = sObjectResult .. "}"
 | 
			
		||||
            sArrayResult = sArrayResult .. "]"
 | 
			
		||||
            if nObjectSize > 0 or nArraySize == 0 then
 | 
			
		||||
                return sObjectResult
 | 
			
		||||
                result = sObjectResult
 | 
			
		||||
            else
 | 
			
		||||
                return sArrayResult
 | 
			
		||||
                result = sArrayResult
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        if options.allow_repetitions then
 | 
			
		||||
            tracking[t] = nil
 | 
			
		||||
        else
 | 
			
		||||
            tracking[t] = false
 | 
			
		||||
        end
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    elseif sType == "string" then
 | 
			
		||||
        return serializeJSONString(t)
 | 
			
		||||
        return serializeJSONString(t, options)
 | 
			
		||||
 | 
			
		||||
    elseif sType == "number" or sType == "boolean" then
 | 
			
		||||
        return tostring(t)
 | 
			
		||||
@@ -682,13 +716,13 @@ do
 | 
			
		||||
 | 
			
		||||
    --[[- Converts a serialised JSON string back into a reassembled Lua object.
 | 
			
		||||
 | 
			
		||||
    This may be used with @{textutils.serializeJSON}, or when communicating
 | 
			
		||||
    This may be used with [`textutils.serializeJSON`], or when communicating
 | 
			
		||||
    with command blocks or web APIs.
 | 
			
		||||
 | 
			
		||||
    If a `null` value is encountered, it is converted into `nil`. It can be converted
 | 
			
		||||
    into @{textutils.json_null} with the `parse_null` option.
 | 
			
		||||
    into [`textutils.json_null`] with the `parse_null` option.
 | 
			
		||||
 | 
			
		||||
    If an empty array is encountered, it is converted into @{textutils.empty_json_array}.
 | 
			
		||||
    If an empty array is encountered, it is converted into [`textutils.empty_json_array`].
 | 
			
		||||
    It can be converted into a new empty table with the `parse_empty_array` option.
 | 
			
		||||
 | 
			
		||||
    @tparam string s The serialised string to deserialise.
 | 
			
		||||
@@ -697,12 +731,12 @@ do
 | 
			
		||||
 | 
			
		||||
    - `nbt_style`: When true, this will accept [stringified NBT][nbt] strings,
 | 
			
		||||
       as produced by many commands.
 | 
			
		||||
    - `parse_null`: When true, `null` will be parsed as @{json_null}, rather than
 | 
			
		||||
    - `parse_null`: When true, `null` will be parsed as [`json_null`], rather than
 | 
			
		||||
       `nil`.
 | 
			
		||||
    - `parse_empty_array`: When false, empty arrays will be parsed as a new table.
 | 
			
		||||
       By default (or when this value is true), they are parsed as @{empty_json_array}.
 | 
			
		||||
       By default (or when this value is true), they are parsed as [`empty_json_array`].
 | 
			
		||||
 | 
			
		||||
    [nbt]: https://minecraft.gamepedia.com/NBT_format
 | 
			
		||||
    [nbt]: https://minecraft.wiki/w/NBT_format
 | 
			
		||||
    @return[1] The deserialised object
 | 
			
		||||
    @treturn[2] nil If the object could not be deserialised.
 | 
			
		||||
    @treturn string A message describing why the JSON string is invalid.
 | 
			
		||||
@@ -714,7 +748,7 @@ do
 | 
			
		||||
 | 
			
		||||
        textutils.unserialiseJSON('{"name": "Steve", "age": null}')
 | 
			
		||||
 | 
			
		||||
    @usage Unserialise a basic JSON object, returning null values as @{json_null}.
 | 
			
		||||
    @usage Unserialise a basic JSON object, returning null values as [`json_null`].
 | 
			
		||||
 | 
			
		||||
        textutils.unserialiseJSON('{"name": "Steve", "age": null}', { parse_null = true })
 | 
			
		||||
    ]]
 | 
			
		||||
@@ -793,7 +827,7 @@ serialise = serialize -- GB version
 | 
			
		||||
 | 
			
		||||
--- Converts a serialised string back into a reassembled Lua object.
 | 
			
		||||
--
 | 
			
		||||
-- This is mainly used together with @{textutils.serialise}.
 | 
			
		||||
-- This is mainly used together with [`textutils.serialise`].
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string s The serialised string to deserialise.
 | 
			
		||||
-- @return[1] The deserialised object
 | 
			
		||||
@@ -813,32 +847,86 @@ end
 | 
			
		||||
 | 
			
		||||
unserialise = unserialize -- GB version
 | 
			
		||||
 | 
			
		||||
--- Returns a JSON representation of the given data.
 | 
			
		||||
--
 | 
			
		||||
-- This function attempts to guess whether a table is a JSON array or
 | 
			
		||||
-- object. However, empty tables are assumed to be empty objects - use
 | 
			
		||||
-- @{textutils.empty_json_array} to mark an empty array.
 | 
			
		||||
--
 | 
			
		||||
-- This is largely intended for interacting with various functions from the
 | 
			
		||||
-- @{commands} API, though may also be used in making @{http} requests.
 | 
			
		||||
--
 | 
			
		||||
-- @param t The value to serialise. Like @{textutils.serialise}, this should not
 | 
			
		||||
-- contain recursive tables or functions.
 | 
			
		||||
-- @tparam[opt] boolean bNBTStyle Whether to produce NBT-style JSON (non-quoted keys)
 | 
			
		||||
-- instead of standard JSON.
 | 
			
		||||
-- @treturn string The JSON representation of the input.
 | 
			
		||||
-- @throws If the object contains a value which cannot be
 | 
			
		||||
-- serialised. This includes functions and tables which appear multiple
 | 
			
		||||
-- times.
 | 
			
		||||
-- @usage textutils.serialiseJSON({ values = { 1, "2", true } })
 | 
			
		||||
-- @since 1.7
 | 
			
		||||
-- @see textutils.json_null Use to serialise a JSON `null` value.
 | 
			
		||||
-- @see textutils.empty_json_array Use to serialise a JSON empty array.
 | 
			
		||||
function serializeJSON(t, bNBTStyle)
 | 
			
		||||
--[[- Returns a JSON representation of the given data.
 | 
			
		||||
 | 
			
		||||
This is largely intended for interacting with various functions from the
 | 
			
		||||
[`commands`] API, though may also be used in making [`http`] requests.
 | 
			
		||||
 | 
			
		||||
Lua has a rather different data model to Javascript/JSON. As a result, some Lua
 | 
			
		||||
values do not serialise cleanly into JSON.
 | 
			
		||||
 | 
			
		||||
 - Lua tables can contain arbitrary key-value pairs, but JSON only accepts arrays,
 | 
			
		||||
   and objects (which require a string key). When serialising a table, if it only
 | 
			
		||||
   has numeric keys, then it will be treated as an array. Otherwise, the table will
 | 
			
		||||
   be serialised to an object using the string keys. Non-string keys (such as numbers
 | 
			
		||||
   or tables) will be dropped.
 | 
			
		||||
 | 
			
		||||
   A consequence of this is that an empty table will always be serialised to an object,
 | 
			
		||||
   not an array. [`textutils.empty_json_array`] may be used to express an empty array.
 | 
			
		||||
 | 
			
		||||
 - Lua strings are an a sequence of raw bytes, and do not have any specific encoding.
 | 
			
		||||
   However, JSON strings must be valid unicode. By default, non-ASCII characters in a
 | 
			
		||||
   string are serialised to their unicode code point (for instance, `"\xfe"` is
 | 
			
		||||
   converted to `"\u00fe"`). The `unicode_strings` option may be set to treat all input
 | 
			
		||||
   strings as UTF-8.
 | 
			
		||||
 | 
			
		||||
 - Lua does not distinguish between missing keys (`undefined` in JS) and ones explicitly
 | 
			
		||||
   set to `null`. As a result `{ x = nil }` is serialised to `{}`. [`textutils.json_null`]
 | 
			
		||||
   may be used to get an explicit null value (`{ x = textutils.json_null }` will serialise
 | 
			
		||||
   to `{"x": null}`).
 | 
			
		||||
 | 
			
		||||
@param[1] t The value to serialise. Like [`textutils.serialise`], this should not
 | 
			
		||||
contain recursive tables or functions.
 | 
			
		||||
@tparam[1,opt] {
 | 
			
		||||
    nbt_style? = boolean,
 | 
			
		||||
    unicode_strings? = boolean,
 | 
			
		||||
    allow_repetitions? = boolean
 | 
			
		||||
} options Options for serialisation.
 | 
			
		||||
 - `nbt_style`: Whether to produce NBT-style JSON (non-quoted keys) instead of standard JSON.
 | 
			
		||||
 - `unicode_strings`: Whether to treat strings as containing UTF-8 characters instead of
 | 
			
		||||
    using the default 8-bit character set.
 | 
			
		||||
 - `allow_repetitions`: Relax the check for recursive tables, allowing them to appear multiple
 | 
			
		||||
   times (as long as tables do not appear inside themselves).
 | 
			
		||||
 | 
			
		||||
@param[2] t The value to serialise. Like [`textutils.serialise`], this should not
 | 
			
		||||
contain recursive tables or functions.
 | 
			
		||||
@tparam[2] boolean bNBTStyle Whether to produce NBT-style JSON (non-quoted keys)
 | 
			
		||||
instead of standard JSON.
 | 
			
		||||
 | 
			
		||||
@treturn string The JSON representation of the input.
 | 
			
		||||
@throws If the object contains a value which cannot be serialised. This includes
 | 
			
		||||
functions and tables which appear multiple times.
 | 
			
		||||
 | 
			
		||||
@usage Serialise a simple object
 | 
			
		||||
 | 
			
		||||
    textutils.serialiseJSON({ values = { 1, "2", true } })
 | 
			
		||||
 | 
			
		||||
@usage Serialise an object to a NBT-style string
 | 
			
		||||
 | 
			
		||||
    textutils.serialiseJSON({ values = { 1, "2", true } }, { nbt_style = true })
 | 
			
		||||
 | 
			
		||||
@since 1.7
 | 
			
		||||
@changed 1.106.0 Added `options` overload and `unicode_strings` option.
 | 
			
		||||
@changed 1.109.0 Added `allow_repetitions` option.
 | 
			
		||||
 | 
			
		||||
@see textutils.json_null Use to serialise a JSON `null` value.
 | 
			
		||||
@see textutils.empty_json_array Use to serialise a JSON empty array.
 | 
			
		||||
]]
 | 
			
		||||
function serializeJSON(t, options)
 | 
			
		||||
    expect(1, t, "table", "string", "number", "boolean")
 | 
			
		||||
    expect(2, bNBTStyle, "boolean", "nil")
 | 
			
		||||
    expect(2, options, "table", "boolean", "nil")
 | 
			
		||||
    if type(options) == "boolean" then
 | 
			
		||||
        options = { nbt_style = options }
 | 
			
		||||
    elseif type(options) == "table" then
 | 
			
		||||
        field(options, "nbt_style", "boolean", "nil")
 | 
			
		||||
        field(options, "unicode_strings", "boolean", "nil")
 | 
			
		||||
        field(options, "allow_repetitions", "boolean", "nil")
 | 
			
		||||
    else
 | 
			
		||||
        options = {}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local tTracking = {}
 | 
			
		||||
    return serializeJSONImpl(t, tTracking, bNBTStyle or false)
 | 
			
		||||
    return serializeJSONImpl(t, tTracking, options)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
serialiseJSON = serializeJSON -- GB version
 | 
			
		||||
@@ -854,22 +942,21 @@ unserialiseJSON = unserialise_json
 | 
			
		||||
-- @since 1.31
 | 
			
		||||
function urlEncode(str)
 | 
			
		||||
    expect(1, str, "string")
 | 
			
		||||
    if str then
 | 
			
		||||
        str = string.gsub(str, "\n", "\r\n")
 | 
			
		||||
        str = string.gsub(str, "([^A-Za-z0-9 %-%_%.])", function(c)
 | 
			
		||||
            local n = string.byte(c)
 | 
			
		||||
            if n < 128 then
 | 
			
		||||
                -- ASCII
 | 
			
		||||
                return string.format("%%%02X", n)
 | 
			
		||||
            else
 | 
			
		||||
                -- Non-ASCII (encode as UTF-8)
 | 
			
		||||
                return
 | 
			
		||||
                    string.format("%%%02X", 192 + bit32.band(bit32.arshift(n, 6), 31)) ..
 | 
			
		||||
                    string.format("%%%02X", 128 + bit32.band(n, 63))
 | 
			
		||||
            end
 | 
			
		||||
        end)
 | 
			
		||||
        str = string.gsub(str, " ", "+")
 | 
			
		||||
    end
 | 
			
		||||
    local gsub, byte, format, band, arshift = string.gsub, string.byte, string.format, bit32.band, bit32.arshift
 | 
			
		||||
 | 
			
		||||
    str = gsub(str, "\n", "\r\n")
 | 
			
		||||
    str = gsub(str, "[^A-Za-z0-9%-%_%.]", function(c)
 | 
			
		||||
        if c == " " then return "+" end
 | 
			
		||||
 | 
			
		||||
        local n = byte(c)
 | 
			
		||||
        if n < 128 then
 | 
			
		||||
            -- ASCII
 | 
			
		||||
            return format("%%%02X", n)
 | 
			
		||||
        else
 | 
			
		||||
            -- Non-ASCII (encode as UTF-8)
 | 
			
		||||
            return format("%%%02X%%%02X", 192 + band(arshift(n, 6), 31), 128 + band(n, 63))
 | 
			
		||||
        end
 | 
			
		||||
    end)
 | 
			
		||||
    return str
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@@ -884,7 +971,7 @@ local tEmpty = {}
 | 
			
		||||
-- variable name or table index.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam[opt] table tSearchTable The table to find variables in, defaulting to
 | 
			
		||||
-- the global environment (@{_G}). The function also searches the "parent"
 | 
			
		||||
-- the global environment ([`_G`]). The function also searches the "parent"
 | 
			
		||||
-- environment via the `__index` metatable field.
 | 
			
		||||
--
 | 
			
		||||
-- @treturn { string... } The (possibly empty) list of completions.
 | 
			
		||||
 
 | 
			
		||||
@@ -14,31 +14,53 @@ end
 | 
			
		||||
-- should not need to use it.
 | 
			
		||||
native = turtle.native or turtle
 | 
			
		||||
 | 
			
		||||
local function addCraftMethod(object)
 | 
			
		||||
    if peripheral.getType("left") == "workbench" then
 | 
			
		||||
        object.craft = function(...)
 | 
			
		||||
            return peripheral.call("left", "craft", ...)
 | 
			
		||||
local function waitForResponse(_id)
 | 
			
		||||
    local event, responseID, success
 | 
			
		||||
    while event ~= "turtle_response" or responseID ~= _id do
 | 
			
		||||
        event, responseID, success = os.pullEvent("turtle_response")
 | 
			
		||||
    end
 | 
			
		||||
    return success
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function wrap(_sCommand)
 | 
			
		||||
    return function(...)
 | 
			
		||||
        local id = native[_sCommand](...)
 | 
			
		||||
        if id == -1 then
 | 
			
		||||
            return false
 | 
			
		||||
        end
 | 
			
		||||
    elseif peripheral.getType("right") == "workbench" then
 | 
			
		||||
        object.craft = function(...)
 | 
			
		||||
            return peripheral.call("right", "craft", ...)
 | 
			
		||||
        return waitForResponse(id)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Wrap standard commands
 | 
			
		||||
local turtle = {}
 | 
			
		||||
turtle["getItemCount"] = native.getItemCount
 | 
			
		||||
turtle["getItemSpace"] = native.getItemSpace
 | 
			
		||||
turtle["getFuelLevel"] = native.getFuelLevel
 | 
			
		||||
turtle["getSelectedSlot"] = native.getSelectedSlot
 | 
			
		||||
turtle["getFuelLimit"] = native.getFuelLimit
 | 
			
		||||
 | 
			
		||||
for k,v in pairs(native) do
 | 
			
		||||
    if type(k) == "string" and type(v) == "function" then
 | 
			
		||||
        if turtle[k] == nil then
 | 
			
		||||
            turtle[k] = wrap(k)
 | 
			
		||||
        end
 | 
			
		||||
    else
 | 
			
		||||
        object.craft = nil
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Wrap peripheral commands
 | 
			
		||||
if peripheral.getType("left") == "workbench" then
 | 
			
		||||
    turtle["craft"] = function(...)
 | 
			
		||||
        local id = peripheral.call("left", "craft", ...)
 | 
			
		||||
        return waitForResponse(id)
 | 
			
		||||
    end
 | 
			
		||||
elseif peripheral.getType("right") == "workbench" then
 | 
			
		||||
    turtle["craft"] = function(...)
 | 
			
		||||
        local id = peripheral.call("right", "craft", ...)
 | 
			
		||||
        return waitForResponse(id)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Put commands into environment table
 | 
			
		||||
local env = _ENV
 | 
			
		||||
for k, v in pairs(native) do
 | 
			
		||||
    if k == "equipLeft" or k == "equipRight" then
 | 
			
		||||
        env[k] = function(...)
 | 
			
		||||
            local result, err = v(...)
 | 
			
		||||
            addCraftMethod(turtle)
 | 
			
		||||
            return result, err
 | 
			
		||||
        end
 | 
			
		||||
    else
 | 
			
		||||
        env[k] = v
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
addCraftMethod(env)
 | 
			
		||||
for k,v in pairs(turtle) do env[k] = v end
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
 | 
			
		||||
--- A basic 3D vector type and some common vector operations. This may be useful
 | 
			
		||||
-- when working with coordinates in Minecraft's world (such as those from the
 | 
			
		||||
-- @{gps} API).
 | 
			
		||||
-- [`gps`] API).
 | 
			
		||||
--
 | 
			
		||||
-- An introduction to vectors can be found on [Wikipedia][wiki].
 | 
			
		||||
--
 | 
			
		||||
@@ -13,6 +13,11 @@
 | 
			
		||||
-- @module vector
 | 
			
		||||
-- @since 1.31
 | 
			
		||||
 | 
			
		||||
local getmetatable = getmetatable
 | 
			
		||||
local expect = dofile("rom/modules/main/cc/expect.lua").expect
 | 
			
		||||
 | 
			
		||||
local vmetatable
 | 
			
		||||
 | 
			
		||||
--- A 3-dimensional vector, with `x`, `y`, and `z` values.
 | 
			
		||||
--
 | 
			
		||||
-- This is suitable for representing both position and directional vectors.
 | 
			
		||||
@@ -27,6 +32,9 @@ local vector = {
 | 
			
		||||
    -- @usage v1:add(v2)
 | 
			
		||||
    -- @usage v1 + v2
 | 
			
		||||
    add = function(self, o)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
 | 
			
		||||
 | 
			
		||||
        return vector.new(
 | 
			
		||||
            self.x + o.x,
 | 
			
		||||
            self.y + o.y,
 | 
			
		||||
@@ -42,6 +50,9 @@ local vector = {
 | 
			
		||||
    -- @usage v1:sub(v2)
 | 
			
		||||
    -- @usage v1 - v2
 | 
			
		||||
    sub = function(self, o)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
 | 
			
		||||
 | 
			
		||||
        return vector.new(
 | 
			
		||||
            self.x - o.x,
 | 
			
		||||
            self.y - o.y,
 | 
			
		||||
@@ -52,30 +63,36 @@ local vector = {
 | 
			
		||||
    --- Multiplies a vector by a scalar value.
 | 
			
		||||
    --
 | 
			
		||||
    -- @tparam Vector self The vector to multiply.
 | 
			
		||||
    -- @tparam number m The scalar value to multiply with.
 | 
			
		||||
    -- @tparam number factor The scalar value to multiply with.
 | 
			
		||||
    -- @treturn Vector A vector with value `(x * m, y * m, z * m)`.
 | 
			
		||||
    -- @usage v:mul(3)
 | 
			
		||||
    -- @usage v * 3
 | 
			
		||||
    mul = function(self, m)
 | 
			
		||||
    -- @usage vector.new(1, 2, 3):mul(3)
 | 
			
		||||
    -- @usage vector.new(1, 2, 3) * 3
 | 
			
		||||
    mul = function(self, factor)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        expect(2, factor, "number")
 | 
			
		||||
 | 
			
		||||
        return vector.new(
 | 
			
		||||
            self.x * m,
 | 
			
		||||
            self.y * m,
 | 
			
		||||
            self.z * m
 | 
			
		||||
            self.x * factor,
 | 
			
		||||
            self.y * factor,
 | 
			
		||||
            self.z * factor
 | 
			
		||||
        )
 | 
			
		||||
    end,
 | 
			
		||||
 | 
			
		||||
    --- Divides a vector by a scalar value.
 | 
			
		||||
    --
 | 
			
		||||
    -- @tparam Vector self The vector to divide.
 | 
			
		||||
    -- @tparam number m The scalar value to divide by.
 | 
			
		||||
    -- @tparam number factor The scalar value to divide by.
 | 
			
		||||
    -- @treturn Vector A vector with value `(x / m, y / m, z / m)`.
 | 
			
		||||
    -- @usage v:div(3)
 | 
			
		||||
    -- @usage v / 3
 | 
			
		||||
    div = function(self, m)
 | 
			
		||||
    -- @usage vector.new(1, 2, 3):div(3)
 | 
			
		||||
    -- @usage vector.new(1, 2, 3) / 3
 | 
			
		||||
    div = function(self, factor)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        expect(2, factor, "number")
 | 
			
		||||
 | 
			
		||||
        return vector.new(
 | 
			
		||||
            self.x / m,
 | 
			
		||||
            self.y / m,
 | 
			
		||||
            self.z / m
 | 
			
		||||
            self.x / factor,
 | 
			
		||||
            self.y / factor,
 | 
			
		||||
            self.z / factor
 | 
			
		||||
        )
 | 
			
		||||
    end,
 | 
			
		||||
 | 
			
		||||
@@ -83,8 +100,9 @@ local vector = {
 | 
			
		||||
    --
 | 
			
		||||
    -- @tparam Vector self The vector to negate.
 | 
			
		||||
    -- @treturn Vector The negated vector.
 | 
			
		||||
    -- @usage -v
 | 
			
		||||
    -- @usage -vector.new(1, 2, 3)
 | 
			
		||||
    unm = function(self)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        return vector.new(
 | 
			
		||||
            -self.x,
 | 
			
		||||
            -self.y,
 | 
			
		||||
@@ -96,9 +114,12 @@ local vector = {
 | 
			
		||||
    --
 | 
			
		||||
    -- @tparam Vector self The first vector to compute the dot product of.
 | 
			
		||||
    -- @tparam Vector o The second vector to compute the dot product of.
 | 
			
		||||
    -- @treturn Vector The dot product of `self` and `o`.
 | 
			
		||||
    -- @treturn number The dot product of `self` and `o`.
 | 
			
		||||
    -- @usage v1:dot(v2)
 | 
			
		||||
    dot = function(self, o)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
 | 
			
		||||
 | 
			
		||||
        return self.x * o.x + self.y * o.y + self.z * o.z
 | 
			
		||||
    end,
 | 
			
		||||
 | 
			
		||||
@@ -109,6 +130,9 @@ local vector = {
 | 
			
		||||
    -- @treturn Vector The cross product of `self` and `o`.
 | 
			
		||||
    -- @usage v1:cross(v2)
 | 
			
		||||
    cross = function(self, o)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        if getmetatable(o) ~= vmetatable then expect(2, o, "vector") end
 | 
			
		||||
 | 
			
		||||
        return vector.new(
 | 
			
		||||
            self.y * o.z - self.z * o.y,
 | 
			
		||||
            self.z * o.x - self.x * o.z,
 | 
			
		||||
@@ -120,6 +144,7 @@ local vector = {
 | 
			
		||||
    -- @tparam Vector self This vector.
 | 
			
		||||
    -- @treturn number The length of this vector.
 | 
			
		||||
    length = function(self)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)
 | 
			
		||||
    end,
 | 
			
		||||
 | 
			
		||||
@@ -141,6 +166,9 @@ local vector = {
 | 
			
		||||
    -- nearest 0.5.
 | 
			
		||||
    -- @treturn Vector The rounded vector.
 | 
			
		||||
    round = function(self, tolerance)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        expect(2, tolerance, "number", "nil")
 | 
			
		||||
 | 
			
		||||
        tolerance = tolerance or 1.0
 | 
			
		||||
        return vector.new(
 | 
			
		||||
            math.floor((self.x + tolerance * 0.5) / tolerance) * tolerance,
 | 
			
		||||
@@ -156,6 +184,8 @@ local vector = {
 | 
			
		||||
    -- @usage v:tostring()
 | 
			
		||||
    -- @usage tostring(v)
 | 
			
		||||
    tostring = function(self)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
 | 
			
		||||
        return self.x .. "," .. self.y .. "," .. self.z
 | 
			
		||||
    end,
 | 
			
		||||
 | 
			
		||||
@@ -165,11 +195,15 @@ local vector = {
 | 
			
		||||
    -- @tparam Vector other The second vector to compare to.
 | 
			
		||||
    -- @treturn boolean Whether or not the vectors are equal.
 | 
			
		||||
    equals = function(self, other)
 | 
			
		||||
        if getmetatable(self) ~= vmetatable then expect(1, self, "vector") end
 | 
			
		||||
        if getmetatable(other) ~= vmetatable then expect(2, other, "vector") end
 | 
			
		||||
 | 
			
		||||
        return self.x == other.x and self.y == other.y and self.z == other.z
 | 
			
		||||
    end,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
local vmetatable = {
 | 
			
		||||
vmetatable = {
 | 
			
		||||
    __name = "vector",
 | 
			
		||||
    __index = vector,
 | 
			
		||||
    __add = vector.add,
 | 
			
		||||
    __sub = vector.sub,
 | 
			
		||||
@@ -180,7 +214,7 @@ local vmetatable = {
 | 
			
		||||
    __eq = vector.equals,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
--- Construct a new @{Vector} with the given coordinates.
 | 
			
		||||
--- Construct a new [`Vector`] with the given coordinates.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam number x The X coordinate or direction of the vector.
 | 
			
		||||
-- @tparam number y The Y coordinate or direction of the vector.
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,10 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--[[- A @{term.Redirect|terminal redirect} occupying a smaller area of an
 | 
			
		||||
--[[- A [terminal redirect][`term.Redirect`] occupying a smaller area of an
 | 
			
		||||
existing terminal. This allows for easy definition of spaces within the display
 | 
			
		||||
that can be written/drawn to, then later redrawn/repositioned/etc as need
 | 
			
		||||
be. The API itself contains only one function, @{window.create}, which returns
 | 
			
		||||
be. The API itself contains only one function, [`window.create`], which returns
 | 
			
		||||
the windows themselves.
 | 
			
		||||
 | 
			
		||||
Windows are considered terminal objects - as such, they have access to nearly
 | 
			
		||||
@@ -58,13 +58,24 @@ local type = type
 | 
			
		||||
local string_rep = string.rep
 | 
			
		||||
local string_sub = string.sub
 | 
			
		||||
 | 
			
		||||
--- A custom version of [`colors.toBlit`], specialised for the window API.
 | 
			
		||||
local function parse_color(color)
 | 
			
		||||
    if type(color) ~= "number" then
 | 
			
		||||
        -- By tail-calling expect, we ensure expect has the right error level.
 | 
			
		||||
        return expect(1, color, "number")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if color < 0 or color > 0xffff then error("Colour out of range", 3) end
 | 
			
		||||
    return 2 ^ math.floor(math.log(color, 2))
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Returns a terminal object that is a space within the specified parent
 | 
			
		||||
terminal object. This can then be used (or even redirected to) in the same
 | 
			
		||||
manner as eg a wrapped monitor. Refer to @{term|the term API} for a list of
 | 
			
		||||
manner as eg a wrapped monitor. Refer to [the term API][`term`] for a list of
 | 
			
		||||
functions available to it.
 | 
			
		||||
 | 
			
		||||
@{term} itself may not be passed as the parent, though @{term.native} is
 | 
			
		||||
acceptable. Generally, @{term.current} or a wrapped monitor will be most
 | 
			
		||||
[`term`] itself may not be passed as the parent, though [`term.native`] is
 | 
			
		||||
acceptable. Generally, [`term.current`] or a wrapped monitor will be most
 | 
			
		||||
suitable, though windows may even have other windows assigned as their
 | 
			
		||||
parents.
 | 
			
		||||
 | 
			
		||||
@@ -131,11 +142,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
        local sEmptyTextColor = tEmptyColorLines[nTextColor]
 | 
			
		||||
        local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
 | 
			
		||||
        for y = 1, nHeight do
 | 
			
		||||
            tLines[y] = {
 | 
			
		||||
                text = sEmptyText,
 | 
			
		||||
                textColor = sEmptyTextColor,
 | 
			
		||||
                backgroundColor = sEmptyBackgroundColor,
 | 
			
		||||
            }
 | 
			
		||||
            tLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor }
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        for i = 0, 15 do
 | 
			
		||||
@@ -165,7 +172,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
    local function redrawLine(n)
 | 
			
		||||
        local tLine = tLines[n]
 | 
			
		||||
        parent.setCursorPos(nX, nY + n - 1)
 | 
			
		||||
        parent.blit(tLine.text, tLine.textColor, tLine.backgroundColor)
 | 
			
		||||
        parent.blit(tLine[1], tLine[2], tLine[3])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local function redraw()
 | 
			
		||||
@@ -188,9 +195,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
                -- Modify line
 | 
			
		||||
                local tLine = tLines[nCursorY]
 | 
			
		||||
                if nStart == 1 and nEnd == nWidth then
 | 
			
		||||
                    tLine.text = sText
 | 
			
		||||
                    tLine.textColor = sTextColor
 | 
			
		||||
                    tLine.backgroundColor = sBackgroundColor
 | 
			
		||||
                    tLine[1] = sText
 | 
			
		||||
                    tLine[2] = sTextColor
 | 
			
		||||
                    tLine[3] = sBackgroundColor
 | 
			
		||||
                else
 | 
			
		||||
                    local sClippedText, sClippedTextColor, sClippedBackgroundColor
 | 
			
		||||
                    if nStart < 1 then
 | 
			
		||||
@@ -210,9 +217,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
                        sClippedBackgroundColor = sBackgroundColor
 | 
			
		||||
                    end
 | 
			
		||||
 | 
			
		||||
                    local sOldText = tLine.text
 | 
			
		||||
                    local sOldTextColor = tLine.textColor
 | 
			
		||||
                    local sOldBackgroundColor = tLine.backgroundColor
 | 
			
		||||
                    local sOldText = tLine[1]
 | 
			
		||||
                    local sOldTextColor = tLine[2]
 | 
			
		||||
                    local sOldBackgroundColor = tLine[3]
 | 
			
		||||
                    local sNewText, sNewTextColor, sNewBackgroundColor
 | 
			
		||||
                    if nStart > 1 then
 | 
			
		||||
                        local nOldEnd = nStart - 1
 | 
			
		||||
@@ -231,9 +238,9 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
                        sNewBackgroundColor = sNewBackgroundColor .. string_sub(sOldBackgroundColor, nOldStart, nWidth)
 | 
			
		||||
                    end
 | 
			
		||||
 | 
			
		||||
                    tLine.text = sNewText
 | 
			
		||||
                    tLine.textColor = sNewTextColor
 | 
			
		||||
                    tLine.backgroundColor = sNewBackgroundColor
 | 
			
		||||
                    tLine[1] = sNewText
 | 
			
		||||
                    tLine[2] = sNewTextColor
 | 
			
		||||
                    tLine[3] = sNewBackgroundColor
 | 
			
		||||
                end
 | 
			
		||||
 | 
			
		||||
                -- Redraw line
 | 
			
		||||
@@ -251,7 +258,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    --- The window object. Refer to the @{window|module's documentation} for
 | 
			
		||||
    --- The window object. Refer to the [module's documentation][`window`] for
 | 
			
		||||
    -- a full description.
 | 
			
		||||
    --
 | 
			
		||||
    -- @type Window
 | 
			
		||||
@@ -280,11 +287,10 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
        local sEmptyTextColor = tEmptyColorLines[nTextColor]
 | 
			
		||||
        local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
 | 
			
		||||
        for y = 1, nHeight do
 | 
			
		||||
            tLines[y] = {
 | 
			
		||||
                text = sEmptyText,
 | 
			
		||||
                textColor = sEmptyTextColor,
 | 
			
		||||
                backgroundColor = sEmptyBackgroundColor,
 | 
			
		||||
            }
 | 
			
		||||
            local line = tLines[y]
 | 
			
		||||
            line[1] = sEmptyText
 | 
			
		||||
            line[2] = sEmptyTextColor
 | 
			
		||||
            line[3] = sEmptyBackgroundColor
 | 
			
		||||
        end
 | 
			
		||||
        if bVisible then
 | 
			
		||||
            redraw()
 | 
			
		||||
@@ -295,14 +301,10 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
 | 
			
		||||
    function window.clearLine()
 | 
			
		||||
        if nCursorY >= 1 and nCursorY <= nHeight then
 | 
			
		||||
            local sEmptyText = sEmptySpaceLine
 | 
			
		||||
            local sEmptyTextColor = tEmptyColorLines[nTextColor]
 | 
			
		||||
            local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
 | 
			
		||||
            tLines[nCursorY] = {
 | 
			
		||||
                text = sEmptyText,
 | 
			
		||||
                textColor = sEmptyTextColor,
 | 
			
		||||
                backgroundColor = sEmptyBackgroundColor,
 | 
			
		||||
            }
 | 
			
		||||
            local line = tLines[nCursorY]
 | 
			
		||||
            line[1] = sEmptySpaceLine
 | 
			
		||||
            line[2] = tEmptyColorLines[nTextColor]
 | 
			
		||||
            line[3] = tEmptyColorLines[nBackgroundColor]
 | 
			
		||||
            if bVisible then
 | 
			
		||||
                redrawLine(nCursorY)
 | 
			
		||||
                updateCursorColor()
 | 
			
		||||
@@ -350,10 +352,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local function setTextColor(color)
 | 
			
		||||
        if type(color) ~= "number" then expect(1, color, "number") end
 | 
			
		||||
        if tHex[color] == nil then
 | 
			
		||||
            error("Invalid color (got " .. color .. ")" , 2)
 | 
			
		||||
        end
 | 
			
		||||
        if tHex[color] == nil then color = parse_color(color) end
 | 
			
		||||
 | 
			
		||||
        nTextColor = color
 | 
			
		||||
        if bVisible then
 | 
			
		||||
@@ -365,11 +364,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
    window.setTextColour = setTextColor
 | 
			
		||||
 | 
			
		||||
    function window.setPaletteColour(colour, r, g, b)
 | 
			
		||||
        if type(colour) ~= "number" then expect(1, colour, "number") end
 | 
			
		||||
 | 
			
		||||
        if tHex[colour] == nil then
 | 
			
		||||
            error("Invalid color (got " .. colour .. ")" , 2)
 | 
			
		||||
        end
 | 
			
		||||
        if tHex[colour] == nil then colour = parse_color(colour) end
 | 
			
		||||
 | 
			
		||||
        local tCol
 | 
			
		||||
        if type(r) == "number" and g == nil and b == nil then
 | 
			
		||||
@@ -394,10 +389,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
    window.setPaletteColor = window.setPaletteColour
 | 
			
		||||
 | 
			
		||||
    function window.getPaletteColour(colour)
 | 
			
		||||
        if type(colour) ~= "number" then expect(1, colour, "number") end
 | 
			
		||||
        if tHex[colour] == nil then
 | 
			
		||||
            error("Invalid color (got " .. colour .. ")" , 2)
 | 
			
		||||
        end
 | 
			
		||||
        if tHex[colour] == nil then colour = parse_color(colour) end
 | 
			
		||||
        local tCol = tPalette[colour]
 | 
			
		||||
        return tCol[1], tCol[2], tCol[3]
 | 
			
		||||
    end
 | 
			
		||||
@@ -405,10 +397,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
    window.getPaletteColor = window.getPaletteColour
 | 
			
		||||
 | 
			
		||||
    local function setBackgroundColor(color)
 | 
			
		||||
        if type(color) ~= "number" then expect(1, color, "number") end
 | 
			
		||||
        if tHex[color] == nil then
 | 
			
		||||
            error("Invalid color (got " .. color .. ")", 2)
 | 
			
		||||
        end
 | 
			
		||||
        if tHex[color] == nil then color = parse_color(color) end
 | 
			
		||||
        nBackgroundColor = color
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@@ -431,11 +420,7 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
                if y >= 1 and y <= nHeight then
 | 
			
		||||
                    tNewLines[newY] = tLines[y]
 | 
			
		||||
                else
 | 
			
		||||
                    tNewLines[newY] = {
 | 
			
		||||
                        text = sEmptyText,
 | 
			
		||||
                        textColor = sEmptyTextColor,
 | 
			
		||||
                        backgroundColor = sEmptyBackgroundColor,
 | 
			
		||||
                    }
 | 
			
		||||
                    tNewLines[newY] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor }
 | 
			
		||||
                end
 | 
			
		||||
            end
 | 
			
		||||
            tLines = tNewLines
 | 
			
		||||
@@ -467,8 +452,8 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
    --
 | 
			
		||||
    -- @tparam number y The y position of the line to get.
 | 
			
		||||
    -- @treturn string The textual content of this line.
 | 
			
		||||
    -- @treturn string The text colours of this line, suitable for use with @{term.blit}.
 | 
			
		||||
    -- @treturn string The background colours of this line, suitable for use with @{term.blit}.
 | 
			
		||||
    -- @treturn string The text colours of this line, suitable for use with [`term.blit`].
 | 
			
		||||
    -- @treturn string The background colours of this line, suitable for use with [`term.blit`].
 | 
			
		||||
    -- @throws If `y` is not between 1 and this window's height.
 | 
			
		||||
    -- @since 1.84.0
 | 
			
		||||
    function window.getLine(y)
 | 
			
		||||
@@ -478,7 +463,8 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
            error("Line is out of range.", 2)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        return tLines[y].text, tLines[y].textColor, tLines[y].backgroundColor
 | 
			
		||||
        local line = tLines[y]
 | 
			
		||||
        return line[1], line[2], line[3]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    -- Other functions
 | 
			
		||||
@@ -574,26 +560,22 @@ function create(parent, nX, nY, nWidth, nHeight, bStartVisible)
 | 
			
		||||
            local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor]
 | 
			
		||||
            for y = 1, new_height do
 | 
			
		||||
                if y > nHeight then
 | 
			
		||||
                    tNewLines[y] = {
 | 
			
		||||
                        text = sEmptyText,
 | 
			
		||||
                        textColor = sEmptyTextColor,
 | 
			
		||||
                        backgroundColor = sEmptyBackgroundColor,
 | 
			
		||||
                    }
 | 
			
		||||
                    tNewLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor }
 | 
			
		||||
                else
 | 
			
		||||
                    local tOldLine = tLines[y]
 | 
			
		||||
                    if new_width == nWidth then
 | 
			
		||||
                        tNewLines[y] = tOldLine
 | 
			
		||||
                    elseif new_width < nWidth then
 | 
			
		||||
                        tNewLines[y] = {
 | 
			
		||||
                            text = string_sub(tOldLine.text, 1, new_width),
 | 
			
		||||
                            textColor = string_sub(tOldLine.textColor, 1, new_width),
 | 
			
		||||
                            backgroundColor = string_sub(tOldLine.backgroundColor, 1, new_width),
 | 
			
		||||
                            string_sub(tOldLine[1], 1, new_width),
 | 
			
		||||
                            string_sub(tOldLine[2], 1, new_width),
 | 
			
		||||
                            string_sub(tOldLine[3], 1, new_width),
 | 
			
		||||
                        }
 | 
			
		||||
                    else
 | 
			
		||||
                        tNewLines[y] = {
 | 
			
		||||
                            text = tOldLine.text .. string_sub(sEmptyText, nWidth + 1, new_width),
 | 
			
		||||
                            textColor = tOldLine.textColor .. string_sub(sEmptyTextColor, nWidth + 1, new_width),
 | 
			
		||||
                            backgroundColor = tOldLine.backgroundColor .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width),
 | 
			
		||||
                            tOldLine[1] .. string_sub(sEmptyText, nWidth + 1, new_width),
 | 
			
		||||
                            tOldLine[2] .. string_sub(sEmptyTextColor, nWidth + 1, new_width),
 | 
			
		||||
                            tOldLine[3] .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width),
 | 
			
		||||
                        }
 | 
			
		||||
                    end
 | 
			
		||||
                end
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,313 @@
 | 
			
		||||
# New features in CC: Tweaked 1.115.1
 | 
			
		||||
 | 
			
		||||
* Update various translations (cyb3r, kevk2156, teamer337, yakku).
 | 
			
		||||
* Support Fabric's item lookup API for registering media providers.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix crashes on Create 6.0 (ellellie).
 | 
			
		||||
* Fix `speaker.playAudio` not updating speaker volume.
 | 
			
		||||
* Resize pocket lectern textures to fix issues with generating mipmaps.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.115.0
 | 
			
		||||
 | 
			
		||||
* Support placing pocket computers on lecterns.
 | 
			
		||||
* Suggest alternative table keys on `nil` errors.
 | 
			
		||||
* Errors from inside `parallel` functions now have source information attached.
 | 
			
		||||
* Expose printout contents to the Java API.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Ignore unrepresentable characters in `char`/`paste` events.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.114.4
 | 
			
		||||
 | 
			
		||||
* Allow typing/pasting any character in the CC charset.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix command computers being exposed as peripherals (Forge only).
 | 
			
		||||
* Fix command computers having NBT set when placed in a Create contraption.
 | 
			
		||||
* Use correct bounding box when checking for entities in turtle movement.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.114.3
 | 
			
		||||
 | 
			
		||||
* `wget` now prints the error that occurred, rather than a generic "Failed" (tizu69).
 | 
			
		||||
* Update several translations.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix `fs.isDriveRoot` returning true for non-existent files.
 | 
			
		||||
* Fix possible memory leak when sending terminal contents.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.114.2
 | 
			
		||||
 | 
			
		||||
One bug fix:
 | 
			
		||||
* Fix OpenGL errors when rendering empty monitors.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.114.1
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix monitor touch events only firing from one monitor.
 | 
			
		||||
* Fix crash when lectern has no item.
 | 
			
		||||
* Fix cursor not blinking on monitors.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.114.0
 | 
			
		||||
 | 
			
		||||
* Add redstone relay peripheral.
 | 
			
		||||
* Add support for `math.atan(y, x)`.
 | 
			
		||||
* Update several translations.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix pocket upgrades not appearing after crafting.
 | 
			
		||||
* Cancel `rednet.receive` and `Websocket.receive` timers after a message is received.
 | 
			
		||||
* Fix several issues with parsing and printing large doubles.
 | 
			
		||||
* Fix in-hand pocket computer being blank after changing dimension.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.113.1
 | 
			
		||||
 | 
			
		||||
* Update Japanese translation (konumatakaki).
 | 
			
		||||
* Improve performance of `textutils.urlEncode`.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix overflow when converting recursive objects from Java to Lua.
 | 
			
		||||
* Fix websocket compression not working under Forge.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.113.0
 | 
			
		||||
 | 
			
		||||
* Allow placing printed pages and books in lecterns.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Various documentation fixes (MCJack123)
 | 
			
		||||
* Fix computers and turtles not being dropped when exploded with TNT.
 | 
			
		||||
* Fix crash when turtles are broken while mining a block.
 | 
			
		||||
* Fix pocket computer terminals not updating when in the off-hand.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.112.0
 | 
			
		||||
 | 
			
		||||
* Report a custom error when using `!` instead of `not`.
 | 
			
		||||
* Update several translations (zyxkad, MineKID-LP).
 | 
			
		||||
* Add `cc.strings.split` function.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix `drive.getAudioTitle` returning `nil` when no disk is inserted.
 | 
			
		||||
* Preserve item data when upgrading pocket computers.
 | 
			
		||||
* Add missing bounds check to `cc.strings.wrap` (Lupus950).
 | 
			
		||||
* Fix modems not moving with Create contraptions.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.111.0
 | 
			
		||||
 | 
			
		||||
* Update several translations (Ale32bit).
 | 
			
		||||
* Split up turtle textures into individual textures.
 | 
			
		||||
* Add `r+`/`w+` support to the `io` library.
 | 
			
		||||
* Warn when capabilities are not registered and Optifine is installed.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Allow planks to be used for building in "adventure" (dan200).
 | 
			
		||||
* Fix `disk.getAudioTitle()` returning untranslated strings for some modded discs.
 | 
			
		||||
* Fix crash when right clicking turtles in spectator.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.110.3
 | 
			
		||||
 | 
			
		||||
* Update several translations (PatriikPlays).
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix some errors missing source positions.
 | 
			
		||||
* Correctly handle multiple threads sending websocket messages at once.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.110.2
 | 
			
		||||
 | 
			
		||||
* Add `speaker sound` command (fatboychummy).
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Improve error when calling `speaker play` with no path (fatboychummy).
 | 
			
		||||
* Prevent playing music discs with `speaker.playSound`.
 | 
			
		||||
* Various documentation fixes (cyberbit).
 | 
			
		||||
* Fix generic peripherals not being able to transfer to some inventories on Forge.
 | 
			
		||||
* Fix rare crash when holding a pocket computer.
 | 
			
		||||
* Fix modems breaking when moved by Create.
 | 
			
		||||
* Fix crash when rendering a turtle through an Immersive Portals portal.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.110.1
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix computers not turning on after they're unloaded/not-ticked for a while.
 | 
			
		||||
* Fix networking cables sometimes not connecting on Forge.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.110.0
 | 
			
		||||
 | 
			
		||||
* Add a new `@c[...]` syntax for selecting computers in the `/computercraft` command.
 | 
			
		||||
* Remove custom breaking progress of modems on Forge.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix client and server DFPWM transcoders getting out of sync.
 | 
			
		||||
* Fix `turtle.suck` reporting incorrect error when failing to suck items.
 | 
			
		||||
* Fix pocket computers displaying state (blinking, modem light) for the wrong computer.
 | 
			
		||||
* Fix crash when wrapping an invalid BE as a generic peripheral.
 | 
			
		||||
* Chest peripherals now reattach when a chest is converted into a double chest.
 | 
			
		||||
* Fix `speaker` program not resolving files relative to the current directory.
 | 
			
		||||
* Skip main-thread tasks if the peripheral is detached.
 | 
			
		||||
* Fix internal Lua VM errors if yielding inside `__tostring`.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.7
 | 
			
		||||
 | 
			
		||||
* Improve performance of removing and unloading wired cables/modems.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix monitors sometimes not updating on the client when chunks are unloaded and reloaded.
 | 
			
		||||
* `colour.toBlit` correctly errors on out-of-bounds values.
 | 
			
		||||
* Round non-standard colours in `window`, like `term.native()` does.
 | 
			
		||||
* Fix the client monitor rendering both the current and outdated contents.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.6
 | 
			
		||||
 | 
			
		||||
* Improve several Lua parser error messages.
 | 
			
		||||
* Allow addon mods to register `require`able modules.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix weak tables becoming malformed when keys are GCed.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.5
 | 
			
		||||
 | 
			
		||||
* Add a new `/computercraft-computer-folder` command to open a computer's folder
 | 
			
		||||
  in singleplayer.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Discard characters being typed into the editor when closing `edit`'s `Run` screen.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.4
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Don't log warnings when a computer allocates no bytes.
 | 
			
		||||
* Fix incorrect list index in command computer's NBT conversion (lonevox).
 | 
			
		||||
* Fix `endPage()` not updating the printer's block state.
 | 
			
		||||
* Several documentation improvements (znepb).
 | 
			
		||||
* Correctly mount disks before computer startup, not afterwards.
 | 
			
		||||
* Update to Cobalt 0.9
 | 
			
		||||
  * Debug hooks are now correctly called for every function.
 | 
			
		||||
  * Fix several minor inconsistencies with `debug.getinfo`.
 | 
			
		||||
  * Fix Lua tables being sized incorrectly when created from varargs.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.3
 | 
			
		||||
 | 
			
		||||
* Command computers now display in the operator items creative tab.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Error if too many websocket messages are queued to be sent at once.
 | 
			
		||||
* Fix trailing-comma on method calls (e.g. `x:f(a, )` not using our custom error message.
 | 
			
		||||
* Fix internal compiler error when using `goto` as the first statement in an `if` block.
 | 
			
		||||
* Fix incorrect resizing of a tables' hash part when adding and removing keys.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.2
 | 
			
		||||
 | 
			
		||||
* `math.random` now uses Lua 5.4's random number generator.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix errors involving `goto` statements having the wrong line number.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.1
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix `mouse_drag` event not firing for right and middle mouse buttons.
 | 
			
		||||
* Fix crash when syntax errors involve `goto` or `::`.
 | 
			
		||||
* Fix deadlock occuring when adding/removing observers.
 | 
			
		||||
* Allow placing seeds into compostor barrels with `turtle.place()`.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.109.0
 | 
			
		||||
 | 
			
		||||
* Update to Lua 5.2
 | 
			
		||||
  * `getfenv`/`setfenv` now only work on Lua functions.
 | 
			
		||||
  * Add support for `goto`.
 | 
			
		||||
  * Remove support for dumping and loading binary chunks.
 | 
			
		||||
* File handles, HTTP requests and websocket messages now use raw bytes rather than converting to UTF-8.
 | 
			
		||||
* Add `allow_repetitions` option to `textutils.serialiseJSON`.
 | 
			
		||||
* Track memory allocated by computers.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix error when using position captures and backreferences in string patterns (e.g. `()(%1)`).
 | 
			
		||||
* Fix formatting non-real numbers with `%d`.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.108.4
 | 
			
		||||
 | 
			
		||||
* Rewrite `@LuaFunction` generation to use `MethodHandle`s instead of ASM.
 | 
			
		||||
* Refactor `ComputerThread` to provide a cleaner interface.
 | 
			
		||||
* Remove `disable_lua51_features` config option.
 | 
			
		||||
* Update several translations (Sammy).
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix monitor peripheral becoming "detached" after breaking and replacing a monitor.
 | 
			
		||||
* Fix signs being empty when placed.
 | 
			
		||||
* Fix several inconsistencies with mount error messages.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.108.3
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix disconnect when joining a dedicated server.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.108.2
 | 
			
		||||
 | 
			
		||||
* Add a tag for which blocks wired modems should ignore.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix monitors sometimes being warped after resizing.
 | 
			
		||||
* Fix the skull recipes using the wrong UUID format.
 | 
			
		||||
* Fix paint canvas not always being redrawn after a term resize.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.108.1
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Prevent no-opped players breaking or placing command computers.
 | 
			
		||||
* Allow using `@LuaFunction`-annotated methods on classes defined in child classloaders.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.108.0
 | 
			
		||||
 | 
			
		||||
* Remove compression from terminal/monitor packets. Vanilla applies its own compression, so this ends up being less helpful than expected.
 | 
			
		||||
* `/computercraft` command now supports permission mods.
 | 
			
		||||
* Split some GUI textures into sprite sheets.
 | 
			
		||||
* Support the `%g` character class in string pattern matching.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix crash when playing some modded records via a disk drive.
 | 
			
		||||
* Fix race condition when computers attach or detach from a monitor.
 | 
			
		||||
* Fix the "max websocket message" config option not being read.
 | 
			
		||||
* `tostring` now correctly obeys `__name`.
 | 
			
		||||
* Fix several inconsistencies with pattern matching character classes.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.107.0
 | 
			
		||||
 | 
			
		||||
* Add `disabled_generic_methods` config option to disable generic methods.
 | 
			
		||||
* Add basic integration with EMI.
 | 
			
		||||
* Enchanted turtle tools now render with a glint.
 | 
			
		||||
* Update several translations (PatriikPlays, 1Turtle, Ale32bit).
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix client config file being generated on a dedicated server.
 | 
			
		||||
* Fix numbers ending in "f" or "d" being treated as avalid.
 | 
			
		||||
* Fix `string.pack`'s "z" specifier causing out-of-bounds errors.
 | 
			
		||||
* Fix several issues with `turtle.dig`'s custom actions (tilling, making paths).
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.106.1
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Block the CGNAT range (100.64.0.0/10) by default.
 | 
			
		||||
* Fix conflicts with other mods replacing reach distance.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.106.0
 | 
			
		||||
 | 
			
		||||
* Numerous documentation improvements (MCJack123, znepb, penguinencounter).
 | 
			
		||||
* Port `fs.find` to Lua. This also allows using `?` as a wildcard.
 | 
			
		||||
* Computers cursors now glow in the dark.
 | 
			
		||||
* Allow changing turtle upgrades from the GUI.
 | 
			
		||||
* Add option to serialize Unicode strings to JSON (MCJack123).
 | 
			
		||||
* Small optimisations to the `window` API.
 | 
			
		||||
* Turtle upgrades can now preserve NBT from upgrade item stack and when broken.
 | 
			
		||||
* Add support for tool enchantments and durability via datapacks. This is disabled for the built-in tools.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Fix turtles rendering incorrectly when upside down.
 | 
			
		||||
* Fix misplaced calls to IArguments.escapes.
 | 
			
		||||
* Lua REPL no longer accepts `)(` as a valid expression.
 | 
			
		||||
* Fix several inconsistencies with `require`/`package.path` in the Lua REPL (Wojbie).
 | 
			
		||||
* Fix turtle being able to place water buckets outside its reach distance.
 | 
			
		||||
* Fix private several IP address ranges not being blocked by the `$private` rule.
 | 
			
		||||
* Improve permission checks in the `/computercraft` command.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.105.0
 | 
			
		||||
 | 
			
		||||
* Optimise JSON string parsing.
 | 
			
		||||
@@ -571,7 +881,7 @@ And several bug fixes:
 | 
			
		||||
# New features in CC: Tweaked 1.86.2
 | 
			
		||||
 | 
			
		||||
* Fix peripheral.getMethods returning an empty table.
 | 
			
		||||
* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing missing features and may be unstable.
 | 
			
		||||
* Update to Minecraft 1.15.2. This is currently alpha-quality and so is missing features and may be unstable.
 | 
			
		||||
 | 
			
		||||
# New features in CC: Tweaked 1.86.1
 | 
			
		||||
 | 
			
		||||
@@ -1187,7 +1497,7 @@ And several bug fixes:
 | 
			
		||||
* Turtles can now compare items in their inventories
 | 
			
		||||
* Turtles can place signs with text on them with `turtle.place( [signText] )`
 | 
			
		||||
* Turtles now optionally require fuel items to move, and can refuel themselves
 | 
			
		||||
* The size of the the turtle inventory has been increased to 16
 | 
			
		||||
* The size of the turtle inventory has been increased to 16
 | 
			
		||||
* The size of the turtle screen has been increased
 | 
			
		||||
* New turtle functions: `turtle.compareTo( [slotNum] )`, `turtle.craft()`, `turtle.attack()`, `turtle.attackUp()`, `turtle.attackDown()`, `turtle.dropUp()`, `turtle.dropDown()`, `turtle.getFuelLevel()`, `turtle.refuel()`
 | 
			
		||||
* New disk function: disk.getID()
 | 
			
		||||
 
 | 
			
		||||
@@ -12,4 +12,4 @@ commands.give( "dan200", "minecraft:diamond", 64 )
 | 
			
		||||
This works with any command. Use "commands.async" instead of "commands" to execute asynchronously.
 | 
			
		||||
 | 
			
		||||
The commands API is only available on Command Computers.
 | 
			
		||||
Visit http://minecraft.gamepedia.com/Commands for documentation on all commands.
 | 
			
		||||
Visit https://minecraft.wiki/w/Commands for documentation on all commands.
 | 
			
		||||
 
 | 
			
		||||
@@ -6,4 +6,4 @@ if sEvent == "key" and nKey == keys.enter then
 | 
			
		||||
   -- Do something
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
See http://www.minecraftwiki.net/wiki/Key_codes, or the source code, for a complete reference.
 | 
			
		||||
See https://www.minecraft.wiki/w/Key_codes, or the source code, for a complete reference.
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ isOpen( channel )
 | 
			
		||||
close( channel )
 | 
			
		||||
closeAll()
 | 
			
		||||
transmit( channel, replyChannel, message )
 | 
			
		||||
isWireless()
 | 
			
		||||
 | 
			
		||||
Events fired by Modems:
 | 
			
		||||
"modem_message" when a message is received on an open channel. Arguments are name, channel, replyChannel, message, distance
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,11 @@
 | 
			
		||||
New features in CC: Tweaked 1.105.0
 | 
			
		||||
New features in CC: Tweaked 1.115.1
 | 
			
		||||
 | 
			
		||||
* Optimise JSON string parsing.
 | 
			
		||||
* Add `colors.fromBlit` (Erb3).
 | 
			
		||||
* Upload file size limit is now configurable (khankul).
 | 
			
		||||
* Wired cables no longer have a distance limit.
 | 
			
		||||
* Java methods now coerce values to strings consistently with Lua.
 | 
			
		||||
* Add custom timeout support to the HTTP API.
 | 
			
		||||
* Support custom proxies for HTTP requests (Lemmmy).
 | 
			
		||||
* The `speaker` program now errors when playing HTML files.
 | 
			
		||||
* `edit` now shows an error message when editing read-only files.
 | 
			
		||||
* Update Ukranian translation (SirEdvin).
 | 
			
		||||
* Update various translations (cyb3r, kevk2156, teamer337, yakku).
 | 
			
		||||
* Support Fabric's item lookup API for registering media providers.
 | 
			
		||||
 | 
			
		||||
Several bug fixes:
 | 
			
		||||
* Allow GPS hosts to only be 1 block apart.
 | 
			
		||||
* Fix "Turn On"/"Turn Off" buttons being inverted in the computer GUI (Erb3).
 | 
			
		||||
* Fix arrow keys not working in the printout UI.
 | 
			
		||||
* Several documentation fixes (zyxkad, Lupus590, Commandcracker).
 | 
			
		||||
* Fix monitor renderer debug text always being visible on Forge.
 | 
			
		||||
* Fix crash when another mod changes the LoggerContext.
 | 
			
		||||
* Fix the `monitor_renderer` option not being present in Fabric config files.
 | 
			
		||||
* Pasting on MacOS/OSX now uses Cmd+V rather than Ctrl+V.
 | 
			
		||||
* Fix turtles placing blocks upside down when at y<0.
 | 
			
		||||
* Fix crashes on Create 6.0 (ellellie).
 | 
			
		||||
* Fix `speaker.playAudio` not updating speaker volume.
 | 
			
		||||
* Resize pocket lectern textures to fix issues with generating mipmaps.
 | 
			
		||||
 | 
			
		||||
Type "help changelog" to see the full version history.
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,13 @@
 | 
			
		||||
Convert between streams of DFPWM audio data and a list of amplitudes.
 | 
			
		||||
 | 
			
		||||
DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by GreaseMonkey. It's a relatively compact
 | 
			
		||||
format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode
 | 
			
		||||
in real time.
 | 
			
		||||
format compared to raw PCM data, only using 1 bit per sample, but is simple enough to encode and decode in real time.
 | 
			
		||||
 | 
			
		||||
Typically DFPWM audio is read from @{fs.BinaryReadHandle|the filesystem} or a @{http.Response|a web request} as a
 | 
			
		||||
string, and converted a format suitable for @{speaker.playAudio}.
 | 
			
		||||
Typically DFPWM audio is read from [the filesystem][`fs.ReadHandle`] or a [a web request][`http.Response`] as a string,
 | 
			
		||||
and converted a format suitable for [`speaker.playAudio`].
 | 
			
		||||
 | 
			
		||||
## Encoding and decoding files
 | 
			
		||||
This modules exposes two key functions, @{make_decoder} and @{make_encoder}, which construct a new decoder or encoder.
 | 
			
		||||
This module exposes two key functions, [`make_decoder`] and [`make_encoder`], which construct a new decoder or encoder.
 | 
			
		||||
The returned encoder/decoder is itself a function, which converts between the two kinds of data.
 | 
			
		||||
 | 
			
		||||
These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for
 | 
			
		||||
@@ -21,9 +20,9 @@ a specific audio stream. Typically you will want to create a decoder for each st
 | 
			
		||||
for each one you write.
 | 
			
		||||
 | 
			
		||||
## Converting audio to DFPWM
 | 
			
		||||
DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it.
 | 
			
		||||
DFPWM is not a popular file format and so standard audio processing tools may not have an option to export to it.
 | 
			
		||||
Instead, you can convert audio files online using [music.madefor.cc], the [LionRay Wav Converter][LionRay] Java
 | 
			
		||||
application or development builds of [FFmpeg].
 | 
			
		||||
application or [FFmpeg] 5.1 or later.
 | 
			
		||||
 | 
			
		||||
[music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked"
 | 
			
		||||
[LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter "
 | 
			
		||||
@@ -95,10 +94,9 @@ end
 | 
			
		||||
The returned encoder is itself a function. This function accepts a table of amplitude data between -128 and 127 and
 | 
			
		||||
returns the encoded DFPWM data.
 | 
			
		||||
 | 
			
		||||
:::caution Reusing encoders
 | 
			
		||||
Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple
 | 
			
		||||
streams, or use different encoders for the same stream, the resulting audio may not sound correct.
 | 
			
		||||
:::
 | 
			
		||||
> [Reusing encoders][!WARNING]
 | 
			
		||||
> Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple
 | 
			
		||||
> streams, or use different encoders for the same stream, the resulting audio may not sound correct.
 | 
			
		||||
 | 
			
		||||
@treturn function(pcm: { number... }):string The encoder function
 | 
			
		||||
@see encode A helper function for encoding an entire file of audio at once.
 | 
			
		||||
@@ -138,10 +136,9 @@ end
 | 
			
		||||
The returned decoder is itself a function. This function accepts a string and returns a table of amplitudes, each value
 | 
			
		||||
between -128 and 127.
 | 
			
		||||
 | 
			
		||||
:::caution Reusing decoders
 | 
			
		||||
Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for multiple
 | 
			
		||||
streams, or use different decoders for the same stream, the resulting audio may not sound correct.
 | 
			
		||||
:::
 | 
			
		||||
> [Reusing decoders][!WARNING]
 | 
			
		||||
> Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for
 | 
			
		||||
> multiple streams, or use different decoders for the same stream, the resulting audio may not sound correct.
 | 
			
		||||
 | 
			
		||||
@treturn function(dfpwm: string):{ number... } The encoder function
 | 
			
		||||
@see decode A helper function for decoding an entire file of audio at once.
 | 
			
		||||
@@ -167,7 +164,7 @@ local function make_decoder()
 | 
			
		||||
    local low_pass_charge = 0
 | 
			
		||||
    local previous_charge, previous_bit = 0, false
 | 
			
		||||
 | 
			
		||||
    return function (input, output)
 | 
			
		||||
    return function (input)
 | 
			
		||||
        expect(1, input, "string")
 | 
			
		||||
 | 
			
		||||
        local output, output_n = {}, 0
 | 
			
		||||
@@ -200,7 +197,7 @@ end
 | 
			
		||||
--[[- A convenience function for decoding a complete file of audio at once.
 | 
			
		||||
 | 
			
		||||
This should only be used for short files. For larger files, one should read the file in chunks and process it using
 | 
			
		||||
@{make_decoder}.
 | 
			
		||||
[`make_decoder`].
 | 
			
		||||
 | 
			
		||||
@tparam string input The DFPWM data to convert.
 | 
			
		||||
@treturn { number... } The produced amplitude data.
 | 
			
		||||
@@ -213,8 +210,8 @@ end
 | 
			
		||||
 | 
			
		||||
--[[- A convenience function for encoding a complete file of audio at once.
 | 
			
		||||
 | 
			
		||||
This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place,
 | 
			
		||||
you should use an encoder returned by @{make_encoder} instead.
 | 
			
		||||
This should only be used for complete pieces of audio. If you are writing multiple chunks to the same place,
 | 
			
		||||
you should use an encoder returned by [`make_encoder`] instead.
 | 
			
		||||
 | 
			
		||||
@tparam { number... } input The table of amplitude data.
 | 
			
		||||
@treturn string The encoded DFPWM data.
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,11 @@
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--- A collection of helper methods for working with input completion, such
 | 
			
		||||
-- as that require by @{_G.read}.
 | 
			
		||||
-- as that require by [`_G.read`].
 | 
			
		||||
--
 | 
			
		||||
-- @module cc.completion
 | 
			
		||||
-- @see cc.shell.completion For additional helpers to use with
 | 
			
		||||
-- @{shell.setCompletionFunction}.
 | 
			
		||||
-- [`shell.setCompletionFunction`].
 | 
			
		||||
-- @since 1.85.0
 | 
			
		||||
 | 
			
		||||
local expect = require "cc.expect".expect
 | 
			
		||||
@@ -34,7 +34,7 @@ end
 | 
			
		||||
-- @tparam { string... } choices The list of choices to complete from.
 | 
			
		||||
-- @tparam[opt] boolean add_space Whether to add a space after the completed item.
 | 
			
		||||
-- @treturn { string... } A list of suffixes of matching strings.
 | 
			
		||||
-- @usage Call @{_G.read}, completing the names of various animals.
 | 
			
		||||
-- @usage Call [`_G.read`], completing the names of various animals.
 | 
			
		||||
--
 | 
			
		||||
--     local completion = require "cc.completion"
 | 
			
		||||
--     local animals = { "dog", "cat", "lion", "unicorn" }
 | 
			
		||||
@@ -76,7 +76,7 @@ local function side(text, add_space)
 | 
			
		||||
    return choice_impl(text, sides, add_space)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Complete a @{settings|setting}.
 | 
			
		||||
--- Complete a [setting][`settings`].
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string text The input string to complete.
 | 
			
		||||
-- @tparam[opt] boolean add_space Whether to add a space after the completed settings.
 | 
			
		||||
@@ -92,7 +92,7 @@ end
 | 
			
		||||
 | 
			
		||||
local command_list
 | 
			
		||||
 | 
			
		||||
--- Complete the name of a Minecraft @{commands|command}.
 | 
			
		||||
--- Complete the name of a Minecraft [command][`commands`].
 | 
			
		||||
--
 | 
			
		||||
-- @tparam string text The input string to complete.
 | 
			
		||||
-- @tparam[opt] boolean add_space Whether to add a space after the completed command.
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
--[[- The @{cc.expect} library provides helper functions for verifying that
 | 
			
		||||
--[[- The [`cc.expect`] library provides helper functions for verifying that
 | 
			
		||||
function arguments are well-formed and of the correct type.
 | 
			
		||||
 | 
			
		||||
@module cc.expect
 | 
			
		||||
@@ -118,8 +118,8 @@ end
 | 
			
		||||
--- Expect a number to be within a specific range.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam number num The value to check.
 | 
			
		||||
-- @tparam number min The minimum value, if nil then `-math.huge` is used.
 | 
			
		||||
-- @tparam number max The maximum value, if nil then `math.huge` is used.
 | 
			
		||||
-- @tparam[opt=-math.huge] number min The minimum value.
 | 
			
		||||
-- @tparam[opt=math.huge] number max The maximum value.
 | 
			
		||||
-- @return The given `value`.
 | 
			
		||||
-- @throws If the value is outside of the allowed range.
 | 
			
		||||
-- @since 1.96.0
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,10 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
--- Read and draw nbt ("Nitrogen Fingers Text") images.
 | 
			
		||||
--- Read and draw nft ("Nitrogen Fingers Text") images.
 | 
			
		||||
--
 | 
			
		||||
-- nft ("Nitrogen Fingers Text") is a file format for drawing basic images.
 | 
			
		||||
-- Unlike the images that @{paintutils.parseImage} uses, nft supports coloured
 | 
			
		||||
-- Unlike the images that [`paintutils.parseImage`] uses, nft supports coloured
 | 
			
		||||
-- text as well as simple coloured pixels.
 | 
			
		||||
--
 | 
			
		||||
-- @module cc.image.nft
 | 
			
		||||
@@ -87,9 +87,9 @@ end
 | 
			
		||||
 | 
			
		||||
--- Draw an nft image to the screen.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam table image An image, as returned from @{load} or @{draw}.
 | 
			
		||||
-- @tparam table image An image, as returned from [`load`] or [`parse`].
 | 
			
		||||
-- @tparam number xPos The x position to start drawing at.
 | 
			
		||||
-- @tparam number xPos The y position to start drawing at.
 | 
			
		||||
-- @tparam number yPos The y position to start drawing at.
 | 
			
		||||
-- @tparam[opt] term.Redirect target The terminal redirect to draw to. Defaults to the
 | 
			
		||||
-- current terminal.
 | 
			
		||||
local function draw(image, xPos, yPos, target)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,167 @@
 | 
			
		||||
-- SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
--[[- Internal tools for diagnosing errors and suggesting fixes.
 | 
			
		||||
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
local debug, type, rawget = debug, type, rawget
 | 
			
		||||
local sub, lower, find, min, abs = string.sub, string.lower, string.find, math.min, math.abs
 | 
			
		||||
 | 
			
		||||
--[[- Compute the Optimal String Distance between two strings.
 | 
			
		||||
 | 
			
		||||
@tparam string str_a The first string.
 | 
			
		||||
@tparam string str_b The second string.
 | 
			
		||||
@treturn number|nil The distance between two strings, or nil if they are two far
 | 
			
		||||
apart.
 | 
			
		||||
]]
 | 
			
		||||
local function osa_distance(str_a, str_b, threshold)
 | 
			
		||||
    local len_a, len_b = #str_a, #str_b
 | 
			
		||||
 | 
			
		||||
    -- If the two strings are too different in length, then bail now.
 | 
			
		||||
    if abs(len_a - len_b) > threshold then return end
 | 
			
		||||
 | 
			
		||||
    -- Zero-initialise our distance table.
 | 
			
		||||
    local d = {}
 | 
			
		||||
    for i = 1, (len_a + 1) * (len_b + 1) do d[i] = 0 end
 | 
			
		||||
 | 
			
		||||
    -- Then fill the first row and column
 | 
			
		||||
    local function idx(a, b) return a * (len_a + 1) + b + 1 end
 | 
			
		||||
    for i = 0, len_a do d[idx(i, 0)] = i end
 | 
			
		||||
    for j = 0, len_b do d[idx(0, j)] = j end
 | 
			
		||||
 | 
			
		||||
    -- Then compute our distance
 | 
			
		||||
    for i = 1, len_a do
 | 
			
		||||
        local char_a = sub(str_a, i, i)
 | 
			
		||||
        for j = 1, len_b do
 | 
			
		||||
            local char_b = sub(str_b, j, j)
 | 
			
		||||
 | 
			
		||||
            local sub_cost
 | 
			
		||||
            if char_a == char_b then
 | 
			
		||||
                sub_cost = 0
 | 
			
		||||
            elseif lower(char_a) == lower(char_b) then
 | 
			
		||||
                sub_cost = 0.5
 | 
			
		||||
            else
 | 
			
		||||
                sub_cost = 1
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            local new_cost = min(
 | 
			
		||||
                d[idx(i - 1, j)] + 1, -- Deletion
 | 
			
		||||
                d[idx(i, j - 1)] + 1, -- Insertion,
 | 
			
		||||
                d[idx(i - 1, j - 1)] + sub_cost -- Substitution
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            -- Transposition
 | 
			
		||||
            if i > 1 and j > 1 and char_a == sub(str_b, j - 1, j - 1) and char_b == sub(str_a, i - 1, i - 1) then
 | 
			
		||||
                local trans_cost = d[idx(i - 2, j - 2)] + 1
 | 
			
		||||
                if trans_cost < new_cost then new_cost = trans_cost end
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            d[idx(i, j)] = new_cost
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local result = d[idx(len_a, len_b)]
 | 
			
		||||
    if result <= threshold then return result else return nil end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Check whether this suggestion is useful.
 | 
			
		||||
local function useful_suggestion(str)
 | 
			
		||||
    local len = #str
 | 
			
		||||
    return len > 0 and len < 32 and find(str, "^[%a_]%w*$")
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function get_suggestions(is_global, value, key, thread, frame_offset)
 | 
			
		||||
    if not useful_suggestion(key) then return end
 | 
			
		||||
 | 
			
		||||
    -- Pick a maximum number of edits. We're more lenient on longer strings, but
 | 
			
		||||
    -- still only allow two mistakes.
 | 
			
		||||
    local threshold = #key >= 5 and 2 or 1
 | 
			
		||||
 | 
			
		||||
    -- Find all items in the table, and see if they seem similar.
 | 
			
		||||
    local suggestions = {}
 | 
			
		||||
    local function process_suggestion(k)
 | 
			
		||||
        if type(k) ~= "string" or not useful_suggestion(k) then return end
 | 
			
		||||
 | 
			
		||||
        local distance = osa_distance(k, key, threshold)
 | 
			
		||||
        if distance then
 | 
			
		||||
            if distance < threshold then
 | 
			
		||||
                -- If this is better than any existing match, then prefer it.
 | 
			
		||||
                suggestions = { k }
 | 
			
		||||
                threshold = distance
 | 
			
		||||
            else
 | 
			
		||||
                -- Otherwise distance==threshold, and so just add it.
 | 
			
		||||
                suggestions[#suggestions + 1] = k
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    while type(value) == "table" do
 | 
			
		||||
        for k in next, value do process_suggestion(k) end
 | 
			
		||||
 | 
			
		||||
        local mt = debug.getmetatable(value)
 | 
			
		||||
        if mt == nil then break end
 | 
			
		||||
        value = rawget(mt, "__index")
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    -- If we're attempting to lookup a global, then also suggest any locals and
 | 
			
		||||
    -- upvalues. Our upvalues will be incomplete, but maybe a little useful?
 | 
			
		||||
    if is_global then
 | 
			
		||||
        for i = 1, 200 do
 | 
			
		||||
            local name = debug.getlocal(thread, frame_offset, i)
 | 
			
		||||
            if not name then break end
 | 
			
		||||
            process_suggestion(name)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        local func = debug.getinfo(thread, frame_offset, "f").func
 | 
			
		||||
        for i = 1, 255 do
 | 
			
		||||
            local name = debug.getupvalue(func, i)
 | 
			
		||||
            if not name then break end
 | 
			
		||||
            process_suggestion(name)
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    table.sort(suggestions)
 | 
			
		||||
 | 
			
		||||
    return suggestions
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Get a tip to display at the end of an error.
 | 
			
		||||
 | 
			
		||||
@tparam string err The error message.
 | 
			
		||||
@tparam coroutine thread The current thread.
 | 
			
		||||
@tparam number frame_offset The offset into the thread where the current frame exists
 | 
			
		||||
@return An optional message to append to the error.
 | 
			
		||||
]]
 | 
			
		||||
local function get_tip(err, thread, frame_offset)
 | 
			
		||||
    local nil_op = err:match("^attempt to (%l+) .* %(a nil value%)")
 | 
			
		||||
    if not nil_op then return end
 | 
			
		||||
 | 
			
		||||
    local has_error_info, error_info = pcall(require, "cc.internal.error_info")
 | 
			
		||||
    if not has_error_info then return end
 | 
			
		||||
    local op, is_global, table, key = error_info.info_for_nil(thread, frame_offset)
 | 
			
		||||
    if op == nil or op ~= nil_op then return end
 | 
			
		||||
 | 
			
		||||
    local suggestions = get_suggestions(is_global, table, key, thread, frame_offset)
 | 
			
		||||
    if not suggestions or next(suggestions) == nil then return end
 | 
			
		||||
 | 
			
		||||
    local pretty = require "cc.pretty"
 | 
			
		||||
    local msg = "Did you mean: "
 | 
			
		||||
 | 
			
		||||
    local n_suggestions = min(3, #suggestions)
 | 
			
		||||
    for i = 1, n_suggestions do
 | 
			
		||||
        if i > 1 then
 | 
			
		||||
            if i == n_suggestions then msg = msg .. " or " else msg = msg .. ", " end
 | 
			
		||||
        end
 | 
			
		||||
        msg = msg .. pretty.text(suggestions[i], colours.lightGrey)
 | 
			
		||||
    end
 | 
			
		||||
    return msg .. "?"
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
return { get_tip = get_tip }
 | 
			
		||||
@@ -4,10 +4,9 @@
 | 
			
		||||
 | 
			
		||||
--[[- A pretty-printer for Lua errors.
 | 
			
		||||
 | 
			
		||||
:::warning
 | 
			
		||||
This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
be removed or changed at any time.
 | 
			
		||||
:::
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
This consumes a list of messages and "annotations" and displays the error to the
 | 
			
		||||
terminal.
 | 
			
		||||
@@ -100,7 +99,7 @@ local code_accent = pretty.text("\x95", colours.cyan)
 | 
			
		||||
    over the underlying source, exposing the following functions:
 | 
			
		||||
    - `get_pos`: Get the line and column of an opaque position.
 | 
			
		||||
    - `get_line`: Get the source code for an opaque position.
 | 
			
		||||
@tparam table message The message to display, as produced by @{cc.internal.syntax.errors}.
 | 
			
		||||
@tparam table message The message to display, as produced by [`cc.internal.syntax.errors`].
 | 
			
		||||
]]
 | 
			
		||||
return function(context, message)
 | 
			
		||||
    expect(1, context, "table")
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
-- SPDX-FileCopyrightText: 2024 The CC: Tweaked Developers
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
--[[- Utilities for working with events.
 | 
			
		||||
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
--[[-
 | 
			
		||||
Attempt to discard a [`event!char`] event that may follow a [`event!key`] event.
 | 
			
		||||
 | 
			
		||||
This attempts to flush the event queue via a timer, stopping early if we observe
 | 
			
		||||
another key or char event.
 | 
			
		||||
 | 
			
		||||
We flush the event queue by waiting a single tick. It is technically possible
 | 
			
		||||
the key and char events will be delivered in different ticks, but it should be
 | 
			
		||||
very rare, and not worth adding extra delay for.
 | 
			
		||||
]]
 | 
			
		||||
local function discard_char()
 | 
			
		||||
    local timer = os.startTimer(0)
 | 
			
		||||
    while true do
 | 
			
		||||
        local event, id = os.pullEvent()
 | 
			
		||||
        if event == "timer" and id == timer then break
 | 
			
		||||
        elseif event == "char" or event == "key" or event == "key_up" then
 | 
			
		||||
            os.cancelTimer(timer)
 | 
			
		||||
            break
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
return { discard_char = discard_char }
 | 
			
		||||
@@ -4,16 +4,15 @@
 | 
			
		||||
 | 
			
		||||
--[[- Internal tools for working with errors.
 | 
			
		||||
 | 
			
		||||
:::warning
 | 
			
		||||
This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
be removed or changed at any time.
 | 
			
		||||
:::
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
local expect = require "cc.expect".expect
 | 
			
		||||
local error_printer = require "cc.internal.error_printer"
 | 
			
		||||
local type, debug, coroutine = type, debug, coroutine
 | 
			
		||||
 | 
			
		||||
local function find_frame(thread, file, line)
 | 
			
		||||
    -- Scan the first 16 frames for something interesting.
 | 
			
		||||
@@ -22,18 +21,136 @@ local function find_frame(thread, file, line)
 | 
			
		||||
        if not frame then break end
 | 
			
		||||
 | 
			
		||||
        if frame.short_src == file and frame.what ~= "C" and frame.currentline == line then
 | 
			
		||||
            return frame
 | 
			
		||||
            return offset, frame
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Check whether this error is an exception.
 | 
			
		||||
 | 
			
		||||
Currently we don't provide a stable API for throwing (and propagating) rich
 | 
			
		||||
errors, like those supported by this module. In lieu of that, we describe the
 | 
			
		||||
exception protocol, which may be used by user-written coroutine managers to
 | 
			
		||||
throw exceptions which are pretty-printed by the shell:
 | 
			
		||||
 | 
			
		||||
An exception is any table with:
 | 
			
		||||
 - The `"exception"` type
 | 
			
		||||
 - A string `message` field,
 | 
			
		||||
 - And a coroutine `thread` fields.
 | 
			
		||||
 | 
			
		||||
To throw such an exception, the inner loop of your coroutine manager may look
 | 
			
		||||
something like this:
 | 
			
		||||
 | 
			
		||||
```lua
 | 
			
		||||
local ok, result = coroutine.resume(co, table.unpack(event, 1, event.n))
 | 
			
		||||
if not ok then
 | 
			
		||||
    -- Rethrow non-string errors directly
 | 
			
		||||
    if type(result) ~= "string" then error(result, 0) end
 | 
			
		||||
    -- Otherwise, wrap it into an exception.
 | 
			
		||||
    error(setmetatable({ message = result, thread = co }, {
 | 
			
		||||
        __name = "exception",
 | 
			
		||||
        __tostring = function(self) return self.message end,
 | 
			
		||||
    }))
 | 
			
		||||
end
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@param exn Some error object
 | 
			
		||||
@treturn boolean Whether this error is an exception.
 | 
			
		||||
]]
 | 
			
		||||
local function is_exception(exn)
 | 
			
		||||
    if type(exn) ~= "table" then return false end
 | 
			
		||||
 | 
			
		||||
    local mt = getmetatable(exn)
 | 
			
		||||
    return mt and mt.__name == "exception" and type(rawget(exn, "message")) == "string" and type(rawget(exn, "thread")) == "thread"
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local exn_mt = {
 | 
			
		||||
    __name = "exception",
 | 
			
		||||
    __tostring = function(self) return self.message end,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
--[[- Create a new exception from a message and thread.
 | 
			
		||||
 | 
			
		||||
@tparam string message The exception message.
 | 
			
		||||
@tparam coroutine thread The coroutine the error occurred on.
 | 
			
		||||
@return The constructed exception.
 | 
			
		||||
]]
 | 
			
		||||
local function make_exception(message, thread)
 | 
			
		||||
    return setmetatable({ message = message, thread = thread }, exn_mt)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A marker function for [`try`] and the wider exception machinery.
 | 
			
		||||
 | 
			
		||||
This function is typically the first function on the call stack. It acts as both
 | 
			
		||||
a signifier that this function is exception aware, and allows us to store
 | 
			
		||||
additional information for the exception machinery on the call stack.
 | 
			
		||||
 | 
			
		||||
@see can_wrap_errors
 | 
			
		||||
]]
 | 
			
		||||
local try_barrier = debug.getregistry().cc_try_barrier
 | 
			
		||||
if not try_barrier then
 | 
			
		||||
    -- We define an extra "bounce" function to prevent f(...) being treated as a
 | 
			
		||||
    -- tail call, and so ensure the barrier remains on the stack.
 | 
			
		||||
    local function bounce(...) return ... end
 | 
			
		||||
 | 
			
		||||
    --- @tparam { co = coroutine, can_wrap ?= boolean } parent The parent coroutine.
 | 
			
		||||
    -- @tparam function f The function to call.
 | 
			
		||||
    -- @param ... The arguments to this function.
 | 
			
		||||
    try_barrier = function(parent, f, ...) return bounce(f(...)) end
 | 
			
		||||
 | 
			
		||||
    debug.getregistry().cc_try_barrier = try_barrier
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
-- Functions that act as a barrier for exceptions.
 | 
			
		||||
local pcall_functions = { [pcall] = true, [xpcall] = true, [load] = true }
 | 
			
		||||
 | 
			
		||||
--[[- Check to see whether we can wrap errors into an exception.
 | 
			
		||||
 | 
			
		||||
This scans the current thread (up to a limit), and any parent threads, to
 | 
			
		||||
determine if there is a pcall anywhere on the callstack. If not, then we know
 | 
			
		||||
the error message is not observed by user code, and so may be wrapped into an
 | 
			
		||||
exception.
 | 
			
		||||
 | 
			
		||||
@tparam[opt] coroutine The thread to check. Defaults to the current thread.
 | 
			
		||||
@treturn boolean Whether we can wrap errors into exceptions.
 | 
			
		||||
]]
 | 
			
		||||
local function can_wrap_errors(thread)
 | 
			
		||||
    if not thread then thread = coroutine.running() end
 | 
			
		||||
 | 
			
		||||
    for offset = 0, 31 do
 | 
			
		||||
        local frame = debug.getinfo(thread, offset, "f")
 | 
			
		||||
        if not frame then return false end
 | 
			
		||||
 | 
			
		||||
        local func = frame.func
 | 
			
		||||
        if func == try_barrier then
 | 
			
		||||
            -- If we've a try barrier, then extract the parent coroutine and
 | 
			
		||||
            -- check if it can wrap errors.
 | 
			
		||||
            local _, parent = debug.getlocal(thread, offset, 1)
 | 
			
		||||
            if type(parent) ~= "table" or type(parent.co) ~= "thread" then return false end
 | 
			
		||||
 | 
			
		||||
            local result = parent.can_wrap
 | 
			
		||||
            if result == nil then
 | 
			
		||||
                result = can_wrap_errors(parent.co)
 | 
			
		||||
                parent.can_wrap = result
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            return result
 | 
			
		||||
        elseif pcall_functions[func] then
 | 
			
		||||
            -- If we're a pcall, then abort.
 | 
			
		||||
            return false
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return false
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Attempt to call the provided function `func` with the provided arguments.
 | 
			
		||||
 | 
			
		||||
@tparam function func The function to call.
 | 
			
		||||
@param ... Arguments to this function.
 | 
			
		||||
 | 
			
		||||
@treturn[1] true If the function ran successfully.
 | 
			
		||||
    @return[1] ... The return values of the function.
 | 
			
		||||
@return[1] ... The return values of the function.
 | 
			
		||||
 | 
			
		||||
@treturn[2] false If the function failed.
 | 
			
		||||
@return[2] The error message
 | 
			
		||||
@@ -42,8 +159,8 @@ end
 | 
			
		||||
local function try(func, ...)
 | 
			
		||||
    expect(1, func, "function")
 | 
			
		||||
 | 
			
		||||
    local co = coroutine.create(func)
 | 
			
		||||
    local result = table.pack(coroutine.resume(co, ...))
 | 
			
		||||
    local co = coroutine.create(try_barrier)
 | 
			
		||||
    local result = table.pack(coroutine.resume(co, { co = co, can_wrap = true }, func, ...))
 | 
			
		||||
 | 
			
		||||
    while coroutine.status(co) ~= "dead" do
 | 
			
		||||
        local event = table.pack(os.pullEventRaw(result[2]))
 | 
			
		||||
@@ -52,8 +169,14 @@ local function try(func, ...)
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if not result[1] then return false, result[2], co end
 | 
			
		||||
    return table.unpack(result, 1, result.n)
 | 
			
		||||
    if result[1] then
 | 
			
		||||
        return table.unpack(result, 1, result.n)
 | 
			
		||||
    elseif is_exception(result[2]) then
 | 
			
		||||
        local exn = result[2]
 | 
			
		||||
        return false, rawget(exn, "message"), rawget(exn, "thread")
 | 
			
		||||
    else
 | 
			
		||||
        return false, result[2], co
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Report additional context about an error.
 | 
			
		||||
@@ -68,11 +191,11 @@ local function report(err, thread, source_map)
 | 
			
		||||
 | 
			
		||||
    if type(err) ~= "string" then return end
 | 
			
		||||
 | 
			
		||||
    local file, line = err:match("^([^:]+):(%d+):")
 | 
			
		||||
    local file, line, err = err:match("^([^:]+):(%d+): (.*)")
 | 
			
		||||
    if not file then return end
 | 
			
		||||
    line = tonumber(line)
 | 
			
		||||
 | 
			
		||||
    local frame = find_frame(thread, file, line)
 | 
			
		||||
    local frame_offset, frame = find_frame(thread, file, line)
 | 
			
		||||
    if not frame or not frame.currentcolumn then return end
 | 
			
		||||
 | 
			
		||||
    local column = frame.currentcolumn
 | 
			
		||||
@@ -109,16 +232,22 @@ local function report(err, thread, source_map)
 | 
			
		||||
    -- Could not determine the line. Bail.
 | 
			
		||||
    if not line_contents or #line_contents == "" then return end
 | 
			
		||||
 | 
			
		||||
    error_printer({
 | 
			
		||||
    require("cc.internal.error_printer")({
 | 
			
		||||
        get_pos = function() return line, column end,
 | 
			
		||||
        get_line = function() return line_contents end,
 | 
			
		||||
    }, {
 | 
			
		||||
        { tag = "annotate", start_pos = column, end_pos = column, msg = "" },
 | 
			
		||||
        require "cc.internal.error_hints".get_tip(err, thread, frame_offset),
 | 
			
		||||
    })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
return {
 | 
			
		||||
    make_exception = make_exception,
 | 
			
		||||
 | 
			
		||||
    try_barrier = try_barrier,
 | 
			
		||||
    can_wrap_errors = can_wrap_errors,
 | 
			
		||||
 | 
			
		||||
    try = try,
 | 
			
		||||
    report = report,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,11 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
--[[- Upload a list of files, as received by the @{event!file_transfer} event.
 | 
			
		||||
--[[- Upload a list of files, as received by the [`event!file_transfer`] event.
 | 
			
		||||
 | 
			
		||||
:::warning
 | 
			
		||||
This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
be removed or changed at any time.
 | 
			
		||||
:::
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
]]
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,13 @@
 | 
			
		||||
 | 
			
		||||
--[[- The error messages reported by our lexer and parser.
 | 
			
		||||
 | 
			
		||||
:::warning
 | 
			
		||||
This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
be removed or changed at any time.
 | 
			
		||||
:::
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
This provides a list of factory methods which take source positions and produce
 | 
			
		||||
appropriate error messages targeting that location. These error messages can
 | 
			
		||||
then be displayed to the user via @{cc.internal.error_printer}.
 | 
			
		||||
then be displayed to the user via [`cc.internal.error_printer`].
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
]]
 | 
			
		||||
@@ -59,6 +58,7 @@ local token_names = setmetatable({
 | 
			
		||||
    [tokens.DO] = code("do"),
 | 
			
		||||
    [tokens.DOT] = code("."),
 | 
			
		||||
    [tokens.DOTS] = code("..."),
 | 
			
		||||
    [tokens.DOUBLE_COLON] = code("::"),
 | 
			
		||||
    [tokens.ELSE] = code("else"),
 | 
			
		||||
    [tokens.ELSEIF] = code("elseif"),
 | 
			
		||||
    [tokens.END] = code("end"),
 | 
			
		||||
@@ -68,6 +68,7 @@ local token_names = setmetatable({
 | 
			
		||||
    [tokens.FOR] = code("for"),
 | 
			
		||||
    [tokens.FUNCTION] = code("function"),
 | 
			
		||||
    [tokens.GE] = code(">="),
 | 
			
		||||
    [tokens.GOTO] = code("goto"),
 | 
			
		||||
    [tokens.GT] = code(">"),
 | 
			
		||||
    [tokens.IF] = code("if"),
 | 
			
		||||
    [tokens.IN] = code("in"),
 | 
			
		||||
@@ -121,7 +122,7 @@ function errors.unfinished_string(start_pos, end_pos, quote)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A string which ends with an escape sequence (so a literal `"foo\`). This
 | 
			
		||||
is slightly different from @{unfinished_string}, as we don't want to suggest
 | 
			
		||||
is slightly different from [`unfinished_string`], as we don't want to suggest
 | 
			
		||||
adding a quote.
 | 
			
		||||
 | 
			
		||||
@tparam number start_pos The start position of the string.
 | 
			
		||||
@@ -283,6 +284,23 @@ function errors.wrong_ne(start_pos, end_pos)
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- `!` was used instead of `not`.
 | 
			
		||||
 | 
			
		||||
@tparam number start_pos The start position of the token.
 | 
			
		||||
@tparam number end_pos The end position of the token.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.wrong_not(start_pos, end_pos)
 | 
			
		||||
    expect(1, start_pos, "number")
 | 
			
		||||
    expect(2, end_pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected character.",
 | 
			
		||||
        annotate(start_pos, end_pos),
 | 
			
		||||
        "Tip: Replace this with " .. code("not") .. " to negate a boolean.",
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- An unexpected character was used.
 | 
			
		||||
 | 
			
		||||
@tparam number pos The position of this character.
 | 
			
		||||
@@ -452,32 +470,53 @@ function errors.local_function_dot(local_start, local_end, dot_start, dot_end)
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A statement of the form `x.y z`
 | 
			
		||||
--[[- A statement of the form `x.y`
 | 
			
		||||
 | 
			
		||||
@tparam number token The token id.
 | 
			
		||||
@tparam number pos The position right after this name.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.standalone_name(pos)
 | 
			
		||||
    expect(1, pos, "number")
 | 
			
		||||
function errors.standalone_name(token, pos)
 | 
			
		||||
    expect(1, token, "number")
 | 
			
		||||
    expect(2, pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected symbol after name.",
 | 
			
		||||
        "Unexpected " .. token_names[token] .. " after name.",
 | 
			
		||||
        annotate(pos),
 | 
			
		||||
        "Did you mean to assign this or call it as a function?",
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A statement of the form `x.y`. This is similar to @{standalone_name}, but
 | 
			
		||||
when the next token is on another line.
 | 
			
		||||
--[[- A statement of the form `x.y, z`
 | 
			
		||||
 | 
			
		||||
@tparam number token The token id.
 | 
			
		||||
@tparam number pos The position right after this name.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.standalone_name_call(pos)
 | 
			
		||||
    expect(1, pos, "number")
 | 
			
		||||
function errors.standalone_names(token, pos)
 | 
			
		||||
    expect(1, token, "number")
 | 
			
		||||
    expect(2, pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected symbol after variable.",
 | 
			
		||||
        "Unexpected " .. token_names[token] .. " after name.",
 | 
			
		||||
        annotate(pos),
 | 
			
		||||
        "Did you mean to assign this?",
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A statement of the form `x.y`. This is similar to [`standalone_name`], but
 | 
			
		||||
when the next token is on another line.
 | 
			
		||||
 | 
			
		||||
@tparam number token The token id.
 | 
			
		||||
@tparam number pos The position right after this name.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.standalone_name_call(token, pos)
 | 
			
		||||
    expect(1, token, "number")
 | 
			
		||||
    expect(2, pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected " .. token_names[token] .. " after name.",
 | 
			
		||||
        annotate(pos + 1, "Expected something before the end of the line."),
 | 
			
		||||
        "Tip: Use " .. code("()") .. " to call with no arguments.",
 | 
			
		||||
    }
 | 
			
		||||
@@ -536,6 +575,28 @@ function errors.unexpected_end(start_pos, end_pos)
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A label statement was opened but not closed.
 | 
			
		||||
 | 
			
		||||
@tparam number open_start The start position of the opening label.
 | 
			
		||||
@tparam number open_end The end position of the opening label.
 | 
			
		||||
@tparam number tok_start The start position of the current token.
 | 
			
		||||
@return The resulting parse error.
 | 
			
		||||
]]
 | 
			
		||||
function errors.unclosed_label(open_start, open_end, token, start_pos, end_pos)
 | 
			
		||||
    expect(1, open_start, "number")
 | 
			
		||||
    expect(2, open_end, "number")
 | 
			
		||||
    expect(3, token, "number")
 | 
			
		||||
    expect(4, start_pos, "number")
 | 
			
		||||
    expect(5, end_pos, "number")
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        "Unexpected " .. token_names[token] .. ".",
 | 
			
		||||
        annotate(open_start, open_end, "Label was started here."),
 | 
			
		||||
        annotate(start_pos, end_pos, "Tip: Try adding " .. code("::") .. " here."),
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--------------------------------------------------------------------------------
 | 
			
		||||
-- Generic parsing errors
 | 
			
		||||
--------------------------------------------------------------------------------
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,9 @@
 | 
			
		||||
 | 
			
		||||
--[[- The main entrypoint to our Lua parser
 | 
			
		||||
 | 
			
		||||
:::warning
 | 
			
		||||
This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
be removed or changed at any time.
 | 
			
		||||
:::
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
]]
 | 
			
		||||
@@ -21,6 +20,8 @@ local error_printer = require "cc.internal.error_printer"
 | 
			
		||||
local error_sentinel = {}
 | 
			
		||||
 | 
			
		||||
local function make_context(input)
 | 
			
		||||
    expect(1, input, "string")
 | 
			
		||||
 | 
			
		||||
    local context = {}
 | 
			
		||||
 | 
			
		||||
    local lines = { 1 }
 | 
			
		||||
@@ -73,8 +74,9 @@ local function parse(input, start_symbol)
 | 
			
		||||
    expect(2, start_symbol, "number")
 | 
			
		||||
 | 
			
		||||
    local context = make_context(input)
 | 
			
		||||
    function context.report(msg)
 | 
			
		||||
        expect(1, msg, "table")
 | 
			
		||||
    function context.report(msg, ...)
 | 
			
		||||
        expect(1, msg, "table", "function")
 | 
			
		||||
        if type(msg) == "function" then msg = msg(...) end
 | 
			
		||||
        error_printer(context, msg)
 | 
			
		||||
        error(error_sentinel)
 | 
			
		||||
    end
 | 
			
		||||
@@ -110,8 +112,9 @@ local function parse_repl(input)
 | 
			
		||||
    local context = make_context(input)
 | 
			
		||||
 | 
			
		||||
    local last_error = nil
 | 
			
		||||
    function context.report(msg)
 | 
			
		||||
        expect(1, msg, "table")
 | 
			
		||||
    function context.report(msg, ...)
 | 
			
		||||
        expect(1, msg, "table", "function")
 | 
			
		||||
        if type(msg) == "function" then msg = msg(...) end
 | 
			
		||||
        last_error = msg
 | 
			
		||||
        error(error_sentinel)
 | 
			
		||||
    end
 | 
			
		||||
 
 | 
			
		||||
@@ -4,17 +4,16 @@
 | 
			
		||||
 | 
			
		||||
--[[- A lexer for Lua source code.
 | 
			
		||||
 | 
			
		||||
:::warning
 | 
			
		||||
This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
be removed or changed at any time.
 | 
			
		||||
:::
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
This module provides utilities for lexing Lua code, returning tokens compatible
 | 
			
		||||
with @{cc.internal.syntax.parser}. While all lexers are roughly the same, there
 | 
			
		||||
with [`cc.internal.syntax.parser`]. While all lexers are roughly the same, there
 | 
			
		||||
are some design choices worth drawing attention to:
 | 
			
		||||
 | 
			
		||||
 - The lexer uses Lua patterns (i.e. @{string.find}) as much as possible,
 | 
			
		||||
   trying to avoid @{string.sub} loops except when needed. This allows us to
 | 
			
		||||
 - The lexer uses Lua patterns (i.e. [`string.find`]) as much as possible,
 | 
			
		||||
   trying to avoid [`string.sub`] loops except when needed. This allows us to
 | 
			
		||||
   move string processing to native code, which ends up being much faster.
 | 
			
		||||
 | 
			
		||||
 - We try to avoid allocating where possible. There are some cases we need to
 | 
			
		||||
@@ -33,12 +32,12 @@ local tokens = require "cc.internal.syntax.parser".tokens
 | 
			
		||||
local sub, find = string.sub, string.find
 | 
			
		||||
 | 
			
		||||
local keywords = {
 | 
			
		||||
    ["and"]      = tokens.AND,      ["break"] = tokens.BREAK, ["do"]    = tokens.DO,    ["else"]   = tokens.ELSE,
 | 
			
		||||
    ["elseif"]   = tokens.ELSEIF,   ["end"]   = tokens.END,   ["false"] = tokens.FALSE, ["for"]    = tokens.FOR,
 | 
			
		||||
    ["function"] = tokens.FUNCTION, ["if"]    = tokens.IF,    ["in"]    = tokens.IN,    ["local"]  = tokens.LOCAL,
 | 
			
		||||
    ["nil"]      = tokens.NIL,      ["not"]   = tokens.NOT,   ["or"]    = tokens.OR,    ["repeat"] = tokens.REPEAT,
 | 
			
		||||
    ["return"]   = tokens.RETURN,   ["then"]  = tokens.THEN,  ["true"]  = tokens.TRUE,  ["until"]  = tokens.UNTIL,
 | 
			
		||||
    ["while"]    = tokens.WHILE,
 | 
			
		||||
    ["and"]      = tokens.AND,      ["break"]  = tokens.BREAK,  ["do"]    = tokens.DO,    ["else"] = tokens.ELSE,
 | 
			
		||||
    ["elseif"]   = tokens.ELSEIF,   ["end"]    = tokens.END,    ["false"] = tokens.FALSE, ["for"]  = tokens.FOR,
 | 
			
		||||
    ["function"] = tokens.FUNCTION, ["goto"]   = tokens.GOTO,   ["if"]    = tokens.IF,    ["in"]   = tokens.IN,
 | 
			
		||||
    ["local"]    = tokens.LOCAL,    ["nil"]    = tokens.NIL,    ["not"]   = tokens.NOT,   ["or"]   = tokens.OR,
 | 
			
		||||
    ["repeat"]   = tokens.REPEAT,   ["return"] = tokens.RETURN, ["then"]  = tokens.THEN,  ["true"] = tokens.TRUE,
 | 
			
		||||
    ["until"]    = tokens.UNTIL,    ["while"]  = tokens.WHILE,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
--- Lex a newline character
 | 
			
		||||
@@ -96,7 +95,7 @@ local function lex_number(context, str, start)
 | 
			
		||||
    local contents = sub(str, start, pos - 1)
 | 
			
		||||
    if not tonumber(contents) then
 | 
			
		||||
        -- TODO: Separate error for "2..3"?
 | 
			
		||||
        context.report(errors.malformed_number(start, pos - 1))
 | 
			
		||||
        context.report(errors.malformed_number, start, pos - 1)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return tokens.NUMBER, pos - 1
 | 
			
		||||
@@ -118,14 +117,14 @@ local function lex_string(context, str, start_pos, quote)
 | 
			
		||||
            return tokens.STRING, pos
 | 
			
		||||
        elseif c == "\n" or c == "\r" or c == "" then
 | 
			
		||||
            -- We don't call newline here, as that's done for the next token.
 | 
			
		||||
            context.report(errors.unfinished_string(start_pos, pos, quote))
 | 
			
		||||
            context.report(errors.unfinished_string, start_pos, pos, quote)
 | 
			
		||||
            return tokens.STRING, pos - 1
 | 
			
		||||
        elseif c == "\\" then
 | 
			
		||||
            c = sub(str, pos + 1, pos + 1)
 | 
			
		||||
            if c == "\n" or c == "\r" then
 | 
			
		||||
                pos = newline(context, str, pos + 1, c)
 | 
			
		||||
            elseif c == "" then
 | 
			
		||||
                context.report(errors.unfinished_string_escape(start_pos, pos, quote))
 | 
			
		||||
                context.report(errors.unfinished_string_escape, start_pos, pos, quote)
 | 
			
		||||
                return tokens.STRING, pos
 | 
			
		||||
            elseif c == "z" then
 | 
			
		||||
                pos = pos + 2
 | 
			
		||||
@@ -133,7 +132,7 @@ local function lex_string(context, str, start_pos, quote)
 | 
			
		||||
                    local next_pos, _, c  = find(str, "([%S\r\n])", pos)
 | 
			
		||||
 | 
			
		||||
                    if not next_pos then
 | 
			
		||||
                        context.report(errors.unfinished_string(start_pos, #str, quote))
 | 
			
		||||
                        context.report(errors.unfinished_string, start_pos, #str, quote)
 | 
			
		||||
                        return tokens.STRING, #str
 | 
			
		||||
                    end
 | 
			
		||||
 | 
			
		||||
@@ -178,7 +177,7 @@ end
 | 
			
		||||
-- @tparam number start The start position, after the input boundary.
 | 
			
		||||
-- @tparam number len The expected length of the boundary. Equal to 1 + the
 | 
			
		||||
-- number of `=`.
 | 
			
		||||
-- @treturn number|nil The end position, or @{nil} if this is not terminated.
 | 
			
		||||
-- @treturn number|nil The end position, or [`nil`] if this is not terminated.
 | 
			
		||||
local function lex_long_str(context, str, start, len)
 | 
			
		||||
    local pos = start
 | 
			
		||||
    while true do
 | 
			
		||||
@@ -196,7 +195,7 @@ local function lex_long_str(context, str, start, len)
 | 
			
		||||
        elseif c == "[" then
 | 
			
		||||
            local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[")
 | 
			
		||||
            if ok and boundary_pos - pos == len and len == 1 then
 | 
			
		||||
                context.report(errors.nested_long_str(pos, boundary_pos))
 | 
			
		||||
                context.report(errors.nested_long_str, pos, boundary_pos)
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
            pos = boundary_pos
 | 
			
		||||
@@ -238,12 +237,12 @@ local function lex_token(context, str, pos)
 | 
			
		||||
            local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - pos)
 | 
			
		||||
            if end_pos then return tokens.STRING, end_pos end
 | 
			
		||||
 | 
			
		||||
            context.report(errors.unfinished_long_string(pos, boundary_pos, boundary_pos - pos))
 | 
			
		||||
            context.report(errors.unfinished_long_string, pos, boundary_pos, boundary_pos - pos)
 | 
			
		||||
            return tokens.ERROR, #str
 | 
			
		||||
        elseif pos + 1 == boundary_pos then -- Just a "["
 | 
			
		||||
            return tokens.OSQUARE, pos
 | 
			
		||||
        else -- Malformed long string, for instance "[="
 | 
			
		||||
            context.report(errors.malformed_long_string(pos, boundary_pos, boundary_pos - pos))
 | 
			
		||||
            context.report(errors.malformed_long_string, pos, boundary_pos, boundary_pos - pos)
 | 
			
		||||
            return tokens.ERROR, boundary_pos
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
@@ -260,7 +259,7 @@ local function lex_token(context, str, pos)
 | 
			
		||||
                local end_pos = lex_long_str(context, str, boundary_pos + 1, boundary_pos - comment_pos)
 | 
			
		||||
                if end_pos then return tokens.COMMENT, end_pos end
 | 
			
		||||
 | 
			
		||||
                context.report(errors.unfinished_long_comment(pos, boundary_pos, boundary_pos - comment_pos))
 | 
			
		||||
                context.report(errors.unfinished_long_comment, pos, boundary_pos, boundary_pos - comment_pos)
 | 
			
		||||
                return tokens.ERROR, #str
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
@@ -293,12 +292,15 @@ local function lex_token(context, str, pos)
 | 
			
		||||
        local next_pos = pos + 1
 | 
			
		||||
        if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end
 | 
			
		||||
        return tokens.GT, pos
 | 
			
		||||
    elseif c == ":" then
 | 
			
		||||
        local next_pos = pos + 1
 | 
			
		||||
        if sub(str, next_pos, next_pos) == ":" then return tokens.DOUBLE_COLON, next_pos end
 | 
			
		||||
        return tokens.COLON, pos
 | 
			
		||||
    elseif c == "~" and sub(str, pos + 1, pos + 1) == "=" then return tokens.NE, pos + 1
 | 
			
		||||
 | 
			
		||||
    -- Single character tokens
 | 
			
		||||
    elseif c == "," then return tokens.COMMA, pos
 | 
			
		||||
    elseif c == ";" then return tokens.SEMICOLON, pos
 | 
			
		||||
    elseif c == ":" then return tokens.COLON, pos
 | 
			
		||||
    elseif c == "(" then return tokens.OPAREN, pos
 | 
			
		||||
    elseif c == ")" then return tokens.CPAREN, pos
 | 
			
		||||
    elseif c == "]" then return tokens.CSQUARE, pos
 | 
			
		||||
@@ -317,18 +319,21 @@ local function lex_token(context, str, pos)
 | 
			
		||||
        if end_pos - pos <= 3 then
 | 
			
		||||
            local contents = sub(str, pos, end_pos)
 | 
			
		||||
            if contents == "&&" then
 | 
			
		||||
                context.report(errors.wrong_and(pos, end_pos))
 | 
			
		||||
                context.report(errors.wrong_and, pos, end_pos)
 | 
			
		||||
                return tokens.AND, end_pos
 | 
			
		||||
            elseif contents == "||" then
 | 
			
		||||
                context.report(errors.wrong_or(pos, end_pos))
 | 
			
		||||
                context.report(errors.wrong_or, pos, end_pos)
 | 
			
		||||
                return tokens.OR, end_pos
 | 
			
		||||
            elseif contents == "!=" or contents == "<>" then
 | 
			
		||||
                context.report(errors.wrong_ne(pos, end_pos))
 | 
			
		||||
                context.report(errors.wrong_ne, pos, end_pos)
 | 
			
		||||
                return tokens.NE, end_pos
 | 
			
		||||
            elseif contents == "!" then
 | 
			
		||||
                context.report(errors.wrong_not, pos, end_pos)
 | 
			
		||||
                return tokens.NOT, end_pos
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context.report(errors.unexpected_character(pos))
 | 
			
		||||
        context.report(errors.unexpected_character, pos)
 | 
			
		||||
        return tokens.ERROR, end_pos
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
-- SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: MPL-2.0
 | 
			
		||||
 | 
			
		||||
--[[- A minimal implementation of require.
 | 
			
		||||
 | 
			
		||||
This is intended for use with APIs, and other internal code which is not run in
 | 
			
		||||
the [`shell`] environment. This allows us to avoid some of the overhead of
 | 
			
		||||
loading the full [`cc.require`] module.
 | 
			
		||||
 | 
			
		||||
> [!DANGER]
 | 
			
		||||
> This is an internal module and SHOULD NOT be used in your own code. It may
 | 
			
		||||
> be removed or changed at any time.
 | 
			
		||||
 | 
			
		||||
@local
 | 
			
		||||
 | 
			
		||||
@tparam string name The module to require.
 | 
			
		||||
@return The required module.
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
local loaded = {}
 | 
			
		||||
local env = setmetatable({}, { __index = _G })
 | 
			
		||||
local function require(name)
 | 
			
		||||
    local result = loaded[name]
 | 
			
		||||
    if result then return result end
 | 
			
		||||
 | 
			
		||||
    local path = "rom/modules/main/" .. name:gsub("%.", "/")
 | 
			
		||||
    if fs.exists(path .. ".lua") then
 | 
			
		||||
        result = assert(loadfile(path .. ".lua", nil, env))()
 | 
			
		||||
    else
 | 
			
		||||
        result = assert(loadfile(path .. "/init.lua", nil, env))()
 | 
			
		||||
    end
 | 
			
		||||
    loaded[name] = result
 | 
			
		||||
    return result
 | 
			
		||||
end
 | 
			
		||||
env.require = require
 | 
			
		||||
return require
 | 
			
		||||
@@ -5,12 +5,12 @@
 | 
			
		||||
--[[- A pretty printer for rendering data structures in an aesthetically
 | 
			
		||||
pleasing manner.
 | 
			
		||||
 | 
			
		||||
In order to display something using @{cc.pretty}, you build up a series of
 | 
			
		||||
@{Doc|documents}. These behave a little bit like strings; you can concatenate
 | 
			
		||||
In order to display something using [`cc.pretty`], you build up a series of
 | 
			
		||||
[documents][`Doc`]. These behave a little bit like strings; you can concatenate
 | 
			
		||||
them together and then print them to the screen.
 | 
			
		||||
 | 
			
		||||
However, documents also allow you to control how they should be printed. There
 | 
			
		||||
are several functions (such as @{nest} and @{group}) which allow you to control
 | 
			
		||||
are several functions (such as [`nest`] and [`group`]) which allow you to control
 | 
			
		||||
the "layout" of the document. When you come to display the document, the 'best'
 | 
			
		||||
(most compact) layout is used.
 | 
			
		||||
 | 
			
		||||
@@ -37,7 +37,7 @@ local expect, field = expect.expect, expect.field
 | 
			
		||||
local type, getmetatable, setmetatable, colours, str_write, tostring = type, getmetatable, setmetatable, colours, write, tostring
 | 
			
		||||
local debug_info, debug_local = debug.getinfo, debug.getlocal
 | 
			
		||||
 | 
			
		||||
--- @{table.insert} alternative, but with the length stored inline.
 | 
			
		||||
--- [`table.insert`] alternative, but with the length stored inline.
 | 
			
		||||
local function append(out, value)
 | 
			
		||||
    local n = out.n + 1
 | 
			
		||||
    out[n], out.n = value, n
 | 
			
		||||
@@ -59,10 +59,10 @@ local empty = mk_doc({ tag = "nil" })
 | 
			
		||||
--- A document with a single space in it.
 | 
			
		||||
local space = mk_doc({ tag = "text", text = " " })
 | 
			
		||||
 | 
			
		||||
--- A line break. When collapsed with @{group}, this will be replaced with @{empty}.
 | 
			
		||||
--- A line break. When collapsed with [`group`], this will be replaced with [`empty`].
 | 
			
		||||
local line = mk_doc({ tag = "line", flat = empty })
 | 
			
		||||
 | 
			
		||||
--- A line break. When collapsed with @{group}, this will be replaced with @{space}.
 | 
			
		||||
--- A line break. When collapsed with [`group`], this will be replaced with [`space`].
 | 
			
		||||
local space_line = mk_doc({ tag = "line", flat = space })
 | 
			
		||||
 | 
			
		||||
local text_cache = { [""] = empty, [" "] = space, ["\n"] = space_line }
 | 
			
		||||
@@ -73,7 +73,7 @@ end
 | 
			
		||||
 | 
			
		||||
--- Create a new document from a string.
 | 
			
		||||
--
 | 
			
		||||
-- If your string contains multiple lines, @{group} will flatten the string
 | 
			
		||||
-- If your string contains multiple lines, [`group`] will flatten the string
 | 
			
		||||
-- into a single line, with spaces between each line.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam      string text   The string to construct a new document with.
 | 
			
		||||
@@ -453,7 +453,7 @@ end
 | 
			
		||||
 | 
			
		||||
--- Pretty-print an arbitrary object, converting it into a document.
 | 
			
		||||
--
 | 
			
		||||
-- This can then be rendered with @{write} or @{print}.
 | 
			
		||||
-- This can then be rendered with [`write`] or [`print`].
 | 
			
		||||
--
 | 
			
		||||
-- @param obj The object to pretty-print.
 | 
			
		||||
-- @tparam[opt] { function_args = boolean, function_source = boolean } options
 | 
			
		||||
@@ -479,7 +479,7 @@ local function pretty(obj, options)
 | 
			
		||||
    return pretty_impl(obj, actual_options, {})
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- A shortcut for calling @{pretty} and @{print} together.
 | 
			
		||||
--[[- A shortcut for calling [`pretty`] and [`print`] together.
 | 
			
		||||
 | 
			
		||||
@param obj The object to pretty-print.
 | 
			
		||||
@tparam[opt] { function_args = boolean, function_source = boolean } options
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@
 | 
			
		||||
--
 | 
			
		||||
-- SPDX-License-Identifier: LicenseRef-CCPL
 | 
			
		||||
 | 
			
		||||
--[[- A pure Lua implementation of the builtin @{require} function and
 | 
			
		||||
@{package} library.
 | 
			
		||||
--[[- A pure Lua implementation of the builtin [`require`] function and
 | 
			
		||||
[`package`] library.
 | 
			
		||||
 | 
			
		||||
Generally you do not need to use this module - it is injected into the every
 | 
			
		||||
program's environment. However, it may be useful when building a custom shell or
 | 
			
		||||
@@ -11,7 +11,7 @@ when running programs yourself.
 | 
			
		||||
 | 
			
		||||
@module cc.require
 | 
			
		||||
@since 1.88.0
 | 
			
		||||
@see using_require For an introduction on how to use @{require}.
 | 
			
		||||
@see using_require For an introduction on how to use [`require`].
 | 
			
		||||
@usage Construct the package and require function, and insert them into a
 | 
			
		||||
custom environment.
 | 
			
		||||
 | 
			
		||||
@@ -110,13 +110,13 @@ local function make_require(package)
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--- Build an implementation of Lua's @{package} library, and a @{require}
 | 
			
		||||
--- Build an implementation of Lua's [`package`] library, and a [`require`]
 | 
			
		||||
-- function to load modules within it.
 | 
			
		||||
--
 | 
			
		||||
-- @tparam table env The environment to load packages into.
 | 
			
		||||
-- @tparam string dir The directory that relative packages are loaded from.
 | 
			
		||||
-- @treturn function The new @{require} function.
 | 
			
		||||
-- @treturn table The new @{package} library.
 | 
			
		||||
-- @treturn function The new [`require`] function.
 | 
			
		||||
-- @treturn table The new [`package`] library.
 | 
			
		||||
local function make_package(env, dir)
 | 
			
		||||
    expect(1, env, "table")
 | 
			
		||||
    expect(2, dir, "string")
 | 
			
		||||
@@ -124,13 +124,22 @@ local function make_package(env, dir)
 | 
			
		||||
    local package = {}
 | 
			
		||||
    package.loaded = {
 | 
			
		||||
        _G = _G,
 | 
			
		||||
        bit32 = bit32,
 | 
			
		||||
        coroutine = coroutine,
 | 
			
		||||
        math = math,
 | 
			
		||||
        package = package,
 | 
			
		||||
        string = string,
 | 
			
		||||
        table = table,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    -- Copy everything from the global package table to this instance.
 | 
			
		||||
    --
 | 
			
		||||
    -- This table is an internal implementation detail - it is NOT intended to
 | 
			
		||||
    -- be extended by user code.
 | 
			
		||||
    local registry = debug.getregistry()
 | 
			
		||||
    if registry and type(registry._LOADED) == "table" then
 | 
			
		||||
        for k, v in next, registry._LOADED do
 | 
			
		||||
            if type(k) == "string" then
 | 
			
		||||
                package.loaded[k] = v
 | 
			
		||||
            end
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    package.path = "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua"
 | 
			
		||||
    if turtle then
 | 
			
		||||
        package.path = package.path .. ";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua"
 | 
			
		||||
 
 | 
			
		||||
@@ -4,16 +4,16 @@
 | 
			
		||||
 | 
			
		||||
--[[- A collection of helper methods for working with shell completion.
 | 
			
		||||
 | 
			
		||||
Most programs may be completed using the @{build} helper method, rather than
 | 
			
		||||
Most programs may be completed using the [`build`] helper method, rather than
 | 
			
		||||
manually switching on the argument index.
 | 
			
		||||
 | 
			
		||||
Note, the helper functions within this module do not accept an argument index,
 | 
			
		||||
and so are not directly usable with the @{shell.setCompletionFunction}. Instead,
 | 
			
		||||
wrap them using @{build}, or your own custom function.
 | 
			
		||||
and so are not directly usable with the [`shell.setCompletionFunction`]. Instead,
 | 
			
		||||
wrap them using [`build`], or your own custom function.
 | 
			
		||||
 | 
			
		||||
@module cc.shell.completion
 | 
			
		||||
@since 1.85.0
 | 
			
		||||
@see cc.completion For more general helpers, suitable for use with @{_G.read}.
 | 
			
		||||
@see cc.completion For more general helpers, suitable for use with [`_G.read`].
 | 
			
		||||
@see shell.setCompletionFunction
 | 
			
		||||
 | 
			
		||||
@usage Register a completion handler for example.lua which prompts for a
 | 
			
		||||
@@ -135,15 +135,15 @@ end
 | 
			
		||||
--[[- A helper function for building shell completion arguments.
 | 
			
		||||
 | 
			
		||||
This accepts a series of single-argument completion functions, and combines
 | 
			
		||||
them into a function suitable for use with @{shell.setCompletionFunction}.
 | 
			
		||||
them into a function suitable for use with [`shell.setCompletionFunction`].
 | 
			
		||||
 | 
			
		||||
@tparam nil|table|function ... Every argument to @{build} represents an argument
 | 
			
		||||
@tparam nil|table|function ... Every argument to [`build`] represents an argument
 | 
			
		||||
to the program you wish to complete. Each argument can be one of three types:
 | 
			
		||||
 | 
			
		||||
 - `nil`: This argument will not be completed.
 | 
			
		||||
 | 
			
		||||
 - A function: This argument will be completed with the given function. It is
 | 
			
		||||
   called with the @{shell} object, the string to complete and the arguments
 | 
			
		||||
   called with the [`shell`] object, the string to complete and the arguments
 | 
			
		||||
   before this one.
 | 
			
		||||
 | 
			
		||||
 - A table: This acts as a more powerful version of the function case. The table
 | 
			
		||||
@@ -197,12 +197,12 @@ return {
 | 
			
		||||
    programWithArgs = programWithArgs,
 | 
			
		||||
 | 
			
		||||
    -- Re-export various other functions
 | 
			
		||||
    help = wrap(help.completeTopic), --- Wraps @{help.completeTopic} as a @{build} compatible function.
 | 
			
		||||
    choice = wrap(completion.choice), --- Wraps @{cc.completion.choice} as a @{build} compatible function.
 | 
			
		||||
    peripheral = wrap(completion.peripheral), --- Wraps @{cc.completion.peripheral} as a @{build} compatible function.
 | 
			
		||||
    side = wrap(completion.side), --- Wraps @{cc.completion.side} as a @{build} compatible function.
 | 
			
		||||
    setting = wrap(completion.setting), --- Wraps @{cc.completion.setting} as a @{build} compatible function.
 | 
			
		||||
    command = wrap(completion.command), --- Wraps @{cc.completion.command} as a @{build} compatible function.
 | 
			
		||||
    help = wrap(help.completeTopic), --- Wraps [`help.completeTopic`] as a [`build`] compatible function.
 | 
			
		||||
    choice = wrap(completion.choice), --- Wraps [`cc.completion.choice`] as a [`build`] compatible function.
 | 
			
		||||
    peripheral = wrap(completion.peripheral), --- Wraps [`cc.completion.peripheral`] as a [`build`] compatible function.
 | 
			
		||||
    side = wrap(completion.side), --- Wraps [`cc.completion.side`] as a [`build`] compatible function.
 | 
			
		||||
    setting = wrap(completion.setting), --- Wraps [`cc.completion.setting`] as a [`build`] compatible function.
 | 
			
		||||
    command = wrap(completion.command), --- Wraps [`cc.completion.command`] as a [`build`] compatible function.
 | 
			
		||||
 | 
			
		||||
    build = build,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,13 @@
 | 
			
		||||
-- @since 1.95.0
 | 
			
		||||
-- @see textutils For additional string related utilities.
 | 
			
		||||
 | 
			
		||||
local expect = (require and require("cc.expect") or dofile("rom/modules/main/cc/expect.lua")).expect
 | 
			
		||||
local expect = require("cc.expect")
 | 
			
		||||
local expect, range = expect.expect, expect.range
 | 
			
		||||
 | 
			
		||||
--[[- Wraps a block of text, so that each line fits within the given width.
 | 
			
		||||
 | 
			
		||||
This may be useful if you want to wrap text before displaying it to a
 | 
			
		||||
@{monitor} or @{printer} without using @{_G.print|print}.
 | 
			
		||||
[`monitor`] or [`printer`] without using [print][`_G.print`].
 | 
			
		||||
 | 
			
		||||
@tparam string text The string to wrap.
 | 
			
		||||
@tparam[opt] number width The width to constrain to, defaults to the width of
 | 
			
		||||
@@ -32,7 +33,7 @@ local function wrap(text, width)
 | 
			
		||||
    expect(1, text, "string")
 | 
			
		||||
    expect(2, width, "number", "nil")
 | 
			
		||||
    width = width or term.getSize()
 | 
			
		||||
 | 
			
		||||
    range(width, 1)
 | 
			
		||||
 | 
			
		||||
    local lines, lines_n, current_line = {}, 0, ""
 | 
			
		||||
    local function push_line()
 | 
			
		||||
@@ -109,7 +110,63 @@ local function ensure_width(line, width)
 | 
			
		||||
    return line
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
--[[- Split a string into parts, each separated by a deliminator.
 | 
			
		||||
 | 
			
		||||
For instance, splitting the string `"a b c"` with the deliminator `" "`, would
 | 
			
		||||
return a table with three strings: `"a"`, `"b"`, and `"c"`.
 | 
			
		||||
 | 
			
		||||
By default, the deliminator is given as a [Lua pattern][pattern]. Passing `true`
 | 
			
		||||
to the `plain` argument will cause the deliminator to be treated as a litteral
 | 
			
		||||
string.
 | 
			
		||||
 | 
			
		||||
[pattern]: https://www.lua.org/manual/5.3/manual.html#6.4.1
 | 
			
		||||
 | 
			
		||||
@tparam string str The string to split.
 | 
			
		||||
@tparam string deliminator The pattern to split this string on.
 | 
			
		||||
@tparam[opt=false] boolean plain Treat the deliminator as a plain string, rather than a pattern.
 | 
			
		||||
@tparam[opt] number limit The maximum number of elements in the returned list.
 | 
			
		||||
@treturn { string... } The list of split strings.
 | 
			
		||||
 | 
			
		||||
@usage Split a string into words.
 | 
			
		||||
 | 
			
		||||
    require "cc.strings".split("This is a sentence.", "%s+")
 | 
			
		||||
 | 
			
		||||
@usage Split a string by "-" into at most 3 elements.
 | 
			
		||||
 | 
			
		||||
    require "cc.strings".split("a-separated-string-of-sorts", "-", true, 3)
 | 
			
		||||
 | 
			
		||||
@see table.concat To join strings together.
 | 
			
		||||
 | 
			
		||||
@since 1.112.0
 | 
			
		||||
]]
 | 
			
		||||
local function split(str, deliminator, plain, limit)
 | 
			
		||||
    expect(1, str, "string")
 | 
			
		||||
    expect(2, deliminator, "string")
 | 
			
		||||
    expect(3, plain, "boolean", "nil")
 | 
			
		||||
    expect(4, limit, "number", "nil")
 | 
			
		||||
 | 
			
		||||
    local out, out_n, pos = {}, 0, 1
 | 
			
		||||
    while not limit or out_n < limit - 1 do
 | 
			
		||||
        local start, finish = str:find(deliminator, pos, plain)
 | 
			
		||||
        if not start then break end
 | 
			
		||||
 | 
			
		||||
        out_n = out_n + 1
 | 
			
		||||
        out[out_n] = str:sub(pos, start - 1)
 | 
			
		||||
 | 
			
		||||
        -- Require us to advance by at least one character.
 | 
			
		||||
        if finish < start then error("separator is empty", 2) end
 | 
			
		||||
 | 
			
		||||
        pos = finish + 1
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if pos == 1 then return { str } end
 | 
			
		||||
 | 
			
		||||
    out[out_n + 1] = str:sub(pos)
 | 
			
		||||
    return out
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
return {
 | 
			
		||||
    wrap = wrap,
 | 
			
		||||
    ensure_width = ensure_width,
 | 
			
		||||
    split = split,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,17 +6,17 @@
 | 
			
		||||
--
 | 
			
		||||
-- When multiple programs are running, it displays a tab bar at the top of the
 | 
			
		||||
-- screen, which allows you to switch between programs. New programs can be
 | 
			
		||||
-- launched using the `fg` or `bg` programs, or using the @{shell.openTab} and
 | 
			
		||||
-- @{multishell.launch} functions.
 | 
			
		||||
-- launched using the `fg` or `bg` programs, or using the [`shell.openTab`] and
 | 
			
		||||
-- [`multishell.launch`] functions.
 | 
			
		||||
--
 | 
			
		||||
-- Each process is identified by its ID, which corresponds to its position in
 | 
			
		||||
-- the tab list. As tabs may be opened and closed, this ID is _not_ constant
 | 
			
		||||
-- over a program's run. As such, be careful not to use stale IDs.
 | 
			
		||||
--
 | 
			
		||||
-- As with @{shell}, @{multishell} is not a "true" API. Instead, it is a
 | 
			
		||||
-- As with [`shell`], [`multishell`] is not a "true" API. Instead, it is a
 | 
			
		||||
-- standard program, which launches a shell and injects its API into the shell's
 | 
			
		||||
-- environment. This API is not available in the global environment, and so is
 | 
			
		||||
-- not available to @{os.loadAPI|APIs}.
 | 
			
		||||
-- not available to [APIs][`os.loadAPI`].
 | 
			
		||||
--
 | 
			
		||||
-- @module[module] multishell
 | 
			
		||||
-- @since 1.6
 | 
			
		||||
@@ -222,7 +222,7 @@ local multishell = {} --- @export
 | 
			
		||||
--- Get the currently visible process. This will be the one selected on
 | 
			
		||||
-- the tab bar.
 | 
			
		||||
--
 | 
			
		||||
-- Note, this is different to @{getCurrent}, which returns the process which is
 | 
			
		||||
-- Note, this is different to [`getCurrent`], which returns the process which is
 | 
			
		||||
-- currently executing.
 | 
			
		||||
--
 | 
			
		||||
-- @treturn number The currently visible process's index.
 | 
			
		||||
@@ -235,7 +235,7 @@ end
 | 
			
		||||
--
 | 
			
		||||
-- @tparam number n The process index to switch to.
 | 
			
		||||
-- @treturn boolean If the process was changed successfully. This will
 | 
			
		||||
-- return @{false} if there is no process with this id.
 | 
			
		||||
-- return [`false`] if there is no process with this id.
 | 
			
		||||
-- @see getFocus
 | 
			
		||||
function multishell.setFocus(n)
 | 
			
		||||
    expect(1, n, "number")
 | 
			
		||||
@@ -250,9 +250,9 @@ end
 | 
			
		||||
--- Get the title of the given tab.
 | 
			
		||||
--
 | 
			
		||||
-- This starts as the name of the program, but may be changed using
 | 
			
		||||
-- @{multishell.setTitle}.
 | 
			
		||||
-- [`multishell.setTitle`].
 | 
			
		||||
-- @tparam number n The process index.
 | 
			
		||||
-- @treturn string|nil The current process title, or @{nil} if the
 | 
			
		||||
-- @treturn string|nil The current process title, or [`nil`] if the
 | 
			
		||||
-- process doesn't exist.
 | 
			
		||||
function multishell.getTitle(n)
 | 
			
		||||
    expect(1, n, "number")
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,7 @@ for i = 1, #wrapped do
 | 
			
		||||
    term.write(wrapped[i])
 | 
			
		||||
end
 | 
			
		||||
os.pullEvent('key')
 | 
			
		||||
require "cc.internal.event".discard_char()
 | 
			
		||||
]]
 | 
			
		||||
 | 
			
		||||
-- Menus
 | 
			
		||||
 
 | 
			
		||||
@@ -275,6 +275,12 @@ local function drawCanvas()
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function termResize()
 | 
			
		||||
    w, h = term.getSize()
 | 
			
		||||
    drawCanvas()
 | 
			
		||||
    drawInterface()
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local menu_choices = {
 | 
			
		||||
    Save = function()
 | 
			
		||||
        if bReadOnly then
 | 
			
		||||
@@ -294,7 +300,7 @@ local menu_choices = {
 | 
			
		||||
        return false
 | 
			
		||||
    end,
 | 
			
		||||
    Exit = function()
 | 
			
		||||
        sleep(0) -- Super janky, but consumes stray "char" events from pressing Ctrl then E separately.
 | 
			
		||||
        require "cc.internal.event".discard_char() -- Consume stray "char" events from pressing Ctrl then E separately.
 | 
			
		||||
        return true
 | 
			
		||||
    end,
 | 
			
		||||
}
 | 
			
		||||
@@ -376,6 +382,8 @@ local function accessMenu()
 | 
			
		||||
                nMenuPosEnd = nMenuPosEnd + 1
 | 
			
		||||
                nMenuPosStart = nMenuPosEnd
 | 
			
		||||
            end
 | 
			
		||||
        elseif id == "term_resize" then
 | 
			
		||||
            termResize()
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
@@ -434,9 +442,7 @@ local function handleEvents()
 | 
			
		||||
                drawInterface()
 | 
			
		||||
            end
 | 
			
		||||
        elseif id == "term_resize" then
 | 
			
		||||
            w, h = term.getSize()
 | 
			
		||||
            drawCanvas()
 | 
			
		||||
            drawInterface()
 | 
			
		||||
            termResize()
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,7 @@ local items = {
 | 
			
		||||
    },
 | 
			
		||||
    ["some planks"] = {
 | 
			
		||||
        aliases = { "planks", "wooden planks", "wood planks" },
 | 
			
		||||
        material = true,
 | 
			
		||||
        desc = "You could easily craft these planks into sticks.",
 | 
			
		||||
    },
 | 
			
		||||
    ["some sticks"] = {
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user