Merge branch 'mc-1.19.x' into mc-1.20.x
| @@ -6,7 +6,7 @@ | ||||
| # See https://pre-commit.com/hooks.html for more hooks | ||||
| repos: | ||||
| - repo: https://github.com/pre-commit/pre-commit-hooks | ||||
|   rev: v4.0.1 | ||||
|   rev: v4.4.0 | ||||
|   hooks: | ||||
|   - id: trailing-whitespace | ||||
|   - id: end-of-file-fixer | ||||
| @@ -20,14 +20,14 @@ repos: | ||||
|     exclude: "tsconfig\\.json$" | ||||
|  | ||||
| - repo: https://github.com/editorconfig-checker/editorconfig-checker.python | ||||
|   rev: 2.3.54 | ||||
|   rev: 2.7.2 | ||||
|   hooks: | ||||
|   - id: editorconfig-checker | ||||
|     args: ['-disable-indentation'] | ||||
|     exclude: "^(.*\\.(bat)|LICENSE)$" | ||||
|  | ||||
| - repo: https://github.com/fsfe/reuse-tool | ||||
|   rev: v1.1.0 | ||||
|   rev: v2.1.0 | ||||
|   hooks: | ||||
|   - id: reuse | ||||
|  | ||||
|   | ||||
| @@ -66,6 +66,7 @@ repositories { | ||||
|             includeGroup("me.shedaniel") | ||||
|             includeGroup("mezz.jei") | ||||
|             includeModule("com.terraformersmc", "modmenu") | ||||
|             includeModule("me.lucko", "fabric-permissions-api") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{alarm} event is fired when an alarm started with @{os.setAlarm} completes. | ||||
| The [`alarm`] event is fired when an alarm started with [`os.setAlarm`] completes. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The ID of the alarm that finished. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The ID of the alarm that finished. | ||||
| 
 | ||||
| ## Example | ||||
| Starts a timer and then waits for it to complete. | ||||
|   | ||||
| @@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: LicenseRef-CCPL | ||||
| --> | ||||
| 
 | ||||
| The @{char} event is fired when a character is typed on the keyboard. | ||||
| The [`char`] event is fired when a character is typed on the keyboard. | ||||
| 
 | ||||
| The @{char} event is different to a key press. Sometimes multiple key presses may result in one character being | ||||
| The [`char`] event is different to a key press. Sometimes multiple key presses may result in one character being | ||||
| typed (for instance, on some European keyboards). Similarly, some keys (e.g. <kbd>Ctrl</kbd>) do not have any | ||||
| corresponding character. The @{key} should be used if you want to listen to key presses themselves. | ||||
| corresponding character. The [`key`] should be used if you want to listen to key presses themselves. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The string representing the character that was pressed. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The string representing the character that was pressed. | ||||
| 
 | ||||
| 
 | ||||
| ## Example | ||||
|   | ||||
| @@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{computer_command} event is fired when the `/computercraft queue` command is run for the current computer. | ||||
| The [`computer_command`] event is fired when the `/computercraft queue` command is run for the current computer. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}<abbr title="Variable number of arguments">…</abbr>: The arguments passed to the command. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]<abbr title="Variable number of arguments">…</abbr>: The arguments passed to the command. | ||||
| 
 | ||||
| ## Example | ||||
| Prints the contents of messages sent: | ||||
|   | ||||
| @@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{disk} event is fired when a disk is inserted into an adjacent or networked disk drive. | ||||
| The [`disk`] event is fired when a disk is inserted into an adjacent or networked disk drive. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The side of the disk drive that had a disk inserted. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The side of the disk drive that had a disk inserted. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when a disk is inserted: | ||||
|   | ||||
| @@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{disk_eject} event is fired when a disk is removed from an adjacent or networked disk drive. | ||||
| The [`disk_eject`] event is fired when a disk is removed from an adjacent or networked disk drive. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The side of the disk drive that had a disk removed. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The side of the disk drive that had a disk removed. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when a disk is removed: | ||||
|   | ||||
| @@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2022 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{file_transfer} event is queued when a user drags-and-drops a file on an open computer. | ||||
| The [`file_transfer`] event is queued when a user drags-and-drops a file on an open computer. | ||||
| 
 | ||||
| This event contains a single argument of type @{TransferredFiles}, which can be used to @{TransferredFiles.getFiles|get | ||||
| the files to be transferred}. Each file returned is a @{fs.BinaryReadHandle|binary file handle} with an additional | ||||
| @{TransferredFile.getName|getName} method. | ||||
| This event contains a single argument of type [`TransferredFiles`], which can be used to [get the files to be | ||||
| transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.BinaryReadHandle`] with an | ||||
| additional [getName][`TransferredFile.getName`] method. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name | ||||
| 2. @{TransferredFiles}: The list of transferred files. | ||||
| 1. [`string`]: The event name | ||||
| 2. [`TransferredFiles`]: The list of transferred files. | ||||
| 
 | ||||
| ## Example | ||||
| Waits for a user to drop files on top of the computer, then prints the list of files and the size of each file. | ||||
|   | ||||
| @@ -9,12 +9,12 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{http_check} event is fired when a URL check finishes. | ||||
| The [`http_check`] event is fired when a URL check finishes. | ||||
| 
 | ||||
| This event is normally handled inside @{http.checkURL}, but it can still be seen when using @{http.checkURLAsync}. | ||||
| This event is normally handled inside [`http.checkURL`], but it can still be seen when using [`http.checkURLAsync`]. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The URL requested to be checked. | ||||
| 3. @{boolean}: Whether the check succeeded. | ||||
| 4. <span class="type">@{string}|@{nil}</span>: If the check failed, a reason explaining why the check failed. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The URL requested to be checked. | ||||
| 3. [`boolean`]: Whether the check succeeded. | ||||
| 4. <span class="type">[`string`]|[`nil`]</span>: If the check failed, a reason explaining why the check failed. | ||||
|   | ||||
| @@ -9,15 +9,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{http_failure} event is fired when an HTTP request fails. | ||||
| The [`http_failure`] event is fired when an HTTP request fails. | ||||
| 
 | ||||
| This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}. | ||||
| This event is normally handled inside [`http.get`] and [`http.post`], but it can still be seen when using [`http.request`]. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The URL of the site requested. | ||||
| 3. @{string}: An error describing the failure. | ||||
| 4. <span class="type">@{http.Response}|@{nil}</span>: A response handle if the connection succeeded, but the server's | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The URL of the site requested. | ||||
| 3. [`string`]: An error describing the failure. | ||||
| 4. <span class="type">[`http.Response`]|[`nil`]</span>: A response handle if the connection succeeded, but the server's | ||||
|    response indicated failure. | ||||
| 
 | ||||
| ## Example | ||||
|   | ||||
| @@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{http_success} event is fired when an HTTP request returns successfully. | ||||
| The [`http_success`] event is fired when an HTTP request returns successfully. | ||||
| 
 | ||||
| This event is normally handled inside @{http.get} and @{http.post}, but it can still be seen when using @{http.request}. | ||||
| This event is normally handled inside [`http.get`] and [`http.post`], but it can still be seen when using [`http.request`]. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The URL of the site requested. | ||||
| 3. @{http.Response}: The successful HTTP response. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The URL of the site requested. | ||||
| 3. [`http.Response`]: The successful HTTP response. | ||||
| 
 | ||||
| ## Example | ||||
| Prints the content of a website (this may fail if the request fails): | ||||
|   | ||||
| @@ -11,15 +11,15 @@ SPDX-License-Identifier: LicenseRef-CCPL | ||||
| This event is fired when any key is pressed while the terminal is focused. | ||||
| 
 | ||||
| This event returns a numerical "key code" (for instance, <kbd>F1</kbd> is 290). This value may vary between versions and | ||||
| so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values. | ||||
| so it is recommended to use the constants in the [`keys`] API rather than hard coding numeric values. | ||||
| 
 | ||||
| If the button pressed represented a printable character, then the @{key} event will be followed immediately by a @{char} | ||||
| event. If you are consuming text input, use a @{char} event instead! | ||||
| If the button pressed represented a printable character, then the [`key`] event will be followed immediately by a [`char`] | ||||
| event. If you are consuming text input, use a [`char`] event instead! | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The numerical key value of the key pressed. | ||||
| 3. @{boolean}: Whether the key event was generated while holding the key (@{true}), rather than pressing it the first time (@{false}). | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The numerical key value of the key pressed. | ||||
| 3. [`boolean`]: Whether the key event was generated while holding the key ([`true`]), rather than pressing it the first time ([`false`]). | ||||
| 
 | ||||
| ## Example | ||||
| Prints each key when the user presses it, and if the key is being held. | ||||
|   | ||||
| @@ -12,14 +12,14 @@ SPDX-License-Identifier: LicenseRef-CCPL | ||||
| Fired whenever a key is released (or the terminal is closed while a key was being pressed). | ||||
| 
 | ||||
| This event returns a numerical "key code" (for instance, <kbd>F1</kbd> is 290). This value may vary between versions and | ||||
| so it is recommended to use the constants in the @{keys} API rather than hard coding numeric values. | ||||
| so it is recommended to use the constants in the [`keys`] API rather than hard coding numeric values. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The numerical key value of the key pressed. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The numerical key value of the key pressed. | ||||
| 
 | ||||
| ## Example | ||||
| Prints each key released on the keyboard whenever a @{key_up} event is fired. | ||||
| Prints each key released on the keyboard whenever a [`key_up`] event is fired. | ||||
| 
 | ||||
| ```lua | ||||
| while true do | ||||
|   | ||||
| @@ -8,18 +8,18 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{modem_message} event is fired when a message is received on an open channel on any @{modem}. | ||||
| The [`modem_message`] event is fired when a message is received on an open channel on any [`modem`]. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The side of the modem that received the message. | ||||
| 3. @{number}: The channel that the message was sent on. | ||||
| 4. @{number}: The reply channel set by the sender. | ||||
| 5. @{any}: The message as sent by the sender. | ||||
| 6. <span class="type">@{number}|@{nil}</span>: The distance between the sender and the receiver in blocks, or @{nil} if the message was sent between dimensions. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The side of the modem that received the message. | ||||
| 3. [`number`]: The channel that the message was sent on. | ||||
| 4. [`number`]: The reply channel set by the sender. | ||||
| 5. [`any`]: The message as sent by the sender. | ||||
| 6. <span class="type">[`number`]|[`nil`]</span>: The distance between the sender and the receiver in blocks, or [`nil`] if the message was sent between dimensions. | ||||
| 
 | ||||
| ## Example | ||||
| Wraps a @{modem} peripheral, opens channel 0 for listening, and prints all received messages. | ||||
| Wraps a [`modem`] peripheral, opens channel 0 for listening, and prints all received messages. | ||||
| 
 | ||||
| ```lua | ||||
| local modem = peripheral.find("modem") or error("No modem attached", 0) | ||||
|   | ||||
| @@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{monitor_resize} event is fired when an adjacent or networked monitor's size is changed. | ||||
| The [`monitor_resize`] event is fired when an adjacent or networked monitor's size is changed. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The side or network ID of the monitor that was resized. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The side or network ID of the monitor that was resized. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when a monitor is resized: | ||||
|   | ||||
| @@ -8,13 +8,13 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{monitor_touch} event is fired when an adjacent or networked Advanced Monitor is right-clicked. | ||||
| The [`monitor_touch`] event is fired when an adjacent or networked Advanced Monitor is right-clicked. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The side or network ID of the monitor that was touched. | ||||
| 3. @{number}: The X coordinate of the touch, in characters. | ||||
| 4. @{number}: The Y coordinate of the touch, in characters. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The side or network ID of the monitor that was touched. | ||||
| 3. [`number`]: The X coordinate of the touch, in characters. | ||||
| 4. [`number`]: The Y coordinate of the touch, in characters. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when a monitor is touched: | ||||
|   | ||||
| @@ -12,13 +12,13 @@ This event is fired when the terminal is clicked with a mouse. This event is onl | ||||
| advanced turtles and pocket computers). | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The mouse button that was clicked. | ||||
| 3. @{number}: The X-coordinate of the click. | ||||
| 4. @{number}: The Y-coordinate of the click. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The mouse button that was clicked. | ||||
| 3. [`number`]: The X-coordinate of the click. | ||||
| 4. [`number`]: The Y-coordinate of the click. | ||||
| 
 | ||||
| ## Mouse buttons | ||||
| Several mouse events (@{mouse_click}, @{mouse_up}, @{mouse_scroll}) contain a "mouse button" code. This takes a | ||||
| Several mouse events ([`mouse_click`], [`mouse_up`], [`mouse_scroll`]) contain a "mouse button" code. This takes a | ||||
| numerical value depending on which button on your mouse was last pressed when this event occurred. | ||||
| 
 | ||||
| | Button Code | Mouse Button  | | ||||
|   | ||||
| @@ -12,10 +12,10 @@ SPDX-License-Identifier: LicenseRef-CCPL | ||||
| This event is fired every time the mouse is moved while a mouse button is being held. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed. | ||||
| 3. @{number}: The X-coordinate of the mouse. | ||||
| 4. @{number}: The Y-coordinate of the mouse. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The [mouse button](mouse_click.html#Mouse_buttons) that is being pressed. | ||||
| 3. [`number`]: The X-coordinate of the mouse. | ||||
| 4. [`number`]: The Y-coordinate of the mouse. | ||||
| 
 | ||||
| ## Example | ||||
| Print the button and the coordinates whenever the mouse is dragged. | ||||
|   | ||||
| @@ -11,10 +11,10 @@ SPDX-License-Identifier: LicenseRef-CCPL | ||||
| This event is fired when a mouse wheel is scrolled in the terminal. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The direction of the scroll. (-1 = up, 1 = down) | ||||
| 3. @{number}: The X-coordinate of the mouse when scrolling. | ||||
| 4. @{number}: The Y-coordinate of the mouse when scrolling. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The direction of the scroll. (-1 = up, 1 = down) | ||||
| 3. [`number`]: The X-coordinate of the mouse when scrolling. | ||||
| 4. [`number`]: The Y-coordinate of the mouse when scrolling. | ||||
| 
 | ||||
| ## Example | ||||
| Prints the direction of each scroll, and the position of the mouse at the time. | ||||
|   | ||||
| @@ -11,10 +11,10 @@ SPDX-License-Identifier: LicenseRef-CCPL | ||||
| This event is fired when a mouse button is released or a held mouse leaves the computer's terminal. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The [mouse button](mouse_click.html#Mouse_buttons) that was released. | ||||
| 3. @{number}: The X-coordinate of the mouse. | ||||
| 4. @{number}: The Y-coordinate of the mouse. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The [mouse button](mouse_click.html#Mouse_buttons) that was released. | ||||
| 3. [`number`]: The X-coordinate of the mouse. | ||||
| 4. [`number`]: The Y-coordinate of the mouse. | ||||
| 
 | ||||
| ## Example | ||||
| Prints the coordinates and button number whenever the mouse is released. | ||||
|   | ||||
| @@ -8,11 +8,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{paste} event is fired when text is pasted into the computer through Ctrl-V (or ⌘V on Mac). | ||||
| The [`paste`] event is fired when text is pasted into the computer through Ctrl-V (or ⌘V on Mac). | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string} The text that was pasted. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`] The text that was pasted. | ||||
| 
 | ||||
| ## Example | ||||
| Prints pasted text: | ||||
|   | ||||
| @@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{peripheral} event is fired when a peripheral is attached on a side or to a modem. | ||||
| The [`peripheral`] event is fired when a peripheral is attached on a side or to a modem. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The side the peripheral was attached to. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The side the peripheral was attached to. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when a peripheral is attached: | ||||
|   | ||||
| @@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{peripheral_detach} event is fired when a peripheral is detached from a side or from a modem. | ||||
| The [`peripheral_detach`] event is fired when a peripheral is detached from a side or from a modem. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The side the peripheral was detached from. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The side the peripheral was detached from. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when a peripheral is detached: | ||||
|   | ||||
| @@ -10,17 +10,17 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{rednet_message} event is fired when a message is sent over Rednet. | ||||
| The [`rednet_message`] event is fired when a message is sent over Rednet. | ||||
| 
 | ||||
| This event is usually handled by @{rednet.receive}, but it can also be pulled manually. | ||||
| This event is usually handled by [`rednet.receive`], but it can also be pulled manually. | ||||
| 
 | ||||
| @{rednet_message} events are sent by @{rednet.run} in the top-level coroutine in response to @{modem_message} events. A @{rednet_message} event is always preceded by a @{modem_message} event. They are generated inside CraftOS rather than being sent by the ComputerCraft machine. | ||||
| [`rednet_message`] events are sent by [`rednet.run`] in the top-level coroutine in response to [`modem_message`] events. A [`rednet_message`] event is always preceded by a [`modem_message`] event. They are generated inside CraftOS rather than being sent by the ComputerCraft machine. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The ID of the sending computer. | ||||
| 3. @{any}: The message sent. | ||||
| 4. <span class="type">@{string}|@{nil}</span>: The protocol of the message, if provided. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The ID of the sending computer. | ||||
| 3. [`any`]: The message sent. | ||||
| 4. <span class="type">[`string`]|[`nil`]</span>: The protocol of the message, if provided. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when one is sent: | ||||
|   | ||||
| @@ -8,10 +8,10 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{event!redstone} event is fired whenever any redstone inputs on the computer change. | ||||
| The [`event!redstone`] event is fired whenever any redstone inputs on the computer change. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 1. [`string`]: The event name. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when a redstone input changes: | ||||
|   | ||||
| @@ -10,13 +10,13 @@ SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The name of the speaker which is available to play more audio. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The name of the speaker which is available to play more audio. | ||||
| 
 | ||||
| 
 | ||||
| ## Example | ||||
| This uses @{io.lines} to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it | ||||
| using @{speaker.playAudio}. If the speaker's buffer is full, it waits for an event and tries again. | ||||
| This uses [`io.lines`] to read audio data in blocks of 16KiB from "example_song.dfpwm", and then attempts to play it | ||||
| using [`speaker.playAudio`]. If the speaker's buffer is full, it waits for an event and tries again. | ||||
| 
 | ||||
