1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-18 07:27:39 +00:00

Compare commits

...

42 Commits

Author SHA1 Message Date
Jonathan Coates
cdc8592aa3 Bump CC:T to 1.108.0 2023-08-28 12:24:35 +01:00
Jonathan Coates
53546b9f57 Split some textures into sprite sheets
- Split buttons.png into individual textures.
 - Split corners_xyz.png into the following:

   - borders_xyz.png: A nine-sliced texture of the computer borders.
   - pocket_bottom_xyz.png: A horizontally 3-sliced texture of the
     bottom part of a pocket computer.
   - sidebar_xyz.png: A vertically 3-sliced texture of the computer
     sidebar.

While not splitting the sliced textures into smaller ones may seem a
little odd, it's consistent with what vanilla does in 1.20.2, and I
think will make editing them easier than juggling 9 textures.

I do want to make this more data-driven in the future, but that will
have to wait until the changes in 1.20.2.

This also adds a tools/update-resources.py program, which performs this
transformation on a given resource pack.
2023-08-27 18:02:51 +01:00
Jonathan Coates
6dfdeb9321 Update Cobalt to 0.7.3
- Add support for Lua 5.2's %g.
 - Fix %p for the upper character ranges.
2023-08-27 15:35:55 +01:00
Jonathan Coates
500406f9eb Merge pull request #1569 from cc-tweaked/feature/no-compression
Remove compression from terminal/monitor packets
2023-08-27 14:15:56 +01:00
Jonathan Coates
b3738a7a63 Use permission APIs for the /computercraft command
- Add a generic PermissionRegistry interface. This behaves similarly to
   our ShaderMod interface, searching all providers until it finds a
   compatible one.

   We could just make this part of the platform code instead, but this
   allows us to support multiple systems on Fabric, where things are
   less standardised.

   This interface behaves like a registry, rather than a straight
   `getPermission(node, player)` method, as Forge requires us to list
   our nodes up-front.

 - Add Forge (using the built-in system) and Fabric (using
   fabric-permissions-api) implementations of the above interface.

 - Register permission nodes for our commands, and use those
   instead. This does mean that the permissions check for the root
   /computercraft command now requires enumerating all child
   commands (and so potential does 7 permission lookups), but hopefully
   this isn't too bad in practice.

 - Remove UserLevel.OWNER - we never used this anywhere, and I can't
   imagine we'll want to in the future.
2023-08-27 12:22:40 +01:00
Jonathan Coates
5f8b1dd67f Update Cobalt to 0.7.2
- Support printing and parsing hex float literals
 - Fix string.format "%q"'s handling of nan and inf (Kan18)
 - Fix string is-letter/is-digit patterns treating characters as
   unicode.
 - tostring(...) now uses __name.
2023-08-24 21:27:00 +01:00
Jonathan Coates
053751b190 Merge pull request #1570 from cc-tweaked/feature/new-md-syntax
Change the syntax of several markdown extensions
2023-08-24 16:26:41 +01:00
Jonathan Coates
52b78f92cd Use standard Markdown link syntax for references
References are now written using normal links: You now use [`print`] or
[print a string][`print`]) instead of @{print} or @{print|print a
string}.
2023-08-24 11:23:33 +01:00
Jonathan Coates
2055052a57 Switch to GitHub-style admonitions/alerts
As these are just a custom syntax on top of blockquotes, these work much
better with text editors.
2023-08-23 18:10:01 +01:00
Jonathan Coates
12ee47ff19 Bump illuaminate version
This has a new Markdown parser, so we need to remove our use of image
attributes.
2023-08-23 18:09:26 +01:00
Jonathan Coates
25776abf61 Fix address rules reading the wrong config key
Should be max_websocket_message, not just websocket_message.

Also add some additional validation to address rules, to check no
unrecognised keys are present.

Closes #1566.
2023-08-23 18:04:22 +01:00
Jonathan Coates
f7411b40a2 Remove compression from terminal/monitor packets 2023-08-23 10:06:15 +01:00
Jonathan Coates
8c8924f54e Fix CME in monitor's set of attached computers
We're very inconsistent with whether we use locks or concurrent maps
here. Something to sort out in the future, but for now add some missing
@GuardedBy annotations.
2023-08-23 10:05:56 +01:00
Jonathan Coates
c1628d077a Small improvements to packet reading/writing improvements
- Prefer {read,write}Nullable when possible.

 - Use SoundEvent.{writeTo,readFrom}Network, instead of sending the
   registry entries. This allows playing discs which don't register
   their SoundEvent on the server.

 - Add a couple of tests for round-tripping these packets.
