mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-15 14:07:38 +00:00
Compare commits
55 Commits
feature/li
...
mc-1.19.x
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cdc8592aa3 | ||
![]() |
53546b9f57 | ||
![]() |
6dfdeb9321 | ||
![]() |
500406f9eb | ||
![]() |
b3738a7a63 | ||
![]() |
5f8b1dd67f | ||
![]() |
053751b190 | ||
![]() |
52b78f92cd | ||
![]() |
2055052a57 | ||
![]() |
12ee47ff19 | ||
![]() |
25776abf61 | ||
![]() |
f7411b40a2 | ||
![]() |
8c8924f54e | ||
![]() |
c1628d077a | ||
![]() |
5b2fdec6ca | ||
![]() |
5a7259e4c9 | ||
![]() |
92b335f45f | ||
![]() |
b93ea9c62e | ||
![]() |
b9edd7c7f6 | ||
![]() |
84a761ddd5 | ||
![]() |
2a04fb71fd | ||
![]() |
df61389304 | ||
![]() |
b6632c9ed9 | ||
![]() |
41b6711b38 | ||
![]() |
02f0b7ec14 | ||
![]() |
a9d31cd3c6 | ||
![]() |
43744b0e85 | ||
![]() |
55510c42db | ||
![]() |
57a944fd90 | ||
![]() |
940f59b116 | ||
![]() |
dd08d1ec8e | ||
![]() |
90ed0b24e7 | ||
![]() |
0ad399a528 | ||
![]() |
1b88213eca | ||
![]() |
eef05b9854 | ||
![]() |
24d74f5c80 | ||
![]() |
c2988366d8 | ||
![]() |
ec0765ead1 | ||
![]() |
b94e34f372 | ||
![]() |
af3263dec2 | ||
![]() |
7f25c9a66b | ||
![]() |
9ca3efff3c | ||
![]() |
aaf8c248a8 | ||
![]() |
df26cd267a | ||
![]() |
8914b78816 | ||
![]() |
9ea7f45fa7 | ||
![]() |
d351bc33c6 | ||
![]() |
5d71770931 | ||
![]() |
4bbde8c50c | ||
![]() |
cc8c1f38e7 | ||
![]() |
cab9c9772a | ||
![]() |
e337a63712 | ||
![]() |
efa92b741b | ||
![]() |
a91ac6f214 | ||
![]() |
943a9406b1 |
39
.github/workflows/main-ci.yml
vendored
39
.github/workflows/main-ci.yml
vendored
@@ -8,16 +8,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
- name: 📥 Clone repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Java
|
||||
- name: 📥 Set up Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
- name: 📥 Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}
|
||||
@@ -27,42 +27,45 @@ jobs:
|
||||
mkdir -p ~/.gradle
|
||||
echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build with Gradle
|
||||
- name: ⚒️ Build
|
||||
run: ./gradlew assemble || ./gradlew assemble
|
||||
|
||||
- name: Download assets for game tests
|
||||
- name: 💡 Lint
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
- name: 🧪 Run tests
|
||||
run: ./gradlew test validateMixinNames checkChangelog
|
||||
|
||||
- name: 📥 Download assets for game tests
|
||||
run: ./gradlew downloadAssets || ./gradlew downloadAssets
|
||||
|
||||
- name: Run tests and linters
|
||||
run: ./gradlew build
|
||||
- name: 🧪 Run integration tests
|
||||
run: ./gradlew runGametest
|
||||
|
||||
- name: Run client tests
|
||||
- name: 🧪 Run client tests
|
||||
run: ./gradlew runGametestClient # Not checkClient, as no point running rendering tests.
|
||||
# These are a little flaky on GH actions: its useful to run them, but don't break the build.
|
||||
continue-on-error: true
|
||||
|
||||
- name: Prepare Jars
|
||||
- name: 🧪 Parse test reports
|
||||
run: ./tools/parse-reports.py
|
||||
if: ${{ failure() }}
|
||||
|
||||
- name: 📦 Prepare Jars
|
||||
run: |
|
||||
# Find the main jar and append the git hash onto it.
|
||||
mkdir -p jars
|
||||
find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \;
|
||||
|
||||
- name: Upload Jar
|
||||
- name: 📤 Upload Jar
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: CC-Tweaked
|
||||
path: ./jars
|
||||
|
||||
- name: Upload coverage
|
||||
- name: 📤 Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
- name: Parse test reports
|
||||
run: ./tools/parse-reports.py
|
||||
if: ${{ failure() }}
|
||||
|
||||
- name: Run linters
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
build-core:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@@ -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: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -20,14 +20,14 @@ repos:
|
||||
exclude: "tsconfig\\.json$"
|
||||
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||
rev: 2.3.54
|
||||
rev: 2.7.2
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
args: ['-disable-indentation']
|
||||
exclude: "^(.*\\.(bat)|LICENSE)$"
|
||||
|
||||
- repo: https://github.com/fsfe/reuse-tool
|
||||
rev: v1.1.0
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: reuse
|
||||
|
||||
|
@@ -6,6 +6,7 @@ Upstream-Contact: Jonathan Coates <git@squiddev.cc>
|
||||
Files:
|
||||
projects/common/src/main/resources/assets/computercraft/sounds.json
|
||||
projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg
|
||||
projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/*
|
||||
projects/common/src/testMod/resources/data/cctest/structures/*
|
||||
projects/fabric/src/generated/*
|
||||
projects/forge/src/generated/*
|
||||
@@ -47,6 +48,7 @@ License: MPL-2.0
|
||||
|
||||
Files:
|
||||
doc/logo.png
|
||||
doc/logo-darkmode.png
|
||||
projects/common/src/main/resources/assets/computercraft/models/*
|
||||
projects/common/src/main/resources/assets/computercraft/textures/*
|
||||
projects/common/src/main/resources/pack.mcmeta
|
||||
|
@@ -6,7 +6,7 @@ SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
# Contributing to CC: Tweaked
|
||||
As with many open source projects, CC: Tweaked thrives on contributions from other people! This document (hopefully)
|
||||
provides an introduction as to how to get started in helping out.
|
||||
provides an introduction as to how to get started with helping out.
|
||||
|
||||
If you've any other questions, [just ask the community][community] or [open an issue][new-issue].
|
||||
|
||||
@@ -28,7 +28,7 @@ automatically with GitHub, so please don't submit PRs adding/changing translatio
|
||||
## Setting up a development environment
|
||||
In order to develop CC: Tweaked, you'll need to download the source code and then run it.
|
||||
|
||||
- Make sure you've got the following software instealled:
|
||||
- Make sure you've got the following software installed:
|
||||
- Java Development Kit (JDK) installed. This can be downloaded from [Adoptium].
|
||||
- [Git](https://git-scm.com/).
|
||||
- If you want to work on documentation, [NodeJS][node].
|
||||
@@ -51,10 +51,10 @@ If you want to run CC:T in a normal Minecraft instance, run `./gradlew assemble`
|
||||
## Developing CC: Tweaked
|
||||
Before making any major changes to CC: Tweaked, I'd recommend you have a read of the [the architecture
|
||||
document][architecture] first. While it's not a comprehensive document, it gives a good hint of where you should start
|
||||
looking to make your changes. As always, if you're not sure [do ask the community][community]!
|
||||
looking to make your changes. As always, if you're not sure, [do ask the community][community]!
|
||||
|
||||
### Testing
|
||||
When making larger changes, it's may be useful to write a test to make sure your code works as expected.
|
||||
When making larger changes, it may be useful to write a test to make sure your code works as expected.
|
||||
|
||||
CC: Tweaked has several test suites, each designed to test something different:
|
||||
|
||||
@@ -91,11 +91,11 @@ file.
|
||||
|
||||
Documentation is built using [illuaminate] which, while not currently documented (somewhat ironic), is largely the same
|
||||
as [ldoc][ldoc]. Documentation comments are written in Markdown, though note that we do not support many GitHub-specific
|
||||
markdown features - if you can, do check what the documentation looks like locally!
|
||||
markdown features. If you can, do check what the documentation looks like locally!
|
||||
|
||||
When writing long-form documentation (such as the guides in [doc/guides](doc/guides)), I find it useful to tell a
|
||||
narrative. Think of what you want the user to learn or achieve, then start introducing a simple concept and then talk
|
||||
about how you can build on that, until you've covered everything!
|
||||
narrative. Think of what you want the user to learn or achieve, then start introducing a simple concept, and then talk
|
||||
about how you can build on that until you've covered everything!
|
||||
|
||||
[new-issue]: https://github.com/cc-tweaked/CC-Tweaked/issues/new/choose "Create a new issue"
|
||||
[community]: README.md#community "Get in touch with the community."
|
||||
|
22
README.md
22
README.md
@@ -4,7 +4,12 @@ SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# 
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./doc/logo-darkmode.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="./doc/logo.png">
|
||||
<img alt="CC: Tweaked" src="./doc/logo.png">
|
||||
</picture>
|
||||
|
||||
[](https://github.com/cc-tweaked/CC-Tweaked/actions "Current build status")
|
||||
[][CurseForge]
|
||||
[][Modrinth]
|
||||
@@ -44,7 +49,7 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
// Vanilla (i.e. for multi-loader systems)
|
||||
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api")
|
||||
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-common-api:$cctVersion")
|
||||
|
||||
// Forge Gradle
|
||||
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-core-api:$cctVersion")
|
||||
@@ -57,6 +62,19 @@ dependencies {
|
||||
}
|
||||
```
|
||||
|
||||
When using ForgeGradle, you may also need to add the following:
|
||||
|
||||
```groovy
|
||||
minecraft {
|
||||
runs {
|
||||
configureEach {
|
||||
property 'mixin.env.remapRefMap', 'true'
|
||||
property 'mixin.env.refMapRemappingFile', "${buildDir}/createSrgToMcp/output.srg"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should also be careful to only use classes within the `dan200.computercraft.api` package. Non-API classes are
|
||||
subject to change at any point. If you depend on functionality outside the API, file an issue, and we can look into
|
||||
exposing more features.
|
||||
|
@@ -56,15 +56,17 @@ repositories {
|
||||
if (fg != null) forRepositories(fg.repository)
|
||||
|
||||
filter {
|
||||
includeGroup("org.squiddev")
|
||||
includeGroup("cc.tweaked")
|
||||
includeModule("org.squiddev", "Cobalt")
|
||||
// Things we mirror
|
||||
includeGroup("dev.architectury")
|
||||
includeGroup("dev.emi")
|
||||
includeGroup("maven.modrinth")
|
||||
includeGroup("me.shedaniel")
|
||||
includeGroup("me.shedaniel.cloth")
|
||||
includeGroup("me.shedaniel")
|
||||
includeGroup("mezz.jei")
|
||||
includeModule("com.terraformersmc", "modmenu")
|
||||
includeModule("me.lucko", "fabric-permissions-api")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import net.minecraftforge.gradle.common.util.RunConfig
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.file.FileSystemOperations
|
||||
import org.gradle.api.invocation.Gradle
|
||||
@@ -53,6 +54,25 @@ abstract class ClientJavaExec : JavaExec() {
|
||||
@get:OutputFile
|
||||
val testResults = project.layout.buildDirectory.file("test-results/$name.xml")
|
||||
|
||||
private fun setTestProperties() {
|
||||
if (!clientDebug) systemProperty("cctest.client", "")
|
||||
if (renderdoc) environment("LD_PRELOAD", "/usr/lib/librenderdoc.so")
|
||||
systemProperty("cctest.gametest-report", testResults.get().asFile.absoluteFile)
|
||||
workingDir(project.buildDir.resolve("gametest").resolve(name))
|
||||
}
|
||||
|
||||
init {
|
||||
setTestProperties()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this task to run a given [RunConfig].
|
||||
*/
|
||||
fun setRunConfig(config: RunConfig) {
|
||||
(this as JavaExec).setRunConfig(config)
|
||||
setTestProperties() // setRunConfig may clobber some properties, ensure everything is set.
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy configuration from a task with the given name.
|
||||
*/
|
||||
@@ -64,11 +84,7 @@ abstract class ClientJavaExec : JavaExec() {
|
||||
fun copyFrom(task: JavaExec) {
|
||||
for (dep in task.dependsOn) dependsOn(dep)
|
||||
task.copyToFull(this)
|
||||
|
||||
if (!clientDebug) systemProperty("cctest.client", "")
|
||||
if (renderdoc) environment("LD_PRELOAD", "/usr/lib/librenderdoc.so")
|
||||
systemProperty("cctest.gametest-report", testResults.get().asFile.absoluteFile)
|
||||
workingDir(project.buildDir.resolve("gametest").resolve(name))
|
||||
setTestProperties() // copyToFull may clobber some properties, ensure everything is set.
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -30,41 +30,22 @@ internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config:
|
||||
val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java)
|
||||
|
||||
// Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts.
|
||||
fun lazyTokens(): MutableMap<String, Supplier<String>> {
|
||||
return RunConfigGenerator.configureTokensLazy(
|
||||
project, config, RunConfigGenerator.mapModClassesToGradle(project, config),
|
||||
originalTask.get().minecraftArtifacts.files,
|
||||
originalTask.get().runtimeClasspathArtifacts.files,
|
||||
)
|
||||
}
|
||||
val lazyTokens = RunConfigGenerator.configureTokensLazy(
|
||||
project, config, RunConfigGenerator.mapModClassesToGradle(project, config),
|
||||
originalTask.get().minecraftArtifacts,
|
||||
originalTask.get().runtimeClasspathArtifacts,
|
||||
)
|
||||
spec.argumentProviders.add(
|
||||
CommandLineArgumentProvider {
|
||||
RunConfigGenerator.getArgsStream(config, lazyTokens(), false).toList()
|
||||
RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList()
|
||||
},
|
||||
)
|
||||
spec.jvmArgumentProviders.add(
|
||||
CommandLineArgumentProvider {
|
||||
val lazyTokens = lazyTokens()
|
||||
(if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } +
|
||||
config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" }
|
||||
},
|
||||
)
|
||||
|
||||
// We can't configure environment variables lazily, so we do these now with a more minimal lazyTokens set.
|
||||
val lazyTokens = mutableMapOf<String, Supplier<String>>()
|
||||
for ((k, v) in config.tokens) lazyTokens[k] = Supplier<String> { v }
|
||||
for ((k, v) in config.lazyTokens) lazyTokens[k] = v
|
||||
lazyTokens.compute(
|
||||
"source_roots",
|
||||
{ _, sourceRoots ->
|
||||
Supplier<String> {
|
||||
val modClasses = RunConfigGenerator.mapModClassesToGradle(project, config)
|
||||
(when (sourceRoots) {
|
||||
null -> modClasses
|
||||
else -> Stream.concat<String>(sourceRoots.get().split(File.pathSeparator).stream(), modClasses)
|
||||
}).distinct().collect(Collectors.joining(File.pathSeparator))
|
||||
}
|
||||
},
|
||||
)
|
||||
for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value))
|
||||
}
|
||||
|
@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{alarm} event is fired when an alarm started with @{os.setAlarm} completes.
|
||||
The [`alarm`] event is fired when an alarm started with [`os.setAlarm`] completes.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The ID of the alarm that finished.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The ID of the alarm that finished.
|
||||
|
||||
## Example
|
||||
Starts a timer and then waits for it to complete.
|
||||
|
@@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: LicenseRef-CCPL
|
||||
-->
|
||||
|
||||
The @{char} event is fired when a character is typed on the keyboard.
|
||||
The [`char`] event is fired when a character is typed on the keyboard.
|
||||
|
||||
The @{char} event is different to a key press. Sometimes multiple key presses may result in one character being
|
||||
The [`char`] event is different to a key press. Sometimes multiple key presses may result in one character being
|
||||
typed (for instance, on some European keyboards). Similarly, some keys (e.g. <kbd>Ctrl</kbd>) do not have any
|
||||
corresponding character. The @{key} should be used if you want to listen to key presses themselves.
|
||||
corresponding character. The [`key`] should be used if you want to listen to key presses themselves.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The string representing the character that was pressed.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The string representing the character that was pressed.
|
||||
|
||||
|
||||
## Example
|
||||
|
@@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{computer_command} event is fired when the `/computercraft queue` command is run for the current computer.
|
||||
The [`computer_command`] event is fired when the `/computercraft queue` command is run for the current computer.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}<abbr title="Variable number of arguments">…</abbr>: The arguments passed to the command.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]<abbr title="Variable number of arguments">…</abbr>: The arguments passed to the command.
|
||||
|
||||
## Example
|
||||
Prints the contents of messages sent:
|
||||
|
@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{disk} event is fired when a disk is inserted into an adjacent or networked disk drive.
|
||||
The [`disk`] event is fired when a disk is inserted into an adjacent or networked disk drive.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The side of the disk drive that had a disk inserted.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The side of the disk drive that had a disk inserted.
|
||||
|
||||
## Example
|
||||
Prints a message when a disk is inserted:
|
||||
|
@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{disk_eject} event is fired when a disk is removed from an adjacent or networked disk drive.
|
||||
The [`disk_eject`] event is fired when a disk is removed from an adjacent or networked disk drive.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The side of the disk drive that had a disk removed.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The side of the disk drive that had a disk removed.
|
||||
|
||||
## Example
|
||||
Prints a message when a disk is removed:
|
||||
|
@@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer.
|
||||
The [`file_transfer`] event is queued when a user drags-and-drops a file on an open computer.
|
||||
|
||||
This event contains a single argument of type @{TransferredFiles}, which can be used to @{TransferredFiles.getFiles|get
|
||||
the files to be transferred}. Each file returned is a @{fs.BinaryReadHandle|binary file handle} with an additional
|
||||
@{TransferredFile.getName|getName} method.
|
||||
This event contains a single argument of type [`TransferredFiles`], which can be used to [get the files to be
|
||||
transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.BinaryReadHandle`] with an
|
||||
additional [getName][`TransferredFile.getName`] method.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name
|
||||
2. @{TransferredFiles}: The list of transferred files.
|
||||
1. [`string`]: The event name
|
||||
2. [`TransferredFiles`]: The list of transferred files.
|
||||
|
||||
## Example
|
||||
Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file.
|
||||
|
@@ -9,12 +9,12 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{http_check} event is fired when a URL check finishes.
|
||||
The [`http_check`] event is fired when a URL check finishes.
|
||||
|
||||
This event is normally handled inside @{http.checkURL}, but it can still be seen when using @{http.checkURLAsync}.
|
||||
This event is normally handled inside [`http.checkURL`], but it can still be seen when using [`http.checkURLAsync`].
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The URL requested to be checked.
|
||||
3. @{boolean}: Whether the check succeeded.
|
||||
4. <span class="type">@{string}|@{nil}</span>: If the check failed, a reason explaining why the check failed.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The URL requested to be checked.
|
||||
3. [`boolean`]: Whether the check succeeded.
|
||||
4. <span class="type">[`string`]|[`nil`]</span>: If the check failed, a reason explaining why the check failed.
|
||||
|
@@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{http_failure} event is fired when an HTTP request fails.
|
||||
The [`http_failure`] event is fired when an HTTP request fails.
|
||||
|
||||
This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}.
|
||||
This event is normally handled inside [`http.get`] and [`http.post`], but it can still be seen when using [`http.request`].
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The URL of the site requested.
|
||||
3. @{string}: An error describing the failure.
|
||||
4. <span class="type">@{http.Response}|@{nil}</span>: A response handle if the connection succeeded, but the server's
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The URL of the site requested.
|
||||
3. [`string`]: An error describing the failure.
|
||||
4. <span class="type">[`http.Response`]|[`nil`]</span>: A response handle if the connection succeeded, but the server's
|
||||
response indicated failure.
|
||||
|
||||
## Example
|
||||
|
@@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{http_success} event is fired when an HTTP request returns successfully.
|
||||
The [`http_success`] event is fired when an HTTP request returns successfully.
|
||||
|
||||
This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}.
|
||||
This event is normally handled inside [`http.get`] and [`http.post`], but it can still be seen when using [`http.request`].
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The URL of the site requested.
|
||||
3. @{http.Response}: The successful HTTP response.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The URL of the site requested.
|
||||
3. [`http.Response`]: The successful HTTP response.
|
||||
|
||||
## Example
|
||||
Prints the content of a website (this may fail if the request fails):
|
||||
|
@@ -11,15 +11,15 @@ SPDX-License-Identifier: LicenseRef-CCPL
|
||||
This event is fired when any key is pressed while the terminal is focused.
|
||||
|
||||
This event returns a numerical "key code" (for instance, <kbd>F1</kbd> is 290). This value may vary between versions and
|
||||
so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values.
|
||||
so it is recommended to use the constants in the [`keys`] API rather than hard coding numeric values.
|
||||
|
||||
If the button pressed represented a printable character, then the @{key} event will be followed immediately by a @{char}
|
||||
event. If you are consuming text input, use a @{char} event instead!
|
||||
If the button pressed represented a printable character, then the [`key`] event will be followed immediately by a [`char`]
|
||||
event. If you are consuming text input, use a [`char`] event instead!
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The numerical key value of the key pressed.
|
||||
3. @{boolean}: Whether the key event was generated while holding the key (@{true}), rather than pressing it the first time (@{false}).
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The numerical key value of the key pressed.
|
||||
3. [`boolean`]: Whether the key event was generated while holding the key ([`true`]), rather than pressing it the first time ([`false`]).
|
||||
|
||||
## Example
|
||||
Prints each key when the user presses it, and if the key is being held.
|
||||
|
@@ -12,14 +12,14 @@ SPDX-License-Identifier: LicenseRef-CCPL
|
||||
Fired whenever a key is released (or the terminal is closed while a key was being pressed).
|
||||
|
||||
This event returns a numerical "key code" (for instance, <kbd>F1</kbd> is 290). This value may vary between versions and
|
||||
so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values.
|
||||
so it is recommended to use the constants in the [`keys`] API rather than hard coding numeric values.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The numerical key value of the key pressed.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The numerical key value of the key pressed.
|
||||
|
||||
## Example
|
||||
Prints each key released on the keyboard whenever a @{key_up} event is fired.
|
||||
Prints each key released on the keyboard whenever a [`key_up`] event is fired.
|
||||
|
||||
```lua
|
||||
while true do
|
||||
|
@@ -8,18 +8,18 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{modem_message} event is fired when a message is received on an open channel on any @{modem}.
|
||||
The [`modem_message`] event is fired when a message is received on an open channel on any [`modem`].
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The side of the modem that received the message.
|
||||
3. @{number}: The channel that the message was sent on.
|
||||
4. @{number}: The reply channel set by the sender.
|
||||
5. @{any}: The message as sent by the sender.
|
||||
6. <span class="type">@{number}|@{nil}</span>: The distance between the sender and the receiver in blocks, or @{nil} if the message was sent between dimensions.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The side of the modem that received the message.
|
||||
3. [`number`]: The channel that the message was sent on.
|
||||
4. [`number`]: The reply channel set by the sender.
|
||||
5. [`any`]: The message as sent by the sender.
|
||||
6. <span class="type">[`number`]|[`nil`]</span>: The distance between the sender and the receiver in blocks, or [`nil`] if the message was sent between dimensions.
|
||||
|
||||
## Example
|
||||
Wraps a @{modem} peripheral, opens channel 0 for listening, and prints all received messages.
|
||||
Wraps a [`modem`] peripheral, opens channel 0 for listening, and prints all received messages.
|
||||
|
||||
```lua
|
||||
local modem = peripheral.find("modem") or error("No modem attached", 0)
|
||||
|
@@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{monitor_resize} event is fired when an adjacent or networked monitor's size is changed.
|
||||
The [`monitor_resize`] event is fired when an adjacent or networked monitor's size is changed.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The side or network ID of the monitor that was resized.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The side or network ID of the monitor that was resized.
|
||||
|
||||
## Example
|
||||
Prints a message when a monitor is resized:
|
||||
|
@@ -8,13 +8,13 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{monitor_touch} event is fired when an adjacent or networked Advanced Monitor is right-clicked.
|
||||
The [`monitor_touch`] event is fired when an adjacent or networked Advanced Monitor is right-clicked.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The side or network ID of the monitor that was touched.
|
||||
3. @{number}: The X coordinate of the touch, in characters.
|
||||
4. @{number}: The Y coordinate of the touch, in characters.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The side or network ID of the monitor that was touched.
|
||||
3. [`number`]: The X coordinate of the touch, in characters.
|
||||
4. [`number`]: The Y coordinate of the touch, in characters.
|
||||
|
||||
## Example
|
||||
Prints a message when a monitor is touched:
|
||||
|
@@ -12,13 +12,13 @@ This event is fired when the terminal is clicked with a mouse. This event is onl
|
||||
advanced turtles and pocket computers).
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The mouse button that was clicked.
|
||||
3. @{number}: The X-coordinate of the click.
|
||||
4. @{number}: The Y-coordinate of the click.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The mouse button that was clicked.
|
||||
3. [`number`]: The X-coordinate of the click.
|
||||
4. [`number`]: The Y-coordinate of the click.
|
||||
|
||||
## Mouse buttons
|
||||
Several mouse events (@{mouse_click}, @{mouse_up}, @{mouse_scroll}) contain a "mouse button" code. This takes a
|
||||
Several mouse events ([`mouse_click`], [`mouse_up`], [`mouse_scroll`]) contain a "mouse button" code. This takes a
|
||||
numerical value depending on which button on your mouse was last pressed when this event occurred.
|
||||
|
||||
| Button Code | Mouse Button |
|
||||
|
@@ -12,10 +12,10 @@ SPDX-License-Identifier: LicenseRef-CCPL
|
||||
This event is fired every time the mouse is moved while a mouse button is being held.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed.
|
||||
3. @{number}: The X-coordinate of the mouse.
|
||||
4. @{number}: The Y-coordinate of the mouse.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed.
|
||||
3. [`number`]: The X-coordinate of the mouse.
|
||||
4. [`number`]: The Y-coordinate of the mouse.
|
||||
|
||||
## Example
|
||||
Print the button and the coordinates whenever the mouse is dragged.
|
||||
|
@@ -11,10 +11,10 @@ SPDX-License-Identifier: LicenseRef-CCPL
|
||||
This event is fired when a mouse wheel is scrolled in the terminal.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The direction of the scroll. (-1 = up, 1 = down)
|
||||
3. @{number}: The X-coordinate of the mouse when scrolling.
|
||||
4. @{number}: The Y-coordinate of the mouse when scrolling.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The direction of the scroll. (-1 = up, 1 = down)
|
||||
3. [`number`]: The X-coordinate of the mouse when scrolling.
|
||||
4. [`number`]: The Y-coordinate of the mouse when scrolling.
|
||||
|
||||
## Example
|
||||
Prints the direction of each scroll, and the position of the mouse at the time.
|
||||
|
@@ -11,10 +11,10 @@ SPDX-License-Identifier: LicenseRef-CCPL
|
||||
This event is fired when a mouse button is released or a held mouse leaves the computer's terminal.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that was released.
|
||||
3. @{number}: The X-coordinate of the mouse.
|
||||
4. @{number}: The Y-coordinate of the mouse.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The [mouse button](mouse_click.html#Mouse_buttons) that was released.
|
||||
3. [`number`]: The X-coordinate of the mouse.
|
||||
4. [`number`]: The Y-coordinate of the mouse.
|
||||
|
||||
## Example
|
||||
Prints the coordinates and button number whenever the mouse is released.
|
||||
|
@@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{paste} event is fired when text is pasted into the computer through Ctrl-V (or ⌘V on Mac).
|
||||
The [`paste`] event is fired when text is pasted into the computer through Ctrl-V (or ⌘V on Mac).
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
2. @{string} The text that was pasted.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`] The text that was pasted.
|
||||
|
||||
## Example
|
||||
Prints pasted text:
|
||||
|
@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{peripheral} event is fired when a peripheral is attached on a side or to a modem.
|
||||
The [`peripheral`] event is fired when a peripheral is attached on a side or to a modem.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The side the peripheral was attached to.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The side the peripheral was attached to.
|
||||
|
||||
## Example
|
||||
Prints a message when a peripheral is attached:
|
||||
|
@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{peripheral_detach} event is fired when a peripheral is detached from a side or from a modem.
|
||||
The [`peripheral_detach`] event is fired when a peripheral is detached from a side or from a modem.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The side the peripheral was detached from.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The side the peripheral was detached from.
|
||||
|
||||
## Example
|
||||
Prints a message when a peripheral is detached:
|
||||
|
@@ -10,17 +10,17 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{rednet_message} event is fired when a message is sent over Rednet.
|
||||
The [`rednet_message`] event is fired when a message is sent over Rednet.
|
||||
|
||||
This event is usually handled by @{rednet.receive}, but it can also be pulled manually.
|
||||
This event is usually handled by [`rednet.receive`], but it can also be pulled manually.
|
||||
|
||||
@{rednet_message} events are sent by @{rednet.run} in the top-level coroutine in response to @{modem_message} events. A @{rednet_message} event is always preceded by a @{modem_message} event. They are generated inside CraftOS rather than being sent by the ComputerCraft machine.
|
||||
[`rednet_message`] events are sent by [`rednet.run`] in the top-level coroutine in response to [`modem_message`] events. A [`rednet_message`] event is always preceded by a [`modem_message`] event. They are generated inside CraftOS rather than being sent by the ComputerCraft machine.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The ID of the sending computer.
|
||||
3. @{any}: The message sent.
|
||||
4. <span class="type">@{string}|@{nil}</span>: The protocol of the message, if provided.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The ID of the sending computer.
|
||||
3. [`any`]: The message sent.
|
||||
4. <span class="type">[`string`]|[`nil`]</span>: The protocol of the message, if provided.
|
||||
|
||||
## Example
|
||||
Prints a message when one is sent:
|
||||
|
@@ -8,10 +8,10 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{event!redstone} event is fired whenever any redstone inputs on the computer change.
|
||||
The [`event!redstone`] event is fired whenever any redstone inputs on the computer change.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
1. [`string`]: The event name.
|
||||
|
||||
## Example
|
||||
Prints a message when a redstone input changes:
|
||||
|
@@ -10,13 +10,13 @@ SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The name of the speaker which is available to play more audio.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The name of the speaker which is available to play more audio.
|
||||
|
||||
|
||||
## Example
|
||||
This uses @{io.lines} to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it
|
||||
using @{speaker.playAudio}. If the speaker's buffer is full, it waits for an event and tries again.
|
||||
This uses [`io.lines`] to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it
|
||||
using [`speaker.playAudio`]. If the speaker's buffer is full, it waits for an event and tries again.
|
||||
|
||||
```lua {data-peripheral=speaker}
|
||||
local dfpwm = require("cc.audio.dfpwm")
|
||||
|
@@ -9,13 +9,13 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{task_complete} event is fired when an asynchronous task completes. This is usually handled inside the function call that queued the task; however, functions such as @{commands.execAsync} return immediately so the user can wait for completion.
|
||||
The [`task_complete`] event is fired when an asynchronous task completes. This is usually handled inside the function call that queued the task; however, functions such as [`commands.execAsync`] return immediately so the user can wait for completion.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The ID of the task that completed.
|
||||
3. @{boolean}: Whether the command succeeded.
|
||||
4. @{string}: If the command failed, an error message explaining the failure. (This is not present if the command succeeded.)
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The ID of the task that completed.
|
||||
3. [`boolean`]: Whether the command succeeded.
|
||||
4. [`string`]: If the command failed, an error message explaining the failure. (This is not present if the command succeeded.)
|
||||
5. <abbr title="Variable number of arguments">…</abbr>: Any parameters returned from the command.
|
||||
|
||||
## Example
|
||||
|
@@ -8,15 +8,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{term_resize} event is fired when the main terminal is resized. For instance:
|
||||
- When a the tab bar is shown or hidden in @{multishell}.
|
||||
The [`term_resize`] event is fired when the main terminal is resized. For instance:
|
||||
- When a the tab bar is shown or hidden in [`multishell`].
|
||||
- When the terminal is redirected to a monitor via the "monitor" program and the monitor is resized.
|
||||
|
||||
When this event fires, some parts of the terminal may have been moved or deleted. Simple terminal programs (those
|
||||
not using @{term.setCursorPos}) can ignore this event, but more complex GUI programs should redraw the entire screen.
|
||||
not using [`term.setCursorPos`]) can ignore this event, but more complex GUI programs should redraw the entire screen.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
1. [`string`]: The event name.
|
||||
|
||||
## Example
|
||||
Print a message each time the terminal is resized.
|
||||
|
@@ -8,14 +8,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{terminate} event is fired when <kbd>Ctrl-T</kbd> is held down.
|
||||
The [`terminate`] event is fired when <kbd>Ctrl-T</kbd> is held down.
|
||||
|
||||
This event is normally handled by @{os.pullEvent}, and will not be returned. However, @{os.pullEventRaw} will return this event when fired.
|
||||
This event is normally handled by [`os.pullEvent`], and will not be returned. However, [`os.pullEventRaw`] will return this event when fired.
|
||||
|
||||
@{terminate} will be sent even when a filter is provided to @{os.pullEventRaw}. When using @{os.pullEventRaw} with a filter, make sure to check that the event is not @{terminate}.
|
||||
[`terminate`] will be sent even when a filter is provided to [`os.pullEventRaw`]. When using [`os.pullEventRaw`] with a filter, make sure to check that the event is not [`terminate`].
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
1. [`string`]: The event name.
|
||||
|
||||
## Example
|
||||
Prints a message when Ctrl-T is held:
|
||||
|
@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{timer} event is fired when a timer started with @{os.startTimer} completes.
|
||||
The [`timer`] event is fired when a timer started with [`os.startTimer`] completes.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{number}: The ID of the timer that finished.
|
||||
1. [`string`]: The event name.
|
||||
2. [`number`]: The ID of the timer that finished.
|
||||
|
||||
## Example
|
||||
Start and wait for a timer to finish.
|
||||
|
@@ -8,10 +8,10 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{turtle_inventory} event is fired when a turtle's inventory is changed.
|
||||
The [`turtle_inventory`] event is fired when a turtle's inventory is changed.
|
||||
|
||||
## Return values
|
||||
1. @{string}: The event name.
|
||||
1. [`string`]: The event name.
|
||||
|
||||
## Example
|
||||
Prints a message when the inventory is changed:
|
||||
|
@@ -8,11 +8,20 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{websocket_closed} event is fired when an open WebSocket connection is closed.
|
||||
The [`websocket_closed`] event is fired when an open WebSocket connection is closed.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The URL of the WebSocket that was closed.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The URL of the WebSocket that was closed.
|
||||
3. <span class="type">[`string`]|[`nil`]</span>: The [server-provided reason][close_reason]
|
||||
the websocket was closed. This will be [`nil`] if the connection was closed
|
||||
abnormally.
|
||||
4. <span class="type">[`number`]|[`nil`]</span>: The [connection close code][close_code],
|
||||
indicating why the socket was closed. This will be [`nil`] if the connection
|
||||
was closed abnormally.
|
||||
|
||||
[close_reason]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 "The WebSocket Connection Close Reason, RFC 6455"
|
||||
[close_code]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 "The WebSocket Connection Close Code, RFC 6455"
|
||||
|
||||
## Example
|
||||
Prints a message when a WebSocket is closed (this may take a minute):
|
||||
|
@@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{websocket_failure} event is fired when a WebSocket connection request fails.
|
||||
The [`websocket_failure`] event is fired when a WebSocket connection request fails.
|
||||
|
||||
This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}.
|
||||
This event is normally handled inside [`http.websocket`], but it can still be seen when using [`http.websocketAsync`].
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The URL of the site requested.
|
||||
3. @{string}: An error describing the failure.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The URL of the site requested.
|
||||
3. [`string`]: An error describing the failure.
|
||||
|
||||
## Example
|
||||
Prints an error why the website cannot be contacted:
|
||||
|
@@ -8,15 +8,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{websocket_message} event is fired when a message is received on an open WebSocket connection.
|
||||
The [`websocket_message`] event is fired when a message is received on an open WebSocket connection.
|
||||
|
||||
This event is normally handled by @{http.Websocket.receive}, but it can also be pulled manually.
|
||||
This event is normally handled by [`http.Websocket.receive`], but it can also be pulled manually.
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The URL of the WebSocket.
|
||||
3. @{string}: The contents of the message.
|
||||
4. @{boolean}: Whether this is a binary message.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The URL of the WebSocket.
|
||||
3. [`string`]: The contents of the message.
|
||||
4. [`boolean`]: Whether this is a binary message.
|
||||
|
||||
## Example
|
||||
Prints a message sent by a WebSocket:
|
||||
|
@@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
The @{websocket_success} event is fired when a WebSocket connection request returns successfully.
|
||||
The [`websocket_success`] event is fired when a WebSocket connection request returns successfully.
|
||||
|
||||
This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}.
|
||||
This event is normally handled inside [`http.websocket`], but it can still be seen when using [`http.websocketAsync`].
|
||||
|
||||
## Return Values
|
||||
1. @{string}: The event name.
|
||||
2. @{string}: The URL of the site.
|
||||
3. @{http.Websocket}: The handle for the WebSocket.
|
||||
1. [`string`]: The event name.
|
||||
2. [`string`]: The URL of the site.
|
||||
3. [`http.Websocket`]: The handle for the WebSocket.
|
||||
|
||||
## Example
|
||||
Prints the content of a website (this may fail if the request fails):
|
||||
|
@@ -9,7 +9,7 @@ SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# Setting up GPS
|
||||
The @{gps} API allows computers and turtles to find their current position using wireless modems.
|
||||
The [`gps`] API allows computers and turtles to find their current position using wireless modems.
|
||||
|
||||
In order to use GPS, you'll need to set up multiple *GPS hosts*. These are computers running the special `gps host`
|
||||
program, which tell other computers the host's position. Several hosts running together are known as a *GPS
|
||||
@@ -19,22 +19,21 @@ In order to give the best results, a GPS constellation needs at least four compu
|
||||
constellation is redundant, but it does not cause problems.
|
||||
|
||||
## Building a GPS constellation
|
||||
{.big-image}
|
||||
<img alt="An example GPS constellation." src="/images/gps-constellation-example.png" class="big-image" />
|
||||
|
||||
We are going to build our GPS constellation as shown in the image above. You will need 4 computers and either 4 wireless
|
||||
modems or 4 ender modems. Try not to mix ender and wireless modems together as you might get some odd behavior when your
|
||||
requesting computers are out of range.
|
||||
|
||||
:::tip Ender modems vs wireless modems
|
||||
Ender modems have a very large range, which makes them very useful for setting up GPS hosts. If you do this then you
|
||||
will likely only need one GPS constellation for the whole dimension (such as the Overworld or Nether).
|
||||
|
||||
If you do use wireless modems then you may find that you need multiple GPS constellations to cover your needs.
|
||||
|
||||
A computer needs a wireless or ender modem and to be in range of a GPS constellation that is in the same dimension as it
|
||||
to use the GPS API. The reason for this is that ComputerCraft mimics real-life GPS by making use of the distance
|
||||
parameter of @{modem_message|modem messages} and some maths.
|
||||
:::
|
||||
> [Ender modems vs wireless modems][!TIP]
|
||||
> Ender modems have a very large range, which makes them very useful for setting up GPS hosts. If you do this then you
|
||||
> will likely only need one GPS constellation for the whole dimension (such as the Overworld or Nether).
|
||||
>
|
||||
> If you do use wireless modems then you may find that you need multiple GPS constellations to cover your needs.
|
||||
>
|
||||
> A computer needs a wireless or ender modem and to be in range of a GPS constellation that is in the same dimension as
|
||||
> it to use the GPS API. The reason for this is that ComputerCraft mimics real-life GPS by making use of the distance
|
||||
> parameter of [modem messages][`modem_message`] and some maths.
|
||||
|
||||
Locate where you want to place your GPS constellation. You will need an area at least 6 blocks high, 6 blocks wide, and
|
||||
6 blocks deep (6x6x6). If you are using wireless modems then you may want to build your constellation as high as you can
|
||||
@@ -79,18 +78,16 @@ To hide Minecraft's debug screen, press <kbd>F3</kbd> again.
|
||||
Create similar startup files for the other computers in your constellation, making sure to input the each computer's own
|
||||
coordinates.
|
||||
|
||||
:::caution Modem messages come from the computer's position, not the modem's
|
||||
Wireless modems transmit from the block that they are attached to *not* the block space that they occupy, the
|
||||
coordinates that you input into your GPS host should be the position of the computer and not the position of the modem.
|
||||
:::
|
||||
> [Modem messages come from the computer's position, not the modem's][!WARNING]
|
||||
> Wireless modems transmit from the block that they are attached to *not* the block space that they occupy, the
|
||||
> coordinates that you input into your GPS host should be the position of the computer and not the position of the modem.
|
||||
|
||||
Congratulations, your constellation is now fully set up! You can test it by placing another computer close by, placing a
|
||||
wireless modem on it, and running the `gps locate` program (or calling the @{gps.locate} function).
|
||||
wireless modem on it, and running the `gps locate` program (or calling the [`gps.locate`] function).
|
||||
|
||||
:::info Why use Minecraft's coordinates?
|
||||
CC doesn't care if you use Minecraft's coordinate system, so long as all of the GPS hosts with overlapping ranges use
|
||||
the same reference point (requesting computers will get confused if hosts have different reference points). However,
|
||||
using MC's coordinate system does provide a nice standard to adopt server-wide. It also is consistent with how command
|
||||
computers get their location, they use MC's command system to get their block which returns that in MC's coordinate
|
||||
system.
|
||||
:::
|
||||
> [Why use Minecraft's coordinates?][!INFO]
|
||||
> CC doesn't care if you use Minecraft's coordinate system, so long as all of the GPS hosts with overlapping ranges use
|
||||
> the same reference point (requesting computers will get confused if hosts have different reference points). However,
|
||||
> using MC's coordinate system does provide a nice standard to adopt server-wide. It also is consistent with how command
|
||||
> computers get their location, they use MC's command system to get their block which returns that in MC's coordinate
|
||||
> system.
|
||||
|
@@ -11,7 +11,7 @@ SPDX-License-Identifier: MPL-2.0
|
||||
-->
|
||||
|
||||
# Playing audio with speakers
|
||||
CC: Tweaked's speaker peripheral provides a powerful way to play any audio you like with the @{speaker.playAudio}
|
||||
CC: Tweaked's speaker peripheral provides a powerful way to play any audio you like with the [`speaker.playAudio`]
|
||||
method. However, for people unfamiliar with digital audio, it's not the most intuitive thing to use. This guide provides
|
||||
an introduction to digital audio, demonstrates how to play music with CC: Tweaked's speakers, and then briefly discusses
|
||||
the more complex topic of audio processing.
|
||||
@@ -60,7 +60,7 @@ sine waves (and why wouldn't you?), you'd need a table with almost 3 _million_.
|
||||
up very quickly, and these tables take up more and more memory.
|
||||
|
||||
Instead of building our entire song (well, sine wave) in one go, we can produce it in small batches, each of which get
|
||||
passed off to @{speaker.playAudio} when the time is right. This allows us to build a _stream_ of audio, where we read
|
||||
passed off to [`speaker.playAudio`] when the time is right. This allows us to build a _stream_ of audio, where we read
|
||||
chunks of audio one at a time (either from a file or a tone generator like above), do some optional processing to each
|
||||
one, and then play them.
|
||||
|
||||
@@ -84,15 +84,15 @@ end
|
||||
```
|
||||
|
||||
It looks pretty similar to before, aside from we've wrapped the generation and playing code in a while loop, and added a
|
||||
rather odd loop with @{speaker.playAudio} and @{os.pullEvent}.
|
||||
rather odd loop with [`speaker.playAudio`] and [`os.pullEvent`].
|
||||
|
||||
Let's talk about this loop, why do we need to keep calling @{speaker.playAudio}? Remember that what we're trying to do
|
||||
Let's talk about this loop, why do we need to keep calling [`speaker.playAudio`]? Remember that what we're trying to do
|
||||
here is avoid keeping too much audio in memory at once. However, if we're generating audio quicker than the speakers can
|
||||
play it, we're not helping at all - all this audio is still hanging around waiting to be played!
|
||||
|
||||
In order to avoid this, the speaker rejects any new chunks of audio if its backlog is too large. When this happens,
|
||||
@{speaker.playAudio} returns false. Once enough audio has played, and the backlog has been reduced, a
|
||||
@{speaker_audio_empty} event is queued, and we can try to play our chunk once more.
|
||||
[`speaker.playAudio`] returns false. Once enough audio has played, and the backlog has been reduced, a
|
||||
[`speaker_audio_empty`] event is queued, and we can try to play our chunk once more.
|
||||
|
||||
## Storing audio
|
||||
PCM is a fantastic way of representing audio when we want to manipulate it, but it's not very efficient when we want to
|
||||
@@ -106,7 +106,7 @@ computer. Instead, we need something much simpler.
|
||||
|
||||
DFPWM (Dynamic Filter Pulse Width Modulation) is the de facto standard audio format of the ComputerCraft (and
|
||||
OpenComputers) world. Originally popularised by the addon mod [Computronics], CC:T now has built-in support for it with
|
||||
the @{cc.audio.dfpwm} module. This allows you to read DFPWM files from disk, decode them to PCM, and then play them
|
||||
the [`cc.audio.dfpwm`] module. This allows you to read DFPWM files from disk, decode them to PCM, and then play them
|
||||
using the speaker.
|
||||
|
||||
Let's dive in with an example, and we'll explain things afterwards:
|
||||
@@ -125,16 +125,16 @@ for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
|
||||
end
|
||||
```
|
||||
|
||||
Once again, we see the @{speaker.playAudio}/@{speaker_audio_empty} loop. However, the rest of the program is a little
|
||||
Once again, we see the [`speaker.playAudio`]/[`speaker_audio_empty`] loop. However, the rest of the program is a little
|
||||
different.
|
||||
|
||||
First, we require the dfpwm module and call @{cc.audio.dfpwm.make_decoder} to construct a new decoder. This decoder
|
||||
First, we require the dfpwm module and call [`cc.audio.dfpwm.make_decoder`] to construct a new decoder. This decoder
|
||||
accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, which we can then play with our speaker.
|
||||
|
||||
As mentioned above, @{speaker.playAudio} accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each
|
||||
As mentioned above, [`speaker.playAudio`] accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each
|
||||
sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use
|
||||
@{io.lines}, which provides a nice way to loop over chunks of a file. You can of course just use @{fs.open} and
|
||||
@{fs.BinaryReadHandle.read} if you prefer.
|
||||
[`io.lines`], which provides a nice way to loop over chunks of a file. You can of course just use [`fs.open`] and
|
||||
[`fs.BinaryReadHandle.read`] if you prefer.
|
||||
|
||||
## Processing audio
|
||||
As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes.
|
||||
@@ -189,10 +189,9 @@ for chunk in io.lines("data/example.dfpwm", 16 * 1024) do
|
||||
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 [GitHub Discussions] or [IRC] either!
|
||||
:::
|
||||
> [Confused?][!NOTE]
|
||||
> 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 [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
|
||||
the wave. If you wanted to modify the _frequency_ (for instance, shifting the pitch), things get rather more complex.
|
||||
|
@@ -13,7 +13,7 @@ A library is a collection of useful functions and other definitions which is sto
|
||||
might want to create a library because you have some functions which are used in multiple programs, or just to split
|
||||
your program into multiple more modular files.
|
||||
|
||||
Let's say we want to create a small library to make working with the @{term|terminal} a little easier. We'll provide two
|
||||
Let's say we want to create a small library to make working with the [terminal][`term`] a little easier. We'll provide two
|
||||
functions: `reset`, which clears the terminal and sets the cursor to (1, 1), and `write_center`, which prints some text
|
||||
in the middle of the screen.
|
||||
|
||||
@@ -48,32 +48,32 @@ more_term.write_center("Hello, world!")
|
||||
When run, this'll clear the screen and print some text in the middle of the first line.
|
||||
|
||||
## require in depth
|
||||
While the previous section is a good introduction to how @{require} operates, there are a couple of remaining points
|
||||
While the previous section is a good introduction to how [`require`] operates, there are a couple of remaining points
|
||||
which are worth mentioning for more advanced usage.
|
||||
|
||||
### Libraries can return anything
|
||||
In our above example, we return a table containing the functions we want to expose. However, it's worth pointing out
|
||||
that you can return ''anything'' from your library - a table, a function or even just a string! @{require} treats them
|
||||
that you can return ''anything'' from your library - a table, a function or even just a string! [`require`] treats them
|
||||
all the same, and just returns whatever your library provides.
|
||||
|
||||
### Module resolution and the package path
|
||||
In the above examples, we defined our library in a file, and @{require} read from it. While this is what you'll do most
|
||||
of the time, it is possible to make @{require} look elsewhere for your library, such as downloading from a website or
|
||||
In the above examples, we defined our library in a file, and [`require`] read from it. While this is what you'll do most
|
||||
of the time, it is possible to make [`require`] look elsewhere for your library, such as downloading from a website or
|
||||
loading from an in-memory library store.
|
||||
|
||||
As a result, the *module name* you pass to @{require} doesn't correspond to a file path. One common mistake is to load
|
||||
As a result, the *module name* you pass to [`require`] doesn't correspond to a file path. One common mistake is to load
|
||||
code from a sub-directory using `require("folder/library")` or even `require("folder/library.lua")`, neither of which
|
||||
will do quite what you expect.
|
||||
|
||||
When loading libraries (also referred to as *modules*) from files, @{require} searches along the *@{package.path|module
|
||||
path}*. By default, this looks something like:
|
||||
When loading libraries (also referred to as *modules*) from files, [`require`] searches along the [*module
|
||||
path*][`package.path`]. By default, this looks something like:
|
||||
|
||||
* `?.lua`
|
||||
* `?/init.lua`
|
||||
* `/rom/modules/main/?.lua`
|
||||
* etc...
|
||||
|
||||
When you call `require("my_library")`, @{require} replaces the `?` in each element of the path with your module name, and
|
||||
When you call `require("my_library")`, [`require`] replaces the `?` in each element of the path with your module name, and
|
||||
checks if the file exists. In this case, we'd look for `my_library.lua`, `my_library/init.lua`,
|
||||
`/rom/modules/main/my_library.lua` and so on. Note that this works *relative to the current program*, so if your
|
||||
program is actually called `folder/program`, then we'll look for `folder/my_library.lua`, etc...
|
||||
@@ -86,4 +86,4 @@ before we start looking for the library.
|
||||
There are several external resources which go into require in a little more detail:
|
||||
|
||||
- The [Lua Module tutorial](http://lua-users.org/wiki/ModulesTutorial) on the Lua wiki.
|
||||
- [Lua's manual section on @{require}](https://www.lua.org/manual/5.1/manual.html#pdf-require).
|
||||
- [Lua's manual section on `require`](https://www.lua.org/manual/5.1/manual.html#pdf-require).
|
||||
|
@@ -15,13 +15,13 @@ CC: Tweaked can be installed from [CurseForge] or [Modrinth]. It runs on both [M
|
||||
Controlled using the [Lua programming language][lua], CC: Tweaked's computers provides all the tools you need to start
|
||||
writing code and automating your Minecraft world.
|
||||
|
||||
{.big-image}
|
||||
<img alt="A ComputerCraft terminal open and ready to be programmed." src="images/basic-terminal.png" class="big-image" />
|
||||
|
||||
While computers are incredibly powerful, they're rather limited by their inability to move about. *Turtles* are the
|
||||
solution here. They can move about the world, placing and breaking blocks, swinging a sword to protect you from zombies,
|
||||
or whatever else you program them to!
|
||||
|
||||
{.big-image}
|
||||
<img alt="A turtle tunneling in Minecraft." src="images/turtle.png" class="big-image" />
|
||||
|
||||
Not all problems can be solved with a pickaxe though, and so CC: Tweaked also provides a bunch of additional peripherals
|
||||
for your computers. You can play a tune with speakers, display text or images on a monitor, connect all your
|
||||
@@ -30,7 +30,7 @@ computers together with modems, and much more.
|
||||
Computers can now also interact with inventories such as chests, allowing you to build complex inventory and item
|
||||
management systems.
|
||||
|
||||
{.big-image}
|
||||
<img alt="A chest's contents being read by a computer and displayed on a monitor." src="images/peripherals.png" class="big-image" />
|
||||
|
||||
## Getting Started
|
||||
While ComputerCraft is lovely for both experienced programmers and for people who have never coded before, it can be a
|
||||
|
BIN
doc/logo-darkmode.png
Normal file
BIN
doc/logo-darkmode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@@ -13,22 +13,20 @@ include standard Lua functions.
|
||||
|
||||
As it waits for a fixed amount of world ticks, `time` will automatically be
|
||||
rounded up to the nearest multiple of 0.05 seconds. If you are using coroutines
|
||||
or the @{parallel|parallel API}, it will only pause execution of the current
|
||||
or the [parallel API][`parallel`], it will only pause execution of the current
|
||||
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
|
||||
the minimum sleep time is 0.05 seconds, it will slow your program down.
|
||||
:::
|
||||
> [!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
|
||||
> the minimum sleep time is 0.05 seconds, it will slow your program down.
|
||||
|
||||
:::caution
|
||||
Internally, this function queues and waits for a timer event (using
|
||||
@{os.startTimer}), however it does not listen for any other events. This means
|
||||
that any event that occurs while sleeping will be entirely discarded. If you
|
||||
need to receive events while sleeping, consider using @{os.startTimer|timers},
|
||||
or the @{parallel|parallel API}.
|
||||
:::
|
||||
> [!WARNING]
|
||||
> Internally, this function queues and waits for a timer event (using
|
||||
> [`os.startTimer`]), however it does not listen for any other events. This means
|
||||
> that any event that occurs while sleeping will be entirely discarded. If you
|
||||
> need to receive events while sleeping, consider using [timers][`os.startTimer`],
|
||||
> or the [parallel API][`parallel`].
|
||||
|
||||
@tparam number time The number of seconds to sleep for, rounded up to the
|
||||
nearest multiple of 0.05.
|
||||
@@ -116,7 +114,7 @@ function read(replaceChar, history, completeFn, default) end
|
||||
|
||||
--- Stores the current ComputerCraft and Minecraft versions.
|
||||
--
|
||||
-- Outside of Minecraft (for instance, in an emulator) @{_HOST} will contain the
|
||||
-- Outside of Minecraft (for instance, in an emulator) [`_HOST`] will contain the
|
||||
-- emulator's version instead.
|
||||
--
|
||||
-- For example, `ComputerCraft 1.93.0 (Minecraft 1.15.2)`.
|
||||
|
@@ -15,27 +15,27 @@ variables and functions exported by it will by available through the use of
|
||||
@deprecated When possible it's best to avoid using this function. It pollutes
|
||||
the global table and can mask errors.
|
||||
|
||||
@{require} should be used to load libraries instead.
|
||||
[`require`] should be used to load libraries instead.
|
||||
]]
|
||||
function loadAPI(path) end
|
||||
|
||||
--- Unloads an API which was loaded by @{os.loadAPI}.
|
||||
--- Unloads an API which was loaded by [`os.loadAPI`].
|
||||
--
|
||||
-- This effectively removes the specified table from `_G`.
|
||||
--
|
||||
-- @tparam string name The name of the API to unload.
|
||||
-- @since 1.2
|
||||
-- @deprecated See @{os.loadAPI} for why.
|
||||
-- @deprecated See [`os.loadAPI`] for why.
|
||||
function unloadAPI(name) end
|
||||
|
||||
--[[- Pause execution of the current thread and waits for any events matching
|
||||
`filter`.
|
||||
|
||||
This function @{coroutine.yield|yields} the current process and waits for it
|
||||
This function [yields][`coroutine.yield`] the current process and waits for it
|
||||
to be resumed with a vararg list where the first element matches `filter`.
|
||||
If no `filter` is supplied, this will match all events.
|
||||
|
||||
Unlike @{os.pullEventRaw}, it will stop the application upon a "terminate"
|
||||
Unlike [`os.pullEventRaw`], it will stop the application upon a "terminate"
|
||||
event, printing the error "Terminated".
|
||||
|
||||
@tparam[opt] string filter Event to filter for.
|
||||
@@ -69,7 +69,7 @@ function pullEvent(filter) end
|
||||
--[[- Pause execution of the current thread and waits for events, including the
|
||||
`terminate` event.
|
||||
|
||||
This behaves almost the same as @{os.pullEvent}, except it allows you to handle
|
||||
This behaves almost the same as [`os.pullEvent`], except it allows you to handle
|
||||
the `terminate` event yourself - the program will not stop execution when
|
||||
<kbd>Ctrl+T</kbd> is pressed.
|
||||
|
||||
@@ -89,7 +89,7 @@ the `terminate` event yourself - the program will not stop execution when
|
||||
]]
|
||||
function pullEventRaw(filter) end
|
||||
|
||||
--- Pauses execution for the specified number of seconds, alias of @{_G.sleep}.
|
||||
--- Pauses execution for the specified number of seconds, alias of [`_G.sleep`].
|
||||
--
|
||||
-- @tparam number time The number of seconds to sleep for, rounded up to the
|
||||
-- nearest multiple of 0.05.
|
||||
@@ -109,12 +109,12 @@ arguments.
|
||||
|
||||
This function does not resolve program names like the shell does. This means
|
||||
that, for example, `os.run("edit")` will not work. As well as this, it does not
|
||||
provide access to the @{shell} API in the environment. For this behaviour, use
|
||||
@{shell.run} instead.
|
||||
provide access to the [`shell`] API in the environment. For this behaviour, use
|
||||
[`shell.run`] instead.
|
||||
|
||||
If the program cannot be found, or failed to run, it will print the error and
|
||||
return `false`. If you want to handle this more gracefully, use an alternative
|
||||
such as @{loadfile}.
|
||||
such as [`loadfile`].
|
||||
|
||||
@tparam table env The environment to run the program with.
|
||||
@tparam string path The exact path of the program to run.
|
||||
|
@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
|
||||
|
||||
# Mod properties
|
||||
isUnstable=false
|
||||
modVersion=1.105.0
|
||||
modVersion=1.108.0
|
||||
|
||||
# Minecraft properties: We want to configure this here so we can read it in settings.gradle
|
||||
mcVersion=1.19.4
|
||||
|
@@ -7,20 +7,20 @@
|
||||
# Minecraft
|
||||
# MC version is specified in gradle.properties, as we need that in settings.gradle.
|
||||
# Remember to update corresponding versions in fabric.mod.json/mods.toml
|
||||
fabric-api = "0.80.0+1.19.4"
|
||||
fabric-loader = "0.14.19"
|
||||
fabric-api = "0.86.1+1.19.4"
|
||||
fabric-loader = "0.14.21"
|
||||
forge = "45.0.42"
|
||||
forgeSpi = "6.0.0"
|
||||
mixin = "0.8.5"
|
||||
parchment = "2023.03.12"
|
||||
parchmentMc = "1.19.3"
|
||||
parchment = "2023.06.26"
|
||||
parchmentMc = "1.19.4"
|
||||
|
||||
# Normal dependencies
|
||||
asm = "9.3"
|
||||
autoService = "1.0.1"
|
||||
checkerFramework = "3.32.0"
|
||||
cobalt = "0.7.0"
|
||||
cobalt-next = "0.7.1" # Not a real version, used to constrain the version we accept.
|
||||
cobalt = "0.7.3"
|
||||
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
|
||||
fastutil = "8.5.9"
|
||||
guava = "31.1-jre"
|
||||
jetbrainsAnnotations = "24.0.1"
|
||||
@@ -33,6 +33,8 @@ nightConfig = "3.6.5"
|
||||
slf4j = "1.7.36"
|
||||
|
||||
# Minecraft mods
|
||||
emi = "1.0.8+1.19.4"
|
||||
fabricPermissions = "0.2.20221016"
|
||||
iris = "1.5.2+1.19.4"
|
||||
jei = "13.1.0.11"
|
||||
modmenu = "6.1.0-rc.1"
|
||||
@@ -48,21 +50,21 @@ jqwik = "1.7.2"
|
||||
junit = "5.9.2"
|
||||
|
||||
# Build tools
|
||||
cctJavadoc = "1.7.0"
|
||||
cctJavadoc = "1.8.0"
|
||||
checkstyle = "10.3.4"
|
||||
curseForgeGradle = "1.0.14"
|
||||
errorProne-core = "2.18.0"
|
||||
errorProne-plugin = "3.0.1"
|
||||
fabric-loom = "1.2.7"
|
||||
forgeGradle = "6.0.6"
|
||||
fabric-loom = "1.3.7"
|
||||
forgeGradle = "6.0.8"
|
||||
githubRelease = "2.2.12"
|
||||
ideaExt = "1.1.6"
|
||||
illuaminate = "0.1.0-28-ga7efd71"
|
||||
illuaminate = "0.1.0-40-g975cbc3"
|
||||
librarian = "1.+"
|
||||
minotaur = "2.+"
|
||||
mixinGradle = "0.7.+"
|
||||
nullAway = "0.9.9"
|
||||
quiltflower = "1.8.0"
|
||||
quiltflower = "1.10.0"
|
||||
spotless = "6.17.0"
|
||||
taskTree = "2.1.1"
|
||||
vanillaGradle = "0.2.1-SNAPSHOT"
|
||||
@@ -92,6 +94,8 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||
# Minecraft mods
|
||||
fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" }
|
||||
fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" }
|
||||
fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" }
|
||||
emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" }
|
||||
iris = { module = "maven.modrinth:iris", version.ref = "iris" }
|
||||
jei-api = { module = "mezz.jei:jei-1.19.4-common-api", version.ref = "jei" }
|
||||
jei-fabric = { module = "mezz.jei:jei-1.19.4-fabric", version.ref = "jei" }
|
||||
@@ -152,7 +156,7 @@ externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"]
|
||||
externalMods-forge-compile = ["oculus", "jei-api"]
|
||||
externalMods-forge-runtime = ["jei-forge"]
|
||||
externalMods-fabric = ["nightConfig-core", "nightConfig-toml"]
|
||||
externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin"]
|
||||
externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"]
|
||||
externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
|
||||
|
||||
# Testing
|
||||
|
@@ -12,7 +12,6 @@
|
||||
/projects/core/src/test/resources/test-rom
|
||||
/projects/web/src/mount)
|
||||
|
||||
|
||||
(doc
|
||||
; Also defined in projects/web/build.gradle.kts
|
||||
(destination /projects/web/build/illuaminate)
|
||||
@@ -50,6 +49,8 @@
|
||||
(at /
|
||||
(linters
|
||||
syntax:string-index
|
||||
doc:docusaurus-admonition
|
||||
doc:ldoc-reference
|
||||
|
||||
;; It'd be nice to avoid this, but right now there's a lot of instances of
|
||||
;; it.
|
||||
@@ -82,23 +83,19 @@
|
||||
;; isn't smart enough.
|
||||
sleep write printError read rs)))
|
||||
|
||||
;; We disable the unused global linter in bios.lua and the APIs. In the future
|
||||
;; hopefully we'll get illuaminate to handle this.
|
||||
;; We disable the unused global linter in bios.lua, APIs and our documentation
|
||||
;; stubs docs. In the future hopefully we'll get illuaminate to handle this.
|
||||
(at
|
||||
(/projects/core/src/main/resources/data/computercraft/lua/bios.lua
|
||||
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/)
|
||||
(linters -var:unused-global)
|
||||
(lint (allow-toplevel-global true)))
|
||||
|
||||
;; Silence some variable warnings in documentation stubs.
|
||||
(at (/doc/stub/ /projects/forge/build/docs/luaJavadoc/)
|
||||
(/doc/stub/
|
||||
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
|
||||
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/
|
||||
/projects/forge/build/docs/luaJavadoc/)
|
||||
(linters -var:unused-global)
|
||||
(lint (allow-toplevel-global true)))
|
||||
|
||||
;; Suppress warnings for currently undocumented modules.
|
||||
(at
|
||||
(; Lua APIs
|
||||
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua
|
||||
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua)
|
||||
|
||||
(linters -doc:undocumented -doc:undocumented-arg -doc:undocumented-return))
|
||||
|
@@ -11,10 +11,13 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.client.resources.model.UnbakedModel;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides models for a {@link ITurtleUpgrade}.
|
||||
@@ -50,9 +53,24 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
|
||||
return getModel(upgrade, (ITurtleAccess) null, side);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getCraftingItem()
|
||||
* crafting item}.
|
||||
* Get a list of models that this turtle modeller depends on.
|
||||
* <p>
|
||||
* Models included in this list will be loaded and baked alongside item and block models, and so may be referenced
|
||||
* by {@link TransformedModel#of(ResourceLocation)}. You do not need to override this method if you will load models
|
||||
* by other means.
|
||||
*
|
||||
* @return A list of models that this modeller depends on.
|
||||
* @see UnbakedModel#getDependencies()
|
||||
*/
|
||||
default Collection<ResourceLocation> getDependencies() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getUpgradeItem(CompoundTag)}
|
||||
* upgrade item}.
|
||||
* <p>
|
||||
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated}
|
||||
* model type. It will not appear correct for 3D models with additional depth, such as blocks.
|
||||
@@ -62,7 +80,7 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> flatItem() {
|
||||
return (TurtleUpgradeModeller<T>) TurtleUpgradeModellers.FLAT_ITEM;
|
||||
return (TurtleUpgradeModeller<T>) TurtleUpgradeModellers.UPGRADE_ITEM;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +92,8 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
|
||||
* @return The constructed modeller.
|
||||
*/
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ModelResourceLocation left, ModelResourceLocation right) {
|
||||
return (upgrade, turtle, side) -> TransformedModel.of(side == TurtleSide.LEFT ? left : right);
|
||||
// TODO(1.21.0): Remove this.
|
||||
return sided((ResourceLocation) left, right);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,6 +105,16 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
|
||||
* @return The constructed modeller.
|
||||
*/
|
||||
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ResourceLocation left, ResourceLocation right) {
|
||||
return (upgrade, turtle, side) -> TransformedModel.of(side == TurtleSide.LEFT ? left : right);
|
||||
return new TurtleUpgradeModeller<>() {
|
||||
@Override
|
||||
public TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side) {
|
||||
return TransformedModel.of(side == TurtleSide.LEFT ? left : right);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ResourceLocation> getDependencies() {
|
||||
return List.of(left, right);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -6,13 +6,20 @@ package dan200.computercraft.api.client.turtle;
|
||||
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.client.TransformedModel;
|
||||
import dan200.computercraft.api.turtle.ITurtleAccess;
|
||||
import dan200.computercraft.api.turtle.ITurtleUpgrade;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.impl.client.ClientPlatformHelper;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
class TurtleUpgradeModellers {
|
||||
private static final Transformation leftTransform = getMatrixFor(-0.40625f);
|
||||
private static final Transformation rightTransform = getMatrixFor(0.40625f);
|
||||
private static final Transformation leftTransform = getMatrixFor(-0.4065f);
|
||||
private static final Transformation rightTransform = getMatrixFor(0.4065f);
|
||||
|
||||
private static Transformation getMatrixFor(float offset) {
|
||||
var matrix = new Matrix4f();
|
||||
@@ -26,6 +33,23 @@ class TurtleUpgradeModellers {
|
||||
return new Transformation(matrix);
|
||||
}
|
||||
|
||||
static final TurtleUpgradeModeller<ITurtleUpgrade> FLAT_ITEM = (upgrade, turtle, side) ->
|
||||
TransformedModel.of(upgrade.getCraftingItem(), side == TurtleSide.LEFT ? leftTransform : rightTransform);
|
||||
static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
|
||||
|
||||
private static class UpgradeItemModeller implements TurtleUpgradeModeller<ITurtleUpgrade> {
|
||||
@Override
|
||||
public TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess turtle, TurtleSide side) {
|
||||
return getModel(turtle == null ? upgrade.getCraftingItem() : upgrade.getUpgradeItem(turtle.getUpgradeNBTData(side)), side);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransformedModel getModel(ITurtleUpgrade upgrade, CompoundTag data, TurtleSide side) {
|
||||
return getModel(upgrade.getUpgradeItem(data), side);
|
||||
}
|
||||
|
||||
private TransformedModel getModel(ItemStack stack, TurtleSide side) {
|
||||
var model = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getItemModel(stack);
|
||||
if (stack.hasFoil()) model = ClientPlatformHelper.get().createdFoiledModel(model);
|
||||
return new TransformedModel(model, side == TurtleSide.LEFT ? leftTransform : rightTransform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
package dan200.computercraft.impl.client;
|
||||
|
||||
import dan200.computercraft.impl.Services;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelManager;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
@@ -24,6 +25,15 @@ public interface ClientPlatformHelper {
|
||||
*/
|
||||
BakedModel getModel(ModelManager manager, ResourceLocation location);
|
||||
|
||||
/**
|
||||
* Wrap this model in a version which renders a foil/enchantment glint.
|
||||
*
|
||||
* @param model The model to wrap.
|
||||
* @return The wrapped model.
|
||||
* @see RenderType#glint()
|
||||
*/
|
||||
BakedModel createdFoiledModel(BakedModel model);
|
||||
|
||||
static ClientPlatformHelper get() {
|
||||
var instance = Instance.INSTANCE;
|
||||
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;
|
||||
|
@@ -0,0 +1,49 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.api.turtle;
|
||||
|
||||
import net.minecraft.util.StringRepresentable;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Indicates if an equipped turtle item will consume durability.
|
||||
*
|
||||
* @see TurtleUpgradeDataProvider.ToolBuilder#consumeDurability(TurtleToolDurability)
|
||||
*/
|
||||
public enum TurtleToolDurability implements StringRepresentable {
|
||||
/**
|
||||
* The equipped tool always consumes durability when using.
|
||||
*/
|
||||
ALWAYS("always"),
|
||||
|
||||
/**
|
||||
* The equipped tool consumes durability if it is {@linkplain ItemStack#isEnchanted() enchanted} or has
|
||||
* {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}.
|
||||
*/
|
||||
WHEN_ENCHANTED("when_enchanted"),
|
||||
|
||||
/**
|
||||
* The equipped tool never consumes durability. Tools which have been damaged cannot be used as upgrades.
|
||||
*/
|
||||
NEVER("never");
|
||||
|
||||
private final String serialisedName;
|
||||
|
||||
/**
|
||||
* The codec which may be used for serialising/deserialising {@link TurtleToolDurability}s.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static final StringRepresentable.EnumCodec<TurtleToolDurability> CODEC = StringRepresentable.fromEnum(TurtleToolDurability::values);
|
||||
|
||||
TurtleToolDurability(String serialisedName) {
|
||||
this.serialisedName = serialisedName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerializedName() {
|
||||
return serialisedName;
|
||||
}
|
||||
}
|
@@ -13,8 +13,10 @@ import net.minecraft.data.DataGenerator;
|
||||
import net.minecraft.data.PackOutput;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
@@ -61,6 +63,8 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
|
||||
private @Nullable Item craftingItem;
|
||||
private @Nullable Float damageMultiplier = null;
|
||||
private @Nullable TagKey<Block> breakable;
|
||||
private boolean allowEnchantments = false;
|
||||
private TurtleToolDurability consumeDurability = TurtleToolDurability.NEVER;
|
||||
|
||||
ToolBuilder(ResourceLocation id, TurtleUpgradeSerialiser<?> serialiser, Item toolItem) {
|
||||
this.id = id;
|
||||
@@ -104,6 +108,28 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that this upgrade allows items which have been {@linkplain ItemStack#isEnchanted() enchanted} or have
|
||||
* {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}.
|
||||
*
|
||||
* @return The tool builder, for further use.
|
||||
*/
|
||||
public ToolBuilder allowEnchantments() {
|
||||
allowEnchantments = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set when the tool will consume durability.
|
||||
*
|
||||
* @param durability The durability predicate.
|
||||
* @return The tool builder, for further use.
|
||||
*/
|
||||
public ToolBuilder consumeDurability(TurtleToolDurability durability) {
|
||||
consumeDurability = durability;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a list of breakable blocks. If not given, the tool can break all blocks. If given, only blocks
|
||||
* in this tag, those in {@link ComputerCraftTags.Blocks#TURTLE_ALWAYS_BREAKABLE} and "insta-mine" ones can
|
||||
@@ -132,6 +158,10 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
|
||||
}
|
||||
if (damageMultiplier != null) s.addProperty("damageMultiplier", damageMultiplier);
|
||||
if (breakable != null) s.addProperty("breakable", breakable.location().toString());
|
||||
if (allowEnchantments) s.addProperty("allowEnchantments", true);
|
||||
if (consumeDurability != TurtleToolDurability.NEVER) {
|
||||
s.addProperty("consumeDurability", consumeDurability.getSerializedName());
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@@ -23,11 +23,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
@@ -167,5 +163,21 @@ public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends Upgra
|
||||
public void add(Consumer<Upgrade<R>> add) {
|
||||
add.accept(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link Upgrade} which requires the given mod to be present.
|
||||
* <p>
|
||||
* This uses mod-loader-specific hooks (Forge's crafting conditions and Fabric's resource conditions). If using
|
||||
* this in a multi-loader setup, you must generate resources separately for the two loaders.
|
||||
*
|
||||
* @param modId The id of the mod.
|
||||
* @return A new upgrade instance.
|
||||
*/
|
||||
public Upgrade<R> requireMod(String modId) {
|
||||
return new Upgrade<>(id, serialiser, json -> {
|
||||
PlatformHelper.get().addRequiredModCondition(json, modId);
|
||||
serialise.accept(json);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@
|
||||
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dan200.computercraft.api.upgrades.UpgradeDataProvider;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
@@ -63,6 +65,15 @@ public interface PlatformHelper {
|
||||
return item.getTag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a resource condition which requires a mod to be loaded. This should be used by data providers such as
|
||||
* {@link UpgradeDataProvider}.
|
||||
*
|
||||
* @param object The JSON object we're generating.
|
||||
* @param modId The mod ID that we require.
|
||||
*/
|
||||
void addRequiredModCondition(JsonObject object, String modId);
|
||||
|
||||
final class Instance {
|
||||
static final @Nullable PlatformHelper INSTANCE;
|
||||
static final @Nullable Throwable ERROR;
|
||||
|
@@ -26,6 +26,7 @@ dependencies {
|
||||
clientImplementation(clientClasses(project(":common-api")))
|
||||
|
||||
compileOnly(libs.bundles.externalMods.common)
|
||||
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
|
||||
|
||||
compileOnly(libs.mixin)
|
||||
annotationProcessorEverywhere(libs.autoService)
|
||||
|
@@ -13,6 +13,7 @@ import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
|
||||
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
|
||||
import dan200.computercraft.client.turtle.TurtleModemModeller;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.common.IColouredItem;
|
||||
@@ -20,6 +21,7 @@ import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
|
||||
import dan200.computercraft.shared.media.items.DiskItem;
|
||||
import dan200.computercraft.shared.media.items.TreasureDiskItem;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.color.item.ItemColor;
|
||||
import net.minecraft.client.gui.screens.MenuScreens;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
@@ -29,6 +31,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderers;
|
||||
import net.minecraft.client.renderer.item.ClampedItemPropertyFunction;
|
||||
import net.minecraft.client.renderer.item.ItemProperties;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.PreparableReloadListener;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.Item;
|
||||
@@ -106,25 +109,11 @@ public final class ClientRegistry {
|
||||
for (var item : items) ItemProperties.register(item.get(), id, getter);
|
||||
}
|
||||
|
||||
public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) {
|
||||
register.accept(GuiSprites.initialise(minecraft.getTextureManager()));
|
||||
}
|
||||
|
||||
private static final String[] EXTRA_MODELS = new String[]{
|
||||
// Turtle upgrades
|
||||
"block/turtle_modem_normal_off_left",
|
||||
"block/turtle_modem_normal_on_left",
|
||||
"block/turtle_modem_normal_off_right",
|
||||
"block/turtle_modem_normal_on_right",
|
||||
|
||||
"block/turtle_modem_advanced_off_left",
|
||||
"block/turtle_modem_advanced_on_left",
|
||||
"block/turtle_modem_advanced_off_right",
|
||||
"block/turtle_modem_advanced_on_right",
|
||||
|
||||
"block/turtle_crafting_table_left",
|
||||
"block/turtle_crafting_table_right",
|
||||
|
||||
"block/turtle_speaker_left",
|
||||
"block/turtle_speaker_right",
|
||||
|
||||
// Turtle block renderer
|
||||
"block/turtle_colour",
|
||||
"block/turtle_elf_overlay",
|
||||
"block/turtle_rainbow_overlay",
|
||||
@@ -133,6 +122,7 @@ public final class ClientRegistry {
|
||||
|
||||
public static void registerExtraModels(Consumer<ResourceLocation> register) {
|
||||
for (var model : EXTRA_MODELS) register.accept(new ResourceLocation(ComputerCraftAPI.MOD_ID, model));
|
||||
TurtleUpgradeModellers.getDependencies().forEach(register);
|
||||
}
|
||||
|
||||
public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {
|
||||
|
@@ -5,15 +5,18 @@
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
|
||||
import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER;
|
||||
import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP;
|
||||
|
||||
/**
|
||||
* A GUI for computers which renders the terminal (and border), but with no UI elements.
|
||||
@@ -39,10 +42,16 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra
|
||||
public void renderBg(PoseStack stack, float partialTicks, int mouseX, int mouseY) {
|
||||
// Draw a border around the terminal
|
||||
var terminal = getTerminal();
|
||||
var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
|
||||
|
||||
var spriteRenderer = SpriteRenderer.createForGui(stack, buffers.getBuffer(RenderTypes.GUI_SPRITES));
|
||||
var computerTextures = GuiSprites.getComputerTextures(family);
|
||||
|
||||
ComputerBorderRenderer.render(
|
||||
stack.last().pose(), ComputerBorderRenderer.getTexture(family), terminal.getX(), terminal.getY(),
|
||||
FULL_BRIGHT_LIGHTMAP, terminal.getWidth(), terminal.getHeight()
|
||||
spriteRenderer, computerTextures,
|
||||
terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
|
||||
);
|
||||
ComputerSidebar.renderBackground(stack, leftPos, topPos + sidebarYOffset);
|
||||
ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
|
||||
buffers.endBatch();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,127 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.gui;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.data.client.ClientDataProviders;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.client.renderer.texture.TextureManager;
|
||||
import net.minecraft.client.resources.TextureAtlasHolder;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Sprite sheet for all GUI texutres in the mod.
|
||||
*/
|
||||
public final class GuiSprites extends TextureAtlasHolder {
|
||||
public static final ResourceLocation SPRITE_SHEET = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui");
|
||||
public static final ResourceLocation TEXTURE = SPRITE_SHEET.withPath(x -> "textures/atlas/" + x + ".png");
|
||||
|
||||
public static final ButtonTextures TURNED_OFF = button("turned_off");
|
||||
public static final ButtonTextures TURNED_ON = button("turned_on");
|
||||
public static final ButtonTextures TERMINATE = button("terminate");
|
||||
|
||||
public static final ComputerTextures COMPUTER_NORMAL = computer("normal", true, true);
|
||||
public static final ComputerTextures COMPUTER_ADVANCED = computer("advanced", true, true);
|
||||
public static final ComputerTextures COMPUTER_COMMAND = computer("command", false, true);
|
||||
public static final ComputerTextures COMPUTER_COLOUR = computer("colour", true, false);
|
||||
|
||||
private static ButtonTextures button(String name) {
|
||||
return new ButtonTextures(
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name),
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/buttons/" + name + "_hover")
|
||||
);
|
||||
}
|
||||
|
||||
private static ComputerTextures computer(String name, boolean pocket, boolean sidebar) {
|
||||
return new ComputerTextures(
|
||||
new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/border_" + name),
|
||||
pocket ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/pocket_bottom_" + name) : null,
|
||||
sidebar ? new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/sidebar_" + name) : null
|
||||
);
|
||||
}
|
||||
|
||||
private static @Nullable GuiSprites instance;
|
||||
|
||||
private GuiSprites(TextureManager textureManager) {
|
||||
super(textureManager, TEXTURE, SPRITE_SHEET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the singleton {@link GuiSprites} instance.
|
||||
*
|
||||
* @param textureManager The current texture manager.
|
||||
* @return The singleton {@link GuiSprites} instance, to register as resource reload listener.
|
||||
*/
|
||||
public static GuiSprites initialise(TextureManager textureManager) {
|
||||
if (instance != null) throw new IllegalStateException("GuiSprites has already been initialised");
|
||||
return instance = new GuiSprites(textureManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a texture on the atlas.
|
||||
*
|
||||
* @param texture The texture to find.
|
||||
* @return The sprite on the atlas.
|
||||
*/
|
||||
public static TextureAtlasSprite get(ResourceLocation texture) {
|
||||
if (instance == null) throw new IllegalStateException("GuiSprites has not been initialised");
|
||||
return instance.getSprite(texture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate textures to use for a particular computer family.
|
||||
*
|
||||
* @param family The computer family.
|
||||
* @return The family-specific textures.
|
||||
*/
|
||||
public static ComputerTextures getComputerTextures(ComputerFamily family) {
|
||||
return switch (family) {
|
||||
case NORMAL -> COMPUTER_NORMAL;
|
||||
case ADVANCED -> COMPUTER_ADVANCED;
|
||||
case COMMAND -> COMPUTER_COMMAND;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of sprites for a button, with both a normal and "active" state.
|
||||
*
|
||||
* @param normal The normal texture for the button.
|
||||
* @param active The texture for the button when it is active (hovered or focused).
|
||||
*/
|
||||
public record ButtonTextures(ResourceLocation normal, ResourceLocation active) {
|
||||
public TextureAtlasSprite get(boolean active) {
|
||||
return GuiSprites.get(active ? this.active : normal);
|
||||
}
|
||||
|
||||
public Stream<ResourceLocation> textures() {
|
||||
return Stream.of(normal, active);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the set of sprites for a computer family.
|
||||
*
|
||||
* @param border The texture for the computer's border.
|
||||
* @param pocketBottom The texture for the bottom of a pocket computer.
|
||||
* @param sidebar The texture for the computer sidebar.
|
||||
* @see ComputerBorderRenderer
|
||||
* @see ClientDataProviders
|
||||
*/
|
||||
public record ComputerTextures(
|
||||
ResourceLocation border,
|
||||
@Nullable ResourceLocation pocketBottom,
|
||||
@Nullable ResourceLocation sidebar
|
||||
) {
|
||||
public Stream<ResourceLocation> textures() {
|
||||
return Stream.of(border, pocketBottom, sidebar).filter(Objects::nonNull);
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,13 +6,16 @@ package dan200.computercraft.client.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.gui.widgets.ComputerSidebar;
|
||||
import dan200.computercraft.client.gui.widgets.TerminalWidget;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
@@ -60,7 +63,10 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
|
||||
);
|
||||
}
|
||||
|
||||
RenderSystem.setShaderTexture(0, advanced ? ComputerBorderRenderer.BACKGROUND_ADVANCED : ComputerBorderRenderer.BACKGROUND_NORMAL);
|
||||
ComputerSidebar.renderBackground(transform, leftPos, topPos + sidebarYOffset);
|
||||
// Render sidebar
|
||||
var buffers = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
|
||||
var spriteRenderer = SpriteRenderer.createForGui(transform, buffers.getBuffer(RenderTypes.GUI_SPRITES));
|
||||
ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset);
|
||||
buffers.endBatch();
|
||||
}
|
||||
}
|
||||
|
@@ -4,16 +4,13 @@
|
||||
|
||||
package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage;
|
||||
import dan200.computercraft.client.render.ComputerBorderRenderer;
|
||||
import dan200.computercraft.client.render.SpriteRenderer;
|
||||
import dan200.computercraft.shared.computer.core.InputHandler;
|
||||
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
@@ -22,22 +19,18 @@ import java.util.function.Consumer;
|
||||
* Registers buttons to interact with a computer.
|
||||
*/
|
||||
public final class ComputerSidebar {
|
||||
private static final ResourceLocation TEXTURE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/buttons.png");
|
||||
|
||||
private static final int TEX_SIZE = 64;
|
||||
|
||||
private static final int ICON_WIDTH = 12;
|
||||
private static final int ICON_HEIGHT = 12;
|
||||
private static final int ICON_MARGIN = 2;
|
||||
|
||||
private static final int ICON_TEX_Y_DIFF = 14;
|
||||
|
||||
private static final int CORNERS_BORDER = 3;
|
||||
private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN;
|
||||
|
||||
private static final int BUTTONS = 2;
|
||||
private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
|
||||
|
||||
private static final int TEX_HEIGHT = 14;
|
||||
|
||||
private ComputerSidebar() {
|
||||
}
|
||||
|
||||
@@ -51,16 +44,18 @@ public final class ComputerSidebar {
|
||||
Component.translatable("gui.computercraft.tooltip.turn_off.key")
|
||||
);
|
||||
add.accept(new DynamicImageButton(
|
||||
x, y, ICON_WIDTH, ICON_HEIGHT, () -> isOn.getAsBoolean() ? 15 : 1, 1, ICON_TEX_Y_DIFF,
|
||||
TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer(isOn, input),
|
||||
x, y, ICON_WIDTH, ICON_HEIGHT,
|
||||
h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h),
|
||||
b -> toggleComputer(isOn, input),
|
||||
() -> isOn.getAsBoolean() ? turnOff : turnOn
|
||||
));
|
||||
|
||||
y += ICON_HEIGHT + ICON_MARGIN * 2;
|
||||
|
||||
add.accept(new DynamicImageButton(
|
||||
x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF,
|
||||
TEXTURE, TEX_SIZE, TEX_SIZE, b -> input.queueEvent("terminate"),
|
||||
x, y, ICON_WIDTH, ICON_HEIGHT,
|
||||
GuiSprites.TERMINATE::get,
|
||||
b -> input.queueEvent("terminate"),
|
||||
new HintedMessage(
|
||||
Component.translatable("gui.computercraft.tooltip.terminate"),
|
||||
Component.translatable("gui.computercraft.tooltip.terminate.key")
|
||||
@@ -68,22 +63,12 @@ public final class ComputerSidebar {
|
||||
));
|
||||
}
|
||||
|
||||
public static void renderBackground(PoseStack transform, int x, int y) {
|
||||
Screen.blit(transform,
|
||||
x, y, 0, 102, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER,
|
||||
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
|
||||
);
|
||||
public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) {
|
||||
var texture = textures.sidebar();
|
||||
if (texture == null) throw new NullPointerException(textures + " has no sidebar texture");
|
||||
var sprite = GuiSprites.get(texture);
|
||||
|
||||
Screen.blit(transform,
|
||||
x, y + FULL_BORDER, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT - FULL_BORDER * 2,
|
||||
0, 107, AbstractComputerMenu.SIDEBAR_WIDTH, 4,
|
||||
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
|
||||
);
|
||||
|
||||
Screen.blit(transform,
|
||||
x, y + HEIGHT - FULL_BORDER, 0, 111, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER,
|
||||
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE
|
||||
);
|
||||
renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT);
|
||||
}
|
||||
|
||||
private static void toggleComputer(BooleanSupplier isOn, InputHandler input) {
|
||||
|
@@ -6,14 +6,14 @@ package dan200.computercraft.client.gui.widgets;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
@@ -21,61 +21,41 @@ import java.util.function.Supplier;
|
||||
* dynamically.
|
||||
*/
|
||||
public class DynamicImageButton extends Button {
|
||||
private final ResourceLocation texture;
|
||||
private final IntSupplier xTexStart;
|
||||
private final int yTexStart;
|
||||
private final int yDiffTex;
|
||||
private final int textureWidth;
|
||||
private final int textureHeight;
|
||||
private final Boolean2ObjectFunction<TextureAtlasSprite> texture;
|
||||
private final Supplier<HintedMessage> message;
|
||||
|
||||
public DynamicImageButton(
|
||||
int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex,
|
||||
ResourceLocation texture, int textureWidth, int textureHeight,
|
||||
OnPress onPress, HintedMessage message
|
||||
int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress,
|
||||
HintedMessage message
|
||||
) {
|
||||
this(
|
||||
x, y, width, height, () -> xTexStart, yTexStart, yDiffTex,
|
||||
texture, textureWidth, textureHeight,
|
||||
onPress, () -> message
|
||||
);
|
||||
this(x, y, width, height, texture, onPress, () -> message);
|
||||
}
|
||||
|
||||
public DynamicImageButton(
|
||||
int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex,
|
||||
ResourceLocation texture, int textureWidth, int textureHeight,
|
||||
int x, int y, int width, int height,
|
||||
Boolean2ObjectFunction<TextureAtlasSprite> texture,
|
||||
OnPress onPress, Supplier<HintedMessage> message
|
||||
) {
|
||||
super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION);
|
||||
this.textureWidth = textureWidth;
|
||||
this.textureHeight = textureHeight;
|
||||
this.xTexStart = xTexStart;
|
||||
this.yTexStart = yTexStart;
|
||||
this.yDiffTex = yDiffTex;
|
||||
this.texture = texture;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderWidget(PoseStack stack, int mouseX, int mouseY, float partialTicks) {
|
||||
RenderSystem.setShaderTexture(0, texture);
|
||||
var texture = this.texture.get(isHoveredOrFocused());
|
||||
RenderSystem.setShaderTexture(0, texture.atlasLocation());
|
||||
RenderSystem.disableDepthTest();
|
||||
|
||||
var yTex = yTexStart;
|
||||
if (isHoveredOrFocused()) yTex += yDiffTex;
|
||||
|
||||
blit(stack, getX(), getY(), xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight);
|
||||
blit(stack, getX(), getY(), 0, width, height, texture);
|
||||
RenderSystem.enableDepthTest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getMessage() {
|
||||
return message.get().message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(PoseStack stack, int mouseX, int mouseY, float partialTicks) {
|
||||
setTooltip(message.get().tooltip());
|
||||
var message = this.message.get();
|
||||
setMessage(message.message());
|
||||
setTooltip(message.tooltip());
|
||||
super.render(stack, mouseX, mouseY, partialTicks);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,44 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.integration.emi;
|
||||
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
|
||||
import dan200.computercraft.shared.turtle.items.TurtleItem;
|
||||
import dev.emi.emi.api.EmiEntrypoint;
|
||||
import dev.emi.emi.api.EmiPlugin;
|
||||
import dev.emi.emi.api.EmiRegistry;
|
||||
import dev.emi.emi.api.stack.Comparison;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
@EmiEntrypoint
|
||||
public class EMIComputerCraft implements EmiPlugin {
|
||||
@Override
|
||||
public void register(EmiRegistry registry) {
|
||||
registry.setDefaultComparison(ModRegistry.Items.TURTLE_NORMAL.get(), turtleComparison);
|
||||
registry.setDefaultComparison(ModRegistry.Items.TURTLE_ADVANCED.get(), turtleComparison);
|
||||
|
||||
registry.setDefaultComparison(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get(), pocketComparison);
|
||||
registry.setDefaultComparison(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get(), pocketComparison);
|
||||
}
|
||||
|
||||
private static final Comparison turtleComparison = compareStacks((left, right) ->
|
||||
left.getItem() instanceof TurtleItem turtle
|
||||
&& turtle.getUpgrade(left, TurtleSide.LEFT) == turtle.getUpgrade(right, TurtleSide.LEFT)
|
||||
&& turtle.getUpgrade(left, TurtleSide.RIGHT) == turtle.getUpgrade(right, TurtleSide.RIGHT));
|
||||
|
||||
private static final Comparison pocketComparison = compareStacks((left, right) ->
|
||||
left.getItem() instanceof PocketComputerItem && PocketComputerItem.getUpgrade(left) == PocketComputerItem.getUpgrade(right));
|
||||
|
||||
private static Comparison compareStacks(BiPredicate<ItemStack, ItemStack> test) {
|
||||
return Comparison.of((left, right) -> {
|
||||
ItemStack leftStack = left.getItemStack(), rightStack = right.getItemStack();
|
||||
return leftStack.getItem() == rightStack.getItem() && test.test(leftStack, rightStack);
|
||||
});
|
||||
}
|
||||
}
|
@@ -26,13 +26,14 @@ import java.util.List;
|
||||
* <p>
|
||||
* This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
|
||||
*/
|
||||
public final class ModelTransformer {
|
||||
public static final int[] ORDER = new int[]{ 3, 2, 1, 0 };
|
||||
public static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
|
||||
public class ModelTransformer {
|
||||
private static final int[] INVERSE_ORDER = new int[]{ 3, 2, 1, 0 };
|
||||
|
||||
private static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
|
||||
private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION);
|
||||
|
||||
private final Matrix4f transformation;
|
||||
private final boolean invert;
|
||||
protected final Matrix4f transformation;
|
||||
protected final boolean invert;
|
||||
private @Nullable TransformedQuads cache;
|
||||
|
||||
public ModelTransformer(Transformation transformation) {
|
||||
@@ -60,7 +61,7 @@ public final class ModelTransformer {
|
||||
for (var i = 0; i < 4; i++) {
|
||||
var inStart = STRIDE * i;
|
||||
// Reverse the order of the quads if we're inverting
|
||||
var outStart = STRIDE * (invert ? ORDER[i] : i);
|
||||
var outStart = getVertexOffset(i, invert);
|
||||
System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
|
||||
|
||||
// Apply the matrix to our position
|
||||
@@ -84,6 +85,10 @@ public final class ModelTransformer {
|
||||
return new BakedQuad(outputData, quad.getTintIndex(), direction, quad.getSprite(), quad.isShade());
|
||||
}
|
||||
|
||||
public static int getVertexOffset(int vertex, boolean invert) {
|
||||
return (invert ? ModelTransformer.INVERSE_ORDER[vertex] : vertex) * ModelTransformer.STRIDE;
|
||||
}
|
||||
|
||||
private record TransformedQuads(List<BakedQuad> original, List<BakedQuad> transformed) {
|
||||
}
|
||||
|
||||
|
@@ -4,8 +4,13 @@
|
||||
|
||||
package dan200.computercraft.client.platform;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.network.server.ServerNetworkContext;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
|
||||
static ClientPlatformHelper get() {
|
||||
@@ -18,4 +23,16 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
|
||||
* @param message The message to send.
|
||||
*/
|
||||
void sendToServer(NetworkMessage<ServerNetworkContext> message);
|
||||
|
||||
/**
|
||||
* Render a {@link BakedModel}, using any loader-specific hooks.
|
||||
*
|
||||
* @param transform The current matrix transformation to apply.
|
||||
* @param buffers The current pool of render buffers.
|
||||
* @param model The model to draw.
|
||||
* @param lightmapCoord The current packed lightmap coordinate.
|
||||
* @param overlayLight The current overlay light.
|
||||
* @param tints Block colour tints to apply to the model.
|
||||
*/
|
||||
void renderBakedModel(PoseStack transform, MultiBufferSource buffers, BakedModel model, int lightmapCoord, int overlayLight, @Nullable int[] tints);
|
||||
}
|
||||
|
@@ -4,25 +4,17 @@
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.Tesselator;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.joml.Matrix4f;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
|
||||
import static dan200.computercraft.client.render.SpriteRenderer.u;
|
||||
import static dan200.computercraft.client.render.SpriteRenderer.v;
|
||||
|
||||
/**
|
||||
* Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or
|
||||
* {@linkplain PocketItemRenderer in-hand pocket computers}.
|
||||
*/
|
||||
public class ComputerBorderRenderer {
|
||||
public static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_normal.png");
|
||||
public static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_advanced.png");
|
||||
public static final ResourceLocation BACKGROUND_COMMAND = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_command.png");
|
||||
public static final ResourceLocation BACKGROUND_COLOUR = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_colour.png");
|
||||
|
||||
public final class ComputerBorderRenderer {
|
||||
/**
|
||||
* The margin between the terminal and its border.
|
||||
*/
|
||||
@@ -33,100 +25,51 @@ public class ComputerBorderRenderer {
|
||||
*/
|
||||
public static final int BORDER = 12;
|
||||
|
||||
private static final int CORNER_TOP_Y = 28;
|
||||
private static final int CORNER_BOTTOM_Y = CORNER_TOP_Y + BORDER;
|
||||
private static final int CORNER_LEFT_X = BORDER;
|
||||
private static final int CORNER_RIGHT_X = CORNER_LEFT_X + BORDER;
|
||||
private static final int BORDER_RIGHT_X = 36;
|
||||
private static final int LIGHT_BORDER_Y = 56;
|
||||
private static final int LIGHT_CORNER_Y = 80;
|
||||
|
||||
public static final int LIGHT_HEIGHT = 8;
|
||||
|
||||
public static final int TEX_SIZE = 256;
|
||||
private static final float TEX_SCALE = 1 / (float) TEX_SIZE;
|
||||
private static final int TEX_SIZE = 36;
|
||||
|
||||
private final Matrix4f transform;
|
||||
private final VertexConsumer builder;
|
||||
private final int light;
|
||||
private final int z;
|
||||
private final float r, g, b;
|
||||
|
||||
public ComputerBorderRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, float r, float g, float b) {
|
||||
this.transform = transform;
|
||||
this.builder = builder;
|
||||
this.z = z;
|
||||
this.light = light;
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
private ComputerBorderRenderer() {
|
||||
}
|
||||
|
||||
public static ResourceLocation getTexture(ComputerFamily family) {
|
||||
return switch (family) {
|
||||
case NORMAL -> BACKGROUND_NORMAL;
|
||||
case ADVANCED -> BACKGROUND_ADVANCED;
|
||||
case COMMAND -> BACKGROUND_COMMAND;
|
||||
};
|
||||
}
|
||||
|
||||
public static RenderType getRenderType(ResourceLocation location) {
|
||||
// See note in RenderTypes about why we use text rather than anything intuitive.
|
||||
return RenderType.text(location);
|
||||
}
|
||||
|
||||
public static void render(Matrix4f transform, ResourceLocation location, int x, int y, int light, int width, int height) {
|
||||
var source = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
|
||||
render(transform, source.getBuffer(getRenderType(location)), x, y, 1, light, width, height, false, 1, 1, 1);
|
||||
source.endBatch();
|
||||
}
|
||||
|
||||
public static void render(Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int light, int width, int height, boolean withLight, float r, float g, float b) {
|
||||
new ComputerBorderRenderer(transform, buffer, z, light, r, g, b).doRender(x, y, width, height, withLight);
|
||||
}
|
||||
|
||||
public void doRender(int x, int y, int width, int height, boolean withLight) {
|
||||
public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) {
|
||||
var endX = x + width;
|
||||
var endY = y + height;
|
||||
|
||||
// Vertical bars
|
||||
renderLine(x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y);
|
||||
renderLine(endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y);
|
||||
var border = GuiSprites.get(textures.border());
|
||||
|
||||
// Top bar
|
||||
renderLine(x, y - BORDER, 0, 0, endX - x, BORDER);
|
||||
renderCorner(x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y);
|
||||
renderCorner(endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y);
|
||||
blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER);
|
||||
blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER);
|
||||
blitBorder(renderer, border, endX, y - BORDER, BORDER * 2, 0, BORDER, BORDER);
|
||||
|
||||
// Vertical bars
|
||||
blitBorder(renderer, border, x - BORDER, y, 0, BORDER, BORDER, height);
|
||||
blitBorder(renderer, border, endX, y, BORDER * 2, BORDER, BORDER, height);
|
||||
|
||||
// Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the
|
||||
// pocket computer's lights).
|
||||
if (withLight) {
|
||||
renderTexture(x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT);
|
||||
renderTexture(x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT);
|
||||
renderTexture(endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT);
|
||||
var pocketBottomTexture = textures.pocketBottom();
|
||||
if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
|
||||
var pocketBottom = GuiSprites.get(pocketBottomTexture);
|
||||
|
||||
renderer.blitHorizontalSliced(
|
||||
pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT,
|
||||
BORDER, BORDER, BORDER * 3
|
||||
);
|
||||
} else {
|
||||
renderLine(x, endY, 0, BORDER, endX - x, BORDER);
|
||||
renderCorner(x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y);
|
||||
renderCorner(endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y);
|
||||
blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER);
|
||||
blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER);
|
||||
blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCorner(int x, int y, int u, int v) {
|
||||
renderTexture(x, y, u, v, BORDER, BORDER, BORDER, BORDER);
|
||||
}
|
||||
|
||||
private void renderLine(int x, int y, int u, int v, int width, int height) {
|
||||
renderTexture(x, y, u, v, width, height, BORDER, BORDER);
|
||||
}
|
||||
|
||||
private void renderTexture(int x, int y, int u, int v, int width, int height) {
|
||||
renderTexture(x, y, u, v, width, height, width, height);
|
||||
}
|
||||
|
||||
private void renderTexture(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) {
|
||||
builder.vertex(transform, x, y + height, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex();
|
||||
builder.vertex(transform, x + width, y + height, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex();
|
||||
builder.vertex(transform, x + width, y, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex();
|
||||
builder.vertex(transform, x, y, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex();
|
||||
private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) {
|
||||
renderer.blit(
|
||||
x, y, width, height,
|
||||
u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE),
|
||||
u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,103 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import dan200.computercraft.client.model.turtle.ModelTransformer;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.block.model.BakedQuad;
|
||||
import net.minecraft.client.renderer.entity.ItemRenderer;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utilities for rendering {@link BakedModel}s and {@link BakedQuad}s.
|
||||
*/
|
||||
public final class ModelRenderer {
|
||||
private ModelRenderer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a list of {@linkplain BakedQuad quads} to a buffer.
|
||||
* <p>
|
||||
* This is not intended to be used directly, but instead by {@link ClientPlatformHelper#renderBakedModel(PoseStack, MultiBufferSource, BakedModel, int, int, int[])}. The
|
||||
* implementation here is pretty similar to {@link ItemRenderer#renderQuadList(PoseStack, VertexConsumer, List, ItemStack, int, int)},
|
||||
* but supports inverted quads (i.e. those with a negative scale).
|
||||
*
|
||||
* @param transform The current matrix transformation to apply.
|
||||
* @param buffer The buffer to draw to.
|
||||
* @param quads The quads to draw.
|
||||
* @param lightmapCoord The current packed lightmap coordinate.
|
||||
* @param overlayLight The current overlay light.
|
||||
* @param tints Block colour tints to apply to the model.
|
||||
*/
|
||||
public static void renderQuads(PoseStack transform, VertexConsumer buffer, List<BakedQuad> quads, int lightmapCoord, int overlayLight, @Nullable int[] tints) {
|
||||
var matrix = transform.last();
|
||||
var inverted = matrix.pose().determinant() < 0;
|
||||
|
||||
for (var bakedquad : quads) {
|
||||
var tint = -1;
|
||||
if (tints != null && bakedquad.isTinted()) {
|
||||
var idx = bakedquad.getTintIndex();
|
||||
if (idx >= 0 && idx < tints.length) tint = tints[bakedquad.getTintIndex()];
|
||||
}
|
||||
|
||||
var r = (float) (tint >> 16 & 255) / 255.0F;
|
||||
var g = (float) (tint >> 8 & 255) / 255.0F;
|
||||
var b = (float) (tint & 255) / 255.0F;
|
||||
putBulkQuad(buffer, matrix, bakedquad, r, g, b, lightmapCoord, overlayLight, inverted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, int, int)} which
|
||||
* will reverse vertex order when the matrix is inverted.
|
||||
*
|
||||
* @param buffer The buffer to draw to.
|
||||
* @param pose The current matrix stack.
|
||||
* @param quad The quad to draw.
|
||||
* @param red The red tint of this quad.
|
||||
* @param green The green tint of this quad.
|
||||
* @param blue The blue tint of this quad.
|
||||
* @param lightmapCoord The lightmap coordinate
|
||||
* @param overlayLight The overlay light.
|
||||
* @param invert Whether to reverse the order of this quad.
|
||||
*/
|
||||
private static void putBulkQuad(VertexConsumer buffer, PoseStack.Pose pose, BakedQuad quad, float red, float green, float blue, int lightmapCoord, int overlayLight, boolean invert) {
|
||||
var matrix = pose.pose();
|
||||
// It's a little dubious to transform using this matrix rather than the normal matrix. This mirrors the logic in
|
||||
// Direction.rotate (so not out of nowhere!), but is a little suspicious.
|
||||
var dirNormal = quad.getDirection().getNormal();
|
||||
var vector = new Vector4f();
|
||||
|
||||
matrix.transform(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f, vector).normalize();
|
||||
float normalX = vector.x(), normalY = vector.y(), normalZ = vector.z();
|
||||
|
||||
var vertices = quad.getVertices();
|
||||
for (var vertex = 0; vertex < 4; vertex++) {
|
||||
var i = ModelTransformer.getVertexOffset(vertex, invert);
|
||||
|
||||
var x = Float.intBitsToFloat(vertices[i]);
|
||||
var y = Float.intBitsToFloat(vertices[i + 1]);
|
||||
var z = Float.intBitsToFloat(vertices[i + 2]);
|
||||
|
||||
matrix.transform(x, y, z, 1, vector);
|
||||
|
||||
var u = Float.intBitsToFloat(vertices[i + 4]);
|
||||
var v = Float.intBitsToFloat(vertices[i + 5]);
|
||||
buffer.vertex(
|
||||
vector.x(), vector.y(), vector.z(),
|
||||
red, green, blue, 1.0F, u, v, overlayLight, lightmapCoord,
|
||||
normalX, normalY, normalZ
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.pocket.ClientPocketComputers;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
@@ -72,13 +73,14 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer {
|
||||
}
|
||||
|
||||
private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) {
|
||||
var texture = colour != -1 ? ComputerBorderRenderer.BACKGROUND_COLOUR : ComputerBorderRenderer.getTexture(family);
|
||||
var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family);
|
||||
|
||||
var r = ((colour >>> 16) & 0xFF) / 255.0f;
|
||||
var g = ((colour >>> 8) & 0xFF) / 255.0f;
|
||||
var b = (colour & 0xFF) / 255.0f;
|
||||
var r = (colour >>> 16) & 0xFF;
|
||||
var g = (colour >>> 8) & 0xFF;
|
||||
var b = colour & 0xFF;
|
||||
|
||||
ComputerBorderRenderer.render(transform, render.getBuffer(ComputerBorderRenderer.getRenderType(texture)), 0, 0, 0, light, width, height, true, r, g, b);
|
||||
var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b);
|
||||
ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true);
|
||||
}
|
||||
|
||||
private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {
|
||||
|
@@ -7,6 +7,7 @@ package dan200.computercraft.client.render;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.VertexFormat;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
|
||||
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
|
||||
import net.minecraft.client.renderer.GameRenderer;
|
||||
@@ -53,6 +54,11 @@ public class RenderTypes {
|
||||
*/
|
||||
public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(new ResourceLocation("computercraft", "textures/gui/printout.png"));
|
||||
|
||||
/**
|
||||
* Render type for {@linkplain GuiSprites GUI sprites}.
|
||||
*/
|
||||
public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE);
|
||||
|
||||
public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
|
||||
if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
|
||||
return monitorTboShader;
|
||||
|
@@ -0,0 +1,131 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import net.minecraft.client.gui.GuiComponent;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* A {@link GuiComponent}-equivalent which is suitable for both rendering in to a GUI and in-world (as part of an entity
|
||||
* renderer).
|
||||
* <p>
|
||||
* This batches all render calls together, though requires that all {@link TextureAtlasSprite}s are on the same sprite
|
||||
* sheet.
|
||||
*/
|
||||
public class SpriteRenderer {
|
||||
private final Matrix4f transform;
|
||||
private final VertexConsumer builder;
|
||||
private final int light;
|
||||
private final int z;
|
||||
private final int r, g, b;
|
||||
|
||||
public SpriteRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, int r, int g, int b) {
|
||||
this.transform = transform;
|
||||
this.builder = builder;
|
||||
this.z = z;
|
||||
this.light = light;
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
public static SpriteRenderer createForGui(PoseStack stack, VertexConsumer builder) {
|
||||
return new SpriteRenderer(stack.last().pose(), builder, 0, RenderTypes.FULL_BRIGHT_LIGHTMAP, 255, 255, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single sprite.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
*/
|
||||
public void blit(TextureAtlasSprite sprite, int x, int y, int width, int height) {
|
||||
blit(x, y, width, height, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a horizontal 3-sliced texture (i.e. split into left, middle and right). Unlike {@link GuiComponent#blitNineSliced},
|
||||
* the middle texture is stretched rather than repeated.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param leftBorder The width of the left border.
|
||||
* @param rightBorder The width of the right border.
|
||||
* @param textureWidth The width of the whole texture.
|
||||
*/
|
||||
public void blitHorizontalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int leftBorder, int rightBorder, int textureWidth) {
|
||||
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
|
||||
if (width < leftBorder + rightBorder) throw new IllegalArgumentException("width is less than two borders");
|
||||
|
||||
var centerStart = SpriteRenderer.u(sprite, leftBorder, textureWidth);
|
||||
var centerEnd = SpriteRenderer.u(sprite, textureWidth - rightBorder, textureWidth);
|
||||
|
||||
blit(x, y, leftBorder, height, sprite.getU0(), sprite.getV0(), centerStart, sprite.getV1());
|
||||
blit(x + leftBorder, y, width - leftBorder - rightBorder, height, centerStart, sprite.getV0(), centerEnd, sprite.getV1());
|
||||
blit(x + width - rightBorder, y, rightBorder, height, centerEnd, sprite.getV0(), sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a vertical 3-sliced texture (i.e. split into top, middle and bottom). Unlike {@link GuiComponent#blitNineSliced},
|
||||
* the middle texture is stretched rather than repeated.
|
||||
*
|
||||
* @param sprite The texture to draw.
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param topBorder The height of the top border.
|
||||
* @param bottomBorder The height of the bottom border.
|
||||
* @param textureHeight The height of the whole texture.
|
||||
*/
|
||||
public void blitVerticalSliced(TextureAtlasSprite sprite, int x, int y, int width, int height, int topBorder, int bottomBorder, int textureHeight) {
|
||||
// TODO(1.20.2)/TODO(1.21.0): Drive this from mcmeta files, like vanilla does.
|
||||
if (width < topBorder + bottomBorder) throw new IllegalArgumentException("height is less than two borders");
|
||||
|
||||
var centerStart = SpriteRenderer.v(sprite, topBorder, textureHeight);
|
||||
var centerEnd = SpriteRenderer.v(sprite, textureHeight - bottomBorder, textureHeight);
|
||||
|
||||
blit(x, y, width, topBorder, sprite.getU0(), sprite.getV0(), sprite.getU1(), centerStart);
|
||||
blit(x, y + topBorder, width, height - topBorder - bottomBorder, sprite.getU0(), centerStart, sprite.getU1(), centerEnd);
|
||||
blit(x, y + height - bottomBorder, width, bottomBorder, sprite.getU0(), centerEnd, sprite.getU1(), sprite.getV1());
|
||||
}
|
||||
|
||||
/**
|
||||
* The low-level blit function, used to render a portion of the sprite sheet. Unlike other functions, this takes uvs rather than a single sprite.
|
||||
*
|
||||
* @param x The x position of the rectangle we'll draw.
|
||||
* @param y The x position of the rectangle we'll draw.
|
||||
* @param width The width of the rectangle we'll draw.
|
||||
* @param height The height of the rectangle we'll draw.
|
||||
* @param u0 The first U coordinate.
|
||||
* @param v0 The first V coordinate.
|
||||
* @param u1 The second U coordinate.
|
||||
* @param v1 The second V coordinate.
|
||||
*/
|
||||
public void blit(
|
||||
int x, int y, int width, int height, float u0, float v0, float u1, float v1) {
|
||||
builder.vertex(transform, x, y + height, z).color(r, g, b, 255).uv(u0, v1).uv2(light).endVertex();
|
||||
builder.vertex(transform, x + width, y + height, z).color(r, g, b, 255).uv(u1, v1).uv2(light).endVertex();
|
||||
builder.vertex(transform, x + width, y, z).color(r, g, b, 255).uv(u1, v0).uv2(light).endVertex();
|
||||
builder.vertex(transform, x, y, z).color(r, g, b, 255).uv(u0, v0).uv2(light).endVertex();
|
||||
}
|
||||
|
||||
public static float u(TextureAtlasSprite sprite, int x, int width) {
|
||||
return sprite.getU((double) x / width * 16);
|
||||
}
|
||||
|
||||
public static float v(TextureAtlasSprite sprite, int y, int height) {
|
||||
return sprite.getV((double) y / height * 16);
|
||||
}
|
||||
}
|
@@ -5,36 +5,28 @@
|
||||
package dan200.computercraft.client.render;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.math.Axis;
|
||||
import com.mojang.math.Transformation;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.turtle.TurtleSide;
|
||||
import dan200.computercraft.client.model.turtle.ModelTransformer;
|
||||
import dan200.computercraft.client.platform.ClientPlatformHelper;
|
||||
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
|
||||
import dan200.computercraft.shared.util.DirectionUtil;
|
||||
import dan200.computercraft.shared.util.Holiday;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.renderer.MultiBufferSource;
|
||||
import net.minecraft.client.renderer.Sheets;
|
||||
import net.minecraft.client.renderer.block.model.BakedQuad;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.resources.model.BakedModel;
|
||||
import net.minecraft.client.resources.model.ModelResourceLocation;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
|
||||
private static final ModelResourceLocation NORMAL_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_normal", "inventory");
|
||||
@@ -42,8 +34,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
|
||||
private static final ResourceLocation ELF_OVERLAY_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
|
||||
|
||||
private final RandomSource random = RandomSource.create(0);
|
||||
|
||||
private final BlockEntityRenderDispatcher renderer;
|
||||
private final Font font;
|
||||
|
||||
@@ -109,23 +99,22 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
var family = turtle.getFamily();
|
||||
var overlay = turtle.getOverlay();
|
||||
|
||||
var buffer = buffers.getBuffer(Sheets.translucentCullBlockSheet());
|
||||
renderModel(transform, buffer, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
|
||||
|
||||
// Render the overlay
|
||||
var overlayModel = getTurtleOverlayModel(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
|
||||
if (overlayModel != null) {
|
||||
renderModel(transform, buffer, lightmapCoord, overlayLight, overlayModel, null);
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, overlayModel, null);
|
||||
}
|
||||
|
||||
// Render the upgrades
|
||||
renderUpgrade(transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.LEFT, partialTicks);
|
||||
renderUpgrade(transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.RIGHT, partialTicks);
|
||||
renderUpgrade(transform, buffers, lightmapCoord, overlayLight, turtle, TurtleSide.LEFT, partialTicks);
|
||||
renderUpgrade(transform, buffers, lightmapCoord, overlayLight, turtle, TurtleSide.RIGHT, partialTicks);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private void renderUpgrade(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, TurtleBlockEntity turtle, TurtleSide side, float f) {
|
||||
private void renderUpgrade(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, TurtleBlockEntity turtle, TurtleSide side, float f) {
|
||||
var upgrade = turtle.getUpgrade(side);
|
||||
if (upgrade == null) return;
|
||||
transform.pushPose();
|
||||
@@ -136,16 +125,15 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
transform.translate(0.0f, -0.5f, -0.5f);
|
||||
|
||||
var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
|
||||
pushPoseFromTransformation(transform, model.getMatrix());
|
||||
renderModel(transform, renderer, lightmapCoord, overlayLight, model.getModel(), null);
|
||||
transform.popPose();
|
||||
applyTransformation(transform, model.getMatrix());
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, model.getModel(), null);
|
||||
|
||||
transform.popPose();
|
||||
}
|
||||
|
||||
private void renderModel(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, @Nullable int[] tints) {
|
||||
private void renderModel(PoseStack transform, MultiBufferSource buffers, int lightmapCoord, int overlayLight, ResourceLocation modelLocation, @Nullable int[] tints) {
|
||||
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper().getModelManager();
|
||||
renderModel(transform, renderer, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
|
||||
renderModel(transform, buffers, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,80 +147,11 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
|
||||
* @param tints Tints for the quads, as an array of RGB values.
|
||||
* @see net.minecraft.client.renderer.block.ModelBlockRenderer#renderModel
|
||||
*/
|
||||
private void renderModel(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, BakedModel model, @Nullable int[] tints) {
|
||||
for (var facing : DirectionUtil.FACINGS) {
|
||||
random.setSeed(42);
|
||||
renderQuads(transform, renderer, lightmapCoord, overlayLight, model.getQuads(null, facing, random), tints);
|
||||
}
|
||||
|
||||
random.setSeed(42);
|
||||
renderQuads(transform, renderer, lightmapCoord, overlayLight, model.getQuads(null, null, random), tints);
|
||||
private void renderModel(PoseStack transform, MultiBufferSource renderer, int lightmapCoord, int overlayLight, BakedModel model, @Nullable int[] tints) {
|
||||
ClientPlatformHelper.get().renderBakedModel(transform, renderer, model, lightmapCoord, overlayLight, tints);
|
||||
}
|
||||
|
||||
private static void renderQuads(PoseStack transform, VertexConsumer buffer, int lightmapCoord, int overlayLight, List<BakedQuad> quads, @Nullable int[] tints) {
|
||||
var matrix = transform.last();
|
||||
var inverted = matrix.pose().determinant() < 0;
|
||||
|
||||
for (var bakedquad : quads) {
|
||||
var tint = -1;
|
||||
if (tints != null && bakedquad.isTinted()) {
|
||||
var idx = bakedquad.getTintIndex();
|
||||
if (idx >= 0 && idx < tints.length) tint = tints[bakedquad.getTintIndex()];
|
||||
}
|
||||
|
||||
var r = (float) (tint >> 16 & 255) / 255.0F;
|
||||
var g = (float) (tint >> 8 & 255) / 255.0F;
|
||||
var b = (float) (tint & 255) / 255.0F;
|
||||
if (inverted) {
|
||||
putBulkQuadInvert(buffer, matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
|
||||
} else {
|
||||
buffer.putBulkData(matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, int, int)} for
|
||||
* when the matrix is inverted.
|
||||
*
|
||||
* @param buffer The buffer to draw to.
|
||||
* @param pose The current matrix stack.
|
||||
* @param quad The quad to draw.
|
||||
* @param red The red tint of this quad.
|
||||
* @param green The green tint of this quad.
|
||||
* @param blue The blue tint of this quad.
|
||||
* @param lightmapCoord The lightmap coordinate
|
||||
* @param overlayLight The overlay light.
|
||||
*/
|
||||
private static void putBulkQuadInvert(VertexConsumer buffer, PoseStack.Pose pose, BakedQuad quad, float red, float green, float blue, int lightmapCoord, int overlayLight) {
|
||||
var matrix = pose.pose();
|
||||
// It's a little dubious to transform using this matrix rather than the normal matrix. This mirrors the logic in
|
||||
// Direction.rotate (so not out of nowhere!), but is a little suspicious.
|
||||
var dirNormal = quad.getDirection().getNormal();
|
||||
var normal = matrix.transform(new Vector4f(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f)).normalize();
|
||||
|
||||
var vertices = quad.getVertices();
|
||||
for (var vertex : ModelTransformer.ORDER) {
|
||||
var i = vertex * ModelTransformer.STRIDE;
|
||||
|
||||
var x = Float.intBitsToFloat(vertices[i]);
|
||||
var y = Float.intBitsToFloat(vertices[i + 1]);
|
||||
var z = Float.intBitsToFloat(vertices[i + 2]);
|
||||
var transformed = matrix.transform(new Vector4f(x, y, z, 1));
|
||||
|
||||
var u = Float.intBitsToFloat(vertices[i + 4]);
|
||||
var v = Float.intBitsToFloat(vertices[i + 5]);
|
||||
buffer.vertex(
|
||||
transformed.x(), transformed.y(), transformed.z(),
|
||||
red, green, blue, 1.0F, u, v, overlayLight, lightmapCoord,
|
||||
normal.x(), normal.y(), normal.z()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pushPoseFromTransformation(PoseStack stack, Transformation transformation) {
|
||||
stack.pushPose();
|
||||
|
||||
private static void applyTransformation(PoseStack stack, Transformation transformation) {
|
||||
var trans = transformation.getTranslation();
|
||||
stack.translate(trans.x(), trans.y(), trans.z());
|
||||
|
||||
|
@@ -13,6 +13,9 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link TurtleUpgradeModeller} for modems, providing different models depending on if the modem is on/off.
|
||||
*/
|
||||
@@ -48,4 +51,9 @@ public class TurtleModemModeller implements TurtleUpgradeModeller<TurtleModem> {
|
||||
? TransformedModel.of(active ? leftOnModel : leftOffModel)
|
||||
: TransformedModel.of(active ? rightOnModel : rightOffModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ResourceLocation> getDependencies() {
|
||||
return List.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel);
|
||||
}
|
||||
}
|
||||
|
@@ -15,10 +15,12 @@ import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.impl.UpgradeManager;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A registry of {@link TurtleUpgradeModeller}s.
|
||||
@@ -71,4 +73,8 @@ public final class TurtleUpgradeModellers {
|
||||
var modeller = turtleModels.get(wrapper.serialiser());
|
||||
return modeller == null ? NULL_TURTLE_MODELLER : modeller;
|
||||
}
|
||||
|
||||
public static Stream<ResourceLocation> getDependencies() {
|
||||
return turtleModels.values().stream().flatMap(x -> x.getDependencies().stream());
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,10 @@
|
||||
|
||||
package dan200.computercraft.data.client;
|
||||
|
||||
import dan200.computercraft.client.gui.GuiSprites;
|
||||
import dan200.computercraft.data.DataProviders;
|
||||
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
|
||||
import net.minecraft.client.renderer.texture.atlas.SpriteSource;
|
||||
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
|
||||
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@@ -13,6 +15,7 @@ import net.minecraft.server.packs.PackType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A version of {@link DataProviders} which relies on client-side classes.
|
||||
@@ -29,6 +32,17 @@ public final class ClientDataProviders {
|
||||
new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
|
||||
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty())
|
||||
));
|
||||
out.accept(GuiSprites.SPRITE_SHEET, Stream.of(
|
||||
// Buttons
|
||||
GuiSprites.TURNED_OFF.textures(),
|
||||
GuiSprites.TURNED_ON.textures(),
|
||||
GuiSprites.TERMINATE.textures(),
|
||||
// Computers
|
||||
GuiSprites.COMPUTER_NORMAL.textures(),
|
||||
GuiSprites.COMPUTER_ADVANCED.textures(),
|
||||
GuiSprites.COMPUTER_COMMAND.textures(),
|
||||
GuiSprites.COMPUTER_COLOUR.textures()
|
||||
).flatMap(x -> x).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ package dan200.computercraft.data;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.api.ComputerCraftTags;
|
||||
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
|
||||
import dan200.computercraft.api.upgrades.UpgradeBase;
|
||||
@@ -20,6 +21,7 @@ import dan200.computercraft.shared.platform.RegistryWrappers;
|
||||
import net.minecraft.data.CachedOutput;
|
||||
import net.minecraft.data.DataProvider;
|
||||
import net.minecraft.data.PackOutput;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
@@ -97,6 +99,12 @@ public final class LanguageProvider implements DataProvider {
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get(), "Advanced Pocket Computer");
|
||||
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded", "Advanced %s Pocket Computer");
|
||||
|
||||
// Tags (for EMI)
|
||||
add(ComputerCraftTags.Items.COMPUTER, "Computers");
|
||||
add(ComputerCraftTags.Items.TURTLE, "Turtles");
|
||||
add(ComputerCraftTags.Items.WIRED_MODEM, "Wired modems");
|
||||
add(ComputerCraftTags.Items.MONITOR, "Monitors");
|
||||
|
||||
// Turtle/pocket upgrades
|
||||
add("upgrade.minecraft.diamond_sword.adjective", "Melee");
|
||||
add("upgrade.minecraft.diamond_shovel.adjective", "Digging");
|
||||
@@ -150,9 +158,6 @@ public final class LanguageProvider implements DataProvider {
|
||||
add("commands.computercraft.track.dump.desc", "Dump the latest results of computer tracking.");
|
||||
add("commands.computercraft.track.dump.no_timings", "No timings available");
|
||||
add("commands.computercraft.track.dump.computer", "Computer");
|
||||
add("commands.computercraft.reload.synopsis", "Reload the ComputerCraft config file");
|
||||
add("commands.computercraft.reload.desc", "Reload the ComputerCraft config file");
|
||||
add("commands.computercraft.reload.done", "Reloaded config");
|
||||
add("commands.computercraft.queue.synopsis", "Send a computer_command event to a command computer");
|
||||
add("commands.computercraft.queue.desc", "Send a computer_command event to a command computer, passing through the additional arguments. This is mostly designed for map makers, acting as a more computer-friendly version of /trigger. Any player can run the command, which would most likely be done through a text component's click event.");
|
||||
|
||||
@@ -214,6 +219,7 @@ public final class LanguageProvider implements DataProvider {
|
||||
addConfigEntry(ConfigSpec.defaultComputerSettings, "Default Computer settings");
|
||||
addConfigEntry(ConfigSpec.logComputerErrors, "Log computer errors");
|
||||
addConfigEntry(ConfigSpec.commandRequireCreative, "Command computers require creative");
|
||||
addConfigEntry(ConfigSpec.disabledGenericMethods, "Disabled generic methods");
|
||||
|
||||
addConfigGroup(ConfigSpec.serverSpec, "execution", "Execution");
|
||||
addConfigEntry(ConfigSpec.computerThreads, "Computer threads");
|
||||
@@ -277,8 +283,8 @@ public final class LanguageProvider implements DataProvider {
|
||||
turtleUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
|
||||
pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
|
||||
Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"),
|
||||
getConfigEntries(ConfigSpec.serverSpec).map(ConfigFile.Entry::translationKey),
|
||||
getConfigEntries(ConfigSpec.clientSpec).map(ConfigFile.Entry::translationKey)
|
||||
ConfigSpec.serverSpec.entries().map(ConfigFile.Entry::translationKey),
|
||||
ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey)
|
||||
).flatMap(x -> x);
|
||||
}
|
||||
|
||||
@@ -298,6 +304,10 @@ public final class LanguageProvider implements DataProvider {
|
||||
add(AggregatedMetric.TRANSLATION_PREFIX + metric.name() + ".name", text);
|
||||
}
|
||||
|
||||
private void add(TagKey<Item> tag, String text) {
|
||||
add("tag.item." + tag.location().getNamespace() + "." + tag.location().getPath(), text);
|
||||
}
|
||||
|
||||
private void addConfigGroup(ConfigFile spec, String path, String text) {
|
||||
var entry = spec.getEntry(path);
|
||||
if (!(entry instanceof ConfigFile.Group)) throw new IllegalArgumentException("Cannot find group " + path);
|
||||
@@ -308,16 +318,4 @@ public final class LanguageProvider implements DataProvider {
|
||||
add(value.translationKey(), text);
|
||||
add(value.translationKey() + ".tooltip", value.comment());
|
||||
}
|
||||
|
||||
private static Stream<ConfigFile.Entry> getConfigEntries(ConfigFile spec) {
|
||||
return spec.entries().flatMap(LanguageProvider::getConfigEntries);
|
||||
}
|
||||
|
||||
private static Stream<ConfigFile.Entry> getConfigEntries(ConfigFile.Entry entry) {
|
||||
if (entry instanceof ConfigFile.Value<?>) return Stream.of(entry);
|
||||
if (entry instanceof ConfigFile.Group group) {
|
||||
return Stream.concat(Stream.of(entry), group.children().flatMap(LanguageProvider::getConfigEntries));
|
||||
}
|
||||
throw new IllegalStateException("Invalid config entry " + entry);
|
||||
}
|
||||
}
|
||||
|
@@ -6,10 +6,12 @@ package dan200.computercraft.impl;
|
||||
|
||||
import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.core.asm.GenericMethod;
|
||||
import dan200.computercraft.shared.config.ConfigSpec;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The global registry for {@link GenericSource}s.
|
||||
@@ -29,6 +31,11 @@ public final class GenericSources {
|
||||
}
|
||||
|
||||
public static Collection<GenericMethod> getAllMethods() {
|
||||
return sources.stream().flatMap(GenericMethod::getMethods).toList();
|
||||
var disabledMethods = Set.copyOf(ConfigSpec.disabledGenericMethods.get());
|
||||
return sources.stream()
|
||||
.filter(x -> !disabledMethods.contains(x.id()))
|
||||
.flatMap(GenericMethod::getMethods)
|
||||
.filter(x -> !disabledMethods.contains(x.id()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import dan200.computercraft.api.upgrades.UpgradeData;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.impl.PocketUpgrades;
|
||||
import dan200.computercraft.impl.TurtleUpgrades;
|
||||
import dan200.computercraft.shared.command.UserLevel;
|
||||
import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
|
||||
import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
|
||||
@@ -37,6 +38,7 @@ import dan200.computercraft.shared.data.HasComputerIdLootCondition;
|
||||
import dan200.computercraft.shared.data.PlayerCreativeLootCondition;
|
||||
import dan200.computercraft.shared.details.BlockDetails;
|
||||
import dan200.computercraft.shared.details.ItemDetails;
|
||||
import dan200.computercraft.shared.integration.PermissionRegistry;
|
||||
import dan200.computercraft.shared.media.items.DiskItem;
|
||||
import dan200.computercraft.shared.media.items.PrintoutItem;
|
||||
import dan200.computercraft.shared.media.items.RecordMedia;
|
||||
@@ -77,6 +79,7 @@ import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
|
||||
import dan200.computercraft.shared.turtle.upgrades.*;
|
||||
import dan200.computercraft.shared.util.ImpostorRecipe;
|
||||
import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.commands.synchronization.SingletonArgumentInfo;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -98,6 +101,7 @@ import net.minecraft.world.level.material.Material;
|
||||
import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Registers ComputerCraft's registry entries and additional objects, such as {@link CauldronInteraction}s and
|
||||
@@ -366,6 +370,18 @@ public final class ModRegistry {
|
||||
public static final RegistryEntry<ImpostorShapelessRecipe.Serializer> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new);
|
||||
}
|
||||
|
||||
public static class Permissions {
|
||||
static final PermissionRegistry REGISTRY = PermissionRegistry.create();
|
||||
|
||||
public static final Predicate<CommandSourceStack> PERMISSION_DUMP = REGISTRY.registerCommand("dump", UserLevel.OWNER_OP);
|
||||
public static final Predicate<CommandSourceStack> PERMISSION_SHUTDOWN = REGISTRY.registerCommand("shutdown", UserLevel.OWNER_OP);
|
||||
public static final Predicate<CommandSourceStack> PERMISSION_TURN_ON = REGISTRY.registerCommand("turn_on", UserLevel.OWNER_OP);
|
||||
public static final Predicate<CommandSourceStack> PERMISSION_TP = REGISTRY.registerCommand("tp", UserLevel.OP);
|
||||
public static final Predicate<CommandSourceStack> PERMISSION_TRACK = REGISTRY.registerCommand("track", UserLevel.OWNER_OP);
|
||||
public static final Predicate<CommandSourceStack> PERMISSION_QUEUE = REGISTRY.registerCommand("queue", UserLevel.ANYONE);
|
||||
public static final Predicate<CommandSourceStack> PERMISSION_VIEW = REGISTRY.registerCommand("view", UserLevel.OP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any objects which don't have to be done on the main thread.
|
||||
*/
|
||||
@@ -379,6 +395,7 @@ public final class ModRegistry {
|
||||
ArgumentTypes.REGISTRY.register();
|
||||
LootItemConditionTypes.REGISTRY.register();
|
||||
RecipeSerializers.REGISTRY.register();
|
||||
Permissions.REGISTRY.register();
|
||||
|
||||
// Register bundled power providers
|
||||
ComputerCraftAPI.registerBundledRedstoneProvider(new DefaultBundledRedstoneProvider());
|
||||
|
@@ -6,9 +6,13 @@ package dan200.computercraft.shared.command;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.shared.ModRegistry;
|
||||
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
|
||||
import dan200.computercraft.shared.command.text.TableBuilder;
|
||||
import dan200.computercraft.shared.computer.core.ComputerFamily;
|
||||
import dan200.computercraft.shared.computer.core.ServerComputer;
|
||||
@@ -57,7 +61,7 @@ public final class CommandComputerCraft {
|
||||
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
dispatcher.register(choice("computercraft")
|
||||
.then(literal("dump")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.requires(ModRegistry.Permissions.PERMISSION_DUMP)
|
||||
.executes(context -> {
|
||||
var table = new TableBuilder("DumpAll", "Computer", "On", "Position");
|
||||
|
||||
@@ -115,7 +119,7 @@ public final class CommandComputerCraft {
|
||||
})))
|
||||
|
||||
.then(command("shutdown")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.executes((context, computerSelectors) -> {
|
||||
var shutdown = 0;
|
||||
@@ -129,7 +133,7 @@ public final class CommandComputerCraft {
|
||||
}))
|
||||
|
||||
.then(command("turn-on")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
|
||||
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
|
||||
.executes((context, computerSelectors) -> {
|
||||
var on = 0;
|
||||
@@ -143,7 +147,7 @@ public final class CommandComputerCraft {
|
||||
}))
|
||||
|
||||
.then(command("tp")
|
||||
.requires(UserLevel.OP)
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TP)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(context -> {
|
||||
var computer = getComputerArgument(context, "computer");
|
||||
@@ -168,8 +172,11 @@ public final class CommandComputerCraft {
|
||||
}))
|
||||
|
||||
.then(command("queue")
|
||||
.requires(UserLevel.ANYONE)
|
||||
.arg("computer", manyComputers())
|
||||
.requires(ModRegistry.Permissions.PERMISSION_QUEUE)
|
||||
.arg(
|
||||
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
|
||||
.suggests((context, builder) -> Suggestions.empty())
|
||||
)
|
||||
.argManyValue("args", StringArgumentType.string(), Collections.emptyList())
|
||||
.executes((ctx, args) -> {
|
||||
var computers = getComputersArgument(ctx, "computer");
|
||||
@@ -187,7 +194,7 @@ public final class CommandComputerCraft {
|
||||
}))
|
||||
|
||||
.then(command("view")
|
||||
.requires(UserLevel.OP)
|
||||
.requires(ModRegistry.Permissions.PERMISSION_VIEW)
|
||||
.arg("computer", oneComputer())
|
||||
.executes(context -> {
|
||||
var player = context.getSource().getPlayerOrException();
|
||||
@@ -207,8 +214,8 @@ public final class CommandComputerCraft {
|
||||
}))
|
||||
|
||||
.then(choice("track")
|
||||
.requires(ModRegistry.Permissions.PERMISSION_TRACK)
|
||||
.then(command("start")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.executes(context -> {
|
||||
getMetricsInstance(context.getSource()).start();
|
||||
|
||||
@@ -221,7 +228,6 @@ public final class CommandComputerCraft {
|
||||
}))
|
||||
|
||||
.then(command("stop")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.executes(context -> {
|
||||
var timings = getMetricsInstance(context.getSource());
|
||||
if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create();
|
||||
@@ -230,7 +236,6 @@ public final class CommandComputerCraft {
|
||||
}))
|
||||
|
||||
.then(command("dump")
|
||||
.requires(UserLevel.OWNER_OP)
|
||||
.argManyValue("fields", metric(), DEFAULT_FIELDS)
|
||||
.executes((context, fields) -> {
|
||||
AggregatedMetric sort;
|
||||
@@ -264,23 +269,25 @@ public final class CommandComputerCraft {
|
||||
out.append(" (id " + computerId + ")");
|
||||
|
||||
// And, if we're a player, some useful links
|
||||
if (serverComputer != null && UserLevel.OP.test(source) && isPlayer(source)) {
|
||||
out
|
||||
.append(" ")
|
||||
.append(link(
|
||||
if (serverComputer != null && isPlayer(source)) {
|
||||
if (ModRegistry.Permissions.PERMISSION_TP.test(source)) {
|
||||
out.append(" ").append(link(
|
||||
text("\u261b"),
|
||||
"/computercraft tp " + serverComputer.getInstanceID(),
|
||||
Component.translatable("commands.computercraft.tp.action")
|
||||
))
|
||||
.append(" ")
|
||||
.append(link(
|
||||
));
|
||||
}
|
||||
|
||||
if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) {
|
||||
out.append(" ").append(link(
|
||||
text("\u20e2"),
|
||||
"/computercraft view " + serverComputer.getInstanceID(),
|
||||
Component.translatable("commands.computercraft.view.action")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (UserLevel.OWNER.test(source) && isPlayer(source)) {
|
||||
if (isPlayer(source) && UserLevel.isOwner(source)) {
|
||||
var linkPath = linkStorage(source, computerId);
|
||||
if (linkPath != null) out.append(" ").append(linkPath);
|
||||
}
|
||||
@@ -289,7 +296,7 @@ public final class CommandComputerCraft {
|
||||
}
|
||||
|
||||
private static Component linkPosition(CommandSourceStack context, ServerComputer computer) {
|
||||
if (UserLevel.OP.test(context)) {
|
||||
if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
|
||||
return link(
|
||||
position(computer.getPosition()),
|
||||
"/computercraft tp " + computer.getInstanceID(),
|
||||
|
@@ -10,7 +10,6 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import dan200.computercraft.shared.platform.PlatformHelper;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
@@ -22,8 +21,8 @@ public final class CommandUtils {
|
||||
}
|
||||
|
||||
public static boolean isPlayer(CommandSourceStack output) {
|
||||
var sender = output.getEntity();
|
||||
return sender instanceof ServerPlayer player && !PlatformHelper.get().isFakePlayer(player);
|
||||
var player = output.getPlayer();
|
||||
return player != null && !PlatformHelper.get().isFakePlayer(player);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
@@ -5,7 +5,7 @@
|
||||
package dan200.computercraft.shared.command;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -13,11 +13,6 @@ import java.util.function.Predicate;
|
||||
* The level a user must be at in order to execute a command.
|
||||
*/
|
||||
public enum UserLevel implements Predicate<CommandSourceStack> {
|
||||
/**
|
||||
* Only can be used by the owner of the server: namely the server console or the player in SSP.
|
||||
*/
|
||||
OWNER,
|
||||
|
||||
/**
|
||||
* Can only be used by ops.
|
||||
*/
|
||||
@@ -35,7 +30,6 @@ public enum UserLevel implements Predicate<CommandSourceStack> {
|
||||
|
||||
public int toLevel() {
|
||||
return switch (this) {
|
||||
case OWNER -> 4;
|
||||
case OP, OWNER_OP -> 2;
|
||||
case ANYONE -> 0;
|
||||
};
|
||||
@@ -44,16 +38,26 @@ public enum UserLevel implements Predicate<CommandSourceStack> {
|
||||
@Override
|
||||
public boolean test(CommandSourceStack source) {
|
||||
if (this == ANYONE) return true;
|
||||
if (this == OWNER) return isOwner(source);
|
||||
if (this == OWNER_OP && isOwner(source)) return true;
|
||||
return source.hasPermission(toLevel());
|
||||
}
|
||||
|
||||
private static boolean isOwner(CommandSourceStack source) {
|
||||
public boolean test(ServerPlayer source) {
|
||||
if (this == ANYONE) return true;
|
||||
if (this == OWNER_OP && isOwner(source)) return true;
|
||||
return source.hasPermissions(toLevel());
|
||||
}
|
||||
|
||||
public static boolean isOwner(CommandSourceStack source) {
|
||||
var server = source.getServer();
|
||||
var sender = source.getEntity();
|
||||
var player = source.getPlayer();
|
||||
return server.isDedicatedServer()
|
||||
? source.getEntity() == null && source.hasPermission(4) && source.getTextName().equals("Server")
|
||||
: sender instanceof Player player && server.isSingleplayerOwner(player.getGameProfile());
|
||||
: player != null && server.isSingleplayerOwner(player.getGameProfile());
|
||||
}
|
||||
|
||||
public static boolean isOwner(ServerPlayer player) {
|
||||
var server = player.getServer();
|
||||
return server != null && server.isSingleplayerOwner(player.getGameProfile());
|
||||
}
|
||||
}
|
||||
|
@@ -44,12 +44,12 @@ public class ArgumentUtils {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>> void serializeToNetwork(FriendlyByteBuf buffer, ArgumentTypeInfo<A, T> type, ArgumentTypeInfo.Template<A> template) {
|
||||
RegistryWrappers.writeId(buffer, RegistryWrappers.COMMAND_ARGUMENT_TYPES, type);
|
||||
buffer.writeId(RegistryWrappers.COMMAND_ARGUMENT_TYPES, type);
|
||||
type.serializeToNetwork((T) template, buffer);
|
||||
}
|
||||
|
||||
public static ArgumentTypeInfo.Template<?> deserialize(FriendlyByteBuf buffer) {
|
||||
var type = RegistryWrappers.readId(buffer, RegistryWrappers.COMMAND_ARGUMENT_TYPES);
|
||||
var type = buffer.readById(RegistryWrappers.COMMAND_ARGUMENT_TYPES);
|
||||
Objects.requireNonNull(type, "Unknown argument type");
|
||||
return type.deserializeFromNetwork(buffer);
|
||||
}
|
||||
|
@@ -44,13 +44,18 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
|
||||
}
|
||||
|
||||
public CommandBuilder<S> requires(Predicate<S> predicate) {
|
||||
requires = requires == null ? predicate : requires.and(predicate);
|
||||
if (requires != null) throw new IllegalStateException("Requires already set");
|
||||
requires = predicate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandBuilder<S> arg(ArgumentBuilder<S, ?> arg) {
|
||||
args.add(arg);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandBuilder<S> arg(String name, ArgumentType<?> type) {
|
||||
args.add(RequiredArgumentBuilder.argument(name, type));
|
||||
return this;
|
||||
return arg(RequiredArgumentBuilder.argument(name, type));
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, List<T> empty) {
|
||||
@@ -74,7 +79,7 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
|
||||
|
||||
return command -> {
|
||||
// The node for no arguments
|
||||
var tail = tail(ctx -> command.run(ctx, empty.get()));
|
||||
var tail = setupTail(ctx -> command.run(ctx, empty.get()));
|
||||
|
||||
// The node for one or more arguments
|
||||
ArgumentBuilder<S, ?> moreArg = RequiredArgumentBuilder
|
||||
@@ -83,7 +88,7 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
|
||||
|
||||
// Chain all of them together!
|
||||
tail.then(moreArg);
|
||||
return link(tail);
|
||||
return buildTail(tail);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,20 +99,16 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
|
||||
|
||||
@Override
|
||||
public CommandNode<S> executes(Command<S> command) {
|
||||
if (args.isEmpty()) throw new IllegalStateException("Cannot have empty arg chain builder");
|
||||
|
||||
return link(tail(command));
|
||||
return buildTail(setupTail(command));
|
||||
}
|
||||
|
||||
private ArgumentBuilder<S, ?> tail(Command<S> command) {
|
||||
var defaultTail = args.get(args.size() - 1);
|
||||
defaultTail.executes(command);
|
||||
if (requires != null) defaultTail.requires(requires);
|
||||
return defaultTail;
|
||||
private ArgumentBuilder<S, ?> setupTail(Command<S> command) {
|
||||
return args.get(args.size() - 1).executes(command);
|
||||
}
|
||||
|
||||
private CommandNode<S> link(ArgumentBuilder<S, ?> tail) {
|
||||
private CommandNode<S> buildTail(ArgumentBuilder<S, ?> tail) {
|
||||
for (var i = args.size() - 2; i >= 0; i--) tail = args.get(i).then(tail);
|
||||
if (requires != null) tail.requires(requires);
|
||||
return tail.build();
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,8 @@ import net.minecraft.network.chat.Component;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static dan200.computercraft.core.util.Nullability.assertNonNull;
|
||||
import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
|
||||
@@ -28,6 +30,7 @@ import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
|
||||
*/
|
||||
public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<CommandSourceStack> {
|
||||
private final Collection<HelpingArgumentBuilder> children = new ArrayList<>();
|
||||
private @Nullable Predicate<CommandSourceStack> requirement;
|
||||
|
||||
private HelpingArgumentBuilder(String literal) {
|
||||
super(literal);
|
||||
@@ -37,6 +40,23 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
|
||||
return new HelpingArgumentBuilder(literal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HelpingArgumentBuilder requires(Predicate<CommandSourceStack> requirement) {
|
||||
this.requirement = requirement;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate<CommandSourceStack> getRequirement() {
|
||||
if (requirement != null) return requirement;
|
||||
|
||||
var requirements = Stream.concat(
|
||||
children.stream().map(ArgumentBuilder::getRequirement),
|
||||
getArguments().stream().map(CommandNode::getRequirement)
|
||||
).toList();
|
||||
return x -> requirements.stream().anyMatch(y -> y.test(x));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiteralArgumentBuilder<CommandSourceStack> executes(final Command<CommandSourceStack> command) {
|
||||
throw new IllegalStateException("Cannot use executes on a HelpingArgumentBuilder");
|
||||
@@ -80,9 +100,7 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
|
||||
helpCommand.node = node;
|
||||
|
||||
// Set up a /... help command
|
||||
var helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal("help")
|
||||
.requires(x -> getArguments().stream().anyMatch(y -> y.getRequirement().test(x)))
|
||||
.executes(helpCommand);
|
||||
var helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal("help").executes(helpCommand);
|
||||
|
||||
// Add all normal command children to this and the help node
|
||||
for (var child : getArguments()) {
|
||||
@@ -157,7 +175,7 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
|
||||
.append(Component.translatable("commands." + id + ".desc"));
|
||||
|
||||
for (var child : node.getChildren()) {
|
||||
if (!child.getRequirement().test(context.getSource()) || !(child instanceof LiteralCommandNode)) {
|
||||
if (!child.canUse(context.getSource()) || !(child instanceof LiteralCommandNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@@ -168,7 +168,7 @@ public class CommandAPI implements ILuaAPI {
|
||||
/**
|
||||
* Get information about a range of blocks.
|
||||
* <p>
|
||||
* This returns the same information as @{getBlockInfo}, just for multiple
|
||||
* This returns the same information as [`getBlockInfo`], just for multiple
|
||||
* blocks at once.
|
||||
* <p>
|
||||
* Blocks are traversed by ascending y level, followed by z and x - the returned
|
||||
@@ -225,7 +225,7 @@ public class CommandAPI implements ILuaAPI {
|
||||
* Get some basic information about a block.
|
||||
* <p>
|
||||
* The returned table contains the current name, metadata and block state (as
|
||||
* with @{turtle.inspect}). If there is a tile entity for that block, its NBT
|
||||
* with [`turtle.inspect`]). If there is a tile entity for that block, its NBT
|
||||
* will also be returned.
|
||||
*
|
||||
* @param x The x position of the block to query.
|
||||
|
@@ -199,6 +199,11 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
return localSide;
|
||||
}
|
||||
|
||||
private void updateRedstoneInputs(ServerComputer computer) {
|
||||
var pos = getBlockPos();
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
|
||||
}
|
||||
|
||||
private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
|
||||
var offsetSide = dir.getOpposite();
|
||||
var localDir = remapToLocalSide(dir);
|
||||
@@ -254,8 +259,7 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
|
||||
// If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
|
||||
// handle this incorrectly.
|
||||
var pos = getBlockPos();
|
||||
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
|
||||
updateRedstoneInputs(computer);
|
||||
invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
|
||||
}
|
||||
|
||||
@@ -264,9 +268,10 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
|
||||
*/
|
||||
public void updateOutput() {
|
||||
BlockEntityHelpers.updateBlock(this);
|
||||
for (var dir : DirectionUtil.FACINGS) {
|
||||
RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
|
||||
}
|
||||
for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
|
||||
|
||||
var computer = getServerComputer();
|
||||
if (computer != null) updateRedstoneInputs(computer);
|
||||
}
|
||||
|
||||
protected abstract ServerComputer createComputer(int id);
|
||||
|
@@ -5,18 +5,10 @@
|
||||
package dan200.computercraft.shared.computer.terminal;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
/**
|
||||
* A snapshot of a terminal's state.
|
||||
@@ -31,20 +23,10 @@ public class TerminalState {
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
||||
private final boolean compress;
|
||||
|
||||
@Nullable
|
||||
private final ByteBuf buffer;
|
||||
|
||||
private @Nullable ByteBuf compressed;
|
||||
|
||||
public TerminalState(@Nullable NetworkedTerminal terminal) {
|
||||
this(terminal, true);
|
||||
}
|
||||
|
||||
public TerminalState(@Nullable NetworkedTerminal terminal, boolean compress) {
|
||||
this.compress = compress;
|
||||
|
||||
if (terminal == null) {
|
||||
colour = false;
|
||||
width = height = 0;
|
||||
@@ -61,14 +43,13 @@ public class TerminalState {
|
||||
|
||||
public TerminalState(FriendlyByteBuf buf) {
|
||||
colour = buf.readBoolean();
|
||||
compress = buf.readBoolean();
|
||||
|
||||
if (buf.readBoolean()) {
|
||||
width = buf.readVarInt();
|
||||
height = buf.readVarInt();
|
||||
|
||||
var length = buf.readVarInt();
|
||||
buffer = readCompressed(buf, length, compress);
|
||||
buffer = buf.readBytes(length);
|
||||
} else {
|
||||
width = height = 0;
|
||||
buffer = null;
|
||||
@@ -77,16 +58,13 @@ public class TerminalState {
|
||||
|
||||
public void write(FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(colour);
|
||||
buf.writeBoolean(compress);
|
||||
|
||||
buf.writeBoolean(buffer != null);
|
||||
if (buffer != null) {
|
||||
buf.writeVarInt(width);
|
||||
buf.writeVarInt(height);
|
||||
|
||||
var sendBuffer = getCompressed();
|
||||
buf.writeVarInt(sendBuffer.readableBytes());
|
||||
buf.writeBytes(sendBuffer, sendBuffer.readerIndex(), sendBuffer.readableBytes());
|
||||
buf.writeVarInt(buffer.readableBytes());
|
||||
buf.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,40 +88,4 @@ public class TerminalState {
|
||||
terminal.read(new FriendlyByteBuf(buffer));
|
||||
return terminal;
|
||||
}
|
||||
|
||||
private ByteBuf getCompressed() {
|
||||
if (buffer == null) throw new NullPointerException("buffer");
|
||||
if (!compress) return buffer;
|
||||
if (compressed != null) return compressed;
|
||||
|
||||
var compressed = Unpooled.buffer();
|
||||
try (OutputStream stream = new GZIPOutputStream(new ByteBufOutputStream(compressed))) {
|
||||
stream.write(buffer.array(), buffer.arrayOffset(), buffer.readableBytes());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
return this.compressed = compressed;
|
||||
}
|
||||
|
||||
private static ByteBuf readCompressed(ByteBuf buf, int length, boolean compress) {
|
||||
if (compress) {
|
||||
var buffer = Unpooled.buffer();
|
||||
try (InputStream stream = new GZIPInputStream(new ByteBufInputStream(buf, length))) {
|
||||
var swap = new byte[8192];
|
||||
while (true) {
|
||||
var bytes = stream.read(swap);
|
||||
if (bytes == -1) break;
|
||||
buffer.writeBytes(swap, 0, bytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return buffer;
|
||||
} else {
|
||||
var buffer = Unpooled.buffer(length);
|
||||
buf.readBytes(buffer, length);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,10 @@ class AddressRuleConfig {
|
||||
|
||||
private static final AddressRule REJECT_ALL = AddressRule.parse("*", OptionalInt.empty(), Action.DENY.toPartial());
|
||||
|
||||
private static final Set<String> knownKeys = Set.of(
|
||||
"host", "action", "max_download", "max_upload", "max_websocket_message", "use_proxy"
|
||||
);
|
||||
|
||||
public static List<UnmodifiableConfig> defaultRules() {
|
||||
return List.of(
|
||||
makeRule(config -> {
|
||||
@@ -88,9 +92,20 @@ class AddressRuleConfig {
|
||||
var port = unboxOptInt(get(builder, "port", Number.class));
|
||||
var maxUpload = unboxOptLong(get(builder, "max_upload", Number.class).map(Number::longValue));
|
||||
var maxDownload = unboxOptLong(get(builder, "max_download", Number.class).map(Number::longValue));
|
||||
var websocketMessage = unboxOptInt(get(builder, "websocket_message", Number.class).map(Number::intValue));
|
||||
var websocketMessage = unboxOptInt(
|
||||
get(builder, "max_websocket_message", Number.class)
|
||||
// Fallback to (incorrect) websocket_message option.
|
||||
.or(() -> get(builder, "websocket_message", Number.class))
|
||||
.map(Number::intValue)
|
||||
);
|
||||
var useProxy = get(builder, "use_proxy", Boolean.class);
|
||||
|
||||
// Find unknown keys and warn about them.
|
||||
var unknownKeys = builder.entrySet().stream().map(UnmodifiableConfig.Entry::getKey).filter(x -> !knownKeys.contains(x)).toList();
|
||||
if (!unknownKeys.isEmpty()) {
|
||||
LOG.warn("Unknown config {} {} in address rule.", unknownKeys.size() == 1 ? "option" : "options", String.join(", ", unknownKeys));
|
||||
}
|
||||
|
||||
var options = new PartialOptions(
|
||||
action,
|
||||
maxUpload,
|
||||
|
@@ -54,12 +54,6 @@ public interface ConfigFile {
|
||||
* A group of config entries.
|
||||
*/
|
||||
non-sealed interface Group extends Entry {
|
||||
/**
|
||||
* Get all entries in this group.
|
||||
*
|
||||
* @return All child entries.
|
||||
*/
|
||||
Stream<Entry> children();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -35,6 +35,7 @@ public final class ConfigSpec {
|
||||
public static final ConfigFile.Value<Boolean> logComputerErrors;
|
||||
public static final ConfigFile.Value<Boolean> commandRequireCreative;
|
||||
public static final ConfigFile.Value<Integer> uploadMaxSize;
|
||||
public static final ConfigFile.Value<List<? extends String>> disabledGenericMethods;
|
||||
|
||||
public static final ConfigFile.Value<Integer> computerThreads;
|
||||
public static final ConfigFile.Value<Integer> maxMainGlobalTime;
|
||||
@@ -139,6 +140,19 @@ public final class ConfigSpec {
|
||||
Require players to be in creative mode and be opped in order to interact with
|
||||
command computers. This is the default behaviour for vanilla's Command blocks.""")
|
||||
.define("command_require_creative", Config.commandRequireCreative);
|
||||
|
||||
disabledGenericMethods = builder
|
||||
.comment("""
|
||||
A list of generic methods or method sources to disable. Generic methods are
|
||||
methods added to a block/block entity when there is no explicit peripheral
|
||||
provider. This includes inventory methods (i.e. inventory.getItemDetail,
|
||||
inventory.pushItems), and (if on Forge), the fluid_storage and energy_storage
|
||||
methods.
|
||||
Methods in this list can either be a whole group of methods (computercraft:inventory)
|
||||
or a single method (computercraft:inventory#pushItems).
|
||||
""")
|
||||
.worldRestart()
|
||||
.defineList("disabled_generic_methods", List.of(), x -> x instanceof String);
|
||||
}
|
||||
|
||||
{
|
||||
|
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package dan200.computercraft.shared.integration;
|
||||
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import dan200.computercraft.shared.command.CommandComputerCraft;
|
||||
import dan200.computercraft.shared.command.UserLevel;
|
||||
import dan200.computercraft.shared.platform.RegistrationHelper;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
|
||||
import javax.annotation.OverridingMethodsMustInvokeSuper;
|
||||
import java.util.Optional;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* A registry of nodes in a permission system.
|
||||
* <p>
|
||||
* This acts as an abstraction layer over permission systems such Forge's built-in permissions API, or Fabric's
|
||||
* unofficial <a href="https://github.com/lucko/fabric-permissions-api">fabric-permissions-api-v0</a>.
|
||||
* <p>
|
||||
* This behaves similarly to {@link RegistrationHelper} (aka Forge's deferred registry), in that you {@linkplain #create()
|
||||
* create a registry}, {@linkplain #registerCommand(String, UserLevel) add nodes to it} and then finally {@linkplain
|
||||
* #register()} all created nodes.
|
||||
*
|
||||
* @see dan200.computercraft.shared.ModRegistry.Permissions
|
||||
*/
|
||||
public abstract class PermissionRegistry {
|
||||
private boolean frozen = false;
|
||||
|
||||
/**
|
||||
* Register a permission node for a command. The registered node should be of the form {@code "command." + command}.
|
||||
*
|
||||
* @param command The name of the command. This should be one of the subcommands under the {@code /computercraft}
|
||||
* subcommand, and not something general.
|
||||
* @param fallback The default/fallback permission check.
|
||||
* @return The resulting predicate which should be passed to {@link ArgumentBuilder#requires(Predicate)}.
|
||||
* @see CommandComputerCraft
|
||||
*/
|
||||
public abstract Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback);
|
||||
|
||||
/**
|
||||
* Check that the registry has not been frozen (namely {@link #register()} has been called). This should be called
|
||||
* before registering each node.
|
||||
*/
|
||||
protected void checkNotFrozen() {
|
||||
if (frozen) throw new IllegalStateException("Permission registry has been frozen.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Freeze the permissions registry and register the underlying nodes.
|
||||
*/
|
||||
@OverridingMethodsMustInvokeSuper
|
||||
public void register() {
|
||||
frozen = true;
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
Optional<PermissionRegistry> get();
|
||||
}
|
||||
|
||||
public static PermissionRegistry create() {
|
||||
return ServiceLoader.load(Provider.class)
|
||||
.stream()
|
||||
.flatMap(x -> x.get().get().stream())
|
||||
.findFirst()
|
||||
.orElseGet(DefaultPermissionRegistry::new);
|
||||
}
|
||||
|
||||
private static class DefaultPermissionRegistry extends PermissionRegistry {
|
||||
@Override
|
||||
public Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback) {
|
||||
checkNotFrozen();
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,13 +6,11 @@ package dan200.computercraft.shared.network.client;
|
||||
|
||||
import dan200.computercraft.shared.network.NetworkMessage;
|
||||
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity;
|
||||
import dan200.computercraft.shared.platform.RegistryWrappers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Starts or stops a record on the client, depending on if {@link #soundEvent} is {@code null}.
|
||||
@@ -40,28 +38,19 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont
|
||||
|
||||
public PlayRecordClientMessage(FriendlyByteBuf buf) {
|
||||
pos = buf.readBlockPos();
|
||||
soundEvent = buf.readBoolean() ? RegistryWrappers.readKey(buf, RegistryWrappers.SOUND_EVENTS) : null;
|
||||
name = buf.readBoolean() ? buf.readUtf(Short.MAX_VALUE) : null;
|
||||
soundEvent = buf.readNullable(SoundEvent::readFromNetwork);
|
||||
name = buf.readNullable(FriendlyByteBuf::readUtf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBytes(FriendlyByteBuf buf) {
|
||||
buf.writeBlockPos(pos);
|
||||
writeOptional(buf, soundEvent, (b, e) -> RegistryWrappers.writeKey(b, RegistryWrappers.SOUND_EVENTS, e));
|
||||
writeOptional(buf, name, FriendlyByteBuf::writeUtf);
|
||||
buf.writeNullable(soundEvent, (b, e) -> e.writeToNetwork(b));
|
||||
buf.writeNullable(name, FriendlyByteBuf::writeUtf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(ClientNetworkContext context) {
|
||||
context.handlePlayRecord(pos, soundEvent, name);
|
||||
}
|
||||
|
||||
private static <T> void writeOptional(FriendlyByteBuf out, @Nullable T object, BiConsumer<FriendlyByteBuf, T> write) {
|
||||
if (object == null) {
|
||||
out.writeBoolean(false);
|
||||
} else {
|
||||
out.writeBoolean(true);
|
||||
write.accept(out, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.modem;
|
||||
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.network.Packet;
|
||||
@@ -20,10 +21,9 @@ import java.util.Set;
|
||||
/**
|
||||
* Modems allow you to send messages between computers over long distances.
|
||||
* <p>
|
||||
* :::tip
|
||||
* Modems provide a fairly basic set of methods, which makes them very flexible but often hard to work with. The
|
||||
* {@literal @}{rednet} API is built on top of modems, and provides a more user-friendly interface.
|
||||
* :::
|
||||
* > [!TIP]
|
||||
* > Modems provide a fairly basic set of methods, which makes them very flexible but often hard to work with. The
|
||||
* > [`rednet`] API is built on top of modems, and provides a more user-friendly interface.
|
||||
* <p>
|
||||
* ## Sending and receiving messages
|
||||
* Modems operate on a series of channels, a bit like frequencies on a radio. Any modem can send a message on a
|
||||
@@ -31,11 +31,11 @@ import java.util.Set;
|
||||
* messages.
|
||||
* <p>
|
||||
* Channels are represented as an integer between 0 and 65535 inclusive. These channels don't have any defined meaning,
|
||||
* though some APIs or programs will assign a meaning to them. For instance, the @{gps} module sends all its messages on
|
||||
* channel 65534 (@{gps.CHANNEL_GPS}), while @{rednet} uses channels equal to the computer's ID.
|
||||
* though some APIs or programs will assign a meaning to them. For instance, the [`gps`] module sends all its messages on
|
||||
* channel 65534 ([`gps.CHANNEL_GPS`]), while [`rednet`] uses channels equal to the computer's ID.
|
||||
* <p>
|
||||
* - Sending messages is done with the {@link #transmit(int, int, Object)} message.
|
||||
* - Receiving messages is done by listening to the @{modem_message} event.
|
||||
* - Receiving messages is done by listening to the [`modem_message`] event.
|
||||
* <p>
|
||||
* ## Types of modem
|
||||
* CC: Tweaked comes with three kinds of modem, with different capabilities.
|
||||
@@ -85,7 +85,7 @@ import java.util.Set;
|
||||
*/
|
||||
public abstract class ModemPeripheral implements IPeripheral, PacketSender, PacketReceiver {
|
||||
private @Nullable PacketNetwork network;
|
||||
private final Set<IComputerAccess> computers = new HashSet<>(1);
|
||||
private final @GuardedBy("computers") Set<IComputerAccess> computers = new HashSet<>(1);
|
||||
private final ModemState state;
|
||||
|
||||
protected ModemPeripheral(ModemState state) {
|
||||
@@ -197,9 +197,8 @@ public abstract class ModemPeripheral implements IPeripheral, PacketSender, Pack
|
||||
* Sends a modem message on a certain channel. Modems listening on the channel will queue a {@code modem_message}
|
||||
* event on adjacent computers.
|
||||
* <p>
|
||||
* :::note
|
||||
* The channel does not need be open to send a message.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > The channel does not need be open to send a message.
|
||||
*
|
||||
* @param channel The channel to send messages on.
|
||||
* @param replyChannel The channel that responses to this message should be sent on. This can be the same as
|
||||
|
@@ -80,9 +80,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
* If this computer is attached to the network, it _will not_ be included in
|
||||
* this list.
|
||||
* <p>
|
||||
* :::note
|
||||
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
*
|
||||
* @param computer The calling computer.
|
||||
* @return Remote peripheral names on the network.
|
||||
@@ -96,9 +95,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
/**
|
||||
* Determine if a peripheral is available on this wired network.
|
||||
* <p>
|
||||
* :::note
|
||||
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
*
|
||||
* @param computer The calling computer.
|
||||
* @param name The peripheral's name.
|
||||
@@ -113,9 +111,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
/**
|
||||
* Get the type of a peripheral is available on this wired network.
|
||||
* <p>
|
||||
* :::note
|
||||
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
*
|
||||
* @param computer The calling computer.
|
||||
* @param name The peripheral's name.
|
||||
@@ -133,9 +130,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
/**
|
||||
* Check a peripheral is of a particular type.
|
||||
* <p>
|
||||
* :::note
|
||||
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
*
|
||||
* @param computer The calling computer.
|
||||
* @param name The peripheral's name.
|
||||
@@ -154,9 +150,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
/**
|
||||
* Get all available methods for the remote peripheral with the given name.
|
||||
* <p>
|
||||
* :::note
|
||||
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
*
|
||||
* @param computer The calling computer.
|
||||
* @param name The peripheral's name.
|
||||
@@ -175,9 +170,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
/**
|
||||
* Call a method on a peripheral on this wired network.
|
||||
* <p>
|
||||
* :::note
|
||||
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
*
|
||||
* @param computer The calling computer.
|
||||
* @param context The Lua context we're executing in.
|
||||
@@ -205,9 +199,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
* may be used by other computers on the network to wrap this computer as a
|
||||
* peripheral.
|
||||
* <p>
|
||||
* :::note
|
||||
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
|
||||
*
|
||||
* @return The current computer's name.
|
||||
* @cc.treturn string|nil The current computer's name on the wired network.
|
||||
|
@@ -25,8 +25,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashSet;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class MonitorBlockEntity extends BlockEntity {
|
||||
@@ -46,7 +47,7 @@ public class MonitorBlockEntity extends BlockEntity {
|
||||
private @Nullable ServerMonitor serverMonitor;
|
||||
private @Nullable ClientMonitor clientMonitor;
|
||||
private @Nullable MonitorPeripheral peripheral;
|
||||
private final Set<IComputerAccess> computers = new HashSet<>();
|
||||
private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private boolean needsUpdate = false;
|
||||
private boolean needsValidating = false;
|
||||
|
@@ -18,7 +18,7 @@ import javax.annotation.Nullable;
|
||||
* Monitors are a block which act as a terminal, displaying information on one side. This allows them to be read and
|
||||
* interacted with in-world without opening a GUI.
|
||||
* <p>
|
||||
* Monitors act as @{term.Redirect|terminal redirects} and so expose the same methods, as well as several additional
|
||||
* Monitors act as [terminal redirects][`term.Redirect`] and so expose the same methods, as well as several additional
|
||||
* ones, which are documented below.
|
||||
* <p>
|
||||
* Like computers, monitors come in both normal (no colour) and advanced (colour) varieties.
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
package dan200.computercraft.shared.peripheral.speaker;
|
||||
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
@@ -57,7 +58,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
|
||||
public static final int SAMPLE_RATE = 48000;
|
||||
|
||||
private final UUID source = UUID.randomUUID();
|
||||
private final Set<IComputerAccess> computers = new HashSet<>();
|
||||
private final @GuardedBy("computers") Set<IComputerAccess> computers = new HashSet<>();
|
||||
|
||||
private long clock = 0;
|
||||
private long lastPositionTime;
|
||||
@@ -271,16 +272,15 @@ public abstract class SpeakerPeripheral implements IPeripheral {
|
||||
* <p>
|
||||
* This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer
|
||||
* and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for
|
||||
* a @{speaker_audio_empty} event before trying again.
|
||||
* a [`speaker_audio_empty`] event before trying again.
|
||||
* <p>
|
||||
* :::note
|
||||
* The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small
|
||||
* number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible
|
||||
* (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
|
||||
* computer is lagging.
|
||||
* :::
|
||||
* > [!NOTE]
|
||||
* > The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small
|
||||
* > number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible
|
||||
* > (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
|
||||
* > computer is lagging.
|
||||
* <p>
|
||||
* {@literal @}{speaker_audio} provides a more complete guide to using speakers
|
||||
* [`speaker_audio`] provides a more complete guide to using speakers
|
||||
*
|
||||
* @param context The Lua context.
|
||||
* @param audio The audio data to play.
|
||||
@@ -291,7 +291,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
|
||||
* @cc.tparam [opt] number volume The volume to play this audio at. If not given, defaults to the previous volume
|
||||
* given to {@link #playAudio}.
|
||||
* @cc.since 1.100
|
||||
* @cc.usage Read an audio file, decode it using @{cc.audio.dfpwm}, and play it using the speaker.
|
||||
* @cc.usage Read an audio file, decode it using [`cc.audio.dfpwm`], and play it using the speaker.
|
||||
*
|
||||
* <pre data-peripheral="speaker">{@code
|
||||
* local dfpwm = require("cc.audio.dfpwm")
|
||||
|
@@ -5,11 +5,11 @@
|
||||
package dan200.computercraft.shared.platform;
|
||||
|
||||
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.core.IdMap;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
@@ -32,13 +32,10 @@ public final class RegistryWrappers {
|
||||
public static final RegistryWrapper<Fluid> FLUIDS = PlatformHelper.get().wrap(Registries.FLUID);
|
||||
public static final RegistryWrapper<Enchantment> ENCHANTMENTS = PlatformHelper.get().wrap(Registries.ENCHANTMENT);
|
||||
public static final RegistryWrapper<ArgumentTypeInfo<?, ?>> COMMAND_ARGUMENT_TYPES = PlatformHelper.get().wrap(Registries.COMMAND_ARGUMENT_TYPE);
|
||||
public static final RegistryWrapper<SoundEvent> SOUND_EVENTS = PlatformHelper.get().wrap(Registries.SOUND_EVENT);
|
||||
public static final RegistryWrapper<RecipeSerializer<?>> RECIPE_SERIALIZERS = PlatformHelper.get().wrap(Registries.RECIPE_SERIALIZER);
|
||||
public static final RegistryWrapper<MenuType<?>> MENU = PlatformHelper.get().wrap(Registries.MENU);
|
||||
|
||||
public interface RegistryWrapper<T> extends Iterable<T> {
|
||||
int getId(T object);
|
||||
|
||||
public interface RegistryWrapper<T> extends IdMap<T> {
|
||||
ResourceLocation getKey(T object);
|
||||
|
||||
T get(ResourceLocation location);
|
||||
@@ -46,8 +43,6 @@ public final class RegistryWrappers {
|
||||
@Nullable
|
||||
T tryGet(ResourceLocation location);
|
||||
|
||||
T get(int id);
|
||||
|
||||
default Stream<T> stream() {
|
||||
return StreamSupport.stream(spliterator(), false);
|
||||
}
|
||||
@@ -56,15 +51,6 @@ public final class RegistryWrappers {
|
||||
private RegistryWrappers() {
|
||||
}
|
||||
|
||||
public static <K> void writeId(FriendlyByteBuf buf, RegistryWrapper<K> registry, K object) {
|
||||
buf.writeVarInt(registry.getId(object));
|
||||
}
|
||||
|
||||
public static <K> K readId(FriendlyByteBuf buf, RegistryWrapper<K> registry) {
|
||||
var id = buf.readVarInt();
|
||||
return registry.get(id);
|
||||
}
|
||||
|
||||
public static <K> void writeKey(FriendlyByteBuf buf, RegistryWrapper<K> registry, K object) {
|
||||
buf.writeResourceLocation(registry.getKey(object));
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user