From 9d18487dc5a352fe188eee192648909d42d466f8 Mon Sep 17 00:00:00 2001 From: Ivo Leal <62177040+IvoLeal72@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:24:47 +0100 Subject: [PATCH 01/10] Fixed usage example of textuils.pagedTabulate --- .../resources/data/computercraft/lua/rom/apis/textutils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua index e5bc459f5..2d5b6101d 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -259,7 +259,7 @@ input should the whole output not fit on the display. local rows = {} for i = 1, 30 do rows[i] = {("Row #%d"):format(i), math.random(1, 400)} end - textutils.tabulate(colors.orange, {"Column", "Value"}, colors.lightBlue, table.unpack(rows)) + textutils.pagedTabulate(colors.orange, {"Column", "Value"}, colors.lightBlue, table.unpack(rows)) ]] function pagedTabulate(...) return tabulateCommon(true, ...) From ba64e06ca7cc5c5ed58a424b2a9a5b22d8236d76 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Sep 2022 14:11:32 +0100 Subject: [PATCH 02/10] Use a Gradle plugin to download illuaminate Previously illumainate required manual users to manually download it and place it in ./bin/. This is both inconvenient for the user, and makes it hard to ensure people are running the "right" version. We now provide a small Gradle plugin which registers illuaminate as a ependency, downloading the appropriate (now versioned!) file. This also theoretically supports Macs, though I don't have access to one to test this. This enables the following changes: - The Lua lint script has been converted to a Gradle task (./gradle lintLua). - illuaminateDocs now uses a task definition with an explicit output directory. We can now consume this output as an input to another task, and get a task dependency implicitly. - Move the pre-commit config into the root of the tree. We can now use the default GitHub action to run our hooks. - Simplify CONTRIBUTING.md a little bit. Hopefully it's less intimidating now. --- .github/workflows/main-ci.yml | 27 +--- .github/workflows/make-doc.yml | 6 - .gitignore | 1 + .gitpod.yml | 2 +- .../config.yml => .pre-commit-config.yaml | 4 +- CONTRIBUTING.md | 41 +++--- build.gradle | 47 +++++-- buildSrc/build.gradle.kts | 18 +++ .../kotlin/cc/tweaked/gradle/ExecTasks.kt | 11 ++ .../kotlin/cc/tweaked/gradle/Illuaminate.kt | 121 ++++++++++++++++++ config/pre-commit/illuaminate-lint.sh | 16 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 12 files changed, 212 insertions(+), 84 deletions(-) rename config/pre-commit/config.yml => .pre-commit-config.yaml (94%) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/cc/tweaked/gradle/ExecTasks.kt create mode 100644 buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt delete mode 100755 config/pre-commit/illuaminate-lint.sh diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 616388d37..1f6477d52 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -15,7 +15,7 @@ jobs: with: java-version: 8 - - name: Cache gradle dependencies + - name: Cache Gradle dependencies uses: actions/cache@v2 with: path: ~/.gradle/caches @@ -32,7 +32,7 @@ jobs: run: | ./gradlew assemble || ./gradlew assemble ./gradlew downloadAssets || ./gradlew downloadAssets - xvfb-run ./gradlew build + ./gradlew build - name: Upload Jar uses: actions/upload-artifact@v2 @@ -40,31 +40,12 @@ jobs: name: CC-Tweaked path: build/libs - - name: Upload Screnshots - uses: actions/upload-artifact@v2 - with: - name: Screenshots - path: test-files/client/screenshots - if-no-files-found: ignore - retention-days: 5 - if: failure() - - - name: Upload Coverage + - name: Upload coverage uses: codecov/codecov-action@v2 - name: Parse test reports run: ./tools/parse-reports.py if: ${{ failure() }} - - name: Cache pre-commit - uses: actions/cache@v2 - with: - path: ~/.cache/pre-commit - key: ${{ runner.os }}-pre-commit-${{ hashFiles('config/pre-commit/config.yml') }} - restore-keys: | - ${{ runner.os }}-pre-commit- - - name: Run linters - run: | - pip install pre-commit - pre-commit run --config config/pre-commit/config.yml --show-diff-on-failure --all --color=always + uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/make-doc.yml b/.github/workflows/make-doc.yml index 4e53e42df..cff59218f 100644 --- a/.github/workflows/make-doc.yml +++ b/.github/workflows/make-doc.yml @@ -26,12 +26,6 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Setup illuaminate - run: | - test -d bin || mkdir bin - test -f bin/illuaminate || wget -q -Obin/illuaminate https://squiddev.cc/illuaminate/linux-x86-64/illuaminate - chmod +x bin/illuaminate - - name: Setup node run: npm ci diff --git a/.gitignore b/.gitignore index 808032e6d..8779a2d80 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /classes /logs /build +/buildSrc/build /out /doc/out/ /node_modules diff --git a/.gitpod.yml b/.gitpod.yml index c83ce9f6c..8fadff01b 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -17,6 +17,6 @@ vscode: tasks: - name: Setup pre-commit hool - init: pre-commit install --config config/pre-commit/config.yml --allow-missing-config + init: pre-commit install --allow-missing-config - name: Install npm packages init: npm ci diff --git a/config/pre-commit/config.yml b/.pre-commit-config.yaml similarity index 94% rename from config/pre-commit/config.yml rename to .pre-commit-config.yaml index e14e36ca8..f23c2af20 100644 --- a/config/pre-commit/config.yml +++ b/.pre-commit-config.yaml @@ -41,8 +41,8 @@ repos: - id: illuaminate name: Check Lua code files: ".*\\.(lua|java|md)" - language: script - entry: config/pre-commit/illuaminate-lint.sh + language: system + entry: ./gradlew lintLua -i pass_filenames: false require_serial: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bae4f7b4f..65ee93263 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,40 +39,30 @@ are run whenever you submit a PR, it's often useful to run this before committin - **[Checkstyle]:** Checks Java code to ensure it is consistently formatted. This can be run with `./gradlew build` or `./gradle check`. - - **[illuaminate]:** Checks Lua code for semantic and styleistic issues. See [the usage section][illuaminate-usage] for - how to download and run it. You may need to generate the Java documentation stubs (see "Documentation" below) for all - lints to pass. + - **[illuaminate]:** Checks Lua code for semantic and styleistic issues. This can be run with `./gradlew lintLua`. ### Documentation When writing documentation for [CC: Tweaked's documentation website][docs], it may be useful to build the documentation and preview it yourself before submitting a PR. -Building all documentation is, sadly, a multi-stage process (though this is largely hidden by Gradle). First we need to -convert Java doc-comments into Lua ones, we also generate some Javascript to embed. All of this is then finally fed into -illuaminate, which spits out our HTML. +Our documentation generation pipeline is rather complex, and involves invoking several external tools. Most of this +complexity is hidden by Gradle, but you will need to perform some initial setup: -#### Setting up the tooling -For various reasons, getting the environment set up to build documentation can be pretty complex. I'd quite like to -automate this via Docker and/or nix in the future, but this needs to be done manually for now. + - Install [Node/npm][node]. + - Run `npm ci` to install our Node dependencies. -This tooling is only needed if you need to build the whole website. If you just want to generate the Lua stubs, you can -skp this section. - - Install Node/npm and install our Node packages with `npm ci`. - - Install [illuaminate][illuaminate-usage] as described above. - -#### Building documentation -Gradle should be your entrypoint to building most documentation. There's two tasks which are of interest: - - - `./gradlew luaJavadoc` - Generate documentation stubs for Java methods. - - `./gradlew docWebsite` - Generate the whole website (including Javascript pages). The resulting HTML is stored at - `./build/docs/site/`. +You can now run `./gradlew docWebsite`. This generates documentation from our Lua and Java code, writing the resulting +HTML into `./build/docs/site`. #### Writing documentation illuaminate's documentation system is not currently documented (somewhat ironic), but is _largely_ the same as [ldoc][ldoc]. Documentation comments are written in Markdown, Our markdown engine does _not_ support GitHub flavoured markdown, and so does not support all the features one might -expect (such as tables). It is very much recommended that you build and preview the docs locally first. +expect. It is recommended that you build and preview the docs locally first. + +When iterating on documentation, you can get Gradle to rebuild the website every time a file changes by running +`./gradlew docWebsite -t`. This will take a couple of seconds to run, but definitely beats running it manually! ### Testing Thankfully running tests is much simpler than running the documentation generator! `./gradlew check` will run the @@ -90,11 +80,10 @@ Before we get into writing tests, it's worth mentioning the various test suites These tests are run by the '"Core" Java' test suite, and so are also run with `./gradlew test`. - - In-game (`./src/testMod/java/dan200/computercraft/ingame/`): These tests are run on an actual Minecraft server and client, - using [the same system Mojang do][mc-test]. The aim of these is to test in-game behaviour of blocks and peripherals. + - In-game (`./src/testMod/java/dan200/computercraft/ingame/`): These tests are run on an actual Minecraft server, using + the same system Mojang do][mc-test]. The aim of these is to test in-game behaviour of blocks and peripherals. - These are run by `./gradlew testClient` and `./gradlew testServer`. You may want to run the client under `xvfb-run` - or similar when running in a headless environment. + These tests are run with `./gradlew testServer`. ## CraftOS tests CraftOS's tests are written using a test system called "mcfly", heavily inspired by [busted] (and thus RSpec). Groups of @@ -107,9 +96,9 @@ asserts that your variable `foo` is equal to the expected value `"bar"`. [community]: README.md#Community "Get in touch with the community." [checkstyle]: https://checkstyle.org/ [illuaminate]: https://github.com/SquidDev/illuaminate/ "Illuaminate on GitHub" -[illuaminate-usage]: https://github.com/SquidDev/illuaminate/blob/master/README.md#usage "Installing Illuaminate" [weblate]: https://i18n.tweaked.cc/projects/cc-tweaked/minecraft/ "CC: Tweaked weblate instance" [docs]: https://tweaked.cc/ "CC: Tweaked documentation" [ldoc]: http://stevedonovan.github.io/ldoc/ "ldoc, a Lua documentation generator." [mc-test]: https://www.youtube.com/watch?v=vXaWOJTCYNg [busted]: https://github.com/Olivine-Labs/busted "busted: Elegant Lua unit testing." +[node]: https://nodejs.org/en/ "Node.js" diff --git a/build.gradle b/build.gradle index 896a2af2a..a680abdb1 100644 --- a/build.gradle +++ b/build.gradle @@ -11,9 +11,12 @@ plugins { id "org.spongepowered.mixin" version "0.7.+" id "org.parchmentmc.librarian.forgegradle" version "1.+" id "com.github.johnrengelman.shadow" version "7.1.2" + id "cc-tweaked.illuaminate" } import org.apache.tools.ant.taskdefs.condition.Os +import cc.tweaked.gradle.IlluaminateExec +import cc.tweaked.gradle.IlluaminateExecToDir version = mod_version @@ -152,6 +155,10 @@ dependencies { cctJavadoc 'cc.tweaked:cct-javadoc:1.4.7' } +illuaminate { + version.set("0.1.0-3-g0f40379") +} + // Compile tasks javadoc { @@ -302,32 +309,36 @@ def rollup = tasks.register("rollup", Exec.class) { commandLine mkCommand('"node_modules/.bin/rollup" --config rollup.config.js') } -def illuaminateDocs = tasks.register("illuaminateDocs", Exec.class) { +def illuaminateDocs = tasks.register("illuaminateDocs", IlluaminateExecToDir.class) { group = "documentation" description = "Generates docs using Illuaminate" - dependsOn(rollup, luaJavadoc) + dependsOn(rollup) - inputs.files(fileTree("doc")).withPropertyName("docs") - inputs.files(fileTree("src/main/resources/data/computercraft/lua/rom")).withPropertyName("lua rom") + // Config files inputs.file("illuaminate.sexp").withPropertyName("illuaminate.sexp") - inputs.dir("$buildDir/docs/luaJavadoc") + // Sources + inputs.files(fileTree("doc")).withPropertyName("docs") + inputs.files(fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom") + inputs.files(luaJavadoc) + // Additional assets inputs.file("$buildDir/rollup/index.js").withPropertyName("scripts") inputs.file("src/web/styles.css").withPropertyName("styles") - outputs.dir("$buildDir/docs/lua") - commandLine mkCommand('"bin/illuaminate" doc-gen') + // Output directory. Also defined in illuaminate.sexp and transform.tsx + output.set(new File(buildDir, "docs/lua")) + + args = ["doc-gen"] } def jsxDocs = tasks.register("jsxDocs", Exec) { group = "documentation" description = "Post-processes documentation to statically render some dynamic content." - dependsOn(illuaminateDocs) inputs.files(fileTree("src/web")).withPropertyName("sources") inputs.file("src/generated/export/index.json").withPropertyName("export") inputs.file("package-lock.json").withPropertyName("package-lock.json") inputs.file("tsconfig.json").withPropertyName("Typescript config") - inputs.files(fileTree("$buildDir/docs/lua")) + inputs.files(illuaminateDocs) outputs.dir("$buildDir/docs/site") commandLine mkCommand('"node_modules/.bin/ts-node" -T --esm src/web/transform.tsx') @@ -391,6 +402,24 @@ license { check.dependsOn("licenseCheck") +def lintLua = tasks.register("lintLua", IlluaminateExec.class) { + group = JavaBasePlugin.VERIFICATION_GROUP + description = "Lint Lua (and Lua docs) with illuaminate" + + // Config files + inputs.file("illuaminate.sexp").withPropertyName("illuaminate.sexp") + // Sources + inputs.files(fileTree("doc")).withPropertyName("docs") + inputs.files(fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom") + inputs.files(luaJavadoc) + + args = ["lint"] + + doFirst { if (System.getenv("GITHUB_ACTIONS") != null) println("::add-matcher::.github/matchers/illuaminate.json") } + doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") } +} + + def setupServer = tasks.register("setupServer", Copy.class) { group "test server" description "Sets up the environment for the test server." diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 000000000..fc582ed5b --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `java-gradle-plugin` + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +gradlePlugin { + plugins { + register("cc-tweaked.illuaminate") { + id = "cc-tweaked.illuaminate" + implementationClass = "cc.tweaked.gradle.IlluaminatePlugin" + } + } +} diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/ExecTasks.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/ExecTasks.kt new file mode 100644 index 000000000..10cf5af9a --- /dev/null +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/ExecTasks.kt @@ -0,0 +1,11 @@ +package cc.tweaked.gradle + +import org.gradle.api.provider.Property +import org.gradle.api.tasks.AbstractExecTask +import org.gradle.api.tasks.OutputDirectory +import java.io.File + +abstract class ExecToDir : AbstractExecTask(ExecToDir::class.java) { + @get:OutputDirectory + abstract val output: Property +} diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt new file mode 100644 index 000000000..5ccc701a7 --- /dev/null +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/Illuaminate.kt @@ -0,0 +1,121 @@ +package cc.tweaked.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.artifacts.Dependency +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider +import org.gradle.api.tasks.AbstractExecTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class IlluaminateExtension { + /** The version of illuaminate to use. */ + abstract val version: Property + + /** The path to illuaminate. If not given, illuaminate will be downloaded automatically. */ + abstract val file: Property +} + +class IlluaminatePlugin : Plugin { + override fun apply(project: Project) { + val extension = project.extensions.create("illuaminate", IlluaminateExtension::class.java) + extension.file.convention(setupDependency(project, extension.version)) + + project.tasks.register(SetupIlluaminate.NAME, SetupIlluaminate::class.java) { + file.set(extension.file.map { it.absolutePath }) + } + } + + /** Set up a repository for illuaminate and download our binary from it. */ + private fun setupDependency(project: Project, version: Provider): Provider { + project.repositories.ivy { + name = "Illuaminate" + setUrl("https://squiddev.cc/illuaminate/bin/") + patternLayout { + artifact("[revision]/[artifact]-[ext]") + } + metadataSources { + artifact() + } + content { + includeModule("cc.squiddev", "illuaminate") + } + } + + return version.map { + val dep = illuaminateArtifact(project, it) + val configuration = project.configurations.detachedConfiguration(dep) + configuration.isTransitive = false + configuration.resolve().single() + } + } + + /** Define a dependency for illuaminate from a version number and the current operating system. */ + private fun illuaminateArtifact(project: Project, version: String): Dependency { + val osName = System.getProperty("os.name").toLowerCase() + val (os, suffix) = when { + osName.contains("windows") -> Pair("windows", ".exe") + osName.contains("mac os") || osName.contains("darwin") -> Pair("macos", "") + osName.contains("linux") -> Pair("linux", "") + else -> error("Unsupported OS $osName for illuaminate") + } + + val osArch = System.getProperty("os.arch").toLowerCase() + val arch = when { + osArch == "arm" || osArch.startsWith("aarch") -> error("Unsupported architecture '$osArch' for illuaminate") + osArch.contains("64") -> "x86_64" + else -> error("Unsupported architecture $osArch for illuaminate") + } + + return project.dependencies.create( + mapOf( + "group" to "cc.squiddev", + "name" to "illuaminate", + "version" to version, + "ext" to "$os-$arch$suffix", + ), + ) + } +} + +private val Task.illuaminatePath: String? // "?" needed to avoid overload ambiguity in setExecutable below. + get() = project.extensions.getByType(IlluaminateExtension::class.java).file.get().absolutePath + +/** Prepares illuaminate for being run. This simply requests the dependency and then marks it as executable. */ +abstract class SetupIlluaminate : DefaultTask() { + @get:Input + abstract val file: Property + + @TaskAction + fun setExecutable() { + val file = File(this.file.get()) + if (file.canExecute()) { + didWork = false + return + } + + file.setExecutable(true) + } + + companion object { + const val NAME: String = "setupIlluaminate" + } +} + +abstract class IlluaminateExec : AbstractExecTask(IlluaminateExec::class.java) { + init { + dependsOn(SetupIlluaminate.NAME) + executable = illuaminatePath + } +} + +abstract class IlluaminateExecToDir : ExecToDir() { + init { + dependsOn(SetupIlluaminate.NAME) + executable = illuaminatePath + } +} diff --git a/config/pre-commit/illuaminate-lint.sh b/config/pre-commit/illuaminate-lint.sh deleted file mode 100755 index ac7b762bf..000000000 --- a/config/pre-commit/illuaminate-lint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env sh -set -e - -test -d bin || mkdir bin -test -f bin/illuaminate || curl -s -obin/illuaminate https://squiddev.cc/illuaminate/linux-x86-64/illuaminate -chmod +x bin/illuaminate - -if [ -n ${GITHUB_ACTIONS+x} ]; then - # Register a problem matcher (see https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md) - # for illuaminate. - echo "::add-matcher::.github/matchers/illuaminate.json" - trap 'echo "::remove-matcher owner=illuaminate::"' EXIT -fi - -./gradlew luaJavadoc -bin/illuaminate lint diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fcea..ae04661ee 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d22e138413bfefc692b5f43dafd2153a07f5286b Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Sep 2022 14:57:31 +0100 Subject: [PATCH 03/10] Fix numerous off-by-one errors in help program We clamped various values to the height of the screen, rather than the height of the content box (height-1). We didn't notice this most of the time as the last line of a file is empty - it only really mattered when a file was the same height as the computer's screen. We now do the following: - Strip the trailing new line from a file when reading. - Replace most usages of height with height-1. --- .../computercraft/lua/rom/programs/help.lua | 22 ++++++----- .../test-rom/spec/programs/help_spec.lua | 38 +++++++++++++++++++ .../resources/test-rom/spec/test_helpers.lua | 15 ++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/help.lua b/src/main/resources/data/computercraft/lua/rom/programs/help.lua index 22c70f83e..14fd9d261 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/help.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/help.lua @@ -146,14 +146,17 @@ end local contents = file:read("*a") file:close() +-- Trim trailing newlines from the file to avoid displaying a blank line. +if contents:sub(-1) == "\n" then contents:sub(1, -2) end local word_wrap = sFile:sub(-3) == ".md" and word_wrap_markdown or word_wrap_basic local width, height = term.getSize() +local content_height = height - 1 -- Height of the content box. local lines, fg, bg, sections = word_wrap(contents, width) local print_height = #lines -- If we fit within the screen, just display without pagination. -if print_height <= height then +if print_height <= content_height then local _, y = term.getCursorPos() for i = 1, print_height do if y + i - 1 > height then @@ -201,7 +204,7 @@ end local function draw() - for y = 1, height - 1 do + for y = 1, content_height do term.setCursorPos(1, y) if y + offset > print_height then -- Should only happen if we resize the terminal to a larger one @@ -228,14 +231,14 @@ while true do if param == keys.up and offset > 0 then offset = offset - 1 draw() - elseif param == keys.down and offset < print_height - height then + elseif param == keys.down and offset < print_height - content_height then offset = offset + 1 draw() elseif param == keys.pageUp and offset > 0 then - offset = math.max(offset - height + 2, 0) + offset = math.max(offset - content_height + 1, 0) draw() - elseif param == keys.pageDown and offset < print_height - height then - offset = math.min(offset + height - 2, print_height - height) + elseif param == keys.pageDown and offset < print_height - content_height then + offset = math.min(offset + content_height - 1, print_height - content_height) draw() elseif param == keys.home then offset = 0 @@ -247,7 +250,7 @@ while true do offset = sections[current_section + 1].offset draw() elseif param == keys["end"] then - offset = print_height - height + offset = print_height - content_height draw() elseif param == keys.q then sleep(0) -- Super janky, but consumes stray "char" events. @@ -257,7 +260,7 @@ while true do if param < 0 and offset > 0 then offset = offset - 1 draw() - elseif param > 0 and offset < print_height - height then + elseif param > 0 and offset <= print_height - content_height then offset = offset + 1 draw() end @@ -270,7 +273,8 @@ while true do end width, height = new_width, new_height - offset = math.max(math.min(offset, print_height - height), 0) + content_height = height - 1 + offset = math.max(math.min(offset, print_height - content_height), 0) draw() draw_menu() elseif event == "terminate" then diff --git a/src/test/resources/test-rom/spec/programs/help_spec.lua b/src/test/resources/test-rom/spec/programs/help_spec.lua index 35ce021b4..9ce15a102 100644 --- a/src/test/resources/test-rom/spec/programs/help_spec.lua +++ b/src/test/resources/test-rom/spec/programs/help_spec.lua @@ -1,8 +1,46 @@ local capture = require "test_helpers".capture_program +local with_window_lines = require "test_helpers".with_window_lines describe("The help program", function() + local function stub_help(content) + local name = "/help_file.txt" + io.open(name, "wb"):write(content):close() + stub(help, "lookup", function() return name end) + end + + local function capture_help(width, height, content) + stub_help(content) + + local co = coroutine.create(shell.run) + local window = with_window_lines(width, height, function() + local ok, err = coroutine.resume(co, "help topic") + if not ok then error(err, 0) end + end) + return coroutine.status(co) == "dead", window + end + it("errors when there is no such help file", function() expect(capture(stub, "help nothing")) :matches { ok = true, error = "No help available\n", output = "" } end) + + it("prints a short file directly", function() + local dead, output = capture_help(10, 3, "a short\nfile") + expect(dead):eq(true) + expect(output):same { + "a short ", + "file ", + " ", + } + end) + + it("launches the viewer for a longer file", function() + local dead, output = capture_help(10, 3, "a longer\nfile\nwith content") + expect(dead):eq(false) + expect(output):same { + "a longer ", + "file ", + "Help: topi", + } + end) end) diff --git a/src/test/resources/test-rom/spec/test_helpers.lua b/src/test/resources/test-rom/spec/test_helpers.lua index 5d1ba9f1c..d11a33b3d 100644 --- a/src/test/resources/test-rom/spec/test_helpers.lua +++ b/src/test/resources/test-rom/spec/test_helpers.lua @@ -56,7 +56,22 @@ local function with_window(width, height, fn) return redirect end +--- Run a function redirecting to a new window with the given dimensions, +-- returning the content of the window. +-- +-- @tparam number width The window's width +-- @tparam number height The window's height +-- @tparam function() fn The action to run +-- @treturn {string...} The content of the window. +local function with_window_lines(width, height, fn) + local window = with_window(width, height, fn) + local out = {} + for i = 1, height do out[i] = window.getLine(i) end + return out +end + return { capture_program = capture_program, with_window = with_window, + with_window_lines = with_window_lines, } From 61ac48c99f5a650c088ff752ce27f9079c64d4b9 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Sep 2022 14:57:45 +0100 Subject: [PATCH 04/10] Mention audio formats in speaker help Closes #1133. I'm not super happy about any of the versions proposed there, but I think this is better than nothing. Co-authored-by: JackMacWindows --- .../resources/data/computercraft/lua/rom/help/speaker.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/help/speaker.md b/src/main/resources/data/computercraft/lua/rom/help/speaker.md index 872f5dfc9..0ea3c2916 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/speaker.md +++ b/src/main/resources/data/computercraft/lua/rom/help/speaker.md @@ -1,5 +1,9 @@ The speaker program plays audio files using speakers attached to this computer. +It supports audio files in a limited number of formats: +* DFPWM: You can convert music to DFPWM with external tools like https://music.madefor.cc. +* WAV: WAV files must be 8-bit PCM or DFPWM, with exactly one channel and a sample rate of 48kHz. + ## Examples: -- `speaker play example.dfpwm left` plays the "example.dfpwm" audio file using the speaker on the left of the computer. -- `speaker stop` stops any currently playing audio. +* `speaker play example.dfpwm left` plays the "example.dfpwm" audio file using the speaker on the left of the computer. +* `speaker stop` stops any currently playing audio. From c3b7302108928fa6b9920c75c28121d67dea70b3 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sun, 11 Sep 2022 15:03:09 +0100 Subject: [PATCH 05/10] Remove some unused arguments in LuaDateTime See comments in #1157 --- .../computercraft/core/apis/LuaDateTime.java | 18 ++++++++---------- .../dan200/computercraft/core/apis/OSAPI.java | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java index ecb55da25..0e88b2243 100644 --- a/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java +++ b/src/main/java/dan200/computercraft/core/apis/LuaDateTime.java @@ -24,7 +24,7 @@ final class LuaDateTime { } - static void format( DateTimeFormatterBuilder formatter, String format, ZoneOffset offset ) throws LuaException + static void format( DateTimeFormatterBuilder formatter, String format ) throws LuaException { for( int i = 0; i < format.length(); ) { @@ -61,7 +61,7 @@ final class LuaDateTime formatter.appendText( ChronoField.MONTH_OF_YEAR, TextStyle.FULL ); break; case 'c': - format( formatter, "%a %b %e %H:%M:%S %Y", offset ); + format( formatter, "%a %b %e %H:%M:%S %Y" ); break; case 'C': formatter.appendValueReduced( CENTURY, 2, 2, 0 ); @@ -71,13 +71,13 @@ final class LuaDateTime break; case 'D': case 'x': - format( formatter, "%m/%d/%y", offset ); + format( formatter, "%m/%d/%y" ); break; case 'e': formatter.padNext( 2 ).appendValue( ChronoField.DAY_OF_MONTH ); break; case 'F': - format( formatter, "%Y-%m-%d", offset ); + format( formatter, "%Y-%m-%d" ); break; case 'g': formatter.appendValueReduced( IsoFields.WEEK_BASED_YEAR, 2, 2, 0 ); @@ -107,10 +107,10 @@ final class LuaDateTime formatter.appendText( ChronoField.AMPM_OF_DAY ); break; case 'r': - format( formatter, "%I:%M:%S %p", offset ); + format( formatter, "%I:%M:%S %p" ); break; case 'R': - format( formatter, "%H:%M", offset ); + format( formatter, "%H:%M" ); break; case 'S': formatter.appendValue( ChronoField.SECOND_OF_MINUTE, 2 ); @@ -120,7 +120,7 @@ final class LuaDateTime break; case 'T': case 'X': - format( formatter, "%H:%M:%S", offset ); + format( formatter, "%H:%M:%S" ); break; case 'u': formatter.appendValue( ChronoField.DAY_OF_WEEK ); @@ -212,15 +212,13 @@ final class LuaDateTime throw new LuaException( "field \"" + field + "\" missing in date table" ); } - private static final TemporalField CENTURY = map( ChronoField.YEAR, ValueRange.of( 0, 6 ), x -> (x / 100) % 100 ); + private static final TemporalField CENTURY = map( ChronoField.YEAR, ValueRange.of( 0, 99 ), x -> (x / 100) % 100 ); private static final TemporalField ZERO_WEEK = map( WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of( 0, 6 ), x -> x - 1 ); private static TemporalField map( TemporalField field, ValueRange range, LongUnaryOperator convert ) { return new TemporalField() { - private final ValueRange range = ValueRange.of( 0, 99 ); - @Override public TemporalUnit getBaseUnit() { diff --git a/src/main/java/dan200/computercraft/core/apis/OSAPI.java b/src/main/java/dan200/computercraft/core/apis/OSAPI.java index 7494c3b1d..9312b4c1d 100644 --- a/src/main/java/dan200/computercraft/core/apis/OSAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/OSAPI.java @@ -493,7 +493,7 @@ public class OSAPI implements ILuaAPI if( format.equals( "*t" ) ) return LuaDateTime.toTable( date, offset, instant ); DateTimeFormatterBuilder formatter = new DateTimeFormatterBuilder(); - LuaDateTime.format( formatter, format, offset ); + LuaDateTime.format( formatter, format ); return formatter.toFormatter( Locale.ROOT ).format( date ); } From 0b7fbcde5373914603810a6d2f706d2933a9c865 Mon Sep 17 00:00:00 2001 From: roland-a <91647122+roland-a@users.noreply.github.com> Date: Thu, 29 Sep 2022 13:49:02 -0400 Subject: [PATCH 06/10] Send block updates to client when the turtle moves #1167 (#1170) Fixes #1167 --- .../dan200/computercraft/shared/turtle/core/TurtleBrain.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java index 2a3887b8a..ba336e923 100644 --- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java +++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java @@ -323,8 +323,9 @@ public class TurtleBrain implements ITurtleAccess try { - // Create a new turtle - if( world.setBlock( pos, newState, 0 ) ) + // We use Block.UPDATE_CLIENTS here to ensure that neighbour updates caused in Block.updateNeighbourShapes + // are sent to the client. We want to avoid doing a full block update until the turtle state is copied over. + if( world.setBlock( pos, newState, 2 ) ) { Block block = world.getBlockState( pos ).getBlock(); if( block == oldBlock.getBlock() ) From e7533f235351f1cb11c13fb9a416f74c22c0a963 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 29 Sep 2022 22:01:51 +0100 Subject: [PATCH 07/10] Improve community links a little --- .github/ISSUE_TEMPLATE/config.yml | 5 +---- README.md | 8 +++++--- doc/guides/speaker_audio.md | 7 +++---- doc/index.md | 7 +++---- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index fc234c84a..222495fd2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,5 @@ blank_issues_enabled: false contact_links: -- name: ComputerCraft Discord - url: https://discord.computercraft.cc - about: Get help on the ComputerCraft Discord. - name: GitHub Discussions url: https://github.com/cc-tweaked/CC-Tweaked/discussions - about: Or ask questions on GitHub Discussions. + about: Ask questions on GitHub Discussions. diff --git a/README.md b/README.md index f03d91cf8..f05ab4cf3 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,8 @@ developing the mod, [check out the instructions here](CONTRIBUTING.md#developing ## Community If you need help getting started with CC: Tweaked, want to show off your latest project, or just want to chat about -ComputerCraft we have a [forum](https://forums.computercraft.cc/) and [Discord guild](https://discord.computercraft.cc)! -There's also a fairly populated, albeit quiet [IRC channel](http://webchat.esper.net/?channels=computercraft), if that's -more your cup of tea. +ComputerCraft, do check out our [forum] and [GitHub discussions page][GitHub discussions]! There's also a fairly +populated, albeit quiet [IRC channel][irc], if that's more your cup of tea. We also host fairly comprehensive documentation at [tweaked.cc](https://tweaked.cc/ "The CC: Tweaked website"). @@ -52,3 +51,6 @@ the generated documentation [can be browsed online](https://tweaked.cc/javadoc/) [modrinth]: https://modrinth.com/mod/gu7yAYhd "Download CC: Tweaked from Modrinth" [forge]: https://files.minecraftforge.net/ "Download Minecraft Forge." [ccrestitched]: https://www.curseforge.com/minecraft/mc-mods/cc-restitched "Download CC: Restitched from CurseForge" +[forum]: https://forums.computercraft.cc/ +[GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions +[IRC]: http://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet" diff --git a/doc/guides/speaker_audio.md b/doc/guides/speaker_audio.md index 4cbe9e437..25d772dd2 100644 --- a/doc/guides/speaker_audio.md +++ b/doc/guides/speaker_audio.md @@ -185,7 +185,7 @@ end :::note Confused? Don't worry if you don't understand this example. It's quite advanced, and does use some ideas that this guide doesn't -cover. That said, don't be afraid to ask on [Discord] or [IRC] either! +cover. That said, don't be afraid to ask on [GitHub Discussions] or [IRC] either! ::: It's worth noting that the examples of audio processing we've mentioned here are about manipulating the _amplitude_ of @@ -200,6 +200,5 @@ This is, I'm afraid, left as an exercise to the reader. [PCM]: https://en.wikipedia.org/wiki/Pulse-code_modulation "Pulse-code Modulation - Wikipedia" [Ring Buffer]: https://en.wikipedia.org/wiki/Circular_buffer "Circular buffer - Wikipedia" [Sine Wave]: https://en.wikipedia.org/wiki/Sine_wave "Sine wave - Wikipedia" - -[Discord]: https://discord.computercraft.cc "The Minecraft Computer Mods Discord" -[IRC]: http://webchat.esper.net/?channels=computercraft "IRC webchat on EsperNet" +[GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions +[IRC]: http://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet" diff --git a/doc/index.md b/doc/index.md index 74d76434b..697fc9421 100644 --- a/doc/index.md +++ b/doc/index.md @@ -37,8 +37,7 @@ little daunting getting started. Thankfully, there's several fantastic tutorials Once you're a little more familiar with the mod, the sidebar and links below provide more detailed documentation on the various APIs and peripherals provided by the mod. -If you get stuck, do pop in to the [Minecraft Computer Mod Discord guild][discord] or ComputerCraft's -[IRC channel][irc]. +If you get stuck, do [ask a question on GitHub][GitHub Discussions] or pop in to the ComputerCraft's [IRC channel][IRC]. ## Get Involved CC: Tweaked lives on [GitHub]. If you've got any ideas, feedback or bugs please do [create an issue][bug]. @@ -51,5 +50,5 @@ CC: Tweaked lives on [GitHub]. If you've got any ideas, feedback or bugs please [forge]: https://files.minecraftforge.net/ "Download Minecraft Forge." [ccrestitched]: https://www.curseforge.com/minecraft/mc-mods/cc-restitched "Download CC: Restitched from CurseForge" [lua]: https://www.lua.org/ "Lua's main website" -[discord]: https://discord.computercraft.cc "The Minecraft Computer Mods Discord" -[irc]: http://webchat.esper.net/?channels=computercraft "IRC webchat on EsperNet" +[GitHub Discussions]: https://github.com/cc-tweaked/CC-Tweaked/discussions +[IRC]: http://webchat.esper.net/?channels=computercraft "#computercraft on EsperNet" From da5956e943e4af9af168ac1c917bca1b84621002 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Thu, 29 Sep 2022 22:21:38 +0100 Subject: [PATCH 08/10] Make the sidebar a little wider I was going to do something productive tonight, but then this happened. Whatever, I'm retired, I'm allowed to make my entire existence just adding 50px to things. Heck, maybe I'll do the same tomorrow too. --- doc/stub/global.lua | 2 +- .../data/computercraft/lua/rom/apis/textutils.lua | 12 ++++++------ src/web/styles.css | 3 +++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/doc/stub/global.lua b/doc/stub/global.lua index 7bd8a7311..c01fb3554 100644 --- a/doc/stub/global.lua +++ b/doc/stub/global.lua @@ -14,7 +14,7 @@ thread, not the whole program. :::tip Because sleep internally uses timers, it is a function that yields. This means -that you can use it to prevent "Too long without yielding" errors, however, as +that you can use it to prevent "Too long without yielding" errors. However, as the minimum sleep time is 0.05 seconds, it will slow your program down. ::: diff --git a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua index 2d5b6101d..6daab3f8b 100644 --- a/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua +++ b/src/main/resources/data/computercraft/lua/rom/apis/textutils.lua @@ -749,9 +749,9 @@ suitable for pretty printing. @usage Demonstrates some of the other options local tbl = { 1, 2, 3 } - print(textutils.serialize({ tbl, tbl }, { allow_repetitions = true })) + print(textutils.serialise({ tbl, tbl }, { allow_repetitions = true })) - print(textutils.serialize(tbl, { compact = true })) + print(textutils.serialise(tbl, { compact = true })) ]] function serialize(t, opts) local tTracking = {} @@ -770,7 +770,7 @@ serialise = serialize -- GB version --- Converts a serialised string back into a reassembled Lua object. -- --- This is mainly used together with @{textutils.serialize}. +-- This is mainly used together with @{textutils.serialise}. -- -- @tparam string s The serialised string to deserialise. -- @return[1] The deserialised object @@ -807,10 +807,10 @@ unserialise = unserialize -- GB version -- @throws If the object contains a value which cannot be -- serialised. This includes functions and tables which appear multiple -- times. --- @usage textutils.serializeJSON({ values = { 1, "2", true } }) +-- @usage textutils.serialiseJSON({ values = { 1, "2", true } }) -- @since 1.7 --- @see textutils.json_null Use to serialize a JSON `null` value. --- @see textutils.empty_json_array Use to serialize a JSON empty array. +-- @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) expect(1, t, "table", "string", "number", "boolean") expect(2, bNBTStyle, "boolean", "nil") diff --git a/src/web/styles.css b/src/web/styles.css index 5ed6964aa..f0901b31c 100644 --- a/src/web/styles.css +++ b/src/web/styles.css @@ -1,3 +1,6 @@ +:root { + --nav-width: 250px; +} /* Some misc styles */ .big-image { From 371f931140f5603da9180e5047d2995724171764 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 30 Sep 2022 10:00:07 +0100 Subject: [PATCH 09/10] Always add HTTP programs to the path (#1172) --- .../data/computercraft/lua/rom/programs/http/pastebin.lua | 4 ++-- .../data/computercraft/lua/rom/programs/http/wget.lua | 4 ++-- src/main/resources/data/computercraft/lua/rom/startup.lua | 5 +---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua b/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua index 807ef603f..f1335ef97 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/http/pastebin.lua @@ -13,8 +13,8 @@ if #tArgs < 2 then end if not http then - printError("Pastebin requires the http API") - printError("Set http.enabled to true in CC: Tweaked's config") + printError("Pastebin requires the http API, but it is not enabled") + printError("Set http.enabled to true in CC: Tweaked's server config") return end diff --git a/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua b/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua index 042660c02..90e566337 100644 --- a/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua +++ b/src/main/resources/data/computercraft/lua/rom/programs/http/wget.lua @@ -21,8 +21,8 @@ end local url = table.remove(tArgs, 1) if not http then - printError("wget requires the http API") - printError("Set http.enabled to true in CC: Tweaked's config") + printError("wget requires the http API, but it is not enabled") + printError("Set http.enabled to true in CC: Tweaked's server config") return end diff --git a/src/main/resources/data/computercraft/lua/rom/startup.lua b/src/main/resources/data/computercraft/lua/rom/startup.lua index e1200de55..99d2e5869 100644 --- a/src/main/resources/data/computercraft/lua/rom/startup.lua +++ b/src/main/resources/data/computercraft/lua/rom/startup.lua @@ -1,7 +1,7 @@ local completion = require "cc.shell.completion" -- Setup paths -local sPath = ".:/rom/programs" +local sPath = ".:/rom/programs:/rom/programs/http" if term.isColor() then sPath = sPath .. ":/rom/programs/advanced" end @@ -19,9 +19,6 @@ end if commands then sPath = sPath .. ":/rom/programs/command" end -if http then - sPath = sPath .. ":/rom/programs/http" -end shell.setPath(sPath) help.setPath("/rom/help") From 5be290a1e2eabd28f4e2632034b9dba18ff1d1c6 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Sat, 1 Oct 2022 12:32:39 +0100 Subject: [PATCH 10/10] Bump version to 1.100.10 One more version and then it's a palendrome! Sort of. --- gradle.properties | 2 +- .../data/computercraft/lua/rom/help/changelog.md | 10 ++++++++++ .../data/computercraft/lua/rom/help/whatsnew.md | 16 ++++++---------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/gradle.properties b/gradle.properties index 446a6c1b7..f25c2da25 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx3G kotlin.stdlib.default.dependency=false # Mod properties -mod_version=1.100.9 +mod_version=1.100.10 # Minecraft properties (update mods.toml when changing) mc_version=1.16.5 diff --git a/src/main/resources/data/computercraft/lua/rom/help/changelog.md b/src/main/resources/data/computercraft/lua/rom/help/changelog.md index f628c5b1f..91b688894 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/changelog.md +++ b/src/main/resources/data/computercraft/lua/rom/help/changelog.md @@ -1,3 +1,13 @@ +# New features in CC: Tweaked 1.100.10 + +* Mention WAV support in speaker help (MCJack123). +* Add http programs to the path, even when http is not enabled. + +Several bug fixes: +* Fix example in textutils.pagedTabulate docs (IvoLeal72). +* Fix help program treating the terminal one line longer than it was. +* Send block updates to client when turtle moves (roland-a). + # New features in CC: Tweaked 1.100.9 * Add documentation for setting up GPS (Lupus590). diff --git a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md index 0a6ac143a..19c5d921f 100644 --- a/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md +++ b/src/main/resources/data/computercraft/lua/rom/help/whatsnew.md @@ -1,15 +1,11 @@ -New features in CC: Tweaked 1.100.9 +New features in CC: Tweaked 1.100.10 -* Add documentation for setting up GPS (Lupus590). -* Add WAV support to the `speaker` program (MCJack123). -* Expose item groups in `getItemDetail` (itisluiz). -* Other fixes to documentation (Erb3, JohnnyIrvin). -* Add Norwegian translation (Erb3). +* Mention WAV support in speaker help (MCJack123). +* Add http programs to the path, even when http is not enabled. Several bug fixes: -* Fix z-fighting on bold printout borders (toad-dev). -* Fix `term.blit` failing on certain strings. -* Fix `getItemLimit()` using the wrong slot (heap-underflow). -* Increase size of monitor depth blocker. +* Fix example in textutils.pagedTabulate docs (IvoLeal72). +* Fix help program treating the terminal one line longer than it was. +* Send block updates to client when turtle moves (roland-a). Type "help changelog" to see the full version history.