2023-08-23 10:05:56 +01:00
Jonathan Coates
5b2fdec6ca Add some jqwik arbitraries for Minecraft types
This requires supporting registries in our platform test
code. Thankfully this is mostly the same as what we can do in Fabric -
the duplication is unfortunate - but it's easy enough.
2023-08-23 10:05:56 +01:00
Jonathan Coates
5a7259e4c9 Add a tiny library for checking structural equality
I want to write some tests to check that various packets round-trip
corretly. However, these packets don't (and shouldn't) implement
.equals, and so we need a more reflective(/hacky) way of comparing them.
2023-08-23 10:05:56 +01:00
Petr Karmashev
92b335f45f Fixed turtle.dropDown documentation (#1564) 2023-08-17 10:52:17 +00:00
Charlotte Herngreen
b93ea9c62e Fix function reference in cc.image.nft (#1559) 2023-08-15 16:59:41 +00:00
MineRobber___T
b9edd7c7f6 Add dark mode styling for recipes (#1558)
Exactly what it says on the tin.
2023-08-14 07:51:14 +01:00
Jonathan Coates
84a761ddd5 Bump pre-commit versions
Hopefully fixes pre-commit issues in CI.
2023-08-13 13:50:49 +01:00
Jonathan Coates
2a04fb71fd Bump CC:T to 1.107.0 2023-08-13 08:34:45 +01:00
Srendi
df61389304 Fix some typos in CONTRIBUTING.md (#1549) 2023-08-05 21:30:19 +00:00
Jonathan Coates
b6632c9ed9 Fix model hook nullability
Only an issue on the latest FAPI, so we also bump that.
2023-08-05 10:32:21 +01:00
Jonathan Coates
41b6711b38 Change button text in render rather than an override
This made more sense on 1.19.2 and before, but now that we have to do
this for tooltips, we might as well do it for messages as well.

Closes #1538, though hopefully will be resolved on the VO side too.
2023-07-27 19:12:27 +01:00
Virtio
02f0b7ec14 Fix typo in shell docs (#1537) 2023-07-25 16:27:23 +00:00
Jonathan Coates
a9d31cd3c6 Re-run datagen
- Remove some unused translation keys.
 - Run tools/language.py to sort the current translations and remove the
   aforementioned unused keys.
 - Update turtle tool impostor recipes - these now include the tool NBT!
2023-07-23 22:22:48 +01:00
Jonathan Coates
43744b0e85 Clean up ApiWrapper a little
We now wrap every Api with an ApiWrapper, rather than making it pretend
to be an ILuaApi.
2023-07-23 22:22:22 +01:00
Weblate
55510c42db Translations for Czech
Translations for Polish

Translations for French

Translations for Spanish

Translations for German

Co-authored-by: Patriik <apatriik0@gmail.com>
Co-authored-by: Sammy <SammyKoch@pm.me>
Co-authored-by: SquidDev <git@squiddev.cc>
2023-07-23 21:22:10 +00:00
Jonathan Coates
57a944fd90 Render enchanted upgrades with a glint (#1532) 2023-07-23 10:18:22 +00:00
Jonathan Coates
940f59b116 Automatically bake models from turtle modellers (#1531)
This avoids us having to list a bunch of model locations twice, and
hopefully makes things a little cleaner for add-on authors too
2023-07-22 19:51:50 +00:00
Jonathan Coates
dd08d1ec8e Fix client tests not running on Forge
This was introduced by the FG6 update - the cctest.client property
didn't get set, and so tests never started!
2023-07-22 19:40:50 +01:00
Jonathan Coates
90ed0b24e7 Send block updates to the computer when updating redstone
Fixes #1520
2023-07-22 13:50:13 +01:00
Jonathan Coates
0ad399a528 Rewrite turtle.dig tool actions
- Move the tool action before the "is block present" check, fixes
   #1527. This is where it was before, but we flipped it around in the
   tool rewrite.

 - Don't reuse as much turtle.place logic for tool actions. This fixes
   some instances where tools could till/level dirt through solid
   blocks.
2023-07-21 08:58:02 +01:00
Jonathan Coates
1b88213eca Make our registry wrapper implement IdMap
This allows us to use some of the byte-buffer helper methods directly
with our registries, rather than rolling our own helpers.
2023-07-19 20:20:38 +01:00
Erlend
eef05b9854 Add dark-mode compatible logo to README (#1521) 2023-07-18 19:26:45 +00:00
Jonathan Coates
24d74f5c80 Update to latest Fabric
- Overhaul model loading to work with the new API. This allows for
   using the emissive texture system in a more generic way, which is
   nice!

 - Convert some of our custom models to use Fabric's model hooks (i.e.
   emitItemQuads). We don't make use of this right now, but might be
   useful for rendering tools with enchantment glints.

   Note this does /not/ change any of the turtle block entity rendering
   code to use Fabric/Forge's model code. This will be a change we want
   to make in the future.

 - Some cleanup of our config API. This fixes us printing lots of
   warnings when creating a new config file on Fabric (same bug also
   occurs on Forge, but that's a loader problem).

 - Fix a few warnings
2023-07-18 19:27:27 +01:00
Jonathan Coates
c2988366d8 Add EMI compatibility
This just adds stack comparisons, so that upgrades are considered (much
like JEI and REI). No dynamic recipes, as EMI doesn't support those :(.
2023-07-10 20:31:06 +01:00
Jonathan Coates
ec0765ead1 Bump Cobalt to 0.7.1
- Fix numbers ending in "f" or "d" being lexed.
 - Fix string.pack's "z" causing out-of-bounds errors.
2023-07-10 20:17:54 +01:00
Weblate
b94e34f372 Translations for Italian
Co-authored-by: Alessandro <ale.proto00@gmail.com>
2023-07-10 15:03:03 +00:00
Jonathan Coates
af3263dec2 Allow disabling generic methods
Suddenly so easy as of the refactoring in 910a63214e395ecae6993d8e0487384c725b3dd3!

Closes #1382
2023-07-09 19:59:08 +01:00
Jonathan Coates
7f25c9a66b Only load client config on the client 2023-07-09 18:44:33 +01:00
Jonathan Coates
9ca3efff3c Add data generator support for required mods
We've supported resource conditions in the upgrade JSON for an age, but
don't expose it in our data generators at all.

Indeed, using these hooks is a bit of a pain to do in multi-loader
setups, as the JSON is different between the two loaders. We could
generate the JSON for all loaders at once, but it feels nicer to use
the per-loader APIs to add the conditions.

For now, we just support generating a single condition - whether a mod
is loaded not, via the requireMod(...) method.
2023-07-09 14:09:44 +01:00
264 changed files with 4548 additions and 2252 deletions

View File

@@ -6,7 +6,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1 rev: v4.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@@ -20,14 +20,14 @@ repos:
exclude: "tsconfig\\.json$" exclude: "tsconfig\\.json$"
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python - repo: https://github.com/editorconfig-checker/editorconfig-checker.python
rev: 2.3.54 rev: 2.7.2
hooks: hooks:
- id: editorconfig-checker - id: editorconfig-checker
args: ['-disable-indentation'] args: ['-disable-indentation']
exclude: "^(.*\\.(bat)|LICENSE)$" exclude: "^(.*\\.(bat)|LICENSE)$"
- repo: https://github.com/fsfe/reuse-tool - repo: https://github.com/fsfe/reuse-tool
rev: v1.1.0 rev: v2.1.0
hooks: hooks:
- id: reuse - id: reuse

View File

@@ -48,6 +48,7 @@ License: MPL-2.0
Files: Files:
doc/logo.png doc/logo.png
doc/logo-darkmode.png
projects/common/src/main/resources/assets/computercraft/models/* projects/common/src/main/resources/assets/computercraft/models/*
projects/common/src/main/resources/assets/computercraft/textures/* projects/common/src/main/resources/assets/computercraft/textures/*
projects/common/src/main/resources/pack.mcmeta projects/common/src/main/resources/pack.mcmeta

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: MPL-2.0
# Contributing to CC: Tweaked # Contributing to CC: Tweaked
As with many open source projects, CC: Tweaked thrives on contributions from other people! This document (hopefully) 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]. 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 ## Setting up a development environment
In order to develop CC: Tweaked, you'll need to download the source code and then run it. 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]. - Java Development Kit (JDK) installed. This can be downloaded from [Adoptium].
- [Git](https://git-scm.com/). - [Git](https://git-scm.com/).
- If you want to work on documentation, [NodeJS][node]. - 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 ## Developing CC: Tweaked
Before making any major changes to CC: Tweaked, I'd recommend you have a read of the [the architecture 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 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 ### 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: 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 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 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 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 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! 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" [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." [community]: README.md#community "Get in touch with the community."

View File

@@ -4,7 +4,12 @@ SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 SPDX-License-Identifier: MPL-2.0
--> -->
# ![CC: Tweaked](doc/logo.png) <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>
[![Current build status](https://github.com/cc-tweaked/CC-Tweaked/workflows/Build/badge.svg)](https://github.com/cc-tweaked/CC-Tweaked/actions "Current build status") [![Current build status](https://github.com/cc-tweaked/CC-Tweaked/workflows/Build/badge.svg)](https://github.com/cc-tweaked/CC-Tweaked/actions "Current build status")
[![Download CC: Tweaked on CurseForge](https://img.shields.io/static/v1?label=Download&message=CC:%20Tweaked&color=E04E14&logoColor=E04E14&logo=CurseForge)][CurseForge] [![Download CC: Tweaked on CurseForge](https://img.shields.io/static/v1?label=Download&message=CC:%20Tweaked&color=E04E14&logoColor=E04E14&logo=CurseForge)][CurseForge]
[![Download CC: Tweaked on Modrinth](https://img.shields.io/static/v1?label=Download&color=00AF5C&logoColor=00AF5C&logo=Modrinth&message=CC:%20Tweaked)][Modrinth] [![Download CC: Tweaked on Modrinth](https://img.shields.io/static/v1?label=Download&color=00AF5C&logoColor=00AF5C&logo=Modrinth&message=CC:%20Tweaked)][Modrinth]
@@ -44,7 +49,7 @@ repositories {
dependencies { dependencies {
// Vanilla (i.e. for multi-loader systems) // 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 // Forge Gradle
compileOnly("cc.tweaked:cc-tweaked-$mcVersion-core-api:$cctVersion") 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 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 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. exposing more features.

View File

@@ -56,15 +56,17 @@ repositories {
if (fg != null) forRepositories(fg.repository) if (fg != null) forRepositories(fg.repository)
filter { filter {
includeGroup("org.squiddev")
includeGroup("cc.tweaked") includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
// Things we mirror // Things we mirror
includeGroup("dev.architectury") includeGroup("dev.architectury")
includeGroup("dev.emi")
includeGroup("maven.modrinth") includeGroup("maven.modrinth")
includeGroup("me.shedaniel")
includeGroup("me.shedaniel.cloth") includeGroup("me.shedaniel.cloth")
includeGroup("me.shedaniel")
includeGroup("mezz.jei") includeGroup("mezz.jei")
includeModule("com.terraformersmc", "modmenu") includeModule("com.terraformersmc", "modmenu")
includeModule("me.lucko", "fabric-permissions-api")
} }
} }
} }

View File

@@ -4,6 +4,7 @@
package cc.tweaked.gradle package cc.tweaked.gradle
import net.minecraftforge.gradle.common.util.RunConfig
import org.gradle.api.GradleException import org.gradle.api.GradleException
import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.FileSystemOperations
import org.gradle.api.invocation.Gradle import org.gradle.api.invocation.Gradle
@@ -53,6 +54,25 @@ abstract class ClientJavaExec : JavaExec() {
@get:OutputFile @get:OutputFile
val testResults = project.layout.buildDirectory.file("test-results/$name.xml") 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. * Copy configuration from a task with the given name.
*/ */
@@ -64,11 +84,7 @@ abstract class ClientJavaExec : JavaExec() {
fun copyFrom(task: JavaExec) { fun copyFrom(task: JavaExec) {
for (dep in task.dependsOn) dependsOn(dep) for (dep in task.dependsOn) dependsOn(dep)
task.copyToFull(this) task.copyToFull(this)
setTestProperties() // copyToFull may clobber some properties, ensure everything is set.
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))
} }
/** /**

View File

@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The ID of the alarm that finished. 2. [`number`]: The ID of the alarm that finished.
## Example ## Example
Starts a timer and then waits for it to complete. Starts a timer and then waits for it to complete.

View File

@@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: LicenseRef-CCPL 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 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 ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The string representing the character that was pressed. 2. [`string`]: The string representing the character that was pressed.
## Example ## Example

View File

@@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}<abbr title="Variable number of arguments">&hellip;</abbr>: The arguments passed to the command. 2. [`string`]<abbr title="Variable number of arguments">&hellip;</abbr>: The arguments passed to the command.
## Example ## Example
Prints the contents of messages sent: Prints the contents of messages sent:

View File

@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The side of the disk drive that had a disk inserted. 2. [`string`]: The side of the disk drive that had a disk inserted.
## Example ## Example
Prints a message when a disk is inserted: Prints a message when a disk is inserted:

View File

@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The side of the disk drive that had a disk removed. 2. [`string`]: The side of the disk drive that had a disk removed.
## Example ## Example
Prints a message when a disk is removed: Prints a message when a disk is removed:

View File

@@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 This event contains a single argument of type [`TransferredFiles`], which can be used to [get the files to be
the files to be transferred}. Each file returned is a @{fs.BinaryReadHandle|binary file handle} with an additional transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.BinaryReadHandle`] with an
@{TransferredFile.getName|getName} method. additional [getName][`TransferredFile.getName`] method.
## Return values ## Return values
1. @{string}: The event name 1. [`string`]: The event name
2. @{TransferredFiles}: The list of transferred files. 2. [`TransferredFiles`]: The list of transferred files.
## Example ## 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. Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file.

View File

@@ -9,12 +9,12 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The URL requested to be checked. 2. [`string`]: The URL requested to be checked.
3. @{boolean}: Whether the check succeeded. 3. [`boolean`]: Whether the check succeeded.
4. <span class="type">@{string}|@{nil}</span>: If the check failed, a reason explaining why the check failed. 4. <span class="type">[`string`]|[`nil`]</span>: If the check failed, a reason explaining why the check failed.

View File

@@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The URL of the site requested. 2. [`string`]: The URL of the site requested.
3. @{string}: An error describing the failure. 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 4. <span class="type">[`http.Response`]|[`nil`]</span>: A response handle if the connection succeeded, but the server's
response indicated failure. response indicated failure.
## Example ## Example

View File

@@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The URL of the site requested. 2. [`string`]: The URL of the site requested.
3. @{http.Response}: The successful HTTP response. 3. [`http.Response`]: The successful HTTP response.
## Example ## Example
Prints the content of a website (this may fail if the request fails): Prints the content of a website (this may fail if the request fails):

View File

@@ -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 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 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} 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! event. If you are consuming text input, use a [`char`] event instead!
## Return values ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The numerical key value of the key pressed. 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}). 3. [`boolean`]: Whether the key event was generated while holding the key ([`true`]), rather than pressing it the first time ([`false`]).
## Example ## Example
Prints each key when the user presses it, and if the key is being held. Prints each key when the user presses it, and if the key is being held.

View File

@@ -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). 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 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 ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The numerical key value of the key pressed. 2. [`number`]: The numerical key value of the key pressed.
## Example ## 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 ```lua
while true do while true do

View File

@@ -8,18 +8,18 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The side of the modem that received the message. 2. [`string`]: The side of the modem that received the message.
3. @{number}: The channel that the message was sent on. 3. [`number`]: The channel that the message was sent on.
4. @{number}: The reply channel set by the sender. 4. [`number`]: The reply channel set by the sender.
5. @{any}: The message as sent 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. 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 ## 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 ```lua
local modem = peripheral.find("modem") or error("No modem attached", 0) local modem = peripheral.find("modem") or error("No modem attached", 0)

View File

@@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The side or network ID of the monitor that was resized. 2. [`string`]: The side or network ID of the monitor that was resized.
## Example ## Example
Prints a message when a monitor is resized: Prints a message when a monitor is resized:

View File

@@ -8,13 +8,13 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The side or network ID of the monitor that was touched. 2. [`string`]: The side or network ID of the monitor that was touched.
3. @{number}: The X coordinate of the touch, in characters. 3. [`number`]: The X coordinate of the touch, in characters.
4. @{number}: The Y coordinate of the touch, in characters. 4. [`number`]: The Y coordinate of the touch, in characters.
## Example ## Example
Prints a message when a monitor is touched: Prints a message when a monitor is touched:

View File

@@ -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). advanced turtles and pocket computers).
## Return values ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The mouse button that was clicked. 2. [`number`]: The mouse button that was clicked.
3. @{number}: The X-coordinate of the click. 3. [`number`]: The X-coordinate of the click.
4. @{number}: The Y-coordinate of the click. 4. [`number`]: The Y-coordinate of the click.
## Mouse buttons ## 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. numerical value depending on which button on your mouse was last pressed when this event occurred.
| Button Code | Mouse Button | | Button Code | Mouse Button |

View File

@@ -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. This event is fired every time the mouse is moved while a mouse button is being held.
## Return values ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed. 2. [`number`]: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed.
3. @{number}: The X-coordinate of the mouse. 3. [`number`]: The X-coordinate of the mouse.
4. @{number}: The Y-coordinate of the mouse. 4. [`number`]: The Y-coordinate of the mouse.
## Example ## Example
Print the button and the coordinates whenever the mouse is dragged. Print the button and the coordinates whenever the mouse is dragged.

View File

@@ -11,10 +11,10 @@ SPDX-License-Identifier: LicenseRef-CCPL
This event is fired when a mouse wheel is scrolled in the terminal. This event is fired when a mouse wheel is scrolled in the terminal.
## Return values ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The direction of the scroll. (-1 = up, 1 = down) 2. [`number`]: The direction of the scroll. (-1 = up, 1 = down)
3. @{number}: The X-coordinate of the mouse when scrolling. 3. [`number`]: The X-coordinate of the mouse when scrolling.
4. @{number}: The Y-coordinate of the mouse when scrolling. 4. [`number`]: The Y-coordinate of the mouse when scrolling.
## Example ## Example
Prints the direction of each scroll, and the position of the mouse at the time. Prints the direction of each scroll, and the position of the mouse at the time.

View File

@@ -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. This event is fired when a mouse button is released or a held mouse leaves the computer's terminal.
## Return values ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that was released. 2. [`number`]: The [mouse button](mouse_click.html#Mouse_buttons) that was released.
3. @{number}: The X-coordinate of the mouse. 3. [`number`]: The X-coordinate of the mouse.
4. @{number}: The Y-coordinate of the mouse. 4. [`number`]: The Y-coordinate of the mouse.
## Example ## Example
Prints the coordinates and button number whenever the mouse is released. Prints the coordinates and button number whenever the mouse is released.

View File

@@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string} The text that was pasted. 2. [`string`] The text that was pasted.
## Example ## Example
Prints pasted text: Prints pasted text:

View File

@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The side the peripheral was attached to. 2. [`string`]: The side the peripheral was attached to.
## Example ## Example
Prints a message when a peripheral is attached: Prints a message when a peripheral is attached:

View File

@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The side the peripheral was detached from. 2. [`string`]: The side the peripheral was detached from.
## Example ## Example
Prints a message when a peripheral is detached: Prints a message when a peripheral is detached:

View File

@@ -10,17 +10,17 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The ID of the sending computer. 2. [`number`]: The ID of the sending computer.
3. @{any}: The message sent. 3. [`any`]: The message sent.
4. <span class="type">@{string}|@{nil}</span>: The protocol of the message, if provided. 4. <span class="type">[`string`]|[`nil`]</span>: The protocol of the message, if provided.
## Example ## Example
Prints a message when one is sent: Prints a message when one is sent:

View File

@@ -8,10 +8,10 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
## Example ## Example
Prints a message when a redstone input changes: Prints a message when a redstone input changes:

View File

@@ -10,13 +10,13 @@ SPDX-License-Identifier: MPL-2.0
--> -->
## Return Values ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The name of the speaker which is available to play more audio. 2. [`string`]: The name of the speaker which is available to play more audio.
## Example ## Example
This uses @{io.lines} to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it 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. using [`speaker.playAudio`]. If the speaker's buffer is full, it waits for an event and tries again.
```lua {data-peripheral=speaker} ```lua {data-peripheral=speaker}
local dfpwm = require("cc.audio.dfpwm") local dfpwm = require("cc.audio.dfpwm")

View File

@@ -9,13 +9,13 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The ID of the task that completed. 2. [`number`]: The ID of the task that completed.
3. @{boolean}: Whether the command succeeded. 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.) 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">&hellip;</abbr>: Any parameters returned from the command. 5. <abbr title="Variable number of arguments">&hellip;</abbr>: Any parameters returned from the command.
## Example ## Example

View File

@@ -8,15 +8,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 SPDX-License-Identifier: MPL-2.0
--> -->
The @{term_resize} event is fired when the main terminal is resized. For instance: 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 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 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 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 ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
## Example ## Example
Print a message each time the terminal is resized. Print a message each time the terminal is resized.

View File

@@ -8,14 +8,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
## Example ## Example
Prints a message when Ctrl-T is held: Prints a message when Ctrl-T is held:

View File

@@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{number}: The ID of the timer that finished. 2. [`number`]: The ID of the timer that finished.
## Example ## Example
Start and wait for a timer to finish. Start and wait for a timer to finish.

View File

@@ -8,10 +8,10 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return values
1. @{string}: The event name. 1. [`string`]: The event name.
## Example ## Example
Prints a message when the inventory is changed: Prints a message when the inventory is changed:

View File

@@ -8,16 +8,16 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The URL of the WebSocket that was closed. 2. [`string`]: The URL of the WebSocket that was closed.
3. <span class="type">@{string}|@{nil}</span>: The [server-provided reason][close_reason] 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 the websocket was closed. This will be [`nil`] if the connection was closed
abnormally. abnormally.
4. <span class="type">@{number}|@{nil}</span>: The [connection close code][close_code], 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 indicating why the socket was closed. This will be [`nil`] if the connection
was closed abnormally. 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_reason]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 "The WebSocket Connection Close Reason, RFC 6455"

View File

@@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The URL of the site requested. 2. [`string`]: The URL of the site requested.
3. @{string}: An error describing the failure. 3. [`string`]: An error describing the failure.
## Example ## Example
Prints an error why the website cannot be contacted: Prints an error why the website cannot be contacted:

View File

@@ -8,15 +8,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The URL of the WebSocket. 2. [`string`]: The URL of the WebSocket.
3. @{string}: The contents of the message. 3. [`string`]: The contents of the message.
4. @{boolean}: Whether this is a binary message. 4. [`boolean`]: Whether this is a binary message.
## Example ## Example
Prints a message sent by a WebSocket: Prints a message sent by a WebSocket:

View File

@@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0 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 ## Return Values
1. @{string}: The event name. 1. [`string`]: The event name.
2. @{string}: The URL of the site. 2. [`string`]: The URL of the site.
3. @{http.Websocket}: The handle for the WebSocket. 3. [`http.Websocket`]: The handle for the WebSocket.
## Example ## Example
Prints the content of a website (this may fail if the request fails): Prints the content of a website (this may fail if the request fails):

View File

@@ -9,7 +9,7 @@ SPDX-License-Identifier: MPL-2.0
--> -->
# Setting up GPS # 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` 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 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. constellation is redundant, but it does not cause problems.
## Building a GPS constellation ## Building a GPS constellation
![An example GPS constellation.](/images/gps-constellation-example.png){.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 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 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. requesting computers are out of range.
:::tip Ender modems vs wireless modems > [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 > 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). > 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. > 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 > A computer needs a wireless or ender modem and to be in range of a GPS constellation that is in the same dimension as
to use the GPS API. The reason for this is that ComputerCraft mimics real-life GPS by making use of the distance > 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. > 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 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 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 Create similar startup files for the other computers in your constellation, making sure to input the each computer's own
coordinates. coordinates.
:::caution Modem messages come from the computer's position, not the modem's > [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 > 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. > 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 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? > [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 > 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, > 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 > 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 > computers get their location, they use MC's command system to get their block which returns that in MC's coordinate
system. > system.
:::

View File

@@ -11,7 +11,7 @@ SPDX-License-Identifier: MPL-2.0
--> -->
# Playing audio with speakers # 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 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 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. 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. 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 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 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. 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 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 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! 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, 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.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_audio_empty`] event is queued, and we can try to play our chunk once more.
## Storing audio ## 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 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 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 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. using the speaker.
Let's dive in with an example, and we'll explain things afterwards: 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 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. 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. 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 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 [`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. [`fs.BinaryReadHandle.read`] if you prefer.
## Processing audio ## 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. 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 end
``` ```
:::note Confused? > [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 > 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! > 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 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. the wave. If you wanted to modify the _frequency_ (for instance, shifting the pitch), things get rather more complex.

View File

@@ -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 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. 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 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. 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. When run, this'll clear the screen and print some text in the middle of the first line.
## require in depth ## 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. which are worth mentioning for more advanced usage.
### Libraries can return anything ### 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 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. all the same, and just returns whatever your library provides.
### Module resolution and the package path ### 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 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 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. 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 code from a sub-directory using `require("folder/library")` or even `require("folder/library.lua")`, neither of which
will do quite what you expect. will do quite what you expect.
When loading libraries (also referred to as *modules*) from files, @{require} searches along the *@{package.path|module When loading libraries (also referred to as *modules*) from files, [`require`] searches along the [*module
path}*. By default, this looks something like: path*][`package.path`]. By default, this looks something like:
* `?.lua` * `?.lua`
* `?/init.lua` * `?/init.lua`
* `/rom/modules/main/?.lua` * `/rom/modules/main/?.lua`
* etc... * 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`, 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 `/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... 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: 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. - 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).

View File

@@ -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 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. writing code and automating your Minecraft world.
![A ComputerCraft terminal open and ready to be programmed.](images/basic-terminal.png){.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 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, 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! or whatever else you program them to!
![A turtle tunneling in Minecraft.](images/turtle.png){.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 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 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 Computers can now also interact with inventories such as chests, allowing you to build complex inventory and item
management systems. management systems.
![A chest's contents being read by a computer and displayed on a monitor.](images/peripherals.png){.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 ## Getting Started
While ComputerCraft is lovely for both experienced programmers and for people who have never coded before, it can be a 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -13,22 +13,20 @@ include standard Lua functions.
As it waits for a fixed amount of world ticks, `time` will automatically be 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 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. thread, not the whole program.
:::tip > [!TIP]
Because sleep internally uses timers, it is a function that yields. This means > Because sleep internally uses timers, it is a function that yields. This means
that you can use it to prevent "Too long without yielding" errors. However, as > that you can use it to prevent "Too long without yielding" errors. However, as
the minimum sleep time is 0.05 seconds, it will slow your program down. > the minimum sleep time is 0.05 seconds, it will slow your program down.
:::
:::caution > [!WARNING]
Internally, this function queues and waits for a timer event (using > Internally, this function queues and waits for a timer event (using
@{os.startTimer}), however it does not listen for any other events. This means > [`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 > that any event that occurs while sleeping will be entirely discarded. If you
need to receive events while sleeping, consider using @{os.startTimer|timers}, > need to receive events while sleeping, consider using [timers][`os.startTimer`],
or the @{parallel|parallel API}. > or the [parallel API][`parallel`].
:::
@tparam number time The number of seconds to sleep for, rounded up to the @tparam number time The number of seconds to sleep for, rounded up to the
nearest multiple of 0.05. nearest multiple of 0.05.
@@ -116,7 +114,7 @@ function read(replaceChar, history, completeFn, default) end
--- Stores the current ComputerCraft and Minecraft versions. --- 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. -- emulator's version instead.
-- --
-- For example, `ComputerCraft 1.93.0 (Minecraft 1.15.2)`. -- For example, `ComputerCraft 1.93.0 (Minecraft 1.15.2)`.

View File

@@ -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 @deprecated When possible it's best to avoid using this function. It pollutes
the global table and can mask errors. 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 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`. -- This effectively removes the specified table from `_G`.
-- --
-- @tparam string name The name of the API to unload. -- @tparam string name The name of the API to unload.
-- @since 1.2 -- @since 1.2
-- @deprecated See @{os.loadAPI} for why. -- @deprecated See [`os.loadAPI`] for why.
function unloadAPI(name) end function unloadAPI(name) end
--[[- Pause execution of the current thread and waits for any events matching --[[- Pause execution of the current thread and waits for any events matching
`filter`. `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`. to be resumed with a vararg list where the first element matches `filter`.
If no `filter` is supplied, this will match all events. 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". event, printing the error "Terminated".
@tparam[opt] string filter Event to filter for. @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 --[[- Pause execution of the current thread and waits for events, including the
`terminate` event. `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 the `terminate` event yourself - the program will not stop execution when
<kbd>Ctrl+T</kbd> is pressed. <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 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 -- @tparam number time The number of seconds to sleep for, rounded up to the
-- nearest multiple of 0.05. -- nearest multiple of 0.05.
@@ -109,12 +109,12 @@ arguments.
This function does not resolve program names like the shell does. This means 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 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 provide access to the [`shell`] API in the environment. For this behaviour, use
@{shell.run} instead. [`shell.run`] instead.
If the program cannot be found, or failed to run, it will print the error and 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 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 table env The environment to run the program with.
@tparam string path The exact path of the program to run. @tparam string path The exact path of the program to run.

View File

@@ -10,7 +10,7 @@ kotlin.jvm.target.validation.mode=error
# Mod properties # Mod properties
isUnstable=false isUnstable=false
modVersion=1.106.1 modVersion=1.108.0
# Minecraft properties: We want to configure this here so we can read it in settings.gradle # Minecraft properties: We want to configure this here so we can read it in settings.gradle
mcVersion=1.19.4 mcVersion=1.19.4

View File

@@ -7,20 +7,20 @@
# Minecraft # Minecraft
# MC version is specified in gradle.properties, as we need that in settings.gradle. # 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 # Remember to update corresponding versions in fabric.mod.json/mods.toml
fabric-api = "0.80.0+1.19.4" fabric-api = "0.86.1+1.19.4"
fabric-loader = "0.14.19" fabric-loader = "0.14.21"
forge = "45.0.42" forge = "45.0.42"
forgeSpi = "6.0.0" forgeSpi = "6.0.0"
mixin = "0.8.5" mixin = "0.8.5"
parchment = "2023.03.12" parchment = "2023.06.26"
parchmentMc = "1.19.3" parchmentMc = "1.19.4"
# Normal dependencies # Normal dependencies
asm = "9.3" asm = "9.3"
autoService = "1.0.1" autoService = "1.0.1"
checkerFramework = "3.32.0" checkerFramework = "3.32.0"
cobalt = "0.7.0" cobalt = "0.7.3"
cobalt-next = "0.7.1" # Not a real version, used to constrain the version we accept. cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
fastutil = "8.5.9" fastutil = "8.5.9"
guava = "31.1-jre" guava = "31.1-jre"
jetbrainsAnnotations = "24.0.1" jetbrainsAnnotations = "24.0.1"
@@ -33,6 +33,8 @@ nightConfig = "3.6.5"
slf4j = "1.7.36" slf4j = "1.7.36"
# Minecraft mods # Minecraft mods
emi = "1.0.8+1.19.4"
fabricPermissions = "0.2.20221016"
iris = "1.5.2+1.19.4" iris = "1.5.2+1.19.4"
jei = "13.1.0.11" jei = "13.1.0.11"
modmenu = "6.1.0-rc.1" modmenu = "6.1.0-rc.1"
@@ -48,7 +50,7 @@ jqwik = "1.7.2"
junit = "5.9.2" junit = "5.9.2"
# Build tools # Build tools
cctJavadoc = "1.7.0" cctJavadoc = "1.8.0"
checkstyle = "10.3.4" checkstyle = "10.3.4"
curseForgeGradle = "1.0.14" curseForgeGradle = "1.0.14"
errorProne-core = "2.18.0" errorProne-core = "2.18.0"
@@ -57,12 +59,12 @@ fabric-loom = "1.3.7"
forgeGradle = "6.0.8" forgeGradle = "6.0.8"
githubRelease = "2.2.12" githubRelease = "2.2.12"
ideaExt = "1.1.6" ideaExt = "1.1.6"
illuaminate = "0.1.0-28-ga7efd71" illuaminate = "0.1.0-40-g975cbc3"
librarian = "1.+" librarian = "1.+"
minotaur = "2.+" minotaur = "2.+"
mixinGradle = "0.7.+" mixinGradle = "0.7.+"
nullAway = "0.9.9" nullAway = "0.9.9"
quiltflower = "1.8.0" quiltflower = "1.10.0"
spotless = "6.17.0" spotless = "6.17.0"
taskTree = "2.1.1" taskTree = "2.1.1"
vanillaGradle = "0.2.1-SNAPSHOT" vanillaGradle = "0.2.1-SNAPSHOT"
@@ -92,6 +94,8 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
# Minecraft mods # Minecraft mods
fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" }
fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } 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" } iris = { module = "maven.modrinth:iris", version.ref = "iris" }
jei-api = { module = "mezz.jei:jei-1.19.4-common-api", version.ref = "jei" } 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" } 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-compile = ["oculus", "jei-api"]
externalMods-forge-runtime = ["jei-forge"] externalMods-forge-runtime = ["jei-forge"]
externalMods-fabric = ["nightConfig-core", "nightConfig-toml"] 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"] externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
# Testing # Testing

View File

@@ -12,7 +12,6 @@
/projects/core/src/test/resources/test-rom /projects/core/src/test/resources/test-rom
/projects/web/src/mount) /projects/web/src/mount)
(doc (doc
; Also defined in projects/web/build.gradle.kts ; Also defined in projects/web/build.gradle.kts
(destination /projects/web/build/illuaminate) (destination /projects/web/build/illuaminate)
@@ -50,6 +49,8 @@
(at / (at /
(linters (linters
syntax:string-index 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'd be nice to avoid this, but right now there's a lot of instances of
;; it. ;; it.
@@ -82,23 +83,19 @@
;; isn't smart enough. ;; isn't smart enough.
sleep write printError read rs))) sleep write printError read rs)))
;; We disable the unused global linter in bios.lua and the APIs. In the future ;; We disable the unused global linter in bios.lua, APIs and our documentation
;; hopefully we'll get illuaminate to handle this. ;; stubs docs. In the future hopefully we'll get illuaminate to handle this.
(at (at
(/projects/core/src/main/resources/data/computercraft/lua/bios.lua (/doc/stub/
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/) /projects/core/src/main/resources/data/computercraft/lua/bios.lua
(linters -var:unused-global) /projects/core/src/main/resources/data/computercraft/lua/rom/apis/
(lint (allow-toplevel-global true))) /projects/forge/build/docs/luaJavadoc/)
;; Silence some variable warnings in documentation stubs.
(at (/doc/stub/ /projects/forge/build/docs/luaJavadoc/)
(linters -var:unused-global) (linters -var:unused-global)
(lint (allow-toplevel-global true))) (lint (allow-toplevel-global true)))
;; Suppress warnings for currently undocumented modules. ;; Suppress warnings for currently undocumented modules.
(at (at
(; Lua APIs (; 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) /projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua)
(linters -doc:undocumented -doc:undocumented-arg -doc:undocumented-return)) (linters -doc:undocumented -doc:undocumented-arg -doc:undocumented-return))

View File

@@ -11,10 +11,13 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser; import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
/** /**
* Provides models for a {@link ITurtleUpgrade}. * Provides models for a {@link ITurtleUpgrade}.
@@ -50,9 +53,24 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
return getModel(upgrade, (ITurtleAccess) null, side); return getModel(upgrade, (ITurtleAccess) null, side);
} }
/** /**
* A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getCraftingItem() * Get a list of models that this turtle modeller depends on.
* crafting item}. * <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> * <p>
* This uses appropriate transformations for "flat" items, namely those extending the {@literal minecraft:item/generated} * 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. * 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") @SuppressWarnings("unchecked")
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> flatItem() { 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. * @return The constructed modeller.
*/ */
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ModelResourceLocation left, ModelResourceLocation right) { 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. * @return The constructed modeller.
*/ */
static <T extends ITurtleUpgrade> TurtleUpgradeModeller<T> sided(ResourceLocation left, ResourceLocation right) { 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);
}
};
} }
} }

View File

@@ -6,13 +6,20 @@ package dan200.computercraft.api.client.turtle;
import com.mojang.math.Transformation; import com.mojang.math.Transformation;
import dan200.computercraft.api.client.TransformedModel; import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade; import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide; 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 org.joml.Matrix4f;
import javax.annotation.Nullable;
class TurtleUpgradeModellers { class TurtleUpgradeModellers {
private static final Transformation leftTransform = getMatrixFor(-0.40625f); private static final Transformation leftTransform = getMatrixFor(-0.4065f);
private static final Transformation rightTransform = getMatrixFor(0.40625f); private static final Transformation rightTransform = getMatrixFor(0.4065f);
private static Transformation getMatrixFor(float offset) { private static Transformation getMatrixFor(float offset) {
var matrix = new Matrix4f(); var matrix = new Matrix4f();
@@ -26,6 +33,23 @@ class TurtleUpgradeModellers {
return new Transformation(matrix); return new Transformation(matrix);
} }
static final TurtleUpgradeModeller<ITurtleUpgrade> FLAT_ITEM = (upgrade, turtle, side) -> static final TurtleUpgradeModeller<ITurtleUpgrade> UPGRADE_ITEM = new UpgradeItemModeller();
TransformedModel.of(upgrade.getCraftingItem(), side == TurtleSide.LEFT ? leftTransform : rightTransform);
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);
}
}
} }

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.impl.client; package dan200.computercraft.impl.client;
import dan200.computercraft.impl.Services; import dan200.computercraft.impl.Services;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager; import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.client.resources.model.ModelResourceLocation;
@@ -24,6 +25,15 @@ public interface ClientPlatformHelper {
*/ */
BakedModel getModel(ModelManager manager, ResourceLocation location); 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() { static ClientPlatformHelper get() {
var instance = Instance.INSTANCE; var instance = Instance.INSTANCE;
return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance; return instance == null ? Services.raise(ClientPlatformHelper.class, Instance.ERROR) : instance;

View File

@@ -35,6 +35,7 @@ public enum TurtleToolDurability implements StringRepresentable {
/** /**
* The codec which may be used for serialising/deserialising {@link TurtleToolDurability}s. * 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); public static final StringRepresentable.EnumCodec<TurtleToolDurability> CODEC = StringRepresentable.fromEnum(TurtleToolDurability::values);
TurtleToolDurability(String serialisedName) { TurtleToolDurability(String serialisedName) {

View File

@@ -23,11 +23,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; 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) { public void add(Consumer<Upgrade<R>> add) {
add.accept(this); 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);
});
}
} }
} }

View File

@@ -4,6 +4,8 @@
package dan200.computercraft.impl; package dan200.computercraft.impl;
import com.google.gson.JsonObject;
import dan200.computercraft.api.upgrades.UpgradeDataProvider;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
@@ -63,6 +65,15 @@ public interface PlatformHelper {
return item.getTag(); 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 { final class Instance {
static final @Nullable PlatformHelper INSTANCE; static final @Nullable PlatformHelper INSTANCE;
static final @Nullable Throwable ERROR; static final @Nullable Throwable ERROR;

View File

@@ -26,6 +26,7 @@ dependencies {
clientImplementation(clientClasses(project(":common-api"))) clientImplementation(clientClasses(project(":common-api")))
compileOnly(libs.bundles.externalMods.common) compileOnly(libs.bundles.externalMods.common)
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
compileOnly(libs.mixin) compileOnly(libs.mixin)
annotationProcessorEverywhere(libs.autoService) annotationProcessorEverywhere(libs.autoService)

View File

@@ -13,6 +13,7 @@ import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer; import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer; import dan200.computercraft.client.render.monitor.MonitorBlockEntityRenderer;
import dan200.computercraft.client.turtle.TurtleModemModeller; import dan200.computercraft.client.turtle.TurtleModemModeller;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.core.util.Colour; import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry; import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.common.IColouredItem; 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.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.media.items.DiskItem; import dan200.computercraft.shared.media.items.DiskItem;
import dan200.computercraft.shared.media.items.TreasureDiskItem; import dan200.computercraft.shared.media.items.TreasureDiskItem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.item.ItemColor; import net.minecraft.client.color.item.ItemColor;
import net.minecraft.client.gui.screens.MenuScreens; import net.minecraft.client.gui.screens.MenuScreens;
import net.minecraft.client.multiplayer.ClientLevel; 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.ClampedItemPropertyFunction;
import net.minecraft.client.renderer.item.ItemProperties; import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceProvider; import net.minecraft.server.packs.resources.ResourceProvider;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
@@ -106,25 +109,11 @@ public final class ClientRegistry {
for (var item : items) ItemProperties.register(item.get(), id, getter); 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[]{ 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_colour",
"block/turtle_elf_overlay", "block/turtle_elf_overlay",
"block/turtle_rainbow_overlay", "block/turtle_rainbow_overlay",
@@ -133,6 +122,7 @@ public final class ClientRegistry {
public static void registerExtraModels(Consumer<ResourceLocation> register) { public static void registerExtraModels(Consumer<ResourceLocation> register) {
for (var model : EXTRA_MODELS) register.accept(new ResourceLocation(ComputerCraftAPI.MOD_ID, model)); 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) { public static void registerItemColours(BiConsumer<ItemColor, ItemLike> register) {

View File

@@ -5,15 +5,18 @@
package dan200.computercraft.client.gui; package dan200.computercraft.client.gui;
import com.mojang.blaze3d.vertex.PoseStack; 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.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.TerminalWidget; import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.render.ComputerBorderRenderer; 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 dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER; 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. * 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) { public void renderBg(PoseStack stack, float partialTicks, int mouseX, int mouseY) {
// Draw a border around the terminal // Draw a border around the terminal
var terminal = getTerminal(); 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( ComputerBorderRenderer.render(
stack.last().pose(), ComputerBorderRenderer.getTexture(family), terminal.getX(), terminal.getY(), spriteRenderer, computerTextures,
FULL_BRIGHT_LIGHTMAP, terminal.getWidth(), terminal.getHeight() terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false
); );
ComputerSidebar.renderBackground(stack, leftPos, topPos + sidebarYOffset); ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset);
buffers.endBatch();
} }
} }

View File

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

View File

@@ -6,13 +6,16 @@ package dan200.computercraft.client.gui;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.widgets.ComputerSidebar; import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.TerminalWidget; 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.core.ComputerFamily;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu; import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory; 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); // Render sidebar
ComputerSidebar.renderBackground(transform, leftPos, topPos + sidebarYOffset); 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();
} }
} }

View File

@@ -4,16 +4,13 @@
package dan200.computercraft.client.gui.widgets; package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.vertex.PoseStack; import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage; 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.core.InputHandler;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -22,22 +19,18 @@ import java.util.function.Consumer;
* Registers buttons to interact with a computer. * Registers buttons to interact with a computer.
*/ */
public final class ComputerSidebar { 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_WIDTH = 12;
private static final int ICON_HEIGHT = 12; private static final int ICON_HEIGHT = 12;
private static final int ICON_MARGIN = 2; 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 CORNERS_BORDER = 3;
private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN; private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN;
private static final int BUTTONS = 2; private static final int BUTTONS = 2;
private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2; private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2;
private static final int TEX_HEIGHT = 14;
private ComputerSidebar() { private ComputerSidebar() {
} }
@@ -51,16 +44,18 @@ public final class ComputerSidebar {
Component.translatable("gui.computercraft.tooltip.turn_off.key") Component.translatable("gui.computercraft.tooltip.turn_off.key")
); );
add.accept(new DynamicImageButton( add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT, () -> isOn.getAsBoolean() ? 15 : 1, 1, ICON_TEX_Y_DIFF, x, y, ICON_WIDTH, ICON_HEIGHT,
TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer(isOn, input), h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h),
b -> toggleComputer(isOn, input),
() -> isOn.getAsBoolean() ? turnOff : turnOn () -> isOn.getAsBoolean() ? turnOff : turnOn
)); ));
y += ICON_HEIGHT + ICON_MARGIN * 2; y += ICON_HEIGHT + ICON_MARGIN * 2;
add.accept(new DynamicImageButton( add.accept(new DynamicImageButton(
x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF, x, y, ICON_WIDTH, ICON_HEIGHT,
TEXTURE, TEX_SIZE, TEX_SIZE, b -> input.queueEvent("terminate"), GuiSprites.TERMINATE::get,
b -> input.queueEvent("terminate"),
new HintedMessage( new HintedMessage(
Component.translatable("gui.computercraft.tooltip.terminate"), Component.translatable("gui.computercraft.tooltip.terminate"),
Component.translatable("gui.computercraft.tooltip.terminate.key") 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) { public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) {
Screen.blit(transform, var texture = textures.sidebar();
x, y, 0, 102, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER, if (texture == null) throw new NullPointerException(textures + " has no sidebar texture");
ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE var sprite = GuiSprites.get(texture);
);
Screen.blit(transform, renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT);
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
);
} }
private static void toggleComputer(BooleanSupplier isOn, InputHandler input) { private static void toggleComputer(BooleanSupplier isOn, InputHandler input) {

View File

@@ -6,14 +6,14 @@ package dan200.computercraft.client.gui.widgets;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.function.IntSupplier;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
@@ -21,61 +21,41 @@ import java.util.function.Supplier;
* dynamically. * dynamically.
*/ */
public class DynamicImageButton extends Button { public class DynamicImageButton extends Button {
private final ResourceLocation texture; private final Boolean2ObjectFunction<TextureAtlasSprite> texture;
private final IntSupplier xTexStart;
private final int yTexStart;
private final int yDiffTex;
private final int textureWidth;
private final int textureHeight;
private final Supplier<HintedMessage> message; private final Supplier<HintedMessage> message;
public DynamicImageButton( public DynamicImageButton(
int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex, int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress,
ResourceLocation texture, int textureWidth, int textureHeight, HintedMessage message
OnPress onPress, HintedMessage message
) { ) {
this( this(x, y, width, height, texture, onPress, () -> message);
x, y, width, height, () -> xTexStart, yTexStart, yDiffTex,
texture, textureWidth, textureHeight,
onPress, () -> message
);
} }
public DynamicImageButton( public DynamicImageButton(
int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex, int x, int y, int width, int height,
ResourceLocation texture, int textureWidth, int textureHeight, Boolean2ObjectFunction<TextureAtlasSprite> texture,
OnPress onPress, Supplier<HintedMessage> message OnPress onPress, Supplier<HintedMessage> message
) { ) {
super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION); 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.texture = texture;
this.message = message; this.message = message;
} }
@Override @Override
public void renderWidget(PoseStack stack, int mouseX, int mouseY, float partialTicks) { 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(); RenderSystem.disableDepthTest();
var yTex = yTexStart; blit(stack, getX(), getY(), 0, width, height, texture);
if (isHoveredOrFocused()) yTex += yDiffTex;
blit(stack, getX(), getY(), xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight);
RenderSystem.enableDepthTest(); RenderSystem.enableDepthTest();
} }
@Override
public Component getMessage() {
return message.get().message;
}
@Override @Override
public void render(PoseStack stack, int mouseX, int mouseY, float partialTicks) { 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); super.render(stack, mouseX, mouseY, partialTicks);
} }

View File

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

View File

@@ -26,13 +26,14 @@ import java.util.List;
* <p> * <p>
* This is typically used with a {@link BakedModel} subclass - see the loader-specific projects. * This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
*/ */
public final class ModelTransformer { public class ModelTransformer {
public static final int[] ORDER = new int[]{ 3, 2, 1, 0 }; private static final int[] INVERSE_ORDER = new int[]{ 3, 2, 1, 0 };
public static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
private static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION); private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION);
private final Matrix4f transformation; protected final Matrix4f transformation;
private final boolean invert; protected final boolean invert;
private @Nullable TransformedQuads cache; private @Nullable TransformedQuads cache;
public ModelTransformer(Transformation transformation) { public ModelTransformer(Transformation transformation) {
@@ -60,7 +61,7 @@ public final class ModelTransformer {
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
var inStart = STRIDE * i; var inStart = STRIDE * i;
// Reverse the order of the quads if we're inverting // 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); System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
// Apply the matrix to our position // 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()); 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) { private record TransformedQuads(List<BakedQuad> original, List<BakedQuad> transformed) {
} }

View File

@@ -4,8 +4,13 @@
package dan200.computercraft.client.platform; package dan200.computercraft.client.platform;
import com.mojang.blaze3d.vertex.PoseStack;
import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.server.ServerNetworkContext; 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 { public interface ClientPlatformHelper extends dan200.computercraft.impl.client.ClientPlatformHelper {
static ClientPlatformHelper get() { static ClientPlatformHelper get() {
@@ -18,4 +23,16 @@ public interface ClientPlatformHelper extends dan200.computercraft.impl.client.C
* @param message The message to send. * @param message The message to send.
*/ */
void sendToServer(NetworkMessage<ServerNetworkContext> message); 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);
} }

View File

@@ -4,25 +4,17 @@
package dan200.computercraft.client.render; package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.Tesselator; import dan200.computercraft.client.gui.GuiSprites;
import com.mojang.blaze3d.vertex.VertexConsumer; import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.computer.core.ComputerFamily; import static dan200.computercraft.client.render.SpriteRenderer.u;
import net.minecraft.client.renderer.MultiBufferSource; import static dan200.computercraft.client.render.SpriteRenderer.v;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.resources.ResourceLocation;
import org.joml.Matrix4f;
/** /**
* Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or * Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or
* {@linkplain PocketItemRenderer in-hand pocket computers}. * {@linkplain PocketItemRenderer in-hand pocket computers}.
*/ */
public class ComputerBorderRenderer { public final 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");
/** /**
* The margin between the terminal and its border. * The margin between the terminal and its border.
*/ */
@@ -33,100 +25,51 @@ public class ComputerBorderRenderer {
*/ */
public static final int BORDER = 12; 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 LIGHT_HEIGHT = 8;
public static final int TEX_SIZE = 256; private static final int TEX_SIZE = 36;
private static final float TEX_SCALE = 1 / (float) TEX_SIZE;
private final Matrix4f transform; private ComputerBorderRenderer() {
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;
} }
public static ResourceLocation getTexture(ComputerFamily family) { public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) {
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) {
var endX = x + width; var endX = x + width;
var endY = y + height; var endY = y + height;
// Vertical bars var border = GuiSprites.get(textures.border());
renderLine(x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y);
renderLine(endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y);
// Top bar // Top bar
renderLine(x, y - BORDER, 0, 0, endX - x, BORDER); blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER);
renderCorner(x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y); blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER);
renderCorner(endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y); 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 // Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the
// pocket computer's lights). // pocket computer's lights).
if (withLight) { if (withLight) {
renderTexture(x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT); var pocketBottomTexture = textures.pocketBottom();
renderTexture(x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture");
renderTexture(endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); var pocketBottom = GuiSprites.get(pocketBottomTexture);
renderer.blitHorizontalSliced(
pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT,
BORDER, BORDER, BORDER * 3
);
} else { } else {
renderLine(x, endY, 0, BORDER, endX - x, BORDER); blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER);
renderCorner(x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y); blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER);
renderCorner(endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y); blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER);
} }
} }
private void renderCorner(int x, int y, int u, int v) { private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) {
renderTexture(x, y, u, v, BORDER, BORDER, BORDER, BORDER); renderer.blit(
} x, y, width, height,
u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE),
private void renderLine(int x, int y, int u, int v, int width, int height) { u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE)
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();
} }
} }