| ```lua {data-peripheral=speaker} | ||||
| local dfpwm = require("cc.audio.dfpwm") | ||||
|   | ||||
| @@ -9,13 +9,13 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{task_complete} event is fired when an asynchronous task completes. This is usually handled inside the function call that queued the task; however, functions such as @{commands.execAsync} return immediately so the user can wait for completion. | ||||
| The [`task_complete`] event is fired when an asynchronous task completes. This is usually handled inside the function call that queued the task; however, functions such as [`commands.execAsync`] return immediately so the user can wait for completion. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The ID of the task that completed. | ||||
| 3. @{boolean}: Whether the command succeeded. | ||||
| 4. @{string}: If the command failed, an error message explaining the failure. (This is not present if the command succeeded.) | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The ID of the task that completed. | ||||
| 3. [`boolean`]: Whether the command succeeded. | ||||
| 4. [`string`]: If the command failed, an error message explaining the failure. (This is not present if the command succeeded.) | ||||
| 5. <abbr title="Variable number of arguments">…</abbr>: Any parameters returned from the command. | ||||
| 
 | ||||
| ## Example | ||||
|   | ||||
| @@ -8,15 +8,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{term_resize} event is fired when the main terminal is resized. For instance: | ||||
|  - When a the tab bar is shown or hidden in @{multishell}. | ||||
| The [`term_resize`] event is fired when the main terminal is resized. For instance: | ||||
|  - When a the tab bar is shown or hidden in [`multishell`]. | ||||
|  - When the terminal is redirected to a monitor via the "monitor" program and the monitor is resized. | ||||
| 
 | ||||
| When this event fires, some parts of the terminal may have been moved or deleted. Simple terminal programs (those | ||||
| not using @{term.setCursorPos}) can ignore this event, but more complex GUI programs should redraw the entire screen. | ||||
| not using [`term.setCursorPos`]) can ignore this event, but more complex GUI programs should redraw the entire screen. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 1. [`string`]: The event name. | ||||
| 
 | ||||
| ## Example | ||||
| Print a message each time the terminal is resized. | ||||
|   | ||||
| @@ -8,14 +8,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{terminate} event is fired when <kbd>Ctrl-T</kbd> is held down. | ||||
| The [`terminate`] event is fired when <kbd>Ctrl-T</kbd> is held down. | ||||
| 
 | ||||
| This event is normally handled by @{os.pullEvent}, and will not be returned. However, @{os.pullEventRaw} will return this event when fired. | ||||
| This event is normally handled by [`os.pullEvent`], and will not be returned. However, [`os.pullEventRaw`] will return this event when fired. | ||||
| 
 | ||||
| @{terminate} will be sent even when a filter is provided to @{os.pullEventRaw}. When using @{os.pullEventRaw} with a filter, make sure to check that the event is not @{terminate}. | ||||
| [`terminate`] will be sent even when a filter is provided to [`os.pullEventRaw`]. When using [`os.pullEventRaw`] with a filter, make sure to check that the event is not [`terminate`]. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 1. [`string`]: The event name. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when Ctrl-T is held: | ||||
|   | ||||
| @@ -9,11 +9,11 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{timer} event is fired when a timer started with @{os.startTimer} completes. | ||||
| The [`timer`] event is fired when a timer started with [`os.startTimer`] completes. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{number}: The ID of the timer that finished. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`number`]: The ID of the timer that finished. | ||||
| 
 | ||||
| ## Example | ||||
| Start and wait for a timer to finish. | ||||
|   | ||||
| @@ -8,10 +8,10 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{turtle_inventory} event is fired when a turtle's inventory is changed. | ||||
| The [`turtle_inventory`] event is fired when a turtle's inventory is changed. | ||||
| 
 | ||||
| ## Return values | ||||
| 1. @{string}: The event name. | ||||
| 1. [`string`]: The event name. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message when the inventory is changed: | ||||
|   | ||||
| @@ -8,16 +8,16 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{websocket_closed} event is fired when an open WebSocket connection is closed. | ||||
| The [`websocket_closed`] event is fired when an open WebSocket connection is closed. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The URL of the WebSocket that was closed. | ||||
| 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 | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The URL of the WebSocket that was closed. | ||||
| 3. <span class="type">[`string`]|[`nil`]</span>: The [server-provided reason][close_reason] | ||||
|    the websocket was closed. This will be [`nil`] if the connection was closed | ||||
|    abnormally. | ||||
| 4. <span class="type">@{number}|@{nil}</span>: The [connection close code][close_code], | ||||
|    indicating why the socket was closed. This will be @{nil} if the connection | ||||
| 4. <span class="type">[`number`]|[`nil`]</span>: The [connection close code][close_code], | ||||
|    indicating why the socket was closed. This will be [`nil`] if the connection | ||||
|    was closed abnormally. | ||||
| 
 | ||||
| [close_reason]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 "The WebSocket Connection Close Reason, RFC 6455" | ||||
|   | ||||
| @@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{websocket_failure} event is fired when a WebSocket connection request fails. | ||||
| The [`websocket_failure`] event is fired when a WebSocket connection request fails. | ||||
| 
 | ||||
| This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}. | ||||
| This event is normally handled inside [`http.websocket`], but it can still be seen when using [`http.websocketAsync`]. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The URL of the site requested. | ||||
| 3. @{string}: An error describing the failure. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The URL of the site requested. | ||||
| 3. [`string`]: An error describing the failure. | ||||
| 
 | ||||
| ## Example | ||||
| Prints an error why the website cannot be contacted: | ||||
|   | ||||
| @@ -8,15 +8,15 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{websocket_message} event is fired when a message is received on an open WebSocket connection. | ||||
| The [`websocket_message`] event is fired when a message is received on an open WebSocket connection. | ||||
| 
 | ||||
| This event is normally handled by @{http.Websocket.receive}, but it can also be pulled manually. | ||||
| This event is normally handled by [`http.Websocket.receive`], but it can also be pulled manually. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The URL of the WebSocket. | ||||
| 3. @{string}: The contents of the message. | ||||
| 4. @{boolean}: Whether this is a binary message. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The URL of the WebSocket. | ||||
| 3. [`string`]: The contents of the message. | ||||
| 4. [`boolean`]: Whether this is a binary message. | ||||
| 
 | ||||
| ## Example | ||||
| Prints a message sent by a WebSocket: | ||||
|   | ||||
| @@ -9,14 +9,14 @@ SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers | ||||
| SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| The @{websocket_success} event is fired when a WebSocket connection request returns successfully. | ||||
| The [`websocket_success`] event is fired when a WebSocket connection request returns successfully. | ||||
| 
 | ||||
| This event is normally handled inside @{http.websocket}, but it can still be seen when using @{http.websocketAsync}. | ||||
| This event is normally handled inside [`http.websocket`], but it can still be seen when using [`http.websocketAsync`]. | ||||
| 
 | ||||
| ## Return Values | ||||
| 1. @{string}: The event name. | ||||
| 2. @{string}: The URL of the site. | ||||
| 3. @{http.Websocket}: The handle for the WebSocket. | ||||
| 1. [`string`]: The event name. | ||||
| 2. [`string`]: The URL of the site. | ||||
| 3. [`http.Websocket`]: The handle for the WebSocket. | ||||
| 
 | ||||
| ## Example | ||||
| Prints the content of a website (this may fail if the request fails): | ||||
|   | ||||
| @@ -9,7 +9,7 @@ SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| # Setting up GPS | ||||
| The @{gps} API allows computers and turtles to find their current position using wireless modems. | ||||
| The [`gps`] API allows computers and turtles to find their current position using wireless modems. | ||||
| 
 | ||||
| In order to use GPS, you'll need to set up multiple *GPS hosts*. These are computers running the special `gps host` | ||||
| program, which tell other computers the host's position. Several hosts running together are known as a *GPS | ||||
| @@ -19,22 +19,21 @@ In order to give the best results, a GPS constellation needs at least four compu | ||||
| constellation is redundant, but it does not cause problems. | ||||
| 
 | ||||
| ## Building a GPS constellation | ||||
| {.big-image} | ||||
| <img alt="An example GPS constellation." src="/images/gps-constellation-example.png" class="big-image" /> | ||||
| 
 | ||||
| We are going to build our GPS constellation as shown in the image above. You will need 4 computers and either 4 wireless | ||||
| modems or 4 ender modems. Try not to mix ender and wireless modems together as you might get some odd behavior when your | ||||
| requesting computers are out of range. | ||||
| 
 | ||||
| :::tip Ender modems vs wireless modems | ||||
| Ender modems have a very large range, which makes them very useful for setting up GPS hosts. If you do this then you | ||||
| will likely only need one GPS constellation for the whole dimension (such as the Overworld or Nether). | ||||
| 
 | ||||
| If you do use wireless modems then you may find that you need multiple GPS constellations to cover your needs. | ||||
| 
 | ||||
| A computer needs a wireless or ender modem and to be in range of a GPS constellation that is in the same dimension as it | ||||
| to use the GPS API. The reason for this is that ComputerCraft mimics real-life GPS by making use of the distance | ||||
| parameter of @{modem_message|modem messages} and some maths. | ||||
| ::: | ||||
| > [Ender modems vs wireless modems][!TIP] | ||||
| > Ender modems have a very large range, which makes them very useful for setting up GPS hosts. If you do this then you | ||||
| > will likely only need one GPS constellation for the whole dimension (such as the Overworld or Nether). | ||||
| > | ||||
| > If you do use wireless modems then you may find that you need multiple GPS constellations to cover your needs. | ||||
| > | ||||
| > A computer needs a wireless or ender modem and to be in range of a GPS constellation that is in the same dimension as | ||||
| > it to use the GPS API. The reason for this is that ComputerCraft mimics real-life GPS by making use of the distance | ||||
| > parameter of [modem messages][`modem_message`] and some maths. | ||||
| 
 | ||||
| Locate where you want to place your GPS constellation. You will need an area at least 6 blocks high, 6 blocks wide, and | ||||
| 6 blocks deep (6x6x6). If you are using wireless modems then you may want to build your constellation as high as you can | ||||
| @@ -79,18 +78,16 @@ To hide Minecraft's debug screen, press <kbd>F3</kbd> again. | ||||
| Create similar startup files for the other computers in your constellation, making sure to input the each computer's own | ||||
| coordinates. | ||||
| 
 | ||||
| :::caution Modem messages come from the computer's position, not the modem's | ||||
| Wireless modems transmit from the block that they are attached to *not* the block space that they occupy, the | ||||
| coordinates that you input into your GPS host should be the position of the computer and not the position of the modem. | ||||
| ::: | ||||
| > [Modem messages come from the computer's position, not the modem's][!WARNING] | ||||
| > Wireless modems transmit from the block that they are attached to *not* the block space that they occupy, the | ||||
| > coordinates that you input into your GPS host should be the position of the computer and not the position of the modem. | ||||
| 
 | ||||
| Congratulations, your constellation is now fully set up! You can test it by placing another computer close by, placing a | ||||
| wireless modem on it, and running the `gps locate` program (or calling the @{gps.locate} function). | ||||
| wireless modem on it, and running the `gps locate` program (or calling the [`gps.locate`] function). | ||||
| 
 | ||||
| :::info Why use Minecraft's coordinates? | ||||
| CC doesn't care if you use Minecraft's coordinate system, so long as all of the GPS hosts with overlapping ranges use | ||||
| the same reference point (requesting computers will get confused if hosts have different reference points). However, | ||||
| using MC's coordinate system does provide a nice standard to adopt server-wide. It also is consistent with how command | ||||
| computers get their location, they use MC's command system to get their block which returns that in MC's coordinate | ||||
| system. | ||||
| ::: | ||||
| > [Why use Minecraft's coordinates?][!INFO] | ||||
| > CC doesn't care if you use Minecraft's coordinate system, so long as all of the GPS hosts with overlapping ranges use | ||||
| > the same reference point (requesting computers will get confused if hosts have different reference points). However, | ||||
| > using MC's coordinate system does provide a nice standard to adopt server-wide. It also is consistent with how command | ||||
| > computers get their location, they use MC's command system to get their block which returns that in MC's coordinate | ||||
| > system. | ||||
|   | ||||
| @@ -11,7 +11,7 @@ SPDX-License-Identifier: MPL-2.0 | ||||
| --> | ||||
| 
 | ||||
| # Playing audio with speakers | ||||
| CC: Tweaked's speaker peripheral provides a powerful way to play any audio you like with the @{speaker.playAudio} | ||||
| CC: Tweaked's speaker peripheral provides a powerful way to play any audio you like with the [`speaker.playAudio`] | ||||
| method. However, for people unfamiliar with digital audio, it's not the most intuitive thing to use. This guide provides | ||||
| an introduction to digital audio, demonstrates how to play music with CC: Tweaked's speakers, and then briefly discusses | ||||
| the more complex topic of audio processing. | ||||
| @@ -60,7 +60,7 @@ sine waves (and why wouldn't you?), you'd need a table with almost 3 _million_. | ||||
| up very quickly, and these tables take up more and more memory. | ||||
| 
 | ||||
| Instead of building our entire song (well, sine wave) in one go, we can produce it in small batches, each of which get | ||||
| passed off to @{speaker.playAudio} when the time is right. This allows us to build a _stream_ of audio, where we read | ||||
| passed off to [`speaker.playAudio`] when the time is right. This allows us to build a _stream_ of audio, where we read | ||||
| chunks of audio one at a time (either from a file or a tone generator like above), do some optional processing to each | ||||
| one, and then play them. | ||||
| 
 | ||||
| @@ -84,15 +84,15 @@ end | ||||
| ``` | ||||
| 
 | ||||
| It looks pretty similar to before, aside from we've wrapped the generation and playing code in a while loop, and added a | ||||
| rather odd loop with @{speaker.playAudio} and @{os.pullEvent}. | ||||
| rather odd loop with [`speaker.playAudio`] and [`os.pullEvent`]. | ||||
| 
 | ||||
| Let's talk about this loop, why do we need to keep calling @{speaker.playAudio}? Remember that what we're trying to do | ||||
| Let's talk about this loop, why do we need to keep calling [`speaker.playAudio`]? Remember that what we're trying to do | ||||
| here is avoid keeping too much audio in memory at once. However, if we're generating audio quicker than the speakers can | ||||
| play it, we're not helping at all - all this audio is still hanging around waiting to be played! | ||||
| 
 | ||||
| In order to avoid this, the speaker rejects any new chunks of audio if its backlog is too large. When this happens, | ||||
| @{speaker.playAudio} returns false. Once enough audio has played, and the backlog has been reduced, a | ||||
| @{speaker_audio_empty} event is queued, and we can try to play our chunk once more. | ||||
| [`speaker.playAudio`] returns false. Once enough audio has played, and the backlog has been reduced, a | ||||
| [`speaker_audio_empty`] event is queued, and we can try to play our chunk once more. | ||||
| 
 | ||||
| ## Storing audio | ||||
| PCM is a fantastic way of representing audio when we want to manipulate it, but it's not very efficient when we want to | ||||
| @@ -106,7 +106,7 @@ computer. Instead, we need something much simpler. | ||||
| 
 | ||||
| DFPWM (Dynamic Filter Pulse Width Modulation) is the de facto standard audio format of the ComputerCraft (and | ||||
| OpenComputers) world. Originally popularised by the addon mod [Computronics], CC:T now has built-in support for it with | ||||
| the @{cc.audio.dfpwm} module. This allows you to read DFPWM files from disk, decode them to PCM, and then play them | ||||
| the [`cc.audio.dfpwm`] module. This allows you to read DFPWM files from disk, decode them to PCM, and then play them | ||||
| using the speaker. | ||||
| 
 | ||||
| Let's dive in with an example, and we'll explain things afterwards: | ||||
| @@ -125,16 +125,16 @@ for chunk in io.lines("data/example.dfpwm", 16 * 1024) do | ||||
| end | ||||
| ``` | ||||
| 
 | ||||
| Once again, we see the @{speaker.playAudio}/@{speaker_audio_empty} loop. However, the rest of the program is a little | ||||
| Once again, we see the [`speaker.playAudio`]/[`speaker_audio_empty`] loop. However, the rest of the program is a little | ||||
| different. | ||||
| 
 | ||||
| First, we require the dfpwm module and call @{cc.audio.dfpwm.make_decoder} to construct a new decoder. This decoder | ||||
| First, we require the dfpwm module and call [`cc.audio.dfpwm.make_decoder`] to construct a new decoder. This decoder | ||||
| accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, which we can then play with our speaker. | ||||
| 
 | ||||
| As mentioned above, @{speaker.playAudio} accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each | ||||
| As mentioned above, [`speaker.playAudio`] accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each | ||||
| sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use | ||||
| @{io.lines}, which provides a nice way to loop over chunks of a file. You can of course just use @{fs.open} and | ||||
| @{fs.BinaryReadHandle.read} if you prefer. | ||||
| [`io.lines`], which provides a nice way to loop over chunks of a file. You can of course just use [`fs.open`] and | ||||
| [`fs.BinaryReadHandle.read`] if you prefer. | ||||
| 
 | ||||
| ## Processing audio | ||||
| As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes. | ||||
| @@ -189,10 +189,9 @@ for chunk in io.lines("data/example.dfpwm", 16 * 1024) do | ||||
| end | ||||
| ``` | ||||
| 
 | ||||
| :::note Confused? | ||||
| Don't worry if you don't understand this example. It's quite advanced, and does use some ideas that this guide doesn't | ||||
| cover. That said, don't be afraid to ask on [GitHub Discussions] or [IRC] either! | ||||
| ::: | ||||
| > [Confused?][!NOTE] | ||||
| > Don't worry if you don't understand this example. It's quite advanced, and does use some ideas that this guide doesn't | ||||
| > cover. That said, don't be afraid to ask on [GitHub Discussions] or [IRC] either! | ||||
| 
 | ||||
| It's worth noting that the examples of audio processing we've mentioned here are about manipulating the _amplitude_ of | ||||
| the wave. If you wanted to modify the _frequency_ (for instance, shifting the pitch), things get rather more complex. | ||||
|   | ||||
| @@ -13,7 +13,7 @@ A library is a collection of useful functions and other definitions which is sto | ||||
| might want to create a library because you have some functions which are used in multiple programs, or just to split | ||||
| your program into multiple more modular files. | ||||
| 
 | ||||
| Let's say we want to create a small library to make working with the @{term|terminal} a little easier. We'll provide two | ||||
| Let's say we want to create a small library to make working with the [terminal][`term`] a little easier. We'll provide two | ||||
| functions: `reset`, which clears the terminal and sets the cursor to (1, 1), and `write_center`, which prints some text | ||||
| in the middle of the screen. | ||||
| 
 | ||||
