1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-11-03 23:22:59 +00:00

Compare commits

..

11 Commits

Author SHA1 Message Date
Jonathan Coates
fc66d15012 Merge pull request #2189 from Fox2Code/1.4.7-concurrent-fix
Fix ConcurrentModificationException caused by timers
2025-04-30 19:21:17 +01:00
Fox2Code
00e57227dc Fix ConcurrentModificationException caused by timers 2025-04-29 21:14:14 +02:00
Jonathan Coates
22c094192b Update to CC:T 1.115.0
- Sync Lua files
 - Backport our Netty HTTP library.
2025-04-06 13:44:48 +01:00
Jonathan Coates
fbf64a0404 Pull in the remainder of CC:T 1.109.6 2024-03-06 11:10:55 +00:00
Jonathan Coates
9f251d7b52 Update Cobalt to use Lua 5.2 2024-02-28 10:16:48 +00:00
Jonathan Coates
58d54e2e70 Update to latest CC:T rom
More or less - we're not updating to Lua 5.2/binary-only handles, so
have skipped some changes.
2024-02-28 09:34:16 +00:00
Jonathan Coates
e62f2630b5 Removes calls to isWireless
Wired modems don't exist on 1.4.7, so there's no method to distinguish
between wireless and wired modems.

Fixes #1733
2024-02-28 09:28:17 +00:00
Jonathan Coates
136fbd2589 Pull in CC:T 1.108.1, some other fixes
- Use Cobalt's new Java patcher, allowing us to use Java 17 syntax. As
   such, update a couple classes to make use of that.
 - Pull in latest ROM. This is very noisy (due to the link syntax
   changes), but mostly trivial changes.
 - Fix wget and pastebin programs using http methods which don't exist.
 - Make fs.open create the parent directory when opening for write, much
   like newer CC versions.
2023-10-04 18:56:11 +01:00
Jonathan Coates
35e227ed02 Update http wrappers to match CC 1.5
It might be fun to switch to CC:T's HTTP API, but for now let's just get
something working.

Fixes #1587.
2023-09-13 22:08:08 +01:00
Jonathan Coates
687a29de95 Bump CC:T to 1.105.1 2023-06-13 22:55:11 +01:00
Jonathan Coates
5426d880f0 Fix a couple of back-compatibility issues
- Add back red{set,probe,pulse}. Remove usage of analogue functions, as
   these don't exist on older versions (and is gonna be really hard to
   patch back in).

 - Remove turtle equip methods, as these are redundant.

 - Add turtle.{getSelectedSlot, getFuelLimit}. The latter is kinda
   useless (fuel limits come later!), but useful for compatibility.
2023-06-13 20:04:13 +01:00
114 changed files with 5810 additions and 1728 deletions

View File

@@ -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
View 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.

View File

@@ -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")
}

View File

@@ -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"
}
}

View File

@@ -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) }
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}

View File

@@ -4,6 +4,6 @@
org.gradle.jvmargs=-Xmx3G
modVersion=1.105.0
modVersion=1.115.1
mcVersion=1.4.7

View File

@@ -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" }

View File

@@ -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

View File

@@ -38,5 +38,3 @@ val mcVersion: String by settings
rootProject.name = "cc-tweaked-$mcVersion"
includeBuild("vendor/Cobalt")
include("build-tools")

View File

@@ -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(

View 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";
}
}

View File

@@ -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) {
}

View File

@@ -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;
}
}

View File

@@ -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;

View 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");
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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/&#42;/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.
*

View File

@@ -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) {

View 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();
}
}

View File

@@ -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);
}
}
/**

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}
}

View 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}

View File

@@ -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) {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)
);
}
}

View File

@@ -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");
}
}

View 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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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();
}
}

View File

@@ -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",
})

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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()

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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 }

View File

@@ -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")

View File

@@ -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 }

View File

@@ -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,
}

View File

@@ -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
]]

View File

@@ -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
--------------------------------------------------------------------------------

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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")

View File

@@ -88,6 +88,7 @@ for i = 1, #wrapped do
term.write(wrapped[i])
end
os.pullEvent('key')
require "cc.internal.event".discard_char()
]]
-- Menus

View File

@@ -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

View File

@@ -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