View File

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

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis; import com.mojang.math.Axis;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.pocket.ClientPocketComputers; import dan200.computercraft.client.pocket.ClientPocketComputers;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.util.Colour; 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) { 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 r = (colour >>> 16) & 0xFF;
var g = ((colour >>> 8) & 0xFF) / 255.0f; var g = (colour >>> 8) & 0xFF;
var b = (colour & 0xFF) / 255.0f; 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) { private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) {

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.DefaultVertexFormat; import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexFormat; import com.mojang.blaze3d.vertex.VertexFormat;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader; import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer; import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import net.minecraft.client.renderer.GameRenderer; 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")); 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() { public static MonitorTextureBufferShader getMonitorTextureBufferShader() {
if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered"); if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered");
return monitorTboShader; return monitorTboShader;

View File

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

View File

@@ -5,36 +5,28 @@
package dan200.computercraft.client.render; package dan200.computercraft.client.render;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis; import com.mojang.math.Axis;
import com.mojang.math.Transformation; import com.mojang.math.Transformation;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide; import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.model.turtle.ModelTransformer;
import dan200.computercraft.client.platform.ClientPlatformHelper; import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers; import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity; import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.DirectionUtil;
import dan200.computercraft.shared.util.Holiday; import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font; import net.minecraft.client.gui.Font;
import net.minecraft.client.renderer.MultiBufferSource; 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.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.HitResult;
import org.joml.Vector4f;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.List;
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> { public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
private static final ModelResourceLocation NORMAL_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_normal", "inventory"); 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 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 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 BlockEntityRenderDispatcher renderer;
private final Font font; private final Font font;
@@ -109,23 +99,22 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
var family = turtle.getFamily(); var family = turtle.getFamily();
var overlay = turtle.getOverlay(); var overlay = turtle.getOverlay();
var buffer = buffers.getBuffer(Sheets.translucentCullBlockSheet()); renderModel(transform, buffers, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
renderModel(transform, buffer, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
// Render the overlay // Render the overlay
var overlayModel = getTurtleOverlayModel(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS); var overlayModel = getTurtleOverlayModel(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);
if (overlayModel != null) { if (overlayModel != null) {
renderModel(transform, buffer, lightmapCoord, overlayLight, overlayModel, null); renderModel(transform, buffers, lightmapCoord, overlayLight, overlayModel, null);
} }
// Render the upgrades // Render the upgrades
renderUpgrade(transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.LEFT, partialTicks); renderUpgrade(transform, buffers, lightmapCoord, overlayLight, turtle, TurtleSide.LEFT, partialTicks);
renderUpgrade(transform, buffer, lightmapCoord, overlayLight, turtle, TurtleSide.RIGHT, partialTicks); renderUpgrade(transform, buffers, lightmapCoord, overlayLight, turtle, TurtleSide.RIGHT, partialTicks);
transform.popPose(); 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); var upgrade = turtle.getUpgrade(side);
if (upgrade == null) return; if (upgrade == null) return;
transform.pushPose(); transform.pushPose();
@@ -136,16 +125,15 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
transform.translate(0.0f, -0.5f, -0.5f); transform.translate(0.0f, -0.5f, -0.5f);
var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side); var model = TurtleUpgradeModellers.getModel(upgrade, turtle.getAccess(), side);
pushPoseFromTransformation(transform, model.getMatrix()); applyTransformation(transform, model.getMatrix());
renderModel(transform, renderer, lightmapCoord, overlayLight, model.getModel(), null); renderModel(transform, buffers, lightmapCoord, overlayLight, model.getModel(), null);
transform.popPose();
transform.popPose(); 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(); 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. * @param tints Tints for the quads, as an array of RGB values.
* @see net.minecraft.client.renderer.block.ModelBlockRenderer#renderModel * @see net.minecraft.client.renderer.block.ModelBlockRenderer#renderModel
*/ */
private void renderModel(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, BakedModel model, @Nullable int[] tints) { private void renderModel(PoseStack transform, MultiBufferSource renderer, int lightmapCoord, int overlayLight, BakedModel model, @Nullable int[] tints) {
for (var facing : DirectionUtil.FACINGS) { ClientPlatformHelper.get().renderBakedModel(transform, renderer, model, lightmapCoord, overlayLight, tints);
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 static void renderQuads(PoseStack transform, VertexConsumer buffer, int lightmapCoord, int overlayLight, List<BakedQuad> quads, @Nullable int[] tints) { private static void applyTransformation(PoseStack stack, Transformation transformation) {
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();
var trans = transformation.getTranslation(); var trans = transformation.getTranslation();
stack.translate(trans.x(), trans.y(), trans.z()); stack.translate(trans.x(), trans.y(), trans.z());

View File

@@ -13,6 +13,9 @@ import dan200.computercraft.shared.turtle.upgrades.TurtleModem;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Nullable; 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. * 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 ? leftOnModel : leftOffModel)
: TransformedModel.of(active ? rightOnModel : rightOffModel); : TransformedModel.of(active ? rightOnModel : rightOffModel);
} }
@Override
public Collection<ResourceLocation> getDependencies() {
return List.of(leftOffModel, rightOffModel, leftOnModel, rightOnModel);
}
} }

View File

@@ -15,10 +15,12 @@ import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager; import dan200.computercraft.impl.UpgradeManager;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
/** /**
* A registry of {@link TurtleUpgradeModeller}s. * A registry of {@link TurtleUpgradeModeller}s.
@@ -71,4 +73,8 @@ public final class TurtleUpgradeModellers {
var modeller = turtleModels.get(wrapper.serialiser()); var modeller = turtleModels.get(wrapper.serialiser());
return modeller == null ? NULL_TURTLE_MODELLER : modeller; return modeller == null ? NULL_TURTLE_MODELLER : modeller;
} }
public static Stream<ResourceLocation> getDependencies() {
return turtleModels.values().stream().flatMap(x -> x.getDependencies().stream());
}
} }

View File

@@ -4,8 +4,10 @@
package dan200.computercraft.data.client; package dan200.computercraft.data.client;
import dan200.computercraft.client.gui.GuiSprites;
import dan200.computercraft.data.DataProviders; import dan200.computercraft.data.DataProviders;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot; 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.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile; import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -13,6 +15,7 @@ import net.minecraft.server.packs.PackType;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
/** /**
* A version of {@link DataProviders} which relies on client-side classes. * 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.LEFT_UPGRADE, Optional.empty()),
new SingleFile(UpgradeSlot.RIGHT_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());
}); });
} }
} }

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.data;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import dan200.computercraft.api.ComputerCraftAPI; import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.ComputerCraftTags;
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider; import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider; import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
import dan200.computercraft.api.upgrades.UpgradeBase; 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.CachedOutput;
import net.minecraft.data.DataProvider; import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput; import net.minecraft.data.PackOutput;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block; 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(), "Advanced Pocket Computer");
add(ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().getDescriptionId() + ".upgraded", "Advanced %s 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 // Turtle/pocket upgrades
add("upgrade.minecraft.diamond_sword.adjective", "Melee"); add("upgrade.minecraft.diamond_sword.adjective", "Melee");
add("upgrade.minecraft.diamond_shovel.adjective", "Digging"); 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.desc", "Dump the latest results of computer tracking.");
add("commands.computercraft.track.dump.no_timings", "No timings available"); add("commands.computercraft.track.dump.no_timings", "No timings available");
add("commands.computercraft.track.dump.computer", "Computer"); 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.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."); 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.defaultComputerSettings, "Default Computer settings");
addConfigEntry(ConfigSpec.logComputerErrors, "Log computer errors"); addConfigEntry(ConfigSpec.logComputerErrors, "Log computer errors");
addConfigEntry(ConfigSpec.commandRequireCreative, "Command computers require creative"); addConfigEntry(ConfigSpec.commandRequireCreative, "Command computers require creative");
addConfigEntry(ConfigSpec.disabledGenericMethods, "Disabled generic methods");
addConfigGroup(ConfigSpec.serverSpec, "execution", "Execution"); addConfigGroup(ConfigSpec.serverSpec, "execution", "Execution");
addConfigEntry(ConfigSpec.computerThreads, "Computer threads"); addConfigEntry(ConfigSpec.computerThreads, "Computer threads");
@@ -277,8 +283,8 @@ public final class LanguageProvider implements DataProvider {
turtleUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective), turtleUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective), pocketUpgrades.getGeneratedUpgrades().stream().map(UpgradeBase::getUnlocalisedAdjective),
Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"), Metric.metrics().values().stream().map(x -> AggregatedMetric.TRANSLATION_PREFIX + x.name() + ".name"),
getConfigEntries(ConfigSpec.serverSpec).map(ConfigFile.Entry::translationKey), ConfigSpec.serverSpec.entries().map(ConfigFile.Entry::translationKey),
getConfigEntries(ConfigSpec.clientSpec).map(ConfigFile.Entry::translationKey) ConfigSpec.clientSpec.entries().map(ConfigFile.Entry::translationKey)
).flatMap(x -> x); ).flatMap(x -> x);
} }
@@ -298,6 +304,10 @@ public final class LanguageProvider implements DataProvider {
add(AggregatedMetric.TRANSLATION_PREFIX + metric.name() + ".name", text); 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) { private void addConfigGroup(ConfigFile spec, String path, String text) {
var entry = spec.getEntry(path); var entry = spec.getEntry(path);
if (!(entry instanceof ConfigFile.Group)) throw new IllegalArgumentException("Cannot find group " + 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(), text);
add(value.translationKey() + ".tooltip", value.comment()); 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);
}
} }

View File

@@ -6,10 +6,12 @@ package dan200.computercraft.impl;
import dan200.computercraft.api.lua.GenericSource; import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.core.asm.GenericMethod; import dan200.computercraft.core.asm.GenericMethod;
import dan200.computercraft.shared.config.ConfigSpec;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
/** /**
* The global registry for {@link GenericSource}s. * The global registry for {@link GenericSource}s.
@@ -29,6 +31,11 @@ public final class GenericSources {
} }
public static Collection<GenericMethod> getAllMethods() { 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();
} }
} }

View File

@@ -15,6 +15,7 @@ import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.util.Colour; import dan200.computercraft.core.util.Colour;
import dan200.computercraft.impl.PocketUpgrades; import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades; import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.command.UserLevel;
import dan200.computercraft.shared.command.arguments.ComputerArgumentType; import dan200.computercraft.shared.command.arguments.ComputerArgumentType;
import dan200.computercraft.shared.command.arguments.ComputersArgumentType; import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
import dan200.computercraft.shared.command.arguments.RepeatArgumentType; 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.data.PlayerCreativeLootCondition;
import dan200.computercraft.shared.details.BlockDetails; import dan200.computercraft.shared.details.BlockDetails;
import dan200.computercraft.shared.details.ItemDetails; 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.DiskItem;
import dan200.computercraft.shared.media.items.PrintoutItem; import dan200.computercraft.shared.media.items.PrintoutItem;
import dan200.computercraft.shared.media.items.RecordMedia; 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.turtle.upgrades.*;
import dan200.computercraft.shared.util.ImpostorRecipe; import dan200.computercraft.shared.util.ImpostorRecipe;
import dan200.computercraft.shared.util.ImpostorShapelessRecipe; import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.commands.synchronization.SingletonArgumentInfo; import net.minecraft.commands.synchronization.SingletonArgumentInfo;
import net.minecraft.core.BlockPos; 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 net.minecraft.world.level.storage.loot.predicates.LootItemConditionType;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Predicate;
/** /**
* Registers ComputerCraft's registry entries and additional objects, such as {@link CauldronInteraction}s and * 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 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. * 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(); ArgumentTypes.REGISTRY.register();
LootItemConditionTypes.REGISTRY.register(); LootItemConditionTypes.REGISTRY.register();
RecipeSerializers.REGISTRY.register(); RecipeSerializers.REGISTRY.register();
Permissions.REGISTRY.register();
// Register bundled power providers // Register bundled power providers
ComputerCraftAPI.registerBundledRedstoneProvider(new DefaultBundledRedstoneProvider()); ComputerCraftAPI.registerBundledRedstoneProvider(new DefaultBundledRedstoneProvider());

View File

@@ -11,6 +11,7 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import dan200.computercraft.core.computer.ComputerSide; import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.Metrics; import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.command.arguments.ComputersArgumentType; import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
import dan200.computercraft.shared.command.text.TableBuilder; import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily; import dan200.computercraft.shared.computer.core.ComputerFamily;
@@ -60,7 +61,7 @@ public final class CommandComputerCraft {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) { public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(choice("computercraft") dispatcher.register(choice("computercraft")
.then(literal("dump") .then(literal("dump")
.requires(UserLevel.OWNER_OP) .requires(ModRegistry.Permissions.PERMISSION_DUMP)
.executes(context -> { .executes(context -> {
var table = new TableBuilder("DumpAll", "Computer", "On", "Position"); var table = new TableBuilder("DumpAll", "Computer", "On", "Position");
@@ -118,7 +119,7 @@ public final class CommandComputerCraft {
}))) })))
.then(command("shutdown") .then(command("shutdown")
.requires(UserLevel.OWNER_OP) .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN)
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
.executes((context, computerSelectors) -> { .executes((context, computerSelectors) -> {
var shutdown = 0; var shutdown = 0;
@@ -132,7 +133,7 @@ public final class CommandComputerCraft {
})) }))
.then(command("turn-on") .then(command("turn-on")
.requires(UserLevel.OWNER_OP) .requires(ModRegistry.Permissions.PERMISSION_TURN_ON)
.argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers())
.executes((context, computerSelectors) -> { .executes((context, computerSelectors) -> {
var on = 0; var on = 0;
@@ -146,7 +147,7 @@ public final class CommandComputerCraft {
})) }))
.then(command("tp") .then(command("tp")
.requires(UserLevel.OP) .requires(ModRegistry.Permissions.PERMISSION_TP)
.arg("computer", oneComputer()) .arg("computer", oneComputer())
.executes(context -> { .executes(context -> {
var computer = getComputerArgument(context, "computer"); var computer = getComputerArgument(context, "computer");
@@ -171,7 +172,7 @@ public final class CommandComputerCraft {
})) }))
.then(command("queue") .then(command("queue")
.requires(UserLevel.ANYONE) .requires(ModRegistry.Permissions.PERMISSION_QUEUE)
.arg( .arg(
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers()) RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
.suggests((context, builder) -> Suggestions.empty()) .suggests((context, builder) -> Suggestions.empty())
@@ -193,7 +194,7 @@ public final class CommandComputerCraft {
})) }))
.then(command("view") .then(command("view")
.requires(UserLevel.OP) .requires(ModRegistry.Permissions.PERMISSION_VIEW)
.arg("computer", oneComputer()) .arg("computer", oneComputer())
.executes(context -> { .executes(context -> {
var player = context.getSource().getPlayerOrException(); var player = context.getSource().getPlayerOrException();
@@ -213,8 +214,8 @@ public final class CommandComputerCraft {
})) }))
.then(choice("track") .then(choice("track")
.requires(ModRegistry.Permissions.PERMISSION_TRACK)
.then(command("start") .then(command("start")
.requires(UserLevel.OWNER_OP)
.executes(context -> { .executes(context -> {
getMetricsInstance(context.getSource()).start(); getMetricsInstance(context.getSource()).start();
@@ -227,7 +228,6 @@ public final class CommandComputerCraft {
})) }))
.then(command("stop") .then(command("stop")
.requires(UserLevel.OWNER_OP)
.executes(context -> { .executes(context -> {
var timings = getMetricsInstance(context.getSource()); var timings = getMetricsInstance(context.getSource());
if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create(); if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create();
@@ -236,7 +236,6 @@ public final class CommandComputerCraft {
})) }))
.then(command("dump") .then(command("dump")
.requires(UserLevel.OWNER_OP)
.argManyValue("fields", metric(), DEFAULT_FIELDS) .argManyValue("fields", metric(), DEFAULT_FIELDS)
.executes((context, fields) -> { .executes((context, fields) -> {
AggregatedMetric sort; AggregatedMetric sort;
@@ -270,23 +269,25 @@ public final class CommandComputerCraft {
out.append(" (id " + computerId + ")"); out.append(" (id " + computerId + ")");
// And, if we're a player, some useful links // And, if we're a player, some useful links
if (serverComputer != null && UserLevel.OP.test(source) && isPlayer(source)) { if (serverComputer != null && isPlayer(source)) {
out if (ModRegistry.Permissions.PERMISSION_TP.test(source)) {
.append(" ") out.append(" ").append(link(
.append(link(
text("\u261b"), text("\u261b"),
"/computercraft tp " + serverComputer.getInstanceID(), "/computercraft tp " + serverComputer.getInstanceID(),
Component.translatable("commands.computercraft.tp.action") Component.translatable("commands.computercraft.tp.action")
)) ));
.append(" ") }
.append(link(
if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) {
out.append(" ").append(link(
text("\u20e2"), text("\u20e2"),
"/computercraft view " + serverComputer.getInstanceID(), "/computercraft view " + serverComputer.getInstanceID(),
Component.translatable("commands.computercraft.view.action") Component.translatable("commands.computercraft.view.action")
)); ));
}
} }
if (UserLevel.OWNER.test(source) && isPlayer(source)) { if (isPlayer(source) && UserLevel.isOwner(source)) {
var linkPath = linkStorage(source, computerId); var linkPath = linkStorage(source, computerId);
if (linkPath != null) out.append(" ").append(linkPath); if (linkPath != null) out.append(" ").append(linkPath);
} }
@@ -295,7 +296,7 @@ public final class CommandComputerCraft {
} }
private static Component linkPosition(CommandSourceStack context, ServerComputer computer) { private static Component linkPosition(CommandSourceStack context, ServerComputer computer) {
if (UserLevel.OP.test(context)) { if (ModRegistry.Permissions.PERMISSION_TP.test(context)) {
return link( return link(
position(computer.getPosition()), position(computer.getPosition()),
"/computercraft tp " + computer.getInstanceID(), "/computercraft tp " + computer.getInstanceID(),

View File

@@ -10,7 +10,6 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import dan200.computercraft.shared.platform.PlatformHelper; import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.server.level.ServerPlayer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
@@ -22,8 +21,8 @@ public final class CommandUtils {
} }
public static boolean isPlayer(CommandSourceStack output) { public static boolean isPlayer(CommandSourceStack output) {
var sender = output.getEntity(); var player = output.getPlayer();
return sender instanceof ServerPlayer player && !PlatformHelper.get().isFakePlayer(player); return player != null && !PlatformHelper.get().isFakePlayer(player);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@@ -5,7 +5,7 @@
package dan200.computercraft.shared.command; package dan200.computercraft.shared.command;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.world.entity.player.Player; import net.minecraft.server.level.ServerPlayer;
import java.util.function.Predicate; 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. * The level a user must be at in order to execute a command.
*/ */
public enum UserLevel implements Predicate<CommandSourceStack> { 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. * Can only be used by ops.
*/ */
@@ -35,7 +30,6 @@ public enum UserLevel implements Predicate<CommandSourceStack> {
public int toLevel() { public int toLevel() {
return switch (this) { return switch (this) {
case OWNER -> 4;
case OP, OWNER_OP -> 2; case OP, OWNER_OP -> 2;
case ANYONE -> 0; case ANYONE -> 0;
}; };
@@ -44,39 +38,26 @@ public enum UserLevel implements Predicate<CommandSourceStack> {
@Override @Override
public boolean test(CommandSourceStack source) { public boolean test(CommandSourceStack source) {
if (this == ANYONE) return true; if (this == ANYONE) return true;
if (this == OWNER) return isOwner(source);
if (this == OWNER_OP && isOwner(source)) return true; if (this == OWNER_OP && isOwner(source)) return true;
return source.hasPermission(toLevel()); return source.hasPermission(toLevel());
} }
/** public boolean test(ServerPlayer source) {
* Take the union of two {@link UserLevel}s. if (this == ANYONE) return true;
* <p> if (this == OWNER_OP && isOwner(source)) return true;
* This satisfies the property that for all sources {@code s}, {@code a.test(s) || b.test(s) == (a b).test(s)}. return source.hasPermissions(toLevel());
*
* @param left The first user level to take the union of.
* @param right The second user level to take the union of.
* @return The union of two levels.
*/
public static UserLevel union(UserLevel left, UserLevel right) {
if (left == right) return left;
// x ANYONE = ANYONE
if (left == ANYONE || right == ANYONE) return ANYONE;
// x OWNER = OWNER
if (left == OWNER) return right;
if (right == OWNER) return left;
// At this point, we have x != y and x, y { OP, OWNER_OP }.
return OWNER_OP;
} }
private static boolean isOwner(CommandSourceStack source) { public static boolean isOwner(CommandSourceStack source) {
var server = source.getServer(); var server = source.getServer();
var sender = source.getEntity(); var player = source.getPlayer();
return server.isDedicatedServer() return server.isDedicatedServer()
? source.getEntity() == null && source.hasPermission(4) && source.getTextName().equals("Server") ? 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());
} }
} }

View File

@@ -44,12 +44,12 @@ public class ArgumentUtils {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>> void serializeToNetwork(FriendlyByteBuf buffer, ArgumentTypeInfo<A, T> type, ArgumentTypeInfo.Template<A> template) { 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); type.serializeToNetwork((T) template, buffer);
} }
public static ArgumentTypeInfo.Template<?> deserialize(FriendlyByteBuf 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"); Objects.requireNonNull(type, "Unknown argument type");
return type.deserializeFromNetwork(buffer); return type.deserializeFromNetwork(buffer);
} }