| @@ -48,32 +48,32 @@ more_term.write_center("Hello, world!") | ||||
| When run, this'll clear the screen and print some text in the middle of the first line. | ||||
| 
 | ||||
| ## require in depth | ||||
| While the previous section is a good introduction to how @{require} operates, there are a couple of remaining points | ||||
| While the previous section is a good introduction to how [`require`] operates, there are a couple of remaining points | ||||
| which are worth mentioning for more advanced usage. | ||||
| 
 | ||||
| ### Libraries can return anything | ||||
| In our above example, we return a table containing the functions we want to expose. However, it's worth pointing out | ||||
| that you can return ''anything'' from your library - a table, a function or even just a string! @{require} treats them | ||||
| that you can return ''anything'' from your library - a table, a function or even just a string! [`require`] treats them | ||||
| all the same, and just returns whatever your library provides. | ||||
| 
 | ||||
| ### Module resolution and the package path | ||||
| In the above examples, we defined our library in a file, and @{require} read from it. While this is what you'll do most | ||||
| of the time, it is possible to make @{require} look elsewhere for your library, such as downloading from a website or | ||||
| In the above examples, we defined our library in a file, and [`require`] read from it. While this is what you'll do most | ||||
| of the time, it is possible to make [`require`] look elsewhere for your library, such as downloading from a website or | ||||
| loading from an in-memory library store. | ||||
| 
 | ||||
| As a result, the *module name* you pass to @{require} doesn't correspond to a file path. One common mistake is to load | ||||
| As a result, the *module name* you pass to [`require`] doesn't correspond to a file path. One common mistake is to load | ||||
| code from a sub-directory using `require("folder/library")` or even `require("folder/library.lua")`, neither of which | ||||
| will do quite what you expect. | ||||
| 
 | ||||
| When loading libraries (also referred to as *modules*) from files, @{require} searches along the *@{package.path|module | ||||
| path}*. By default, this looks something like: | ||||
| When loading libraries (also referred to as *modules*) from files, [`require`] searches along the [*module | ||||
| path*][`package.path`]. By default, this looks something like: | ||||
| 
 | ||||
| * `?.lua` | ||||
| * `?/init.lua` | ||||
| * `/rom/modules/main/?.lua` | ||||
| * etc... | ||||
| 
 | ||||
| When you call `require("my_library")`, @{require} replaces the `?` in each element of the path with your module name, and | ||||
| When you call `require("my_library")`, [`require`] replaces the `?` in each element of the path with your module name, and | ||||
| checks if the file exists. In this case, we'd look for `my_library.lua`, `my_library/init.lua`, | ||||
| `/rom/modules/main/my_library.lua` and so on. Note that this works *relative to the current program*, so if your | ||||
| program is actually called `folder/program`, then we'll look for `folder/my_library.lua`, etc... | ||||
| @@ -86,4 +86,4 @@ before we start looking for the library. | ||||
| There are several external resources which go into require in a little more detail: | ||||
| 
 | ||||