View File

@@ -44,7 +44,8 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
} }
public CommandBuilder<S> requires(Predicate<S> predicate) { 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; return this;
} }

View File

@@ -10,7 +10,6 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.LiteralCommandNode;
import dan200.computercraft.shared.command.UserLevel;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.ClickEvent;
@@ -31,6 +30,7 @@ import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
*/ */
public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<CommandSourceStack> { public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<CommandSourceStack> {
private final Collection<HelpingArgumentBuilder> children = new ArrayList<>(); private final Collection<HelpingArgumentBuilder> children = new ArrayList<>();
private @Nullable Predicate<CommandSourceStack> requirement;
private HelpingArgumentBuilder(String literal) { private HelpingArgumentBuilder(String literal) {
super(literal); super(literal);
@@ -41,26 +41,20 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
} }
@Override @Override
public LiteralArgumentBuilder<CommandSourceStack> requires(Predicate<CommandSourceStack> requirement) { public HelpingArgumentBuilder requires(Predicate<CommandSourceStack> requirement) {
throw new IllegalStateException("Cannot use requires on a HelpingArgumentBuilder"); this.requirement = requirement;
return this;
} }
@Override @Override
public Predicate<CommandSourceStack> getRequirement() { public Predicate<CommandSourceStack> getRequirement() {
// The requirement of this node is the union of all child's requirements. if (requirement != null) return requirement;
var requirements = Stream.concat( var requirements = Stream.concat(
children.stream().map(ArgumentBuilder::getRequirement), children.stream().map(ArgumentBuilder::getRequirement),
getArguments().stream().map(CommandNode::getRequirement) getArguments().stream().map(CommandNode::getRequirement)
).toList(); ).toList();
return x -> requirements.stream().anyMatch(y -> y.test(x));
// If all requirements are a UserLevel, take the union of those instead.
var userLevel = UserLevel.OWNER;
for (var requirement : requirements) {
if (!(requirement instanceof UserLevel level)) return x -> requirements.stream().anyMatch(y -> y.test(x));
userLevel = UserLevel.union(userLevel, level);
}
return userLevel;
} }
@Override @Override
@@ -181,7 +175,7 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
.append(Component.translatable("commands." + id + ".desc")); .append(Component.translatable("commands." + id + ".desc"));
for (var child : node.getChildren()) { for (var child : node.getChildren()) {
if (!child.getRequirement().test(context.getSource()) || !(child instanceof LiteralCommandNode)) { if (!child.canUse(context.getSource()) || !(child instanceof LiteralCommandNode)) {
continue; continue;
} }

View File

@@ -168,7 +168,7 @@ public class CommandAPI implements ILuaAPI {
/** /**
* Get information about a range of blocks. * Get information about a range of blocks.
* <p> * <p>
* This returns the same information as @{getBlockInfo}, just for multiple * This returns the same information as [`getBlockInfo`], just for multiple
* blocks at once. * blocks at once.
* <p> * <p>
* Blocks are traversed by ascending y level, followed by z and x - the returned * 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. * Get some basic information about a block.
* <p> * <p>
* The returned table contains the current name, metadata and block state (as * 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. * will also be returned.
* *
* @param x The x position of the block to query. * @param x The x position of the block to query.

View File

@@ -199,6 +199,11 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
return localSide; 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) { private void updateRedstoneInput(ServerComputer computer, Direction dir, BlockPos targetPos) {
var offsetSide = dir.getOpposite(); var offsetSide = dir.getOpposite();
var localDir = remapToLocalSide(dir); 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 // If the position is not any adjacent one, update all inputs. This is pretty terrible, but some redstone mods
// handle this incorrectly. // handle this incorrectly.
var pos = getBlockPos(); updateRedstoneInputs(computer);
for (var dir : DirectionUtil.FACINGS) updateRedstoneInput(computer, dir, pos.relative(dir));
invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty. invalidSides = (1 << 6) - 1; // Mark all peripherals as dirty.
} }
@@ -264,9 +268,10 @@ public abstract class AbstractComputerBlockEntity extends BlockEntity implements
*/ */
public void updateOutput() { public void updateOutput() {
BlockEntityHelpers.updateBlock(this); BlockEntityHelpers.updateBlock(this);
for (var dir : DirectionUtil.FACINGS) { for (var dir : DirectionUtil.FACINGS) RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
RedstoneUtil.propagateRedstoneOutput(getLevel(), getBlockPos(), dir);
} var computer = getServerComputer();
if (computer != null) updateRedstoneInputs(computer);
} }
protected abstract ServerComputer createComputer(int id); protected abstract ServerComputer createComputer(int id);

View File

@@ -5,18 +5,10 @@
package dan200.computercraft.shared.computer.terminal; package dan200.computercraft.shared.computer.terminal;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import javax.annotation.Nullable; 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. * A snapshot of a terminal's state.
@@ -31,20 +23,10 @@ public class TerminalState {
public final int width; public final int width;
public final int height; public final int height;
private final boolean compress;
@Nullable @Nullable
private final ByteBuf buffer; private final ByteBuf buffer;
private @Nullable ByteBuf compressed;
public TerminalState(@Nullable NetworkedTerminal terminal) { public TerminalState(@Nullable NetworkedTerminal terminal) {
this(terminal, true);
}
public TerminalState(@Nullable NetworkedTerminal terminal, boolean compress) {
this.compress = compress;
if (terminal == null) { if (terminal == null) {
colour = false; colour = false;
width = height = 0; width = height = 0;
@@ -61,14 +43,13 @@ public class TerminalState {
public TerminalState(FriendlyByteBuf buf) { public TerminalState(FriendlyByteBuf buf) {
colour = buf.readBoolean(); colour = buf.readBoolean();
compress = buf.readBoolean();
if (buf.readBoolean()) { if (buf.readBoolean()) {
width = buf.readVarInt(); width = buf.readVarInt();
height = buf.readVarInt(); height = buf.readVarInt();
var length = buf.readVarInt(); var length = buf.readVarInt();
buffer = readCompressed(buf, length, compress); buffer = buf.readBytes(length);
} else { } else {
width = height = 0; width = height = 0;
buffer = null; buffer = null;
@@ -77,16 +58,13 @@ public class TerminalState {
public void write(FriendlyByteBuf buf) { public void write(FriendlyByteBuf buf) {
buf.writeBoolean(colour); buf.writeBoolean(colour);
buf.writeBoolean(compress);
buf.writeBoolean(buffer != null); buf.writeBoolean(buffer != null);
if (buffer != null) { if (buffer != null) {
buf.writeVarInt(width); buf.writeVarInt(width);
buf.writeVarInt(height); buf.writeVarInt(height);
buf.writeVarInt(buffer.readableBytes());
var sendBuffer = getCompressed(); buf.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes());
buf.writeVarInt(sendBuffer.readableBytes());
buf.writeBytes(sendBuffer, sendBuffer.readerIndex(), sendBuffer.readableBytes());
} }
} }
@@ -110,40 +88,4 @@ public class TerminalState {
terminal.read(new FriendlyByteBuf(buffer)); terminal.read(new FriendlyByteBuf(buffer));
return terminal; 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;
}
}
} }

View File

@@ -26,6 +26,10 @@ class AddressRuleConfig {
private static final AddressRule REJECT_ALL = AddressRule.parse("*", OptionalInt.empty(), Action.DENY.toPartial()); 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() { public static List<UnmodifiableConfig> defaultRules() {
return List.of( return List.of(
makeRule(config -> { makeRule(config -> {
@@ -88,9 +92,20 @@ class AddressRuleConfig {
var port = unboxOptInt(get(builder, "port", Number.class)); var port = unboxOptInt(get(builder, "port", Number.class));
var maxUpload = unboxOptLong(get(builder, "max_upload", Number.class).map(Number::longValue)); 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 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); 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( var options = new PartialOptions(
action, action,
maxUpload, maxUpload,

View File

@@ -54,12 +54,6 @@ public interface ConfigFile {
* A group of config entries. * A group of config entries.
*/ */
non-sealed interface Group extends Entry { non-sealed interface Group extends Entry {
/**
* Get all entries in this group.
*
* @return All child entries.
*/
Stream<Entry> children();
} }
/** /**

View File

@@ -35,6 +35,7 @@ public final class ConfigSpec {
public static final ConfigFile.Value<Boolean> logComputerErrors; public static final ConfigFile.Value<Boolean> logComputerErrors;
public static final ConfigFile.Value<Boolean> commandRequireCreative; public static final ConfigFile.Value<Boolean> commandRequireCreative;
public static final ConfigFile.Value<Integer> uploadMaxSize; 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> computerThreads;
public static final ConfigFile.Value<Integer> maxMainGlobalTime; 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 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.""") command computers. This is the default behaviour for vanilla's Command blocks.""")
.define("command_require_creative", Config.commandRequireCreative); .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);
} }
{ {

View File

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

View File

@@ -6,13 +6,11 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.NetworkMessage; import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity; import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity;
import dan200.computercraft.shared.platform.RegistryWrappers;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.sounds.SoundEvent; import net.minecraft.sounds.SoundEvent;
import javax.annotation.Nullable; 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}. * 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) { public PlayRecordClientMessage(FriendlyByteBuf buf) {
pos = buf.readBlockPos(); pos = buf.readBlockPos();
soundEvent = buf.readBoolean() ? RegistryWrappers.readKey(buf, RegistryWrappers.SOUND_EVENTS) : null; soundEvent = buf.readNullable(SoundEvent::readFromNetwork);
name = buf.readBoolean() ? buf.readUtf(Short.MAX_VALUE) : null; name = buf.readNullable(FriendlyByteBuf::readUtf);
} }
@Override @Override
public void toBytes(FriendlyByteBuf buf) { public void toBytes(FriendlyByteBuf buf) {
buf.writeBlockPos(pos); buf.writeBlockPos(pos);
writeOptional(buf, soundEvent, (b, e) -> RegistryWrappers.writeKey(b, RegistryWrappers.SOUND_EVENTS, e)); buf.writeNullable(soundEvent, (b, e) -> e.writeToNetwork(b));
writeOptional(buf, name, FriendlyByteBuf::writeUtf); buf.writeNullable(name, FriendlyByteBuf::writeUtf);
} }
@Override @Override
public void handle(ClientNetworkContext context) { public void handle(ClientNetworkContext context) {
context.handlePlayRecord(pos, soundEvent, name); 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);
}
}
} }

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.shared.peripheral.modem; package dan200.computercraft.shared.peripheral.modem;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.network.Packet; 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. * Modems allow you to send messages between computers over long distances.
* <p> * <p>
* :::tip * > [!TIP]
* Modems provide a fairly basic set of methods, which makes them very flexible but often hard to work with. The * > 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. * > [`rednet`] API is built on top of modems, and provides a more user-friendly interface.
* :::
* <p> * <p>
* ## Sending and receiving messages * ## 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 * 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. * messages.
* <p> * <p>
* Channels are represented as an integer between 0 and 65535 inclusive. These channels don't have any defined meaning, * 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 * 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. * channel 65534 ([`gps.CHANNEL_GPS`]), while [`rednet`] uses channels equal to the computer's ID.
* <p> * <p>
* - Sending messages is done with the {@link #transmit(int, int, Object)} message. * - 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> * <p>
* ## Types of modem * ## Types of modem
* CC: Tweaked comes with three kinds of modem, with different capabilities. * 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 { public abstract class ModemPeripheral implements IPeripheral, PacketSender, PacketReceiver {
private @Nullable PacketNetwork network; 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; private final ModemState state;
protected ModemPeripheral(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} * Sends a modem message on a certain channel. Modems listening on the channel will queue a {@code modem_message}
* event on adjacent computers. * event on adjacent computers.
* <p> * <p>
* :::note * > [!NOTE]
* The channel does not need be open to send a message. * > The channel does not need be open to send a message.
* :::
* *
* @param channel The channel to send messages on. * @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 * @param replyChannel The channel that responses to this message should be sent on. This can be the same as

View File

@@ -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 * If this computer is attached to the network, it _will not_ be included in
* this list. * this list.
* <p> * <p>
* :::note * > [!NOTE]
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
* *
* @param computer The calling computer. * @param computer The calling computer.
* @return Remote peripheral names on the network. * @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. * Determine if a peripheral is available on this wired network.
* <p> * <p>
* :::note * > [!NOTE]
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
* *
* @param computer The calling computer. * @param computer The calling computer.
* @param name The peripheral's name. * @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. * Get the type of a peripheral is available on this wired network.
* <p> * <p>
* :::note * > [!NOTE]
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
* *
* @param computer The calling computer. * @param computer The calling computer.
* @param name The peripheral's name. * @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. * Check a peripheral is of a particular type.
* <p> * <p>
* :::note * > [!NOTE]
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
* *
* @param computer The calling computer. * @param computer The calling computer.
* @param name The peripheral's name. * @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. * Get all available methods for the remote peripheral with the given name.
* <p> * <p>
* :::note * > [!NOTE]
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
* *
* @param computer The calling computer. * @param computer The calling computer.
* @param name The peripheral's name. * @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. * Call a method on a peripheral on this wired network.
* <p> * <p>
* :::note * > [!NOTE]
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
* *
* @param computer The calling computer. * @param computer The calling computer.
* @param context The Lua context we're executing in. * @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 * may be used by other computers on the network to wrap this computer as a
* peripheral. * peripheral.
* <p> * <p>
* :::note * > [!NOTE]
* This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it.
* :::
* *
* @return The current computer's name. * @return The current computer's name.
* @cc.treturn string|nil The current computer's name on the wired network. * @cc.treturn string|nil The current computer's name on the wired network.

View File

@@ -25,8 +25,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.HashSet; import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
public class MonitorBlockEntity extends BlockEntity { public class MonitorBlockEntity extends BlockEntity {
@@ -46,7 +47,7 @@ public class MonitorBlockEntity extends BlockEntity {
private @Nullable ServerMonitor serverMonitor; private @Nullable ServerMonitor serverMonitor;
private @Nullable ClientMonitor clientMonitor; private @Nullable ClientMonitor clientMonitor;
private @Nullable MonitorPeripheral peripheral; 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 needsUpdate = false;
private boolean needsValidating = false; private boolean needsValidating = false;

View File

@@ -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 * 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. * interacted with in-world without opening a GUI.
* <p> * <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. * ones, which are documented below.
* <p> * <p>
* Like computers, monitors come in both normal (no colour) and advanced (colour) varieties. * Like computers, monitors come in both normal (no colour) and advanced (colour) varieties.

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.shared.peripheral.speaker; package dan200.computercraft.shared.peripheral.speaker;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import dan200.computercraft.api.lua.ILuaContext; import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException; import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction; import dan200.computercraft.api.lua.LuaFunction;
@@ -57,7 +58,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
public static final int SAMPLE_RATE = 48000; public static final int SAMPLE_RATE = 48000;
private final UUID source = UUID.randomUUID(); 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 clock = 0;
private long lastPositionTime; private long lastPositionTime;
@@ -271,16 +272,15 @@ public abstract class SpeakerPeripheral implements IPeripheral {
* <p> * <p>
* This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer * 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 * 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> * <p>
* :::note * > [!NOTE]
* The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small * > 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 * > 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 * > (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or
* computer is lagging. * > computer is lagging.
* :::
* <p> * <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 context The Lua context.
* @param audio The audio data to play. * @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 * @cc.tparam [opt] number volume The volume to play this audio at. If not given, defaults to the previous volume
* given to {@link #playAudio}. * given to {@link #playAudio}.
* @cc.since 1.100 * @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 * <pre data-peripheral="speaker">{@code
* local dfpwm = require("cc.audio.dfpwm") * local dfpwm = require("cc.audio.dfpwm")

View File

@@ -5,11 +5,11 @@
package dan200.computercraft.shared.platform; package dan200.computercraft.shared.platform;
import net.minecraft.commands.synchronization.ArgumentTypeInfo; import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.core.IdMap;
import net.minecraft.core.Registry; import net.minecraft.core.Registry;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.crafting.RecipeSerializer; 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<Fluid> FLUIDS = PlatformHelper.get().wrap(Registries.FLUID);
public static final RegistryWrapper<Enchantment> ENCHANTMENTS = PlatformHelper.get().wrap(Registries.ENCHANTMENT); 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<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<RecipeSerializer<?>> RECIPE_SERIALIZERS = PlatformHelper.get().wrap(Registries.RECIPE_SERIALIZER);
public static final RegistryWrapper<MenuType<?>> MENU = PlatformHelper.get().wrap(Registries.MENU); public static final RegistryWrapper<MenuType<?>> MENU = PlatformHelper.get().wrap(Registries.MENU);
public interface RegistryWrapper<T> extends Iterable<T> { public interface RegistryWrapper<T> extends IdMap<T> {
int getId(T object);
ResourceLocation getKey(T object); ResourceLocation getKey(T object);
T get(ResourceLocation location); T get(ResourceLocation location);
@@ -46,8 +43,6 @@ public final class RegistryWrappers {
@Nullable @Nullable
T tryGet(ResourceLocation location); T tryGet(ResourceLocation location);
T get(int id);
default Stream<T> stream() { default Stream<T> stream() {
return StreamSupport.stream(spliterator(), false); return StreamSupport.stream(spliterator(), false);
} }
@@ -56,15 +51,6 @@ public final class RegistryWrappers {
private 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) { public static <K> void writeKey(FriendlyByteBuf buf, RegistryWrapper<K> registry, K object) {
buf.writeResourceLocation(registry.getKey(object)); buf.writeResourceLocation(registry.getKey(object));
} }

View File

@@ -23,40 +23,38 @@ import java.util.Optional;
* Turtles are capable of moving through the world. As turtles are blocks themselves, they are confined to Minecraft's * Turtles are capable of moving through the world. As turtles are blocks themselves, they are confined to Minecraft's
* grid, moving a single block at a time. * grid, moving a single block at a time.
* <p> * <p>
* {@literal @}{turtle.forward} and @{turtle.back} move the turtle in the direction it is facing, while @{turtle.up} and * [`turtle.forward`] and [`turtle.back`] move the turtle in the direction it is facing, while [`turtle.up`] and
* {@literal @}{turtle.down} move it up and down (as one might expect!). In order to move left or right, you first need * [`turtle.down`] move it up and down (as one might expect!). In order to move left or right, you first need
* to turn the turtle using @{turtle.turnLeft}/@{turtle.turnRight} and then move forward or backwards. * to turn the turtle using [`turtle.turnLeft`]/[`turtle.turnRight`] and then move forward or backwards.
* <p> * <p>
* :::info * > [!INFO]
* The name "turtle" comes from [Turtle graphics], which originated from the Logo programming language. Here you'd move * > The name "turtle" comes from [Turtle graphics], which originated from the Logo programming language. Here you'd
* a turtle with various commands like "move 10" and "turn left", much like ComputerCraft's turtles! * > move a turtle with various commands like "move 10" and "turn left", much like ComputerCraft's turtles!
* :::
* <p> * <p>
* Moving a turtle (though not turning it) consumes *fuel*. If a turtle does not have any @{turtle.refuel|fuel}, it * Moving a turtle (though not turning it) consumes *fuel*. If a turtle does not have any [fuel][`turtle.refuel`], it
* won't move, and the movement functions will return @{false}. If your turtle isn't going anywhere, the first thing to * won't move, and the movement functions will return [`false`]. If your turtle isn't going anywhere, the first thing to
* check is if you've fuelled your turtle. * check is if you've fuelled your turtle.
* <p> * <p>
* :::tip Handling errors * > [Handling errors][!TIP]
* Many turtle functions can fail in various ways. For instance, a turtle cannot move forward if there's already a block * > Many turtle functions can fail in various ways. For instance, a turtle cannot move forward if there's already a
* there. Instead of erroring, functions which can fail either return @{true} if they succeed, or @{false} and some * > block there. Instead of erroring, functions which can fail either return [`true`] if they succeed, or [`false`] and
* error message if they fail. * > some error message if they fail.
* <p> * >
* Unexpected failures can often lead to strange behaviour. It's often a good idea to check the return values of these * > Unexpected failures can often lead to strange behaviour. It's often a good idea to check the return values of these
* functions, or wrap them in @{assert} (for instance, use `assert(turtle.forward())` rather than `turtle.forward()`), * > functions, or wrap them in [`assert`] (for instance, use `assert(turtle.forward())` rather than `turtle.forward()`),
* so the program doesn't misbehave. * > so the program doesn't misbehave.
* :::
* <p> * <p>
* ## Turtle upgrades * ## Turtle upgrades
* While a normal turtle can move about the world and place blocks, its functionality is limited. Thankfully, turtles * While a normal turtle can move about the world and place blocks, its functionality is limited. Thankfully, turtles
* can be upgraded with *tools* and @{peripheral|peripherals}. Turtles have two upgrade slots, one on the left and right * can be upgraded with *tools* and [peripherals][`peripheral`]. Turtles have two upgrade slots, one on the left and right
* sides. Upgrades can be equipped by crafting a turtle with the upgrade, or calling the @{turtle.equipLeft}/@{turtle.equipRight} * sides. Upgrades can be equipped by crafting a turtle with the upgrade, or calling the [`turtle.equipLeft`]/[`turtle.equipRight`]
* functions. * functions.
* <p> * <p>
* Turtle tools allow you to break blocks (@{turtle.dig}) and attack entities (@{turtle.attack}). Some tools are more * Turtle tools allow you to break blocks ([`turtle.dig`]) and attack entities ([`turtle.attack`]). Some tools are more
* suitable to a task than others. For instance, a diamond pickaxe can break every block, while a sword does more * suitable to a task than others. For instance, a diamond pickaxe can break every block, while a sword does more
* damage. Other tools have more niche use-cases, for instance hoes can til dirt. * damage. Other tools have more niche use-cases, for instance hoes can til dirt.
* <p> * <p>
* Peripherals (such as the @{modem|wireless modem} or @{speaker}) can also be equipped as upgrades. These are then * Peripherals (such as the [wireless modem][`modem`] or [`speaker`]) can also be equipped as upgrades. These are then
* accessible by accessing the `"left"` or `"right"` peripheral. * accessible by accessing the `"left"` or `"right"` peripheral.
* <p> * <p>
* [Turtle Graphics]: https://en.wikipedia.org/wiki/Turtle_graphics "Turtle graphics" * [Turtle Graphics]: https://en.wikipedia.org/wiki/Turtle_graphics "Turtle graphics"
@@ -290,7 +288,7 @@ public class TurtleAPI implements ILuaAPI {
} }
/** /**
* Drop the currently selected stack into the inventory in front of the turtle, or as an item into the world if * Drop the currently selected stack into the inventory below the turtle, or as an item into the world if
* there is no inventory. * there is no inventory.
* *
* @param count The number of items to drop. If not given, the entire stack will be dropped. * @param count The number of items to drop. If not given, the entire stack will be dropped.

View File

@@ -74,7 +74,7 @@ public class TurtlePlaceCommand implements TurtleCommand {
} }
} }
public static boolean deploy( private static boolean deploy(
ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction, ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction,
@Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) { ) {
@@ -140,6 +140,22 @@ public class TurtlePlaceCommand implements TurtleCommand {
return true; return true;
} }
/**
* Calculate where a turtle would interact with a block.
*
* @param position The position of the block.
* @param side The side the turtle is clicking on.
* @return The hit result.
*/
public static BlockHitResult getHitResult(BlockPos position, Direction side) {
var hitX = 0.5 + side.getStepX() * 0.5;
var hitY = 0.5 + side.getStepY() * 0.5;
var hitZ = 0.5 + side.getStepZ() * 0.5;
if (Math.abs(hitY - 0.5) < 0.01) hitY = 0.45;
return new BlockHitResult(new Vec3(position.getX() + hitX, position.getY() + hitY, position.getZ() + hitZ), side, position, false);
}
private static boolean deployOnBlock( private static boolean deployOnBlock(
ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, Direction side, ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, Direction side,
@Nullable Object[] extraArguments, boolean adjacent, @Nullable ErrorMessage outErrorMessage @Nullable Object[] extraArguments, boolean adjacent, @Nullable ErrorMessage outErrorMessage
@@ -149,14 +165,8 @@ public class TurtlePlaceCommand implements TurtleCommand {
var playerPosition = position.relative(side); var playerPosition = position.relative(side);
turtlePlayer.setPosition(turtle, playerPosition, playerDir); turtlePlayer.setPosition(turtle, playerPosition, playerDir);
// Calculate where the turtle would hit the block
var hitX = 0.5f + side.getStepX() * 0.5f;
var hitY = 0.5f + side.getStepY() * 0.5f;
var hitZ = 0.5f + side.getStepZ() * 0.5f;
if (Math.abs(hitY - 0.5f) < 0.01f) hitY = 0.45f;
// Check if there's something suitable to place onto // Check if there's something suitable to place onto
var hit = new BlockHitResult(new Vec3(position.getX() + hitX, position.getY() + hitY, position.getZ() + hitZ), side, position, false); var hit = getHitResult(position, side);
var context = new UseOnContext(turtlePlayer.player(), InteractionHand.MAIN_HAND, hit); var context = new UseOnContext(turtlePlayer.player(), InteractionHand.MAIN_HAND, hit);
if (!canDeployOnBlock(new BlockPlaceContext(context), turtle, turtlePlayer, position, side, adjacent, outErrorMessage)) { if (!canDeployOnBlock(new BlockPlaceContext(context), turtle, turtlePlayer, position, side, adjacent, outErrorMessage)) {
return false; return false;

View File

@@ -82,7 +82,7 @@ public class TurtleTool extends AbstractTurtleUpgrade {
private static boolean isEnchanted(@Nullable CompoundTag tag) { private static boolean isEnchanted(@Nullable CompoundTag tag) {
if (tag == null || tag.isEmpty()) return false; if (tag == null || tag.isEmpty()) return false;
return (tag.contains(ItemStack.TAG_ENCH, TAG_LIST) && !tag.getList(ItemStack.TAG_ENCH, TAG_COMPOUND).isEmpty()) return (tag.contains(ItemStack.TAG_ENCH, TAG_LIST) && !tag.getList(ItemStack.TAG_ENCH, TAG_COMPOUND).isEmpty())
|| (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()); || (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty());
} }
@Override @Override
@@ -100,12 +100,12 @@ public class TurtleTool extends AbstractTurtleUpgrade {
public ItemStack getUpgradeItem(CompoundTag upgradeData) { public ItemStack getUpgradeItem(CompoundTag upgradeData) {
// Copy upgrade data back to the item. // Copy upgrade data back to the item.
var item = super.getUpgradeItem(upgradeData).copy(); var item = super.getUpgradeItem(upgradeData).copy();
item.setTag(upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) ? upgradeData.getCompound(TAG_ITEM_TAG).copy() : null); item.setTag(upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) ? upgradeData.getCompound(TAG_ITEM_TAG) : null);
return item; return item;
} }
private ItemStack getToolStack(ITurtleAccess turtle, TurtleSide side) { private ItemStack getToolStack(ITurtleAccess turtle, TurtleSide side) {
return getUpgradeItem(turtle.getUpgradeNBTData(side)); return getUpgradeItem(turtle.getUpgradeNBTData(side)).copy();
} }
private void setToolStack(ITurtleAccess turtle, TurtleSide side, ItemStack stack) { private void setToolStack(ITurtleAccess turtle, TurtleSide side, ItemStack stack) {
@@ -294,19 +294,20 @@ public class TurtleTool extends AbstractTurtleUpgrade {
private TurtleCommandResult dig(ITurtleAccess turtle, TurtleSide side, Direction direction) { private TurtleCommandResult dig(ITurtleAccess turtle, TurtleSide side, Direction direction) {
var level = (ServerLevel) turtle.getLevel(); var level = (ServerLevel) turtle.getLevel();
var blockPosition = turtle.getPosition().relative(direction);
if (level.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(level, blockPosition)) {
return TurtleCommandResult.failure("Nothing to dig here");
}
return withEquippedItem(turtle, side, direction, turtlePlayer -> { return withEquippedItem(turtle, side, direction, turtlePlayer -> {
var stack = turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND); var stack = turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND);
// Right-click the block when using a shovel/hoe. // Right-click the block when using a shovel/hoe. Important that we do this before checking the block is
if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deploy(stack, turtle, turtlePlayer, direction, null, null)) { // present, as we allow doing these actions from slightly further away.
if (PlatformHelper.get().hasToolUsage(stack) && useTool(level, turtle, turtlePlayer, stack, direction)) {
return TurtleCommandResult.success(); return TurtleCommandResult.success();
} }
var blockPosition = turtle.getPosition().relative(direction);
if (level.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(level, blockPosition)) {
return TurtleCommandResult.failure("Nothing to dig here");
}
// Check if we can break the block // Check if we can break the block
var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer); var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer);
if (!breakable.isSuccess()) return breakable; if (!breakable.isSuccess()) return breakable;
@@ -320,9 +321,34 @@ public class TurtleTool extends AbstractTurtleUpgrade {
}); });
} }
/**
* Attempt to use a tool against a block instead.
*
* @param level The current level.
* @param turtle The current turtle.
* @param turtlePlayer The turtle player, already positioned and with a stack equipped.
* @param stack The current tool's stack.
* @param direction The direction this action occurs in.
* @return Whether the tool was successfully used.
* @see PlatformHelper#hasToolUsage(ItemStack)
*/
private boolean useTool(ServerLevel level, ITurtleAccess turtle, TurtlePlayer turtlePlayer, ItemStack stack, Direction direction) {
var position = turtle.getPosition().relative(direction);
// Allow digging one extra block below the turtle, as you can't till dirt/flatten grass if there's a block
// above.
if (direction == Direction.DOWN && level.isEmptyBlock(position)) position = position.relative(direction);
if (!level.isInWorldBounds(position) || level.isEmptyBlock(position) || turtlePlayer.isBlockProtected(level, position)) {
return false;
}
var hit = TurtlePlaceCommand.getHitResult(position, direction.getOpposite());
var result = PlatformHelper.get().useOn(turtlePlayer.player(), stack, hit, x -> false);
return result.consumesAction();
}
private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) { private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) {
return return state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE)
state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE)
// Allow breaking any "instabreak" block. // Allow breaking any "instabreak" block.
|| state.getDestroySpeed(reader, pos) == 0; || state.getDestroySpeed(reader, pos) == 0;
} }

Some files were not shown because too many files have changed in this diff Show More