|  - The [Lua Module tutorial](http://lua-users.org/wiki/ModulesTutorial) on the Lua wiki. | ||||
|  - [Lua's manual section on @{require}](https://www.lua.org/manual/5.1/manual.html#pdf-require). | ||||
|  - [Lua's manual section on `require`](https://www.lua.org/manual/5.1/manual.html#pdf-require). | ||||
|   | ||||
| @@ -15,13 +15,13 @@ CC: Tweaked can be installed from [CurseForge] or [Modrinth]. It runs on both [M | ||||
| Controlled using the [Lua programming language][lua], CC: Tweaked's computers provides all the tools you need to start | ||||
| writing code and automating your Minecraft world. | ||||
| 
 | ||||
| {.big-image} | ||||
| <img alt="A ComputerCraft terminal open and ready to be programmed." src="images/basic-terminal.png" class="big-image" /> | ||||
| 
 | ||||
| While computers are incredibly powerful, they're rather limited by their inability to move about. *Turtles* are the | ||||
| solution here. They can move about the world, placing and breaking blocks, swinging a sword to protect you from zombies, | ||||
| or whatever else you program them to! | ||||
| 
 | ||||
| {.big-image} | ||||
| <img alt="A turtle tunneling in Minecraft." src="images/turtle.png" class="big-image" /> | ||||
| 
 | ||||
| Not all problems can be solved with a pickaxe though, and so CC: Tweaked also provides a bunch of additional peripherals | ||||
| for your computers. You can play a tune with speakers, display text or images on a monitor, connect all your | ||||
| @@ -30,7 +30,7 @@ computers together with modems, and much more. | ||||
| Computers can now also interact with inventories such as chests, allowing you to build complex inventory and item | ||||
| management systems. | ||||
| 
 | ||||
| {.big-image} | ||||
| <img alt="A chest's contents being read by a computer and displayed on a monitor." src="images/peripherals.png" class="big-image" /> | ||||
| 
 | ||||
| ## Getting Started | ||||
| While ComputerCraft is lovely for both experienced programmers and for people who have never coded before, it can be a | ||||
|   | ||||
| @@ -13,22 +13,20 @@ include standard Lua functions. | ||||
|  | ||||
| As it waits for a fixed amount of world ticks, `time` will automatically be | ||||
| rounded up to the nearest multiple of 0.05 seconds. If you are using coroutines | ||||
| or the @{parallel|parallel API}, it will only pause execution of the current | ||||
| or the [parallel API][`parallel`], it will only pause execution of the current | ||||
| thread, not the whole program. | ||||
|  | ||||
| :::tip | ||||
| Because sleep internally uses timers, it is a function that yields. This means | ||||
| that you can use it to prevent "Too long without yielding" errors. However, as | ||||
| the minimum sleep time is 0.05 seconds, it will slow your program down. | ||||
| ::: | ||||
| > [!TIP] | ||||
| > Because sleep internally uses timers, it is a function that yields. This means | ||||
| > that you can use it to prevent "Too long without yielding" errors. However, as | ||||
| > the minimum sleep time is 0.05 seconds, it will slow your program down. | ||||
|  | ||||
| :::caution | ||||
| Internally, this function queues and waits for a timer event (using | ||||
| @{os.startTimer}), however it does not listen for any other events. This means | ||||
| that any event that occurs while sleeping will be entirely discarded. If you | ||||
| need to receive events while sleeping, consider using @{os.startTimer|timers}, | ||||
| or the @{parallel|parallel API}. | ||||
| ::: | ||||
| > [!WARNING] | ||||
| > Internally, this function queues and waits for a timer event (using | ||||
| > [`os.startTimer`]), however it does not listen for any other events. This means | ||||
| > that any event that occurs while sleeping will be entirely discarded. If you | ||||
| > need to receive events while sleeping, consider using [timers][`os.startTimer`], | ||||
| > or the [parallel API][`parallel`]. | ||||
|  | ||||
| @tparam number time The number of seconds to sleep for, rounded up to the | ||||
| nearest multiple of 0.05. | ||||
| @@ -116,7 +114,7 @@ function read(replaceChar, history, completeFn, default) end | ||||
|  | ||||
| --- Stores the current ComputerCraft and Minecraft versions. | ||||
| -- | ||||
| -- Outside of Minecraft (for instance, in an emulator) @{_HOST} will contain the | ||||
| -- Outside of Minecraft (for instance, in an emulator) [`_HOST`] will contain the | ||||
| -- emulator's version instead. | ||||
| -- | ||||
| -- For example, `ComputerCraft 1.93.0 (Minecraft 1.15.2)`. | ||||
|   | ||||
| @@ -15,27 +15,27 @@ variables and functions exported by it will by available through the use of | ||||
| @deprecated When possible it's best to avoid using this function. It pollutes | ||||
| the global table and can mask errors. | ||||
|  | ||||
| @{require} should be used to load libraries instead. | ||||
| [`require`] should be used to load libraries instead. | ||||
| ]] | ||||
| function loadAPI(path) end | ||||
|  | ||||
| --- Unloads an API which was loaded by @{os.loadAPI}. | ||||
| --- Unloads an API which was loaded by [`os.loadAPI`]. | ||||
| -- | ||||
| -- This effectively removes the specified table from `_G`. | ||||
| -- | ||||
| -- @tparam string name The name of the API to unload. | ||||
| -- @since 1.2 | ||||
| -- @deprecated See @{os.loadAPI} for why. | ||||
| -- @deprecated See [`os.loadAPI`] for why. | ||||
| function unloadAPI(name) end | ||||
|  | ||||
| --[[- Pause execution of the current thread and waits for any events matching | ||||
| `filter`. | ||||
|  | ||||
| This function @{coroutine.yield|yields} the current process and waits for it | ||||
| This function [yields][`coroutine.yield`] the current process and waits for it | ||||
| to be resumed with a vararg list where the first element matches `filter`. | ||||
| If no `filter` is supplied, this will match all events. | ||||
|  | ||||
| Unlike @{os.pullEventRaw}, it will stop the application upon a "terminate" | ||||
| Unlike [`os.pullEventRaw`], it will stop the application upon a "terminate" | ||||
| event, printing the error "Terminated". | ||||
|  | ||||
| @tparam[opt] string filter Event to filter for. | ||||
| @@ -69,7 +69,7 @@ function pullEvent(filter) end | ||||
| --[[- Pause execution of the current thread and waits for events, including the | ||||
| `terminate` event. | ||||
|  | ||||
| This behaves almost the same as @{os.pullEvent}, except it allows you to handle | ||||
| This behaves almost the same as [`os.pullEvent`], except it allows you to handle | ||||
| the `terminate` event yourself - the program will not stop execution when | ||||
| <kbd>Ctrl+T</kbd> is pressed. | ||||
|  | ||||
| @@ -89,7 +89,7 @@ the `terminate` event yourself - the program will not stop execution when | ||||
| ]] | ||||
| function pullEventRaw(filter) end | ||||
|  | ||||
| --- Pauses execution for the specified number of seconds, alias of @{_G.sleep}. | ||||
| --- Pauses execution for the specified number of seconds, alias of [`_G.sleep`]. | ||||
| -- | ||||
| -- @tparam number time The number of seconds to sleep for, rounded up to the | ||||
| -- nearest multiple of 0.05. | ||||
| @@ -109,12 +109,12 @@ arguments. | ||||
|  | ||||
| This function does not resolve program names like the shell does. This means | ||||
| that, for example, `os.run("edit")` will not work. As well as this, it does not | ||||
| provide access to the @{shell} API in the environment. For this behaviour, use | ||||
| @{shell.run} instead. | ||||
| provide access to the [`shell`] API in the environment. For this behaviour, use | ||||
| [`shell.run`] instead. | ||||
|  | ||||
| If the program cannot be found, or failed to run, it will print the error and | ||||
| return `false`. If you want to handle this more gracefully, use an alternative | ||||
| such as @{loadfile}. | ||||
| such as [`loadfile`]. | ||||
|  | ||||
| @tparam table env The environment to run the program with. | ||||
| @tparam string path The exact path of the program to run. | ||||
|   | ||||
| @@ -19,8 +19,8 @@ parchmentMc = "1.19.4" | ||||
| asm = "9.3" | ||||
| autoService = "1.0.1" | ||||
| checkerFramework = "3.32.0" | ||||
| cobalt = "0.7.1" | ||||
| cobalt-next = "0.7.2" # Not a real version, used to constrain the version we accept. | ||||
| cobalt = "0.7.3" | ||||
| cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept. | ||||
| fastutil = "8.5.9" | ||||
| guava = "31.1-jre" | ||||
| jetbrainsAnnotations = "24.0.1" | ||||
| @@ -34,6 +34,7 @@ slf4j = "1.7.36" | ||||
|  | ||||
| # Minecraft mods | ||||
| emi = "1.0.8+1.20.1" | ||||
| fabricPermissions = "0.3.20230723" | ||||
| iris = "1.6.4+1.20" | ||||
| jei = "15.2.0.22" | ||||
| modmenu = "7.1.0" | ||||
| @@ -49,7 +50,7 @@ jqwik = "1.7.2" | ||||
| junit = "5.9.2" | ||||
|  | ||||
| # Build tools | ||||
| cctJavadoc = "1.7.0" | ||||
| cctJavadoc = "1.8.0" | ||||
| checkstyle = "10.3.4" | ||||
| curseForgeGradle = "1.0.14" | ||||
| errorProne-core = "2.18.0" | ||||
| @@ -58,7 +59,7 @@ fabric-loom = "1.3.7" | ||||
| forgeGradle = "6.0.8" | ||||
| githubRelease = "2.2.12" | ||||
| ideaExt = "1.1.6" | ||||
| illuaminate = "0.1.0-28-ga7efd71" | ||||
| illuaminate = "0.1.0-40-g975cbc3" | ||||
| librarian = "1.+" | ||||
| minotaur = "2.+" | ||||
| mixinGradle = "0.7.+" | ||||
| @@ -93,6 +94,7 @@ slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } | ||||
| # Minecraft mods | ||||
| fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" } | ||||
| fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-loader" } | ||||
| fabricPermissions = { module = "me.lucko:fabric-permissions-api", version.ref = "fabricPermissions" } | ||||
| emi = { module = "dev.emi:emi-xplat-mojmap", version.ref = "emi" } | ||||
| iris = { module = "maven.modrinth:iris", version.ref = "iris" } | ||||
| jei-api = { module = "mezz.jei:jei-1.20.1-common-api", version.ref = "jei" } | ||||
| @@ -154,7 +156,7 @@ externalMods-common = ["jei-api", "nightConfig-core", "nightConfig-toml"] | ||||
| externalMods-forge-compile = ["oculus", "jei-api"] | ||||
| externalMods-forge-runtime = ["jei-forge"] | ||||
| externalMods-fabric = ["nightConfig-core", "nightConfig-toml"] | ||||
| externalMods-fabric-compile = ["iris", "jei-api", "rei-api", "rei-builtin"] | ||||
| externalMods-fabric-compile = ["fabricPermissions", "iris", "jei-api", "rei-api", "rei-builtin"] | ||||
| externalMods-fabric-runtime = ["jei-fabric", "modmenu"] | ||||
|  | ||||
| # Testing | ||||
|   | ||||
| @@ -12,7 +12,6 @@ | ||||
|   /projects/core/src/test/resources/test-rom | ||||
|   /projects/web/src/mount) | ||||
|  | ||||
|  | ||||
| (doc | ||||
|   ; Also defined in projects/web/build.gradle.kts | ||||
|   (destination /projects/web/build/illuaminate) | ||||
| @@ -50,6 +49,8 @@ | ||||
| (at / | ||||
|   (linters | ||||
|     syntax:string-index | ||||
|     doc:docusaurus-admonition | ||||
|     doc:ldoc-reference | ||||
|  | ||||
|     ;; It'd be nice to avoid this, but right now there's a lot of instances of | ||||
|     ;; it. | ||||
| @@ -82,23 +83,19 @@ | ||||
|       ;; isn't smart enough. | ||||
|       sleep write printError read rs))) | ||||
|  | ||||
| ;; We disable the unused global linter in bios.lua and the APIs. In the future | ||||
| ;; hopefully we'll get illuaminate to handle this. | ||||
| ;; We disable the unused global linter in bios.lua, APIs and our documentation | ||||
| ;; stubs docs. In the future hopefully we'll get illuaminate to handle this. | ||||
| (at | ||||
|   (/projects/core/src/main/resources/data/computercraft/lua/bios.lua | ||||
|    /projects/core/src/main/resources/data/computercraft/lua/rom/apis/) | ||||
|   (linters -var:unused-global) | ||||
|   (lint (allow-toplevel-global true))) | ||||
|  | ||||
| ;; Silence some variable warnings in documentation stubs. | ||||
| (at (/doc/stub/ /projects/forge/build/docs/luaJavadoc/) | ||||
|   (/doc/stub/ | ||||
|    /projects/core/src/main/resources/data/computercraft/lua/bios.lua | ||||
|    /projects/core/src/main/resources/data/computercraft/lua/rom/apis/ | ||||
|    /projects/forge/build/docs/luaJavadoc/) | ||||
|   (linters -var:unused-global) | ||||
|   (lint (allow-toplevel-global true))) | ||||
|  | ||||
| ;; Suppress warnings for currently undocumented modules. | ||||
| (at | ||||
|   (; Lua APIs | ||||
|    /projects/core/src/main/resources/data/computercraft/lua/rom/apis/io.lua | ||||
|    /projects/core/src/main/resources/data/computercraft/lua/rom/apis/window.lua) | ||||
|  | ||||
|   (linters -doc:undocumented -doc:undocumented-arg -doc:undocumented-return)) | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||
| import dan200.computercraft.shared.computer.inventory.ViewComputerMenu; | ||||
| import dan200.computercraft.shared.media.items.DiskItem; | ||||
| import dan200.computercraft.shared.media.items.TreasureDiskItem; | ||||
| import net.minecraft.client.Minecraft; | ||||
| import net.minecraft.client.color.item.ItemColor; | ||||
| import net.minecraft.client.gui.screens.MenuScreens; | ||||
| import net.minecraft.client.multiplayer.ClientLevel; | ||||
| @@ -30,6 +31,7 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderers; | ||||
| import net.minecraft.client.renderer.item.ClampedItemPropertyFunction; | ||||
| import net.minecraft.client.renderer.item.ItemProperties; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| import net.minecraft.server.packs.resources.PreparableReloadListener; | ||||
| import net.minecraft.server.packs.resources.ResourceProvider; | ||||
| import net.minecraft.world.entity.LivingEntity; | ||||
| import net.minecraft.world.item.Item; | ||||
| @@ -107,6 +109,10 @@ public final class ClientRegistry { | ||||
|         for (var item : items) ItemProperties.register(item.get(), id, getter); | ||||
|     } | ||||
| 
 | ||||
|     public static void registerReloadListeners(Consumer<PreparableReloadListener> register, Minecraft minecraft) { | ||||
|         register.accept(GuiSprites.initialise(minecraft.getTextureManager())); | ||||
|     } | ||||
| 
 | ||||
|     private static final String[] EXTRA_MODELS = new String[]{ | ||||
|         "block/turtle_colour", | ||||
|         "block/turtle_elf_overlay", | ||||
|   | ||||
| @@ -7,13 +7,14 @@ package dan200.computercraft.client.gui; | ||||
| import dan200.computercraft.client.gui.widgets.ComputerSidebar; | ||||
| import dan200.computercraft.client.gui.widgets.TerminalWidget; | ||||
| import dan200.computercraft.client.render.ComputerBorderRenderer; | ||||
| import dan200.computercraft.client.render.RenderTypes; | ||||
| import dan200.computercraft.client.render.SpriteRenderer; | ||||
| import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||
| import net.minecraft.client.gui.GuiGraphics; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.world.entity.player.Inventory; | ||||
| 
 | ||||
| import static dan200.computercraft.client.render.ComputerBorderRenderer.BORDER; | ||||
| import static dan200.computercraft.client.render.RenderTypes.FULL_BRIGHT_LIGHTMAP; | ||||
| 
 | ||||
| /** | ||||
|  * A GUI for computers which renders the terminal (and border), but with no UI elements. | ||||
| @@ -39,11 +40,13 @@ public final class ComputerScreen<T extends AbstractComputerMenu> extends Abstra | ||||
|     public void renderBg(GuiGraphics graphics, float partialTicks, int mouseX, int mouseY) { | ||||
|         // Draw a border around the terminal | ||||
|         var terminal = getTerminal(); | ||||
|         var texture = ComputerBorderRenderer.getTexture(family); | ||||
|         var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES); | ||||
|         var computerTextures = GuiSprites.getComputerTextures(family); | ||||
| 
 | ||||
|         ComputerBorderRenderer.render( | ||||
|             graphics.pose().last().pose(), texture, terminal.getX(), terminal.getY(), | ||||
|             FULL_BRIGHT_LIGHTMAP, terminal.getWidth(), terminal.getHeight() | ||||
|             spriteRenderer, computerTextures, | ||||
|             terminal.getX(), terminal.getY(), terminal.getWidth(), terminal.getHeight(), false | ||||
|         ); | ||||
|         ComputerSidebar.renderBackground(graphics, texture, leftPos, topPos + sidebarYOffset); | ||||
|         ComputerSidebar.renderBackground(spriteRenderer, computerTextures, leftPos, topPos + sidebarYOffset); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -7,7 +7,8 @@ package dan200.computercraft.client.gui; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.client.gui.widgets.ComputerSidebar; | ||||
| import dan200.computercraft.client.gui.widgets.TerminalWidget; | ||||
| import dan200.computercraft.client.render.ComputerBorderRenderer; | ||||
| import dan200.computercraft.client.render.RenderTypes; | ||||
| import dan200.computercraft.client.render.SpriteRenderer; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||
| import dan200.computercraft.shared.turtle.inventory.TurtleMenu; | ||||
| @@ -59,6 +60,8 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> { | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         ComputerSidebar.renderBackground(graphics, ComputerBorderRenderer.getTexture(family), leftPos, topPos + sidebarYOffset); | ||||
|         // Render sidebar | ||||
|         var spriteRenderer = SpriteRenderer.createForGui(graphics, RenderTypes.GUI_SPRITES); | ||||
|         ComputerSidebar.renderBackground(spriteRenderer, GuiSprites.getComputerTextures(family), leftPos, topPos + sidebarYOffset); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,15 +4,13 @@ | ||||
| 
 | ||||
| package dan200.computercraft.client.gui.widgets; | ||||
| 
 | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import dan200.computercraft.client.gui.widgets.DynamicImageButton.HintedMessage; | ||||
| import dan200.computercraft.client.render.ComputerBorderRenderer; | ||||
| import dan200.computercraft.client.render.SpriteRenderer; | ||||
| import dan200.computercraft.shared.computer.core.InputHandler; | ||||
| import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu; | ||||
| import net.minecraft.client.gui.GuiGraphics; | ||||
| import net.minecraft.client.gui.components.AbstractWidget; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| 
 | ||||
| import java.util.function.BooleanSupplier; | ||||
| import java.util.function.Consumer; | ||||
| @@ -21,22 +19,18 @@ import java.util.function.Consumer; | ||||
|  * Registers buttons to interact with a computer. | ||||
|  */ | ||||
| public final class ComputerSidebar { | ||||
|     private static final ResourceLocation TEXTURE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/buttons.png"); | ||||
| 
 | ||||
|     private static final int TEX_SIZE = 64; | ||||
| 
 | ||||
|     private static final int ICON_WIDTH = 12; | ||||
|     private static final int ICON_HEIGHT = 12; | ||||
|     private static final int ICON_MARGIN = 2; | ||||
| 
 | ||||
|     private static final int ICON_TEX_Y_DIFF = 14; | ||||
| 
 | ||||
|     private static final int CORNERS_BORDER = 3; | ||||
|     private static final int FULL_BORDER = CORNERS_BORDER + ICON_MARGIN; | ||||
| 
 | ||||
|     private static final int BUTTONS = 2; | ||||
|     private static final int HEIGHT = (ICON_HEIGHT + ICON_MARGIN * 2) * BUTTONS + CORNERS_BORDER * 2; | ||||
| 
 | ||||
|     private static final int TEX_HEIGHT = 14; | ||||
| 
 | ||||
|     private ComputerSidebar() { | ||||
|     } | ||||
| 
 | ||||
| @@ -50,16 +44,18 @@ public final class ComputerSidebar { | ||||
|             Component.translatable("gui.computercraft.tooltip.turn_off.key") | ||||
|         ); | ||||
|         add.accept(new DynamicImageButton( | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, () -> isOn.getAsBoolean() ? 15 : 1, 1, ICON_TEX_Y_DIFF, | ||||
|             TEXTURE, TEX_SIZE, TEX_SIZE, b -> toggleComputer(isOn, input), | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, | ||||
|             h -> isOn.getAsBoolean() ? GuiSprites.TURNED_ON.get(h) : GuiSprites.TURNED_OFF.get(h), | ||||
|             b -> toggleComputer(isOn, input), | ||||
|             () -> isOn.getAsBoolean() ? turnOff : turnOn | ||||
|         )); | ||||
| 
 | ||||
|         y += ICON_HEIGHT + ICON_MARGIN * 2; | ||||
| 
 | ||||
|         add.accept(new DynamicImageButton( | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, 29, 1, ICON_TEX_Y_DIFF, | ||||
|             TEXTURE, TEX_SIZE, TEX_SIZE, b -> input.queueEvent("terminate"), | ||||
|             x, y, ICON_WIDTH, ICON_HEIGHT, | ||||
|             GuiSprites.TERMINATE::get, | ||||
|             b -> input.queueEvent("terminate"), | ||||
|             new HintedMessage( | ||||
|                 Component.translatable("gui.computercraft.tooltip.terminate"), | ||||
|                 Component.translatable("gui.computercraft.tooltip.terminate.key") | ||||
| @@ -67,22 +63,12 @@ public final class ComputerSidebar { | ||||
|         )); | ||||
|     } | ||||
| 
 | ||||
|     public static void renderBackground(GuiGraphics graphics, ResourceLocation texture, int x, int y) { | ||||
|         graphics.blit(texture, | ||||
|             x, y, 0, 102, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER, | ||||
|             ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE | ||||
|         ); | ||||
|     public static void renderBackground(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y) { | ||||
|         var texture = textures.sidebar(); | ||||
|         if (texture == null) throw new NullPointerException(textures + " has no sidebar texture"); | ||||
|         var sprite = GuiSprites.get(texture); | ||||
| 
 | ||||
|         graphics.blit(texture, | ||||
|             x, y + FULL_BORDER, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT - FULL_BORDER * 2, | ||||
|             0, 107, AbstractComputerMenu.SIDEBAR_WIDTH, 4, | ||||
|             ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE | ||||
|         ); | ||||
| 
 | ||||
|         graphics.blit(texture, | ||||
|             x, y + HEIGHT - FULL_BORDER, 0, 111, AbstractComputerMenu.SIDEBAR_WIDTH, FULL_BORDER, | ||||
|             ComputerBorderRenderer.TEX_SIZE, ComputerBorderRenderer.TEX_SIZE | ||||
|         ); | ||||
|         renderer.blitVerticalSliced(sprite, x, y, AbstractComputerMenu.SIDEBAR_WIDTH, HEIGHT, FULL_BORDER, FULL_BORDER, TEX_HEIGHT); | ||||
|     } | ||||
| 
 | ||||
|     private static void toggleComputer(BooleanSupplier isOn, InputHandler input) { | ||||
|   | ||||
| @@ -5,15 +5,15 @@ | ||||
| package dan200.computercraft.client.gui.widgets; | ||||
| 
 | ||||
| import com.mojang.blaze3d.systems.RenderSystem; | ||||
| import it.unimi.dsi.fastutil.booleans.Boolean2ObjectFunction; | ||||
| import net.minecraft.ChatFormatting; | ||||
| import net.minecraft.client.gui.GuiGraphics; | ||||
| import net.minecraft.client.gui.components.Button; | ||||
| import net.minecraft.client.gui.components.Tooltip; | ||||
| import net.minecraft.client.renderer.texture.TextureAtlasSprite; | ||||
| import net.minecraft.network.chat.Component; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.function.IntSupplier; | ||||
| import java.util.function.Supplier; | ||||
| 
 | ||||
| /** | ||||
| @@ -21,50 +21,34 @@ import java.util.function.Supplier; | ||||
|  * dynamically. | ||||
|  */ | ||||
| public class DynamicImageButton extends Button { | ||||
|     private final ResourceLocation texture; | ||||
|     private final IntSupplier xTexStart; | ||||
|     private final int yTexStart; | ||||
|     private final int yDiffTex; | ||||
|     private final int textureWidth; | ||||
|     private final int textureHeight; | ||||
|     private final Boolean2ObjectFunction<TextureAtlasSprite> texture; | ||||
|     private final Supplier<HintedMessage> message; | ||||
| 
 | ||||
|     public DynamicImageButton( | ||||
|         int x, int y, int width, int height, int xTexStart, int yTexStart, int yDiffTex, | ||||
|         ResourceLocation texture, int textureWidth, int textureHeight, | ||||
|         OnPress onPress, HintedMessage message | ||||
|         int x, int y, int width, int height, Boolean2ObjectFunction<TextureAtlasSprite> texture, OnPress onPress, | ||||
|         HintedMessage message | ||||
|     ) { | ||||
|         this( | ||||
|             x, y, width, height, () -> xTexStart, yTexStart, yDiffTex, | ||||
|             texture, textureWidth, textureHeight, | ||||
|             onPress, () -> message | ||||
|         ); | ||||
|         this(x, y, width, height, texture, onPress, () -> message); | ||||
|     } | ||||
| 
 | ||||
|     public DynamicImageButton( | ||||
|         int x, int y, int width, int height, IntSupplier xTexStart, int yTexStart, int yDiffTex, | ||||
|         ResourceLocation texture, int textureWidth, int textureHeight, | ||||
|         int x, int y, int width, int height, | ||||
|         Boolean2ObjectFunction<TextureAtlasSprite> texture, | ||||
|         OnPress onPress, Supplier<HintedMessage> message | ||||
|     ) { | ||||
|         super(x, y, width, height, Component.empty(), onPress, DEFAULT_NARRATION); | ||||
|         this.textureWidth = textureWidth; | ||||
|         this.textureHeight = textureHeight; | ||||
|         this.xTexStart = xTexStart; | ||||
|         this.yTexStart = yTexStart; | ||||
|         this.yDiffTex = yDiffTex; | ||||
|         this.texture = texture; | ||||
|         this.message = message; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTicks) { | ||||
|         RenderSystem.enableBlend(); | ||||
|         var texture = this.texture.get(isHoveredOrFocused()); | ||||
|         RenderSystem.setShaderTexture(0, texture.atlasLocation()); | ||||
|         RenderSystem.disableDepthTest(); | ||||
| 
 | ||||
|         graphics.blit(getX(), getY(), 0, width, height, texture); | ||||
|         RenderSystem.enableDepthTest(); | ||||
| 
 | ||||
|         var yTex = yTexStart; | ||||
|         if (isHoveredOrFocused()) yTex += yDiffTex; | ||||
| 
 | ||||
|         graphics.blit(texture, getX(), getY(), xTexStart.getAsInt(), yTex, width, height, textureWidth, textureHeight); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|   | ||||
| @@ -4,25 +4,17 @@ | ||||
| 
 | ||||
| package dan200.computercraft.client.render; | ||||
| 
 | ||||
| import com.mojang.blaze3d.vertex.Tesselator; | ||||
| import com.mojang.blaze3d.vertex.VertexConsumer; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| import net.minecraft.client.renderer.MultiBufferSource; | ||||
| import net.minecraft.client.renderer.RenderType; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| import org.joml.Matrix4f; | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import net.minecraft.client.renderer.texture.TextureAtlasSprite; | ||||
| 
 | ||||
| import static dan200.computercraft.client.render.SpriteRenderer.u; | ||||
| import static dan200.computercraft.client.render.SpriteRenderer.v; | ||||
| 
 | ||||
| /** | ||||
|  * Renders the borders of computers, either for a GUI ({@link dan200.computercraft.client.gui.ComputerScreen}) or | ||||
|  * {@linkplain PocketItemRenderer in-hand pocket computers}. | ||||
|  */ | ||||
| public class ComputerBorderRenderer { | ||||
|     private static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_normal.png"); | ||||
|     private static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_advanced.png"); | ||||
|     private static final ResourceLocation BACKGROUND_COMMAND = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_command.png"); | ||||
|     public static final ResourceLocation BACKGROUND_COLOUR = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/corners_colour.png"); | ||||
| 
 | ||||
| public final class ComputerBorderRenderer { | ||||
|     /** | ||||
|      * The margin between the terminal and its border. | ||||
|      */ | ||||
| @@ -33,100 +25,51 @@ public class ComputerBorderRenderer { | ||||
|      */ | ||||
|     public static final int BORDER = 12; | ||||
| 
 | ||||
|     private static final int CORNER_TOP_Y = 28; | ||||
|     private static final int CORNER_BOTTOM_Y = CORNER_TOP_Y + BORDER; | ||||
|     private static final int CORNER_LEFT_X = BORDER; | ||||
|     private static final int CORNER_RIGHT_X = CORNER_LEFT_X + BORDER; | ||||
|     private static final int BORDER_RIGHT_X = 36; | ||||
|     private static final int LIGHT_BORDER_Y = 56; | ||||
|     private static final int LIGHT_CORNER_Y = 80; | ||||
| 
 | ||||
|     public static final int LIGHT_HEIGHT = 8; | ||||
| 
 | ||||
|     public static final int TEX_SIZE = 256; | ||||
|     private static final float TEX_SCALE = 1 / (float) TEX_SIZE; | ||||
|     private static final int TEX_SIZE = 36; | ||||
| 
 | ||||
|     private final Matrix4f transform; | ||||
|     private final VertexConsumer builder; | ||||
|     private final int light; | ||||
|     private final int z; | ||||
|     private final float r, g, b; | ||||
| 
 | ||||
|     public ComputerBorderRenderer(Matrix4f transform, VertexConsumer builder, int z, int light, float r, float g, float b) { | ||||
|         this.transform = transform; | ||||
|         this.builder = builder; | ||||
|         this.z = z; | ||||
|         this.light = light; | ||||
|         this.r = r; | ||||
|         this.g = g; | ||||
|         this.b = b; | ||||
|     private ComputerBorderRenderer() { | ||||
|     } | ||||
| 
 | ||||
|     public static ResourceLocation getTexture(ComputerFamily family) { | ||||
|         return switch (family) { | ||||
|             case NORMAL -> BACKGROUND_NORMAL; | ||||
|             case ADVANCED -> BACKGROUND_ADVANCED; | ||||
|             case COMMAND -> BACKGROUND_COMMAND; | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     public static RenderType getRenderType(ResourceLocation location) { | ||||
|         // See note in RenderTypes about why we use text rather than anything intuitive. | ||||
|         return RenderType.text(location); | ||||
|     } | ||||
| 
 | ||||
|     public static void render(Matrix4f transform, ResourceLocation location, int x, int y, int light, int width, int height) { | ||||
|         var source = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder()); | ||||
|         render(transform, source.getBuffer(getRenderType(location)), x, y, 1, light, width, height, false, 1, 1, 1); | ||||
|         source.endBatch(); | ||||
|     } | ||||
| 
 | ||||
|     public static void render(Matrix4f transform, VertexConsumer buffer, int x, int y, int z, int light, int width, int height, boolean withLight, float r, float g, float b) { | ||||
|         new ComputerBorderRenderer(transform, buffer, z, light, r, g, b).doRender(x, y, width, height, withLight); | ||||
|     } | ||||
| 
 | ||||
|     public void doRender(int x, int y, int width, int height, boolean withLight) { | ||||
|     public static void render(SpriteRenderer renderer, GuiSprites.ComputerTextures textures, int x, int y, int width, int height, boolean withLight) { | ||||
|         var endX = x + width; | ||||
|         var endY = y + height; | ||||
| 
 | ||||
|         // Vertical bars | ||||
|         renderLine(x - BORDER, y, 0, CORNER_TOP_Y, BORDER, endY - y); | ||||
|         renderLine(endX, y, BORDER_RIGHT_X, CORNER_TOP_Y, BORDER, endY - y); | ||||
|         var border = GuiSprites.get(textures.border()); | ||||
| 
 | ||||
|         // Top bar | ||||
|         renderLine(x, y - BORDER, 0, 0, endX - x, BORDER); | ||||
|         renderCorner(x - BORDER, y - BORDER, CORNER_LEFT_X, CORNER_TOP_Y); | ||||
|         renderCorner(endX, y - BORDER, CORNER_RIGHT_X, CORNER_TOP_Y); | ||||
|         blitBorder(renderer, border, x - BORDER, y - BORDER, 0, 0, BORDER, BORDER); | ||||
|         blitBorder(renderer, border, x, y - BORDER, BORDER, 0, width, BORDER); | ||||
|         blitBorder(renderer, border, endX, y - BORDER, BORDER * 2, 0, BORDER, BORDER); | ||||
| 
 | ||||
|         // Vertical bars | ||||
|         blitBorder(renderer, border, x - BORDER, y, 0, BORDER, BORDER, height); | ||||
|         blitBorder(renderer, border, endX, y, BORDER * 2, BORDER, BORDER, height); | ||||
| 
 | ||||
|         // Bottom bar. We allow for drawing a stretched version, which allows for additional elements (such as the | ||||
|         // pocket computer's lights). | ||||
|         if (withLight) { | ||||
|             renderTexture(x, endY, 0, LIGHT_BORDER_Y, endX - x, BORDER + LIGHT_HEIGHT, BORDER, BORDER + LIGHT_HEIGHT); | ||||
|             renderTexture(x - BORDER, endY, CORNER_LEFT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); | ||||
|             renderTexture(endX, endY, CORNER_RIGHT_X, LIGHT_CORNER_Y, BORDER, BORDER + LIGHT_HEIGHT); | ||||
|             var pocketBottomTexture = textures.pocketBottom(); | ||||
|             if (pocketBottomTexture == null) throw new NullPointerException(textures + " has no pocket texture"); | ||||
|             var pocketBottom = GuiSprites.get(pocketBottomTexture); | ||||
| 
 | ||||
|             renderer.blitHorizontalSliced( | ||||
|                 pocketBottom, x - BORDER, endY, width + BORDER * 2, BORDER + LIGHT_HEIGHT, | ||||
|                 BORDER, BORDER, BORDER * 3 | ||||
|             ); | ||||
|         } else { | ||||
|             renderLine(x, endY, 0, BORDER, endX - x, BORDER); | ||||
|             renderCorner(x - BORDER, endY, CORNER_LEFT_X, CORNER_BOTTOM_Y); | ||||
|             renderCorner(endX, endY, CORNER_RIGHT_X, CORNER_BOTTOM_Y); | ||||
|             blitBorder(renderer, border, x - BORDER, endY, 0, BORDER * 2, BORDER, BORDER); | ||||
|             blitBorder(renderer, border, x, endY, BORDER, BORDER * 2, width, BORDER); | ||||
|             blitBorder(renderer, border, endX, endY, BORDER * 2, BORDER * 2, BORDER, BORDER); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void renderCorner(int x, int y, int u, int v) { | ||||
|         renderTexture(x, y, u, v, BORDER, BORDER, BORDER, BORDER); | ||||
|     } | ||||
| 
 | ||||
|     private void renderLine(int x, int y, int u, int v, int width, int height) { | ||||
|         renderTexture(x, y, u, v, width, height, BORDER, BORDER); | ||||
|     } | ||||
| 
 | ||||
|     private void renderTexture(int x, int y, int u, int v, int width, int height) { | ||||
|         renderTexture(x, y, u, v, width, height, width, height); | ||||
|     } | ||||
| 
 | ||||
|     private void renderTexture(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) { | ||||
|         builder.vertex(transform, x, y + height, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex(); | ||||
|         builder.vertex(transform, x + width, y + height, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, (v + textureHeight) * TEX_SCALE).uv2(light).endVertex(); | ||||
|         builder.vertex(transform, x + width, y, z).color(r, g, b, 1.0f).uv((u + textureWidth) * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex(); | ||||
|         builder.vertex(transform, x, y, z).color(r, g, b, 1.0f).uv(u * TEX_SCALE, v * TEX_SCALE).uv2(light).endVertex(); | ||||
|     private static void blitBorder(SpriteRenderer renderer, TextureAtlasSprite sprite, int x, int y, int u, int v, int width, int height) { | ||||
|         renderer.blit( | ||||
|             x, y, width, height, | ||||
|             u(sprite, u, TEX_SIZE), v(sprite, v, TEX_SIZE), | ||||
|             u(sprite, u + BORDER, TEX_SIZE), v(sprite, v + BORDER, TEX_SIZE) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ package dan200.computercraft.client.render; | ||||
| 
 | ||||
| import com.mojang.blaze3d.vertex.PoseStack; | ||||
| import com.mojang.math.Axis; | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import dan200.computercraft.client.pocket.ClientPocketComputers; | ||||
| import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | ||||
| import dan200.computercraft.core.util.Colour; | ||||
| @@ -72,13 +73,14 @@ public final class PocketItemRenderer extends ItemMapLikeRenderer { | ||||
|     } | ||||
| 
 | ||||
|     private static void renderFrame(Matrix4f transform, MultiBufferSource render, ComputerFamily family, int colour, int light, int width, int height) { | ||||
|         var texture = colour != -1 ? ComputerBorderRenderer.BACKGROUND_COLOUR : ComputerBorderRenderer.getTexture(family); | ||||
|         var texture = colour != -1 ? GuiSprites.COMPUTER_COLOUR : GuiSprites.getComputerTextures(family); | ||||
| 
 | ||||
|         var r = ((colour >>> 16) & 0xFF) / 255.0f; | ||||
|         var g = ((colour >>> 8) & 0xFF) / 255.0f; | ||||
|         var b = (colour & 0xFF) / 255.0f; | ||||
|         var r = (colour >>> 16) & 0xFF; | ||||
|         var g = (colour >>> 8) & 0xFF; | ||||
|         var b = colour & 0xFF; | ||||
| 
 | ||||
|         ComputerBorderRenderer.render(transform, render.getBuffer(ComputerBorderRenderer.getRenderType(texture)), 0, 0, 0, light, width, height, true, r, g, b); | ||||
|         var spriteRenderer = new SpriteRenderer(transform, render.getBuffer(RenderTypes.GUI_SPRITES), 0, light, r, g, b); | ||||
|         ComputerBorderRenderer.render(spriteRenderer, texture, 0, 0, width, height, true); | ||||
|     } | ||||
| 
 | ||||
|     private static void renderLight(PoseStack transform, MultiBufferSource render, int colour, int width, int height) { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ package dan200.computercraft.client.render; | ||||
| import com.mojang.blaze3d.vertex.DefaultVertexFormat; | ||||
| import com.mojang.blaze3d.vertex.VertexFormat; | ||||
| import dan200.computercraft.api.ComputerCraftAPI; | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import dan200.computercraft.client.render.monitor.MonitorTextureBufferShader; | ||||
| import dan200.computercraft.client.render.text.FixedWidthFontRenderer; | ||||
| import net.minecraft.client.renderer.GameRenderer; | ||||
| @@ -53,6 +54,11 @@ public class RenderTypes { | ||||
|      */ | ||||
|     public static final RenderType PRINTOUT_BACKGROUND = RenderType.text(new ResourceLocation("computercraft", "textures/gui/printout.png")); | ||||
| 
 | ||||
|     /** | ||||
|      * Render type for {@linkplain GuiSprites GUI sprites}. | ||||
|      */ | ||||
|     public static final RenderType GUI_SPRITES = RenderType.text(GuiSprites.TEXTURE); | ||||
| 
 | ||||
|     public static MonitorTextureBufferShader getMonitorTextureBufferShader() { | ||||
|         if (monitorTboShader == null) throw new NullPointerException("MonitorTboShader has not been registered"); | ||||
|         return monitorTboShader; | ||||
|   | ||||
| @@ -0,0 +1,134 @@ | ||||
| // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.client.render; | ||||
| 
 | ||||
| import com.mojang.blaze3d.vertex.VertexConsumer; | ||||
| import net.minecraft.client.gui.GuiGraphics; | ||||
| import net.minecraft.client.renderer.RenderType; | ||||
| import net.minecraft.client.renderer.texture.TextureAtlasSprite; | ||||
| import org.joml.Matrix4f; | ||||
| 
 | ||||
| /** | ||||
|  * A {@link GuiGraphics}-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(GuiGraphics graphics, RenderType renderType) { | ||||
|         return new SpriteRenderer( | ||||
|             graphics.pose().last().pose(), graphics.bufferSource().getBuffer(renderType), | ||||
|             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 GuiGraphics#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 GuiGraphics#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); | ||||
|     } | ||||
| } | ||||
| @@ -4,8 +4,10 @@ | ||||
| 
 | ||||
| package dan200.computercraft.data.client; | ||||
| 
 | ||||
| import dan200.computercraft.client.gui.GuiSprites; | ||||
| import dan200.computercraft.data.DataProviders; | ||||
| import dan200.computercraft.shared.turtle.inventory.UpgradeSlot; | ||||
| import net.minecraft.client.renderer.texture.atlas.SpriteSource; | ||||
| import net.minecraft.client.renderer.texture.atlas.SpriteSources; | ||||
| import net.minecraft.client.renderer.texture.atlas.sources.SingleFile; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| @@ -13,6 +15,7 @@ import net.minecraft.server.packs.PackType; | ||||
| 
 | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
| import java.util.stream.Stream; | ||||
| 
 | ||||
| /** | ||||
|  * A version of {@link DataProviders} which relies on client-side classes. | ||||
| @@ -29,6 +32,17 @@ public final class ClientDataProviders { | ||||
|                 new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()), | ||||
|                 new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty()) | ||||
|             )); | ||||
|             out.accept(GuiSprites.SPRITE_SHEET, Stream.of( | ||||
|                 // Buttons | ||||
|                 GuiSprites.TURNED_OFF.textures(), | ||||
|                 GuiSprites.TURNED_ON.textures(), | ||||
|                 GuiSprites.TERMINATE.textures(), | ||||
|                 // Computers | ||||
|                 GuiSprites.COMPUTER_NORMAL.textures(), | ||||
|                 GuiSprites.COMPUTER_ADVANCED.textures(), | ||||
|                 GuiSprites.COMPUTER_COMMAND.textures(), | ||||
|                 GuiSprites.COMPUTER_COLOUR.textures() | ||||
|             ).flatMap(x -> x).<SpriteSource>map(x -> new SingleFile(x, Optional.empty())).toList()); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import dan200.computercraft.api.upgrades.UpgradeData; | ||||
| import dan200.computercraft.core.util.Colour; | ||||
| import dan200.computercraft.impl.PocketUpgrades; | ||||
| import dan200.computercraft.impl.TurtleUpgrades; | ||||
| import dan200.computercraft.shared.command.UserLevel; | ||||
| import dan200.computercraft.shared.command.arguments.ComputerArgumentType; | ||||
| import dan200.computercraft.shared.command.arguments.ComputersArgumentType; | ||||
| import dan200.computercraft.shared.command.arguments.RepeatArgumentType; | ||||
| @@ -37,6 +38,7 @@ import dan200.computercraft.shared.data.HasComputerIdLootCondition; | ||||
| import dan200.computercraft.shared.data.PlayerCreativeLootCondition; | ||||
| import dan200.computercraft.shared.details.BlockDetails; | ||||
| import dan200.computercraft.shared.details.ItemDetails; | ||||
| import dan200.computercraft.shared.integration.PermissionRegistry; | ||||
| import dan200.computercraft.shared.media.items.DiskItem; | ||||
| import dan200.computercraft.shared.media.items.PrintoutItem; | ||||
| import dan200.computercraft.shared.media.items.RecordMedia; | ||||
| @@ -77,6 +79,7 @@ import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe; | ||||
| import dan200.computercraft.shared.turtle.upgrades.*; | ||||
| import dan200.computercraft.shared.util.ImpostorRecipe; | ||||
| import dan200.computercraft.shared.util.ImpostorShapelessRecipe; | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| import net.minecraft.commands.synchronization.ArgumentTypeInfo; | ||||
| import net.minecraft.commands.synchronization.SingletonArgumentInfo; | ||||
| import net.minecraft.core.BlockPos; | ||||
| @@ -98,6 +101,7 @@ import net.minecraft.world.level.material.MapColor; | ||||
| import net.minecraft.world.level.storage.loot.predicates.LootItemConditionType; | ||||
| 
 | ||||
| import java.util.function.BiFunction; | ||||
| import java.util.function.Predicate; | ||||
| 
 | ||||
| /** | ||||
|  * Registers ComputerCraft's registry entries and additional objects, such as {@link CauldronInteraction}s and | ||||
| @@ -366,6 +370,18 @@ public final class ModRegistry { | ||||
|         public static final RegistryEntry<ImpostorShapelessRecipe.Serializer> IMPOSTOR_SHAPELESS = REGISTRY.register("impostor_shapeless", ImpostorShapelessRecipe.Serializer::new); | ||||
|     } | ||||
| 
 | ||||
|     public static class Permissions { | ||||
|         static final PermissionRegistry REGISTRY = PermissionRegistry.create(); | ||||
| 
 | ||||
|         public static final Predicate<CommandSourceStack> PERMISSION_DUMP = REGISTRY.registerCommand("dump", UserLevel.OWNER_OP); | ||||
|         public static final Predicate<CommandSourceStack> PERMISSION_SHUTDOWN = REGISTRY.registerCommand("shutdown", UserLevel.OWNER_OP); | ||||
|         public static final Predicate<CommandSourceStack> PERMISSION_TURN_ON = REGISTRY.registerCommand("turn_on", UserLevel.OWNER_OP); | ||||
|         public static final Predicate<CommandSourceStack> PERMISSION_TP = REGISTRY.registerCommand("tp", UserLevel.OP); | ||||
|         public static final Predicate<CommandSourceStack> PERMISSION_TRACK = REGISTRY.registerCommand("track", UserLevel.OWNER_OP); | ||||
|         public static final Predicate<CommandSourceStack> PERMISSION_QUEUE = REGISTRY.registerCommand("queue", UserLevel.ANYONE); | ||||
|         public static final Predicate<CommandSourceStack> PERMISSION_VIEW = REGISTRY.registerCommand("view", UserLevel.OP); | ||||
|     } | ||||
| 
 | ||||
|     static class CreativeTabs { | ||||
|         static final RegistrationHelper<CreativeModeTab> REGISTRY = PlatformHelper.get().createRegistrationHelper(Registries.CREATIVE_MODE_TAB); | ||||
| 
 | ||||
| @@ -419,6 +435,7 @@ public final class ModRegistry { | ||||
|         ArgumentTypes.REGISTRY.register(); | ||||
|         LootItemConditionTypes.REGISTRY.register(); | ||||
|         RecipeSerializers.REGISTRY.register(); | ||||
|         Permissions.REGISTRY.register(); | ||||
|         CreativeTabs.REGISTRY.register(); | ||||
| 
 | ||||
|         // Register bundled power providers | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; | ||||
| import com.mojang.brigadier.suggestion.Suggestions; | ||||
| import dan200.computercraft.core.computer.ComputerSide; | ||||
| import dan200.computercraft.core.metrics.Metrics; | ||||
| import dan200.computercraft.shared.ModRegistry; | ||||
| import dan200.computercraft.shared.command.arguments.ComputersArgumentType; | ||||
| import dan200.computercraft.shared.command.text.TableBuilder; | ||||
| import dan200.computercraft.shared.computer.core.ComputerFamily; | ||||
| @@ -60,7 +61,7 @@ public final class CommandComputerCraft { | ||||
|     public static void register(CommandDispatcher<CommandSourceStack> dispatcher) { | ||||
|         dispatcher.register(choice("computercraft") | ||||
|             .then(literal("dump") | ||||
|                 .requires(UserLevel.OWNER_OP) | ||||
|                 .requires(ModRegistry.Permissions.PERMISSION_DUMP) | ||||
|                 .executes(context -> { | ||||
|                     var table = new TableBuilder("DumpAll", "Computer", "On", "Position"); | ||||
| 
 | ||||
| @@ -118,7 +119,7 @@ public final class CommandComputerCraft { | ||||
|                     }))) | ||||
| 
 | ||||
|             .then(command("shutdown") | ||||
|                 .requires(UserLevel.OWNER_OP) | ||||
|                 .requires(ModRegistry.Permissions.PERMISSION_SHUTDOWN) | ||||
|                 .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) | ||||
|                 .executes((context, computerSelectors) -> { | ||||
|                     var shutdown = 0; | ||||
| @@ -134,7 +135,7 @@ public final class CommandComputerCraft { | ||||
|                 })) | ||||
| 
 | ||||
|             .then(command("turn-on") | ||||
|                 .requires(UserLevel.OWNER_OP) | ||||
|                 .requires(ModRegistry.Permissions.PERMISSION_TURN_ON) | ||||
|                 .argManyValue("computers", manyComputers(), s -> ServerContext.get(s.getServer()).registry().getComputers()) | ||||
|                 .executes((context, computerSelectors) -> { | ||||
|                     var on = 0; | ||||
| @@ -150,7 +151,7 @@ public final class CommandComputerCraft { | ||||
|                 })) | ||||
| 
 | ||||
|             .then(command("tp") | ||||
|                 .requires(UserLevel.OP) | ||||
|                 .requires(ModRegistry.Permissions.PERMISSION_TP) | ||||
|                 .arg("computer", oneComputer()) | ||||
|                 .executes(context -> { | ||||
|                     var computer = getComputerArgument(context, "computer"); | ||||
| @@ -175,7 +176,7 @@ public final class CommandComputerCraft { | ||||
|                 })) | ||||
| 
 | ||||
|             .then(command("queue") | ||||
|                 .requires(UserLevel.ANYONE) | ||||
|                 .requires(ModRegistry.Permissions.PERMISSION_QUEUE) | ||||
|                 .arg( | ||||
|                     RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers()) | ||||
|                         .suggests((context, builder) -> Suggestions.empty()) | ||||
| @@ -197,7 +198,7 @@ public final class CommandComputerCraft { | ||||
|                 })) | ||||
| 
 | ||||
|             .then(command("view") | ||||
|                 .requires(UserLevel.OP) | ||||
|                 .requires(ModRegistry.Permissions.PERMISSION_VIEW) | ||||
|                 .arg("computer", oneComputer()) | ||||
|                 .executes(context -> { | ||||
|                     var player = context.getSource().getPlayerOrException(); | ||||
| @@ -217,8 +218,8 @@ public final class CommandComputerCraft { | ||||
|                 })) | ||||
| 
 | ||||
|             .then(choice("track") | ||||
|                 .requires(ModRegistry.Permissions.PERMISSION_TRACK) | ||||
|                 .then(command("start") | ||||
|                     .requires(UserLevel.OWNER_OP) | ||||
|                     .executes(context -> { | ||||
|                         getMetricsInstance(context.getSource()).start(); | ||||
| 
 | ||||
| @@ -231,7 +232,6 @@ public final class CommandComputerCraft { | ||||
|                     })) | ||||
| 
 | ||||
|                 .then(command("stop") | ||||
|                     .requires(UserLevel.OWNER_OP) | ||||
|                     .executes(context -> { | ||||
|                         var timings = getMetricsInstance(context.getSource()); | ||||
|                         if (!timings.stop()) throw NOT_TRACKING_EXCEPTION.create(); | ||||
| @@ -240,7 +240,6 @@ public final class CommandComputerCraft { | ||||
|                     })) | ||||
| 
 | ||||
|                 .then(command("dump") | ||||
|                     .requires(UserLevel.OWNER_OP) | ||||
|                     .argManyValue("fields", metric(), DEFAULT_FIELDS) | ||||
|                     .executes((context, fields) -> { | ||||
|                         AggregatedMetric sort; | ||||
| @@ -274,23 +273,25 @@ public final class CommandComputerCraft { | ||||
|         out.append(" (id " + computerId + ")"); | ||||
| 
 | ||||
|         // And, if we're a player, some useful links | ||||
|         if (serverComputer != null && UserLevel.OP.test(source) && isPlayer(source)) { | ||||
|             out | ||||
|                 .append(" ") | ||||
|                 .append(link( | ||||
|         if (serverComputer != null && isPlayer(source)) { | ||||
|             if (ModRegistry.Permissions.PERMISSION_TP.test(source)) { | ||||
|                 out.append(" ").append(link( | ||||
|                     text("\u261b"), | ||||
|                     "/computercraft tp " + serverComputer.getInstanceID(), | ||||
|                     Component.translatable("commands.computercraft.tp.action") | ||||
|                 )) | ||||
|                 .append(" ") | ||||
|                 .append(link( | ||||
|                 )); | ||||
|             } | ||||
| 
 | ||||
|             if (ModRegistry.Permissions.PERMISSION_VIEW.test(source)) { | ||||
|                 out.append(" ").append(link( | ||||
|                     text("\u20e2"), | ||||
|                     "/computercraft view " + serverComputer.getInstanceID(), | ||||
|                     Component.translatable("commands.computercraft.view.action") | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (UserLevel.OWNER.test(source) && isPlayer(source)) { | ||||
|         if (isPlayer(source) && UserLevel.isOwner(source)) { | ||||
|             var linkPath = linkStorage(source, computerId); | ||||
|             if (linkPath != null) out.append(" ").append(linkPath); | ||||
|         } | ||||
| @@ -299,7 +300,7 @@ public final class CommandComputerCraft { | ||||
|     } | ||||
| 
 | ||||
|     private static Component linkPosition(CommandSourceStack context, ServerComputer computer) { | ||||
|         if (UserLevel.OP.test(context)) { | ||||
|         if (ModRegistry.Permissions.PERMISSION_TP.test(context)) { | ||||
|             return link( | ||||
|                 position(computer.getPosition()), | ||||
|                 "/computercraft tp " + computer.getInstanceID(), | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; | ||||
| import dan200.computercraft.shared.platform.PlatformHelper; | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| import net.minecraft.commands.SharedSuggestionProvider; | ||||
| import net.minecraft.server.level.ServerPlayer; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.Locale; | ||||
| @@ -22,8 +21,8 @@ public final class CommandUtils { | ||||
|     } | ||||
| 
 | ||||
|     public static boolean isPlayer(CommandSourceStack output) { | ||||
|         var sender = output.getEntity(); | ||||
|         return sender instanceof ServerPlayer player && !PlatformHelper.get().isFakePlayer(player); | ||||
|         var player = output.getPlayer(); | ||||
|         return player != null && !PlatformHelper.get().isFakePlayer(player); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("unchecked") | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| package dan200.computercraft.shared.command; | ||||
| 
 | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| import net.minecraft.world.entity.player.Player; | ||||
| import net.minecraft.server.level.ServerPlayer; | ||||
| 
 | ||||
| import java.util.function.Predicate; | ||||
| 
 | ||||
| @@ -13,11 +13,6 @@ import java.util.function.Predicate; | ||||
|  * The level a user must be at in order to execute a command. | ||||
|  */ | ||||
| public enum UserLevel implements Predicate<CommandSourceStack> { | ||||
|     /** | ||||
|      * Only can be used by the owner of the server: namely the server console or the player in SSP. | ||||
|      */ | ||||
|     OWNER, | ||||
| 
 | ||||
|     /** | ||||
|      * Can only be used by ops. | ||||
|      */ | ||||
| @@ -35,7 +30,6 @@ public enum UserLevel implements Predicate<CommandSourceStack> { | ||||
| 
 | ||||
|     public int toLevel() { | ||||
|         return switch (this) { | ||||
|             case OWNER -> 4; | ||||
|             case OP, OWNER_OP -> 2; | ||||
|             case ANYONE -> 0; | ||||
|         }; | ||||
| @@ -44,39 +38,26 @@ public enum UserLevel implements Predicate<CommandSourceStack> { | ||||
|     @Override | ||||
|     public boolean test(CommandSourceStack source) { | ||||
|         if (this == ANYONE) return true; | ||||
|         if (this == OWNER) return isOwner(source); | ||||
|         if (this == OWNER_OP && isOwner(source)) return true; | ||||
|         return source.hasPermission(toLevel()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Take the union of two {@link UserLevel}s. | ||||
|      * <p> | ||||
|      * This satisfies the property that for all sources {@code s}, {@code a.test(s) || b.test(s) == (a ∪ b).test(s)}. | ||||
|      * | ||||
|      * @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; | ||||
|     public boolean test(ServerPlayer source) { | ||||
|         if (this == ANYONE) return true; | ||||
|         if (this == OWNER_OP && isOwner(source)) return true; | ||||
|         return source.hasPermissions(toLevel()); | ||||
|     } | ||||
| 
 | ||||
|     private static boolean isOwner(CommandSourceStack source) { | ||||
|     public static boolean isOwner(CommandSourceStack source) { | ||||
|         var server = source.getServer(); | ||||
|         var sender = source.getEntity(); | ||||
|         var player = source.getPlayer(); | ||||
|         return server.isDedicatedServer() | ||||
|             ? source.getEntity() == null && source.hasPermission(4) && source.getTextName().equals("Server") | ||||
|             : sender instanceof Player player && server.isSingleplayerOwner(player.getGameProfile()); | ||||
|             : player != null && server.isSingleplayerOwner(player.getGameProfile()); | ||||
|     } | ||||
| 
 | ||||
|     public static boolean isOwner(ServerPlayer player) { | ||||
|         var server = player.getServer(); | ||||
|         return server != null && server.isSingleplayerOwner(player.getGameProfile()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -44,7 +44,8 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> { | ||||
|     } | ||||
| 
 | ||||
|     public CommandBuilder<S> requires(Predicate<S> predicate) { | ||||
|         requires = requires == null ? predicate : requires.and(predicate); | ||||
|         if (requires != null) throw new IllegalStateException("Requires already set"); | ||||
|         requires = predicate; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; | ||||
| import com.mojang.brigadier.context.CommandContext; | ||||
| import com.mojang.brigadier.tree.CommandNode; | ||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | ||||
| import dan200.computercraft.shared.command.UserLevel; | ||||
| import net.minecraft.ChatFormatting; | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| 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> { | ||||
|     private final Collection<HelpingArgumentBuilder> children = new ArrayList<>(); | ||||
|     private @Nullable Predicate<CommandSourceStack> requirement; | ||||
| 
 | ||||
|     private HelpingArgumentBuilder(String literal) { | ||||
|         super(literal); | ||||
| @@ -41,26 +41,20 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public LiteralArgumentBuilder<CommandSourceStack> requires(Predicate<CommandSourceStack> requirement) { | ||||
|         throw new IllegalStateException("Cannot use requires on a HelpingArgumentBuilder"); | ||||
|     public HelpingArgumentBuilder requires(Predicate<CommandSourceStack> requirement) { | ||||
|         this.requirement = requirement; | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     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( | ||||
|             children.stream().map(ArgumentBuilder::getRequirement), | ||||
|             getArguments().stream().map(CommandNode::getRequirement) | ||||
|         ).toList(); | ||||
| 
 | ||||
|         // 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; | ||||
|         return x -> requirements.stream().anyMatch(y -> y.test(x)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -181,7 +175,7 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command | ||||
|             .append(Component.translatable("commands." + id + ".desc")); | ||||
| 
 | ||||
|         for (var child : node.getChildren()) { | ||||
|             if (!child.getRequirement().test(context.getSource()) || !(child instanceof LiteralCommandNode)) { | ||||
|             if (!child.canUse(context.getSource()) || !(child instanceof LiteralCommandNode)) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|   | ||||
| @@ -168,7 +168,7 @@ public class CommandAPI implements ILuaAPI { | ||||
|     /** | ||||
|      * Get information about a range of blocks. | ||||
|      * <p> | ||||
|      * This returns the same information as @{getBlockInfo}, just for multiple | ||||
|      * This returns the same information as [`getBlockInfo`], just for multiple | ||||
|      * blocks at once. | ||||
|      * <p> | ||||
|      * Blocks are traversed by ascending y level, followed by z and x - the returned | ||||
| @@ -225,7 +225,7 @@ public class CommandAPI implements ILuaAPI { | ||||
|      * Get some basic information about a block. | ||||
|      * <p> | ||||
|      * The returned table contains the current name, metadata and block state (as | ||||
|      * with @{turtle.inspect}). If there is a tile entity for that block, its NBT | ||||
|      * with [`turtle.inspect`]). If there is a tile entity for that block, its NBT | ||||
|      * will also be returned. | ||||
|      * | ||||
|      * @param x         The x position of the block to query. | ||||
|   | ||||
| @@ -5,18 +5,10 @@ | ||||
| package dan200.computercraft.shared.computer.terminal; | ||||
| 
 | ||||
| import io.netty.buffer.ByteBuf; | ||||
| import io.netty.buffer.ByteBufInputStream; | ||||
| import io.netty.buffer.ByteBufOutputStream; | ||||
| import io.netty.buffer.Unpooled; | ||||
| import net.minecraft.network.FriendlyByteBuf; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.OutputStream; | ||||
| import java.io.UncheckedIOException; | ||||
| import java.util.zip.GZIPInputStream; | ||||
| import java.util.zip.GZIPOutputStream; | ||||
| 
 | ||||
| /** | ||||
|  * A snapshot of a terminal's state. | ||||
| @@ -31,20 +23,10 @@ public class TerminalState { | ||||
|     public final int width; | ||||
|     public final int height; | ||||
| 
 | ||||
|     private final boolean compress; | ||||
| 
 | ||||
|     @Nullable | ||||
|     private final ByteBuf buffer; | ||||
| 
 | ||||
|     private @Nullable ByteBuf compressed; | ||||
| 
 | ||||
|     public TerminalState(@Nullable NetworkedTerminal terminal) { | ||||
|         this(terminal, true); | ||||
|     } | ||||
| 
 | ||||
|     public TerminalState(@Nullable NetworkedTerminal terminal, boolean compress) { | ||||
|         this.compress = compress; | ||||
| 
 | ||||
|         if (terminal == null) { | ||||
|             colour = false; | ||||
|             width = height = 0; | ||||
| @@ -61,14 +43,13 @@ public class TerminalState { | ||||
| 
 | ||||
|     public TerminalState(FriendlyByteBuf buf) { | ||||
|         colour = buf.readBoolean(); | ||||
|         compress = buf.readBoolean(); | ||||
| 
 | ||||
|         if (buf.readBoolean()) { | ||||
|             width = buf.readVarInt(); | ||||
|             height = buf.readVarInt(); | ||||
| 
 | ||||
|             var length = buf.readVarInt(); | ||||
|             buffer = readCompressed(buf, length, compress); | ||||
|             buffer = buf.readBytes(length); | ||||
|         } else { | ||||
|             width = height = 0; | ||||
|             buffer = null; | ||||
| @@ -77,16 +58,13 @@ public class TerminalState { | ||||
| 
 | ||||
|     public void write(FriendlyByteBuf buf) { | ||||
|         buf.writeBoolean(colour); | ||||
|         buf.writeBoolean(compress); | ||||
| 
 | ||||
|         buf.writeBoolean(buffer != null); | ||||
|         if (buffer != null) { | ||||
|             buf.writeVarInt(width); | ||||
|             buf.writeVarInt(height); | ||||
| 
 | ||||
|             var sendBuffer = getCompressed(); | ||||
|             buf.writeVarInt(sendBuffer.readableBytes()); | ||||
|             buf.writeBytes(sendBuffer, sendBuffer.readerIndex(), sendBuffer.readableBytes()); | ||||
|             buf.writeVarInt(buffer.readableBytes()); | ||||
|             buf.writeBytes(buffer, buffer.readerIndex(), buffer.readableBytes()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @@ -110,40 +88,4 @@ public class TerminalState { | ||||
|         terminal.read(new FriendlyByteBuf(buffer)); | ||||
|         return terminal; | ||||
|     } | ||||
| 
 | ||||
|     private ByteBuf getCompressed() { | ||||
|         if (buffer == null) throw new NullPointerException("buffer"); | ||||
|         if (!compress) return buffer; | ||||
|         if (compressed != null) return compressed; | ||||
| 
 | ||||
|         var compressed = Unpooled.buffer(); | ||||
|         try (OutputStream stream = new GZIPOutputStream(new ByteBufOutputStream(compressed))) { | ||||
|             stream.write(buffer.array(), buffer.arrayOffset(), buffer.readableBytes()); | ||||
|         } catch (IOException e) { | ||||
|             throw new UncheckedIOException(e); | ||||
|         } | ||||
| 
 | ||||
|         return this.compressed = compressed; | ||||
|     } | ||||
| 
 | ||||
|     private static ByteBuf readCompressed(ByteBuf buf, int length, boolean compress) { | ||||
|         if (compress) { | ||||
|             var buffer = Unpooled.buffer(); | ||||
|             try (InputStream stream = new GZIPInputStream(new ByteBufInputStream(buf, length))) { | ||||
|                 var swap = new byte[8192]; | ||||
|                 while (true) { | ||||
|                     var bytes = stream.read(swap); | ||||
|                     if (bytes == -1) break; | ||||
|                     buffer.writeBytes(swap, 0, bytes); | ||||
|                 } | ||||
|             } catch (IOException e) { | ||||
|                 throw new UncheckedIOException(e); | ||||
|             } | ||||
|             return buffer; | ||||
|         } else { | ||||
|             var buffer = Unpooled.buffer(length); | ||||
|             buf.readBytes(buffer, length); | ||||
|             return buffer; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,10 @@ class AddressRuleConfig { | ||||
| 
 | ||||
|     private static final AddressRule REJECT_ALL = AddressRule.parse("*", OptionalInt.empty(), Action.DENY.toPartial()); | ||||
| 
 | ||||
|     private static final Set<String> knownKeys = Set.of( | ||||
|         "host", "action", "max_download", "max_upload", "max_websocket_message", "use_proxy" | ||||
|     ); | ||||
| 
 | ||||
|     public static List<UnmodifiableConfig> defaultRules() { | ||||
|         return List.of( | ||||
|             makeRule(config -> { | ||||
| @@ -88,9 +92,20 @@ class AddressRuleConfig { | ||||
|         var port = unboxOptInt(get(builder, "port", Number.class)); | ||||
|         var maxUpload = unboxOptLong(get(builder, "max_upload", Number.class).map(Number::longValue)); | ||||
|         var maxDownload = unboxOptLong(get(builder, "max_download", Number.class).map(Number::longValue)); | ||||
|         var websocketMessage = unboxOptInt(get(builder, "websocket_message", Number.class).map(Number::intValue)); | ||||
|         var websocketMessage = unboxOptInt( | ||||
|             get(builder, "max_websocket_message", Number.class) | ||||
|                 // Fallback to (incorrect) websocket_message option. | ||||
|                 .or(() -> get(builder, "websocket_message", Number.class)) | ||||
|                 .map(Number::intValue) | ||||
|         ); | ||||
|         var useProxy = get(builder, "use_proxy", Boolean.class); | ||||
| 
 | ||||
|         // Find unknown keys and warn about them. | ||||
|         var unknownKeys = builder.entrySet().stream().map(UnmodifiableConfig.Entry::getKey).filter(x -> !knownKeys.contains(x)).toList(); | ||||
|         if (!unknownKeys.isEmpty()) { | ||||
|             LOG.warn("Unknown config {} {} in address rule.", unknownKeys.size() == 1 ? "option" : "options", String.join(", ", unknownKeys)); | ||||
|         } | ||||
| 
 | ||||
|         var options = new PartialOptions( | ||||
|             action, | ||||
|             maxUpload, | ||||
|   | ||||
| @@ -0,0 +1,79 @@ | ||||
| // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.integration; | ||||
| 
 | ||||
| import com.mojang.brigadier.builder.ArgumentBuilder; | ||||
| import dan200.computercraft.shared.command.CommandComputerCraft; | ||||
| import dan200.computercraft.shared.command.UserLevel; | ||||
| import dan200.computercraft.shared.platform.RegistrationHelper; | ||||
| import net.minecraft.commands.CommandSourceStack; | ||||
| 
 | ||||
| import javax.annotation.OverridingMethodsMustInvokeSuper; | ||||
| import java.util.Optional; | ||||
| import java.util.ServiceLoader; | ||||
| import java.util.function.Predicate; | ||||
| 
 | ||||
| /** | ||||
|  * A registry of nodes in a permission system. | ||||
|  * <p> | ||||
|  * This acts as an abstraction layer over permission systems such Forge's built-in permissions API, or Fabric's | ||||
|  * unofficial <a href="https://github.com/lucko/fabric-permissions-api">fabric-permissions-api-v0</a>. | ||||
|  * <p> | ||||
|  * This behaves similarly to {@link RegistrationHelper} (aka Forge's deferred registry), in that you {@linkplain #create() | ||||
|  * create a registry}, {@linkplain #registerCommand(String, UserLevel) add nodes to it} and then finally {@linkplain | ||||
|  * #register()} all created nodes. | ||||
|  * | ||||
|  * @see dan200.computercraft.shared.ModRegistry.Permissions | ||||
|  */ | ||||
| public abstract class PermissionRegistry { | ||||
|     private boolean frozen = false; | ||||
| 
 | ||||
|     /** | ||||
|      * Register a permission node for a command. The registered node should be of the form {@code "command." + command}. | ||||
|      * | ||||
|      * @param command  The name of the command. This should be one of the subcommands under the {@code /computercraft} | ||||
|      *                 subcommand, and not something general. | ||||
|      * @param fallback The default/fallback permission check. | ||||
|      * @return The resulting predicate which should be passed to {@link ArgumentBuilder#requires(Predicate)}. | ||||
|      * @see CommandComputerCraft | ||||
|      */ | ||||
|     public abstract Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback); | ||||
| 
 | ||||
|     /** | ||||
|      * Check that the registry has not been frozen (namely {@link #register()} has been called). This should be called | ||||
|      * before registering each node. | ||||
|      */ | ||||
|     protected void checkNotFrozen() { | ||||
|         if (frozen) throw new IllegalStateException("Permission registry has been frozen."); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Freeze the permissions registry and register the underlying nodes. | ||||
|      */ | ||||
|     @OverridingMethodsMustInvokeSuper | ||||
|     public void register() { | ||||
|         frozen = true; | ||||
|     } | ||||
| 
 | ||||
|     public interface Provider { | ||||
|         Optional<PermissionRegistry> get(); | ||||
|     } | ||||
| 
 | ||||
|     public static PermissionRegistry create() { | ||||
|         return ServiceLoader.load(Provider.class) | ||||
|             .stream() | ||||
|             .flatMap(x -> x.get().get().stream()) | ||||
|             .findFirst() | ||||
|             .orElseGet(DefaultPermissionRegistry::new); | ||||
|     } | ||||
| 
 | ||||
|     private static class DefaultPermissionRegistry extends PermissionRegistry { | ||||
|         @Override | ||||
|         public Predicate<CommandSourceStack> registerCommand(String command, UserLevel fallback) { | ||||
|             checkNotFrozen(); | ||||
|             return fallback; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -6,13 +6,11 @@ package dan200.computercraft.shared.network.client; | ||||
| 
 | ||||
| import dan200.computercraft.shared.network.NetworkMessage; | ||||
| import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity; | ||||
| import dan200.computercraft.shared.platform.RegistryWrappers; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.network.FriendlyByteBuf; | ||||
| import net.minecraft.sounds.SoundEvent; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.function.BiConsumer; | ||||
| 
 | ||||
| /** | ||||
|  * Starts or stops a record on the client, depending on if {@link #soundEvent} is {@code null}. | ||||
| @@ -40,28 +38,19 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont | ||||
| 
 | ||||
|     public PlayRecordClientMessage(FriendlyByteBuf buf) { | ||||
|         pos = buf.readBlockPos(); | ||||
|         soundEvent = buf.readBoolean() ? RegistryWrappers.readKey(buf, RegistryWrappers.SOUND_EVENTS) : null; | ||||
|         name = buf.readBoolean() ? buf.readUtf(Short.MAX_VALUE) : null; | ||||
|         soundEvent = buf.readNullable(SoundEvent::readFromNetwork); | ||||
|         name = buf.readNullable(FriendlyByteBuf::readUtf); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void toBytes(FriendlyByteBuf buf) { | ||||
|         buf.writeBlockPos(pos); | ||||
|         writeOptional(buf, soundEvent, (b, e) -> RegistryWrappers.writeKey(b, RegistryWrappers.SOUND_EVENTS, e)); | ||||
|         writeOptional(buf, name, FriendlyByteBuf::writeUtf); | ||||
|         buf.writeNullable(soundEvent, (b, e) -> e.writeToNetwork(b)); | ||||
|         buf.writeNullable(name, FriendlyByteBuf::writeUtf); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handle(ClientNetworkContext context) { | ||||
|         context.handlePlayRecord(pos, soundEvent, name); | ||||
|     } | ||||
| 
 | ||||
|     private static <T> void writeOptional(FriendlyByteBuf out, @Nullable T object, BiConsumer<FriendlyByteBuf, T> write) { | ||||
|         if (object == null) { | ||||
|             out.writeBoolean(false); | ||||
|         } else { | ||||
|             out.writeBoolean(true); | ||||
|             write.accept(out, object); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| 
 | ||||
| package dan200.computercraft.shared.peripheral.modem; | ||||
| 
 | ||||
| import com.google.errorprone.annotations.concurrent.GuardedBy; | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| import dan200.computercraft.api.lua.LuaFunction; | ||||
| import dan200.computercraft.api.network.Packet; | ||||
| @@ -20,10 +21,9 @@ import java.util.Set; | ||||
| /** | ||||
|  * Modems allow you to send messages between computers over long distances. | ||||
|  * <p> | ||||
|  * :::tip | ||||
|  * Modems provide a fairly basic set of methods, which makes them very flexible but often hard to work with. The | ||||
|  * {@literal @}{rednet} API is built on top of modems, and provides a more user-friendly interface. | ||||
|  * ::: | ||||
|  * > [!TIP] | ||||
|  * > Modems provide a fairly basic set of methods, which makes them very flexible but often hard to work with. The | ||||
|  * > [`rednet`] API is built on top of modems, and provides a more user-friendly interface. | ||||
|  * <p> | ||||
|  * ## Sending and receiving messages | ||||
|  * Modems operate on a series of channels, a bit like frequencies on a radio. Any modem can send a message on a | ||||
| @@ -31,11 +31,11 @@ import java.util.Set; | ||||
|  * messages. | ||||
|  * <p> | ||||
|  * Channels are represented as an integer between 0 and 65535 inclusive. These channels don't have any defined meaning, | ||||
|  * though some APIs or programs will assign a meaning to them. For instance, the @{gps} module sends all its messages on | ||||
|  * channel 65534 (@{gps.CHANNEL_GPS}), while @{rednet} uses channels equal to the computer's ID. | ||||
|  * though some APIs or programs will assign a meaning to them. For instance, the [`gps`] module sends all its messages on | ||||
|  * channel 65534 ([`gps.CHANNEL_GPS`]), while [`rednet`] uses channels equal to the computer's ID. | ||||
|  * <p> | ||||
|  * - Sending messages is done with the {@link #transmit(int, int, Object)} message. | ||||
|  * - Receiving messages is done by listening to the @{modem_message} event. | ||||
|  * - Receiving messages is done by listening to the [`modem_message`] event. | ||||
|  * <p> | ||||
|  * ## Types of modem | ||||
|  * CC: Tweaked comes with three kinds of modem, with different capabilities. | ||||
| @@ -85,7 +85,7 @@ import java.util.Set; | ||||
|  */ | ||||
| public abstract class ModemPeripheral implements IPeripheral, PacketSender, PacketReceiver { | ||||
|     private @Nullable PacketNetwork network; | ||||
|     private final Set<IComputerAccess> computers = new HashSet<>(1); | ||||
|     private final @GuardedBy("computers") Set<IComputerAccess> computers = new HashSet<>(1); | ||||
|     private final ModemState state; | ||||
| 
 | ||||
|     protected ModemPeripheral(ModemState state) { | ||||
| @@ -197,9 +197,8 @@ public abstract class ModemPeripheral implements IPeripheral, PacketSender, Pack | ||||
|      * Sends a modem message on a certain channel. Modems listening on the channel will queue a {@code modem_message} | ||||
|      * event on adjacent computers. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * The channel does not need be open to send a message. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > The channel does not need be open to send a message. | ||||
|      * | ||||
|      * @param channel      The channel to send messages on. | ||||
|      * @param replyChannel The channel that responses to this message should be sent on. This can be the same as | ||||
|   | ||||
| @@ -80,9 +80,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi | ||||
|      * If this computer is attached to the network, it _will not_ be included in | ||||
|      * this list. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * | ||||
|      * @param computer The calling computer. | ||||
|      * @return Remote peripheral names on the network. | ||||
| @@ -96,9 +95,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi | ||||
|     /** | ||||
|      * Determine if a peripheral is available on this wired network. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * | ||||
|      * @param computer The calling computer. | ||||
|      * @param name     The peripheral's name. | ||||
| @@ -113,9 +111,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi | ||||
|     /** | ||||
|      * Get the type of a peripheral is available on this wired network. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * | ||||
|      * @param computer The calling computer. | ||||
|      * @param name     The peripheral's name. | ||||
| @@ -133,9 +130,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi | ||||
|     /** | ||||
|      * Check a peripheral is of a particular type. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * | ||||
|      * @param computer The calling computer. | ||||
|      * @param name     The peripheral's name. | ||||
| @@ -154,9 +150,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi | ||||
|     /** | ||||
|      * Get all available methods for the remote peripheral with the given name. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * | ||||
|      * @param computer The calling computer. | ||||
|      * @param name     The peripheral's name. | ||||
| @@ -175,9 +170,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi | ||||
|     /** | ||||
|      * Call a method on a peripheral on this wired network. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * | ||||
|      * @param computer  The calling computer. | ||||
|      * @param context   The Lua context we're executing in. | ||||
| @@ -205,9 +199,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi | ||||
|      * may be used by other computers on the network to wrap this computer as a | ||||
|      * peripheral. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > This function only appears on wired modems. Check {@link #isWireless} returns false before calling it. | ||||
|      * | ||||
|      * @return The current computer's name. | ||||
|      * @cc.treturn string|nil The current computer's name on the wired network. | ||||
|   | ||||
| @@ -25,8 +25,9 @@ import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.HashSet; | ||||
| import java.util.Collections; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.function.Consumer; | ||||
| 
 | ||||
| public class MonitorBlockEntity extends BlockEntity { | ||||
| @@ -46,7 +47,7 @@ public class MonitorBlockEntity extends BlockEntity { | ||||
|     private @Nullable ServerMonitor serverMonitor; | ||||
|     private @Nullable ClientMonitor clientMonitor; | ||||
|     private @Nullable MonitorPeripheral peripheral; | ||||
|     private final Set<IComputerAccess> computers = new HashSet<>(); | ||||
|     private final Set<IComputerAccess> computers = Collections.newSetFromMap(new ConcurrentHashMap<>()); | ||||
| 
 | ||||
|     private boolean needsUpdate = false; | ||||
|     private boolean needsValidating = false; | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import javax.annotation.Nullable; | ||||
|  * Monitors are a block which act as a terminal, displaying information on one side. This allows them to be read and | ||||
|  * interacted with in-world without opening a GUI. | ||||
|  * <p> | ||||
|  * Monitors act as @{term.Redirect|terminal redirects} and so expose the same methods, as well as several additional | ||||
|  * Monitors act as [terminal redirects][`term.Redirect`] and so expose the same methods, as well as several additional | ||||
|  * ones, which are documented below. | ||||
|  * <p> | ||||
|  * Like computers, monitors come in both normal (no colour) and advanced (colour) varieties. | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| 
 | ||||
| package dan200.computercraft.shared.peripheral.speaker; | ||||
| 
 | ||||
| import com.google.errorprone.annotations.concurrent.GuardedBy; | ||||
| import dan200.computercraft.api.lua.ILuaContext; | ||||
| import dan200.computercraft.api.lua.LuaException; | ||||
| import dan200.computercraft.api.lua.LuaFunction; | ||||
| @@ -57,7 +58,7 @@ public abstract class SpeakerPeripheral implements IPeripheral { | ||||
|     public static final int SAMPLE_RATE = 48000; | ||||
| 
 | ||||
|     private final UUID source = UUID.randomUUID(); | ||||
|     private final Set<IComputerAccess> computers = new HashSet<>(); | ||||
|     private final @GuardedBy("computers") Set<IComputerAccess> computers = new HashSet<>(); | ||||
| 
 | ||||
|     private long clock = 0; | ||||
|     private long lastPositionTime; | ||||
| @@ -271,16 +272,15 @@ public abstract class SpeakerPeripheral implements IPeripheral { | ||||
|      * <p> | ||||
|      * This accepts a list of audio samples as amplitudes between -128 and 127. These are stored in an internal buffer | ||||
|      * and played back at 48kHz. If this buffer is full, this function will return {@literal false}. You should wait for | ||||
|      * a @{speaker_audio_empty} event before trying again. | ||||
|      * a [`speaker_audio_empty`] event before trying again. | ||||
|      * <p> | ||||
|      * :::note | ||||
|      * The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small | ||||
|      * number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible | ||||
|      * (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or | ||||
|      * computer is lagging. | ||||
|      * ::: | ||||
|      * > [!NOTE] | ||||
|      * > The speaker only buffers a single call to {@link #playAudio} at once. This means if you try to play a small | ||||
|      * > number of samples, you'll have a lot of stutter. You should try to play as many samples in one call as possible | ||||
|      * > (up to 128×1024), as this reduces the chances of audio stuttering or halting, especially when the server or | ||||
|      * > computer is lagging. | ||||
|      * <p> | ||||
|      * {@literal @}{speaker_audio} provides a more complete guide to using speakers | ||||
|      * [`speaker_audio`] provides a more complete guide to using speakers | ||||
|      * | ||||
|      * @param context The Lua context. | ||||
|      * @param audio   The audio data to play. | ||||
| @@ -291,7 +291,7 @@ public abstract class SpeakerPeripheral implements IPeripheral { | ||||
|      * @cc.tparam [opt] number volume The volume to play this audio at. If not given, defaults to the previous volume | ||||
|      * given to {@link #playAudio}. | ||||
|      * @cc.since 1.100 | ||||
|      * @cc.usage Read an audio file, decode it using @{cc.audio.dfpwm}, and play it using the speaker. | ||||
|      * @cc.usage Read an audio file, decode it using [`cc.audio.dfpwm`], and play it using the speaker. | ||||
|      * | ||||
|      * <pre data-peripheral="speaker">{@code | ||||
|      * local dfpwm = require("cc.audio.dfpwm") | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import net.minecraft.core.Registry; | ||||
| import net.minecraft.core.registries.Registries; | ||||
| import net.minecraft.network.FriendlyByteBuf; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| import net.minecraft.sounds.SoundEvent; | ||||
| import net.minecraft.world.inventory.MenuType; | ||||
| import net.minecraft.world.item.Item; | ||||
| import net.minecraft.world.item.crafting.RecipeSerializer; | ||||
| @@ -33,7 +32,6 @@ public final class RegistryWrappers { | ||||
|     public static final RegistryWrapper<Fluid> FLUIDS = PlatformHelper.get().wrap(Registries.FLUID); | ||||
|     public static final RegistryWrapper<Enchantment> ENCHANTMENTS = PlatformHelper.get().wrap(Registries.ENCHANTMENT); | ||||
|     public static final RegistryWrapper<ArgumentTypeInfo<?, ?>> COMMAND_ARGUMENT_TYPES = PlatformHelper.get().wrap(Registries.COMMAND_ARGUMENT_TYPE); | ||||
|     public static final RegistryWrapper<SoundEvent> SOUND_EVENTS = PlatformHelper.get().wrap(Registries.SOUND_EVENT); | ||||
|     public static final RegistryWrapper<RecipeSerializer<?>> RECIPE_SERIALIZERS = PlatformHelper.get().wrap(Registries.RECIPE_SERIALIZER); | ||||
|     public static final RegistryWrapper<MenuType<?>> MENU = PlatformHelper.get().wrap(Registries.MENU); | ||||
| 
 | ||||
|   | ||||
| @@ -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 | ||||
|  * grid, moving a single block at a time. | ||||
|  * <p> | ||||
|  * {@literal @}{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 | ||||
|  * to turn the turtle using @{turtle.turnLeft}/@{turtle.turnRight} and then move forward or backwards. | ||||
|  * [`turtle.forward`] and [`turtle.back`] move the turtle in the direction it is facing, while [`turtle.up`] and | ||||
|  * [`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. | ||||
|  * <p> | ||||
|  * :::info | ||||
|  * The name "turtle" comes from [Turtle graphics], which originated from the Logo programming language. Here you'd move | ||||
|  * a turtle with various commands like "move 10" and "turn left", much like ComputerCraft's turtles! | ||||
|  * ::: | ||||
|  * > [!INFO] | ||||
|  * > The name "turtle" comes from [Turtle graphics], which originated from the Logo programming language. Here you'd | ||||
|  * > move a turtle with various commands like "move 10" and "turn left", much like ComputerCraft's turtles! | ||||
|  * <p> | ||||
|  * Moving a turtle (though not turning it) consumes *fuel*. If a turtle does not have any @{turtle.refuel|fuel}, it | ||||
|  * won't move, and the movement functions will return @{false}. If your turtle isn't going anywhere, the first thing to | ||||
|  * 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 | ||||
|  * check is if you've fuelled your turtle. | ||||
|  * <p> | ||||
|  * :::tip Handling errors | ||||
|  * Many turtle functions can fail in various ways. For instance, a turtle cannot move forward if there's already a block | ||||
|  * there. Instead of erroring, functions which can fail either return @{true} if they succeed, or @{false} and 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 | ||||
|  * functions, or wrap them in @{assert} (for instance, use `assert(turtle.forward())` rather than `turtle.forward()`), | ||||
|  * so the program doesn't misbehave. | ||||
|  * ::: | ||||
|  * > [Handling errors][!TIP] | ||||
|  * > Many turtle functions can fail in various ways. For instance, a turtle cannot move forward if there's already a | ||||
|  * > block there. Instead of erroring, functions which can fail either return [`true`] if they succeed, or [`false`] and | ||||
|  * > some error message if they fail. | ||||
|  * > | ||||
|  * > 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()`), | ||||
|  * > so the program doesn't misbehave. | ||||
|  * <p> | ||||
|  * ## Turtle upgrades | ||||
|  * 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 | ||||
|  * sides. Upgrades can be equipped by crafting a turtle with the upgrade, or calling the @{turtle.equipLeft}/@{turtle.equipRight} | ||||
|  * 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`] | ||||
|  * functions. | ||||
|  * <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 | ||||
|  * damage. Other tools have more niche use-cases, for instance hoes can til dirt. | ||||
|  * <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. | ||||
|  * <p> | ||||
|  * [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. | ||||
|      * | ||||
|      * @param count The number of items to drop. If not given, the entire stack will be dropped. | ||||
|   | ||||
| @@ -55,7 +55,7 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl | ||||
|         var allowsEnchantments = buffer.readBoolean(); | ||||
|         var consumesDurability = buffer.readEnum(TurtleToolDurability.class); | ||||
| 
 | ||||
|         var breakable = buffer.readBoolean() ? TagKey.create(Registries.BLOCK, buffer.readResourceLocation()) : null; | ||||
|         var breakable = buffer.readNullable(b -> TagKey.create(Registries.BLOCK, b.readResourceLocation())); | ||||
|         return new TurtleTool(id, adjective, craftingItem, toolItem, damageMultiplier, allowsEnchantments, consumesDurability, breakable); | ||||
|     } | ||||
| 
 | ||||
| @@ -67,7 +67,6 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl | ||||
|         buffer.writeFloat(upgrade.damageMulitiplier); | ||||
|         buffer.writeBoolean(upgrade.allowEnchantments); | ||||
|         buffer.writeEnum(upgrade.consumeDurability); | ||||
|         buffer.writeBoolean(upgrade.breakable != null); | ||||
|         if (upgrade.breakable != null) buffer.writeResourceLocation(upgrade.breakable.location()); | ||||
|         buffer.writeNullable(upgrade.breakable, (b, x) -> b.writeResourceLocation(x.location())); | ||||
|     } | ||||
| } | ||||
|   | ||||
| After Width: | Height: | Size: 334 B | 
| After Width: | Height: | Size: 294 B | 
| After Width: | Height: | Size: 300 B | 
| After Width: | Height: | Size: 299 B | 
| Before Width: | Height: | Size: 303 B | 
| After Width: | Height: | Size: 145 B | 
| After Width: | Height: | Size: 144 B | 
| After Width: | Height: | Size: 145 B | 
| After Width: | Height: | Size: 145 B | 
| After Width: | Height: | Size: 146 B | 
| After Width: | Height: | Size: 146 B | 
| Before Width: | Height: | Size: 405 B | 
| Before Width: | Height: | Size: 352 B | 
| Before Width: | Height: | Size: 345 B | 
| Before Width: | Height: | Size: 399 B | 
| After Width: | Height: | Size: 223 B | 
| After Width: | Height: | Size: 211 B | 
| After Width: | Height: | Size: 216 B | 
| After Width: | Height: | Size: 148 B | 
| After Width: | Height: | Size: 141 B | 
| After Width: | Height: | Size: 141 B | 
| @@ -21,6 +21,7 @@ import net.minecraft.commands.synchronization.ArgumentTypeInfo; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.core.Direction; | ||||
| import net.minecraft.core.Registry; | ||||
| import net.minecraft.core.registries.BuiltInRegistries; | ||||
| import net.minecraft.network.FriendlyByteBuf; | ||||
| import net.minecraft.resources.ResourceKey; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| @@ -51,6 +52,7 @@ import net.minecraft.world.phys.Vec3; | ||||
| 
 | ||||
| import javax.annotation.Nullable; | ||||
| import java.util.Collection; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.function.BiFunction; | ||||
| import java.util.function.Consumer; | ||||
| @@ -69,30 +71,41 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat | ||||
|         throw new UnsupportedOperationException("Cannot create config file inside tests"); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <T> RegistryWrappers.RegistryWrapper<T> wrap(ResourceKey<Registry<T>> registry) { | ||||
|         throw new UnsupportedOperationException("Cannot query registry inside tests"); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <T> RegistrationHelper<T> createRegistrationHelper(ResourceKey<Registry<T>> registry) { | ||||
|         throw new UnsupportedOperationException("Cannot query registry inside tests"); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <K> ResourceLocation getRegistryKey(ResourceKey<Registry<K>> registry, K object) { | ||||
|         throw new UnsupportedOperationException("Cannot query registry inside tests"); | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private static <T> Registry<T> getRegistry(ResourceKey<Registry<T>> id) { | ||||
|         var registry = (Registry<T>) BuiltInRegistries.REGISTRY.get(id.location()); | ||||
|         if (registry == null) throw new IllegalArgumentException("Unknown registry " + id); | ||||
|         return registry; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <K> K getRegistryObject(ResourceKey<Registry<K>> registry, ResourceLocation id) { | ||||
|         throw new UnsupportedOperationException("Cannot query registry inside tests"); | ||||
|     public <T> ResourceLocation getRegistryKey(ResourceKey<Registry<T>> registry, T object) { | ||||
|         var key = getRegistry(registry).getKey(object); | ||||
|         if (key == null) throw new IllegalArgumentException(object + " was not registered in " + registry); | ||||
|         return key; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <T> T getRegistryObject(ResourceKey<Registry<T>> registry, ResourceLocation id) { | ||||
|         var value = getRegistry(registry).get(id); | ||||
|         if (value == null) throw new IllegalArgumentException(id + " was not registered in " + registry); | ||||
|         return value; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public <T> RegistryWrappers.RegistryWrapper<T> wrap(ResourceKey<Registry<T>> registry) { | ||||
|         return new RegistryWrapperImpl<>(registry.location(), getRegistry(registry)); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     @Override | ||||
|     public <T> T tryGetRegistryObject(ResourceKey<Registry<T>> registry, ResourceLocation id) { | ||||
|         throw new UnsupportedOperationException("Cannot query registries"); | ||||
|         return getRegistry(registry).get(id); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -245,4 +258,48 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat | ||||
|     public String getInstalledVersion() { | ||||
|         return "1.0"; | ||||
|     } | ||||
| 
 | ||||
|     private record RegistryWrapperImpl<T>( | ||||
|         ResourceLocation name, Registry<T> registry | ||||
|     ) implements RegistryWrappers.RegistryWrapper<T> { | ||||
|         @Override | ||||
|         public int getId(T object) { | ||||
|             return registry.getId(object); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public ResourceLocation getKey(T object) { | ||||
|             var key = registry.getKey(object); | ||||
|             if (key == null) throw new IllegalArgumentException(object + " was not registered in " + name); | ||||
|             return key; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public T get(ResourceLocation location) { | ||||
|             var object = registry.get(location); | ||||
|             if (object == null) throw new IllegalArgumentException(location + " was not registered in " + name); | ||||
|             return object; | ||||
|         } | ||||
| 
 | ||||
|         @Nullable | ||||
|         @Override | ||||
|         public T tryGet(ResourceLocation location) { | ||||
|             return registry.get(location); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public @Nullable T byId(int id) { | ||||
|             return registry.byId(id); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int size() { | ||||
|             return registry.size(); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public Iterator<T> iterator() { | ||||
|             return registry.iterator(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -18,22 +18,11 @@ import static org.junit.jupiter.api.Assertions.*; | ||||
|  */ | ||||
| public class TerminalStateTest { | ||||
|     @RepeatedTest(5) | ||||
|     public void testCompressed() { | ||||
|     public void testRoundTrip() { | ||||
|         var terminal = randomTerminal(); | ||||
| 
 | ||||
|         var buffer = new FriendlyByteBuf(Unpooled.directBuffer()); | ||||
|         new TerminalState(terminal, true).write(buffer); | ||||
| 
 | ||||
|         checkEqual(terminal, read(buffer)); | ||||
|         assertEquals(0, buffer.readableBytes()); | ||||
|     } | ||||
| 
 | ||||
|     @RepeatedTest(5) | ||||
|     public void testUncompressed() { | ||||
|         var terminal = randomTerminal(); | ||||
| 
 | ||||
|         var buffer = new FriendlyByteBuf(Unpooled.directBuffer()); | ||||
|         new TerminalState(terminal, false).write(buffer); | ||||
|         new TerminalState(terminal).write(buffer); | ||||
| 
 | ||||
|         checkEqual(terminal, read(buffer)); | ||||
|         assertEquals(0, buffer.readableBytes()); | ||||
|   | ||||
| @@ -0,0 +1,58 @@ | ||||
| // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.network.client; | ||||
| 
 | ||||
| import dan200.computercraft.test.core.StructuralEquality; | ||||
| import dan200.computercraft.test.shared.MinecraftArbitraries; | ||||
| import dan200.computercraft.test.shared.WithMinecraft; | ||||
| import io.netty.buffer.Unpooled; | ||||
| import net.jqwik.api.*; | ||||
| import net.minecraft.network.FriendlyByteBuf; | ||||
| import net.minecraft.sounds.SoundEvent; | ||||
| 
 | ||||
| import static org.hamcrest.MatcherAssert.assertThat; | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| 
 | ||||
| @WithMinecraft | ||||
| class PlayRecordClientMessageTest { | ||||
|     static { | ||||
|         WithMinecraft.Setup.bootstrap(); // @Property doesn't run test lifecycle methods. | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sends packets on a roundtrip, ensuring that their contents are reassembled on the other end. | ||||
|      * | ||||
|      * @param message The message to send. | ||||
|      */ | ||||
|     @Property | ||||
|     public void testRoundTrip(@ForAll("message") PlayRecordClientMessage message) { | ||||
|         var buffer = new FriendlyByteBuf(Unpooled.directBuffer()); | ||||
|         message.toBytes(buffer); | ||||
| 
 | ||||
|         var converted = new PlayRecordClientMessage(buffer); | ||||
|         assertEquals(buffer.readableBytes(), 0, "Whole packet was read"); | ||||
| 
 | ||||
|         assertThat("Messages are equal", converted, equality.asMatcher(PlayRecordClientMessage.class, message)); | ||||
|     } | ||||
| 
 | ||||
|     @Provide | ||||
|     Arbitrary<PlayRecordClientMessage> message() { | ||||
|         return Combinators.combine( | ||||
|             MinecraftArbitraries.blockPos(), | ||||
|             MinecraftArbitraries.soundEvent().injectNull(0.3), | ||||
|             Arbitraries.strings().ofMaxLength(1000).injectNull(0.3) | ||||
|         ).as(PlayRecordClientMessage::new); | ||||
|     } | ||||
| 
 | ||||
|     private static final StructuralEquality<PlayRecordClientMessage> equality = StructuralEquality.all( | ||||
|         StructuralEquality.field(PlayRecordClientMessage.class, "pos"), | ||||
|         StructuralEquality.field(PlayRecordClientMessage.class, "name"), | ||||
|         StructuralEquality.field(PlayRecordClientMessage.class, "soundEvent", StructuralEquality.all( | ||||
|             StructuralEquality.at("location", SoundEvent::getLocation), | ||||
|             StructuralEquality.field(SoundEvent.class, "range"), | ||||
|             StructuralEquality.field(SoundEvent.class, "newSystem") | ||||
|         )) | ||||
|     ); | ||||
| } | ||||
| @@ -0,0 +1,82 @@ | ||||
| // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.shared.turtle.upgrades; | ||||
| 
 | ||||
| import dan200.computercraft.api.turtle.ITurtleUpgrade; | ||||
| import dan200.computercraft.api.turtle.TurtleToolDurability; | ||||
| import dan200.computercraft.test.core.StructuralEquality; | ||||
| import dan200.computercraft.test.shared.MinecraftArbitraries; | ||||
| import dan200.computercraft.test.shared.WithMinecraft; | ||||
| import io.netty.buffer.Unpooled; | ||||
| import net.jqwik.api.*; | ||||
| import net.minecraft.core.registries.Registries; | ||||
| import net.minecraft.network.FriendlyByteBuf; | ||||
| import net.minecraft.world.item.ItemStack; | ||||
| import org.hamcrest.Description; | ||||
| 
 | ||||
| import static org.hamcrest.MatcherAssert.assertThat; | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| 
 | ||||
| @WithMinecraft | ||||
| class TurtleToolSerialiserTest { | ||||
|     static { | ||||
|         WithMinecraft.Setup.bootstrap(); // @Property doesn't run test lifecycle methods. | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sends turtle tools on a roundtrip, ensuring that their contents are reassembled on the other end. | ||||
|      * | ||||
|      * @param tool The message to send. | ||||
|      */ | ||||
|     @Property | ||||
|     public void testRoundTrip(@ForAll("tool") TurtleTool tool) { | ||||
|         var buffer = new FriendlyByteBuf(Unpooled.directBuffer()); | ||||
|         TurtleToolSerialiser.INSTANCE.toNetwork(buffer, tool); | ||||
| 
 | ||||
|         var converted = TurtleToolSerialiser.INSTANCE.fromNetwork(tool.getUpgradeID(), buffer); | ||||
|         assertEquals(buffer.readableBytes(), 0, "Whole packet was read"); | ||||
| 
 | ||||
|         if (!equality.equals(tool, converted)) { | ||||
|             System.out.println("Break"); | ||||
|         } | ||||
|         assertThat("Messages are equal", converted, equality.asMatcher(TurtleTool.class, tool)); | ||||
|     } | ||||
| 
 | ||||
|     @Provide | ||||
|     Arbitrary<TurtleTool> tool() { | ||||
|         return Combinators.combine( | ||||
|             MinecraftArbitraries.resourceLocation(), | ||||
|             Arbitraries.strings().ofMaxLength(100), | ||||
|             MinecraftArbitraries.item(), | ||||
|             MinecraftArbitraries.itemStack(), | ||||
|             Arbitraries.floats(), | ||||
|             Arbitraries.of(true, false), | ||||
|             Arbitraries.of(TurtleToolDurability.values()), | ||||
|             MinecraftArbitraries.tagKey(Registries.BLOCK) | ||||
|         ).as(TurtleTool::new); | ||||
|     } | ||||
| 
 | ||||
|     private static final StructuralEquality<ItemStack> stackEquality = new StructuralEquality<>() { | ||||
|         @Override | ||||
|         public boolean equals(ItemStack left, ItemStack right) { | ||||
|             return ItemStack.isSameItemSameTags(left, right) && left.getCount() == right.getCount(); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void describe(Description description, ItemStack object) { | ||||
|             description.appendValue(object).appendValue(object.getTag()); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     private static final StructuralEquality<TurtleTool> equality = StructuralEquality.all( | ||||
|         StructuralEquality.at("id", ITurtleUpgrade::getUpgradeID), | ||||
|         StructuralEquality.at("craftingItem", ITurtleUpgrade::getCraftingItem, stackEquality), | ||||
|         StructuralEquality.at("tool", x -> x.item, stackEquality), | ||||
|         StructuralEquality.at("damageMulitiplier", x -> x.damageMulitiplier), | ||||
|         StructuralEquality.at("allowEnchantments", x -> x.allowEnchantments), | ||||
|         StructuralEquality.at("consumeDurability", x -> x.consumeDurability), | ||||
|         StructuralEquality.at("breakable", x -> x.breakable) | ||||
|     ); | ||||
| } | ||||
| @@ -0,0 +1,67 @@ | ||||
| // SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers | ||||
| // | ||||
| // SPDX-License-Identifier: MPL-2.0 | ||||
| 
 | ||||
| package dan200.computercraft.test.shared; | ||||
| 
 | ||||
| import dan200.computercraft.shared.platform.RegistryWrappers; | ||||
| import net.jqwik.api.Arbitraries; | ||||
| import net.jqwik.api.Arbitrary; | ||||
| import net.jqwik.api.Combinators; | ||||
| import net.minecraft.core.BlockPos; | ||||
| import net.minecraft.core.Registry; | ||||
| import net.minecraft.resources.ResourceKey; | ||||
| import net.minecraft.resources.ResourceLocation; | ||||
| import net.minecraft.sounds.SoundEvent; | ||||
| import net.minecraft.tags.TagKey; | ||||
| import net.minecraft.world.item.Item; | ||||
| import net.minecraft.world.item.ItemStack; | ||||
| import net.minecraft.world.item.Items; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * {@link Arbitrary} implementations for Minecraft types. | ||||
|  */ | ||||
| public final class MinecraftArbitraries { | ||||
|     public static <T> Arbitrary<T> ofRegistry(RegistryWrappers.RegistryWrapper<T> registry) { | ||||
|         return Arbitraries.of(registry.stream().toList()); | ||||
|     } | ||||
| 
 | ||||
|     public static <T> Arbitrary<TagKey<T>> tagKey(ResourceKey<? extends Registry<T>> registry) { | ||||
|         return resourceLocation().map(x -> TagKey.create(registry, x)); | ||||
|     } | ||||
| 
 | ||||
|     public static Arbitrary<Item> item() { | ||||
|         return ofRegistry(RegistryWrappers.ITEMS); | ||||
|     } | ||||
| 
 | ||||
|     public static Arbitrary<ItemStack> nonEmptyItemStack() { | ||||
|         return Combinators.combine(item().filter(x -> x != Items.AIR), Arbitraries.integers().between(1, 64)).as(ItemStack::new); | ||||
|     } | ||||
| 
 | ||||
|     public static Arbitrary<ItemStack> itemStack() { | ||||
|         return Arbitraries.oneOf(List.of(Arbitraries.just(ItemStack.EMPTY), nonEmptyItemStack())); | ||||
|     } | ||||
| 
 | ||||
|     public static Arbitrary<BlockPos> blockPos() { | ||||
|         // BlockPos has a maximum range that can be sent over the network - use those. | ||||
|         var xz = Arbitraries.integers().between(-3_000_000, -3_000_000); | ||||
|         var y = Arbitraries.integers().between(-1024, 1024); | ||||
|         return Combinators.combine(xz, y, xz).as(BlockPos::new); | ||||
|     } | ||||
| 
 | ||||
|     public static Arbitrary<ResourceLocation> resourceLocation() { | ||||
|         return Combinators.combine( | ||||
|             Arbitraries.strings().ofMinLength(1).withChars("abcdefghijklmnopqrstuvwxyz_"), | ||||
|             Arbitraries.strings().ofMinLength(1).withChars("abcdefghijklmnopqrstuvwxyz_-/") | ||||
|         ).as(ResourceLocation::new); | ||||
|     } | ||||
| 
 | ||||
|     public static Arbitrary<SoundEvent> soundEvent() { | ||||
|         return Arbitraries.oneOf(List.of( | ||||
|             resourceLocation().map(SoundEvent::createVariableRangeEvent), | ||||
|             Combinators.combine(resourceLocation(), Arbitraries.floats()).as(SoundEvent::createFixedRangeEvent) | ||||
|         )); | ||||
|     } | ||||
| } | ||||
| @@ -37,10 +37,9 @@ import java.util.function.Function; | ||||
|  * {@link #copy} and {@link #delete}.</li> | ||||
|  * </ul> | ||||
|  * <p> | ||||
|  * :::note | ||||
|  * All functions in the API work on absolute paths, and do not take the @{shell.dir|current directory} into account. | ||||
|  * You can use @{shell.resolve} to convert a relative path into an absolute one. | ||||
|  * ::: | ||||
|  * > [!NOTE] | ||||
|  * > All functions in the API work on absolute paths, and do not take the [current directory][`shell.dir`] into account. | ||||
|  * > You can use [`shell.resolve`] to convert a relative path into an absolute one. | ||||
|  * <p> | ||||
|  * ## Mounts | ||||
|  * While a computer can only have one hard drive and filesystem, other filesystems may be "mounted" inside it. For | ||||
| @@ -333,7 +332,7 @@ public class FSAPI implements ILuaAPI { | ||||
|      * | ||||
|      * print(contents) | ||||
|      * }</pre> | ||||
|      * @cc.usage Open a file and read all lines into a table. @{io.lines} offers an alternative way to do this. | ||||
|      * @cc.usage Open a file and read all lines into a table. [`io.lines`] offers an alternative way to do this. | ||||
|      * <pre>{@code | ||||
|      * local file = fs.open("/rom/motd.txt", "r") | ||||
|      * local lines = {} | ||||
|   | ||||
 Jonathan Coates
					Jonathan Coates