1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-17 15:07:38 +00:00

Compare commits

...

48 Commits

Author SHA1 Message Date
Jonathan Coates
9ea7f45fa7 Fix class cast exception with ObjectSource
We were generating methods with the original object, rather than the
extra one.

Updated our tests to actually catch this. Unfortunately the only places
we use this interface is in HTTP responses and transferred files,
neither of which show up in the Lua-side tests.
2023-07-07 18:05:02 +01:00
Jonathan Coates
d351bc33c6 Bump CC:T to 1.106.0 2023-07-07 00:16:48 +01:00
Jonathan Coates
5d71770931 Several command permission fixes
- Attach permission checks to the first argument (so the literal
   command name) rather than the last argument. This fixes commands
   showing up when they shouldn't.

 - HelpingArgumentBuilder now inherits permissions of its leaf nodes.
   This only really impacts the "track" subcommand.

 - Don't autocomplete the computer selector for the "queue" subcommand.
   As everyone has permission for this command, it's possible to find
   all computer ids and labels in the world.

   I'm in mixed minds about this, but don't think this is an exploit -
   computer ids/labels are sent to in-range players so shouldn't be
   considered secret - but worth patching none-the-less.
2023-07-06 23:16:22 +01:00
Jonathan Coates
4bbde8c50c Tighten up the $private HTTP rule
- Block multicast and the fd00::/8 address ranges.
 - Block several cloud metadata providers which sit outside the standard
   address ranges.
2023-07-06 23:16:22 +01:00
Jonathan Coates
cc8c1f38e7 Small documentation improvements
- Document that settings.set doesn't persist values. I think this
   closes #1512 - haven't heard back from them.

 - Add missing close reasons to the websocket_closed event. Closes #1493.

 - Mention what values are preserved by os.queueEvent. This is just the
   same as modem.transmit. Closes #1490.
2023-07-06 23:03:22 +01:00
Jonathan Coates
cab9c9772a Add some tests for new turtle tool functionality
- Normalise upgrade keys, to be "allowEnchantments" and
   "consumeDurability". We were previously inconsistent with
   allow/allows and consumes.
 - Add tests for durability and enchantments of pickaxes.
 - Fix a couple of issues with the original upgrade NBT being modified.
 - Now store the item's tag under a separate key rather than on the
   root. This makes syncing the NBT between the two much nicer.
2023-07-06 22:28:56 +01:00
Jonathan Coates
e337a63712 Bump FG and Loom
Also split up CI steps a little, so that it's more clear what failed.
2023-07-05 20:58:15 +01:00
Weblate
efa92b741b Translations for Russian (ru_ru)
Translations for French

Co-authored-by: chesiren <chesiren63@gmail.com>
Co-authored-by: ego-rick <vsevidcev@gmail.com>
2023-07-04 23:03:03 +00:00
Jonathan Coates
a91ac6f214 Make turtle tools a little more flexible
Turtle tools now accept two additional JSON fields

 - allowEnchantments: Whether items with enchantments (or any
   non-standard NBT) can be equipped.
 - consumesDurability: Whether durability will be consumed. This can be
   "never" (the current and default behaviour), "always", and
   "when_enchanted".

Closes #1501.
2023-07-03 22:14:31 +01:00
PenguinEncounter
943a9406b1 Fix turtle.refuel example (#1510) 2023-07-02 17:19:09 +00:00
Jonathan Coates
0b2bb5e7b5 Use exclusiveContent for our maven
This is a little nasty as we need to include ForgeGradle's repo too, but
should still help a bit!
2023-07-02 12:21:03 +01:00
Jonathan Coates
8708048b6e Try to make turtle_test.peripheral_change more robust
Replace the arbitrary sleep with a thenWaitUntil.
2023-07-02 11:46:03 +01:00
Jonathan Coates
d138d9c4a5 Preserve item NBT for turtle tools
This is a pre-requisite for #1501, and some other refactorings I want to do.

Also fix items in the turtle upgrade slots vanishing. We now explicitly
invalidate the cache when setting the item.
2023-07-02 11:02:10 +01:00
Edvin
f54cb8a432 Allow upgrades to read/write upgrade data from ItemStacks (#1465) 2023-07-02 10:55:55 +01:00
Jonathan Coates
94f5ede75a Remove superflous Nonnull/Notnull annotations 2023-07-01 19:32:28 +01:00
Jonathan Coates
1977556da4 Deprecate IPocketAccess.getUpgrades
I think this left over from CCTweaks or Peripheral++. It doesn't really
make sense as an API - if/when we add multiple upgrades, we'll want a
different API for this.
2023-07-01 18:34:17 +01:00
Jonathan Coates
9eabb29999 Move the model cache inside TurtleModelParts
This removes a tiny bit of duplication (at the cost of mode code), but
makes the interface more intuitive, as there's no bouncing between
getCombination -> cache -> buildModel.
2023-07-01 18:27:36 +01:00
Jonathan Coates
ecf880ed82 Document HTTP rules a little better
It turns out we don't document the "port" option anywhere, so probably
worth doing a bit of an overhaul here.

 - Expand the top-level HTTP rules comment, clarifying how things are
   matched and describing each field.

 - Improve the comments on the default HTTP rule. We now also describe
   the $private rule and its motivation.

 - Don't drop/ignore invalid rules. This gets written back to the
   original config file, so is very annoying! Instead we now log an
   error and convert the rule into a "deny all" rule, which should make
   it obvious something is wrong.
2023-07-01 16:16:06 +01:00
Jonathan Coates
655d5aeca8 Improve REPL's handling of expressions
- Remove the "force_print" code. This is a relic of before we used
   table.pack, and so didn't know how many expressions had been
   returned.

 - Check the input string is a valid expression separately before
   wrapping it in an _echo(...). Fixes #1506.
2023-07-01 12:37:48 +01:00
Jonathan Coates
34f41c4039 Be lazier in configuring Forge runs 2023-06-29 22:31:49 +01:00
Jonathan Coates
f5b16261cc Update to Gradle 8.x
- Update to Loom 1.2 and FG 6.0. ForgeGradle has changed how it
   generates the runXyz tasks, which makes running our tests much
   harder. I've raised an issue upstream, but for now we do some nasty
   poking of internals.

 - Fix Sodium/Iris tests. Loom 1.1 changed how remapped configurations
   are generated - we create a dummy source set and associate the
   remapped configuration with that. All nasty stuff.

 - Publish the common library. I'm not a fan of this, but given how much
   internals I'm poking elsewhere, should probably get off my high
   horse.

 - Add renderdoc support to the client gametests, enabled with
   -Prenderdoc.
2023-06-29 20:10:17 +01:00
Jonathan Coates
7eb3b691da Fix misplaced calls to IArguments.escapes
- Fix mainThread=true methods calling IArguments.escapes too late. This
   should be done before scheduling on the main thread, not on the main
   thread itself!

 - Fix VarargsArguments.escapes not checking that the argument haven't
   been closed. This is slightly prone to race conditions, but I don't
   think it's worth the overhead of tracking the owning thread.

   Maybe when panama and its resource scopes are released.

Thanks Sara for pointing this out!

Slightly irked that none of our tests caught this. Alas.

Also fix a typo in AddressPredicate. Yes, no commit discipline.
2023-06-27 18:28:54 +01:00
Jonathan Coates
910a63214e Make Generic methods per-ComputerContext
- Move the class cache out of Generator into MethodSupplierImpl. This
   means we cache class generation globally (that's really expensive!),
   but the class -> method list lookup is local.

 - Move the global GenericSource/GenericMethod registry out of core,
   passing in the list of generic methods to the ComputerContext.

I'm not entirely thrilled by the slight overlap of MethodSupplierImpl and
Generator here, something to clean up in the future.
2023-06-26 21:46:55 +01:00
Jonathan Coates
591a7eca23 Clean up how we enumerate Lua/peripheral methods
- Move several interfaces out of `d00.computercraft.core.asm` into a
   new `aethods` package. It may make sense to expose this to the
   public API in a future commit (possibly part of #1462).

 - Add a new MethodSupplier<T> interface, which provides methods to
   iterate over all methods exported by an object (either directly, or
   including those from ObjectSources).

   This interface's concrete implementation (asm.MethodSupplierImpl),
   uses Generators and IntCaches as before - we can now make that all
   package-private though, which is nice!

 - Make the LuaMethod and PeripheralMethod MethodSupplier local to the
   ComputerContext. This currently has no effect (the underlying
   Generator is still global), but eventually we'll make GenericMethods
   non-global, which unlocks the door for #1382.

 - Update everything to use this new interface. This is mostly pretty
   sensible, but is a little uglier on the MC side (especially in
   generic peripherals), as we need to access the global ServerContext.
2023-06-26 19:42:42 +01:00
Jonathan Coates
a29a516a3f Small refactoring to generic peripherals
- Remove SidedGenericPeripheral (we never used this!), adding the
   functionality to GenericPeripheral directly. This is just used on the
   Fabric side for now, but might make sense with Forge too.

 - Move GenericPeripheralBuilder into the common project - this is
   identical between the two projects!

 - GenericPeripheralBuilder now generates a list of methods internally,
   rather than being passed the methods.

 - Add a tiny bit of documentation.
2023-06-26 19:11:59 +01:00
Jonathan Coates
4a5e03c11a Convert NamedMethod into a record 2023-06-26 18:51:14 +01:00
Jonathan Coates
50d460624f Make the list of API factories per-ComputerContext
The registry is still a singleton inside the Minecraft code, but this
makes the core a little cleaner.
2023-06-26 18:48:26 +01:00
Jonathan Coates
bc500df921 Use a builder for constructing ComputerContexts
We've got a few optional fields here, and more on their way, so this
ends up being a little nicer.
2023-06-26 18:42:19 +01:00
Jonathan Coates
4accda6b8e Some tiny optimisations to the window API
- Use integer indexes instead of strings (i.e. text, textColour). This
   is a tiny bit faster.
 - Avoid re-creating tables when clearing.

We're still mostly limited by the VM (slow) and string concatenation
(slow!). Short of having some low-level mutable buffer type, I don't
think we can improve this much :(.
2023-06-25 21:04:05 +01:00
Jonathan Coates
54ab98473f Be lazy in reporting errors in the lexer
Instead of reporting an error with `.report(f(...))`, we now do
`.report(f, ...)`. This allows consumers to ignore error messages when
not needed, such as when just doing syntax highlighting.
2023-06-25 15:48:57 +01:00
Jonathan Coates
7ffdbb2316 Publish docs for 1.20 as well 2023-06-25 09:47:56 +01:00
Jonathan Coates
672c2cf029 Limit turtle's reach distance in Item.use
When a turtle attempts to place a block, it does so by searching for
nearby blocks and attempting to place the item against that block.

This has slightly strange behaviour when working with "placable"
non-block items though (such as buckets or boats). In this case, we call
Item.use, which doesn't take in the position of the block we're placing
against. Instead these items do their own ray trace, using the default
reach distance.

If the block we're trying to place against is non-solid, the ray trace
will go straight through it and continue (up to the maximum of 5
blocks), allowing placing the item much further away.

Our fix here is to override the default reach distance of our fake
players, limiting it to 2. This is easy on Forge (it has built-in
support), and requires a mixin on Fabric.

Closes #1497.
2023-06-24 17:09:34 +01:00
Jonathan Coates
c3bdb0440e Merge pull request #1494 from Wojbie/lua.lua-require-fix
Update lua.lua require logic.
2023-06-20 23:38:22 +01:00
Wojbie
88f0c44152 Update lua.lua require logic.
This makes it more consistent in situations when someone requires with path starting with /.
2023-06-20 23:33:53 +02:00
JackMacWindows
c8523bf479 Add ability to serialize Unicode strings to JSON (#1489) 2023-06-18 21:42:28 +00:00
Jonathan Coates
953372b1b7 Fix quad order when rendering turtles upside down
- Reverse quads in our model transformer and when rendering as a block
   entity.
 - Correctly recompute normals when the quads have been inverted.

Closes #1283
2023-06-18 19:32:23 +01:00
Jonathan Coates
36b9f4ec55 Merge pull request #1485 from cc-tweaked/feature/turtle-upgrade-ui
Allow changing turtle upgrades from the GUI
2023-06-18 08:09:47 +01:00
Jonathan Coates
ccfed0059b Render the computer cursor as emissive
- Split the front face of the computer model into two layers - one for
   the main texture, and one for the cursor. This is actually a
   simplification of what we had before, which is nice.

 - Make the cursor layer render as an emissive quad, meaning it glows in
   the dark. This is very easy on Forge (just some model JSON) and very
   hard on Fabric (requires a custom model loader).
2023-06-17 18:02:05 +01:00
Jonathan Coates
7b4ba11fb4 Allow changing turtle upgrades from the GUI
This adds two slots to the right of the turtle interface which contain
the left and right upgrades of a turtle.

 - Add turtle_upgrade_{left,right} indicators, which used as the
   background texture for the two upgrade slots. In order to use
   Slot.getNoItemIcon, we need to bake these into the block texture
   atlas.

   This is done with the new atlas JSON and a data generator - it's
   mostly pretty simple, but we do now need a client-side data
   generator, which is a little ugly to do.

 - Add a new UpgradeContainer/UpgradeSlot, which exposes a turtle's
   upgrades in an inventory-like way.

 - Update the turtle menu and screen to handle these new slots.
2023-06-17 10:48:44 +01:00
Jonathan Coates
8ccd5a560c Add support for codecs to our data generator system
This already exists in both upstream loaders, we just need to abstract
over it.
2023-06-17 10:37:19 +01:00
Weblate
0f866836a0 Translations for Russian (ru_ru)
Co-authored-by: Andrew71 <andrey.nikitin.vladimirovich@gmail.com>
2023-06-16 21:03:01 +00:00
Jonathan Coates
df591cd7c6 Allow extending UpgradeDataProvider
Closes #1477
2023-06-15 18:57:25 +01:00
Jonathan Coates
c7f3d4f45d Port fs.find to CraftOS
Also add support for "?" style wildcards.

Closes #1455.
2023-06-15 18:57:05 +01:00
Jonathan Coates
77ac04cb7a Fix changelog notes
Also exclude data generator cache from the Forge jar. Didn't have any
better place to put this 😳.
2023-06-15 18:32:30 +01:00
Jonathan Coates
201df7e987 Merge pull request #1481 from znepb/fix-flute-code
Fix missing curly brace in instrument list
2023-06-13 14:24:27 +01:00
Marcus
5722e51735 Fix missing curly brace in instrument list 2023-06-13 06:04:35 -04:00
Jonathan Coates
7a291619ab Merge pull request #1480 from MCJack123/patch-16
Fixed typo in cc.image.nft docs
2023-06-13 07:26:59 +01:00
JackMacWindows
4b9b19b02d Fixed typo in cc.image.nft docs 2023-06-12 19:49:45 -04:00
201 changed files with 4054 additions and 1280 deletions

View File

@@ -8,16 +8,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone repository
- name: 📥 Clone repository
uses: actions/checkout@v3
- name: Set up Java
- name: 📥 Set up Java
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'temurin'
- name: Setup Gradle
- name: 📥 Setup Gradle
uses: gradle/gradle-build-action@v2
with:
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/mc-') }}
@@ -27,39 +27,45 @@ jobs:
mkdir -p ~/.gradle
echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties
- name: Build with Gradle
run: |
./gradlew assemble || ./gradlew assemble
./gradlew downloadAssets || ./gradlew downloadAssets
./gradlew build
- name: ⚒️ Build
run: ./gradlew assemble || ./gradlew assemble
- name: Run client tests
- name: 💡 Lint
uses: pre-commit/action@v3.0.0
- name: 🧪 Run tests
run: ./gradlew test validateMixinNames checkChangelog
- name: 📥 Download assets for game tests
run: ./gradlew downloadAssets || ./gradlew downloadAssets
- name: 🧪 Run integration tests
run: ./gradlew runGametest
- name: 🧪 Run client tests
run: ./gradlew runGametestClient # Not checkClient, as no point running rendering tests.
# These are a little flaky on GH actions: its useful to run them, but don't break the build.
continue-on-error: true
- name: Prepare Jars
- name: 🧪 Parse test reports
run: ./tools/parse-reports.py
if: ${{ failure() }}
- name: 📦 Prepare Jars
run: |
# Find the main jar and append the git hash onto it.
mkdir -p jars
find projects/forge/build/libs projects/fabric/build/libs -type f -regex '.*[0-9.]+\(-SNAPSHOT\)?\.jar$' -exec bash -c 'cp {} "jars/$(basename {} .jar)-$(git rev-parse HEAD).jar"' \;
- name: Upload Jar
- name: 📤 Upload Jar
uses: actions/upload-artifact@v3
with:
name: CC-Tweaked
path: ./jars
- name: Upload coverage
- name: 📤 Upload coverage
uses: codecov/codecov-action@v3
- name: Parse test reports
run: ./tools/parse-reports.py
if: ${{ failure() }}
- name: Run linters
uses: pre-commit/action@v3.0.0
build-core:
strategy:
fail-fast: false

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- mc-1.19.x
- mc-1.20.x
jobs:
make_doc:

View File

@@ -6,6 +6,7 @@ Upstream-Contact: Jonathan Coates <git@squiddev.cc>
Files:
projects/common/src/main/resources/assets/computercraft/sounds.json
projects/common/src/main/resources/assets/computercraft/sounds/empty.ogg
projects/common/src/testMod/resources/data/cctest/computercraft/turtle_upgrades/*
projects/common/src/testMod/resources/data/cctest/structures/*
projects/fabric/src/generated/*
projects/forge/src/generated/*

View File

@@ -10,8 +10,9 @@ import cc.tweaked.gradle.IdeaRunConfigurations
import cc.tweaked.gradle.MinecraftConfigurations
plugins {
id("cc-tweaked.java-convention")
id("net.minecraftforge.gradle")
// We must apply java-convention after Forge, as we need the fg extension to be present.
id("cc-tweaked.java-convention")
id("org.parchmentmc.librarian.forgegradle")
}

View File

@@ -37,9 +37,25 @@ java {
repositories {
mavenCentral()
maven("https://squiddev.cc/maven") {
val mainMaven = maven("https://squiddev.cc/maven") {
name = "SquidDev"
content {
// Until https://github.com/SpongePowered/Mixin/pull/593 is merged
includeModule("org.spongepowered", "mixin")
}
}
exclusiveContent {
forRepositories(mainMaven)
// Include the ForgeGradle repository if present. This requires that ForgeGradle is already present, which we
// enforce in our Forge overlay.
val fg =
project.extensions.findByType(net.minecraftforge.gradle.userdev.DependencyManagementExtension::class.java)
if (fg != null) forRepositories(fg.repository)
filter {
includeGroup("org.squiddev")
includeGroup("cc.tweaked")
// Things we mirror
@@ -49,8 +65,6 @@ repositories {
includeGroup("me.shedaniel.cloth")
includeGroup("mezz.jei")
includeModule("com.terraformersmc", "modmenu")
// Until https://github.com/SpongePowered/Mixin/pull/593 is merged
includeModule("org.spongepowered", "mixin")
}
}
}
@@ -104,6 +118,7 @@ tasks.withType(JavaCompile::class.java).configureEach {
tasks.processResources {
exclude("**/*.license")
exclude(".cache")
}
tasks.withType(AbstractArchiveTask::class.java).configureEach {

View File

@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.gradle
import net.minecraftforge.gradle.common.util.RunConfig
import net.minecraftforge.gradle.common.util.runs.setRunConfigInternal
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.JavaExec
import org.gradle.jvm.toolchain.JavaToolchainService
import java.nio.file.Files
/**
* Set [JavaExec] task to run a given [RunConfig].
*/
fun JavaExec.setRunConfig(config: RunConfig) {
dependsOn("prepareRuns")
setRunConfigInternal(project, this, config)
doFirst("Create working directory") { Files.createDirectories(workingDir.toPath()) }
javaLauncher.set(
project.extensions.getByType(JavaToolchainService::class.java)
.launcherFor(project.extensions.getByType(JavaPluginExtension::class.java).toolchain),
)
}

View File

@@ -60,7 +60,7 @@ class IlluaminatePlugin : Plugin<Project> {
/** Define a dependency for illuaminate from a version number and the current operating system. */
private fun illuaminateArtifact(project: Project, version: String): Dependency {
val osName = System.getProperty("os.name").toLowerCase()
val osName = System.getProperty("os.name").lowercase()
val (os, suffix) = when {
osName.contains("windows") -> Pair("windows", ".exe")
osName.contains("mac os") || osName.contains("darwin") -> Pair("macos", "")
@@ -68,7 +68,7 @@ class IlluaminatePlugin : Plugin<Project> {
else -> error("Unsupported OS $osName for illuaminate")
}
val osArch = System.getProperty("os.arch").toLowerCase()
val osArch = System.getProperty("os.arch").lowercase()
val arch = when {
// On macOS the x86_64 binary will work for both ARM and Intel Macs through Rosetta.
os == "macos" -> "x86_64"

View File

@@ -32,11 +32,14 @@ abstract class ClientJavaExec : JavaExec() {
usesService(clientRunner)
}
@get:Input
val renderdoc get() = project.hasProperty("renderdoc")
/**
* When [false], tests will not be run automatically, allowing the user to debug rendering.
*/
@get:Input
val clientDebug get() = project.hasProperty("clientDebug")
val clientDebug get() = renderdoc || project.hasProperty("clientDebug")
/**
* When [false], tests will not run under a framebuffer.
@@ -63,6 +66,7 @@ abstract class ClientJavaExec : JavaExec() {
task.copyToFull(this)
if (!clientDebug) systemProperty("cctest.client", "")
if (renderdoc) environment("LD_PRELOAD", "/usr/lib/librenderdoc.so")
systemProperty("cctest.gametest-report", testResults.get().asFile.absoluteFile)
workingDir(project.buildDir.resolve("gametest").resolve(name))
}

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package net.minecraftforge.gradle.common.util.runs
import net.minecraftforge.gradle.common.util.RunConfig
import org.gradle.api.Project
import org.gradle.process.CommandLineArgumentProvider
import org.gradle.process.JavaExecSpec
import java.io.File
import java.util.function.Supplier
import java.util.stream.Collectors
import java.util.stream.Stream
/**
* Set up a [JavaExecSpec] to execute a [RunConfig].
*
* [MinecraftRunTask] sets up all its properties when the task is executed, rather than when configured. As such, it's
* not possible to use [cc.tweaked.gradle.copyToFull] like we do for Fabric. Instead, we set up the task manually.
*
* Unfortunately most of the functionality we need is package-private, and so we have to put our code into the package.
*/
internal fun setRunConfigInternal(project: Project, spec: JavaExecSpec, config: RunConfig) {
spec.workingDir = File(config.workingDirectory)
spec.mainClass.set(config.main)
for (source in config.allSources) spec.classpath(source.runtimeClasspath)
val originalTask = project.tasks.named(config.taskName, MinecraftRunTask::class.java)
// Add argument and JVM argument via providers, to be as lazy as possible with fetching artifacts.
val lazyTokens = RunConfigGenerator.configureTokensLazy(
project, config, RunConfigGenerator.mapModClassesToGradle(project, config),
originalTask.get().minecraftArtifacts,
originalTask.get().runtimeClasspathArtifacts,
)
spec.argumentProviders.add(
CommandLineArgumentProvider {
RunConfigGenerator.getArgsStream(config, lazyTokens, false).toList()
},
)
spec.jvmArgumentProviders.add(
CommandLineArgumentProvider {
(if (config.isClient) config.jvmArgs + originalTask.get().additionalClientArgs.get() else config.jvmArgs).map { config.replace(lazyTokens, it) } +
config.properties.map { (k, v) -> "-D${k}=${config.replace(lazyTokens, v)}" }
},
)
for ((key, value) in config.environment) spec.environment(key, config.replace(lazyTokens, value))
}

View File

@@ -13,6 +13,15 @@ The @{websocket_closed} event is fired when an open WebSocket connection is clos
## 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
abnormally.
4. <span class="type">@{number}|@{nil}</span>: The [connection close code][close_code],
indicating why the socket was closed. This will be @{nil} if the connection
was closed abnormally.
[close_reason]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 "The WebSocket Connection Close Reason, RFC 6455"
[close_code]: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 "The WebSocket Connection Close Code, RFC 6455"
## Example
Prints a message when a WebSocket is closed (this may take a minute):

View File

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

View File

@@ -53,11 +53,11 @@ checkstyle = "10.3.4"
curseForgeGradle = "1.0.14"
errorProne-core = "2.18.0"
errorProne-plugin = "3.0.1"
fabric-loom = "1.1.10"
forgeGradle = "5.1.+"
fabric-loom = "1.3.7"
forgeGradle = "6.0.8"
githubRelease = "2.2.12"
ideaExt = "1.1.6"
illuaminate = "0.1.0-24-gdb28902"
illuaminate = "0.1.0-28-ga7efd71"
librarian = "1.+"
minotaur = "2.+"
mixinGradle = "0.7.+"

Binary file not shown.

View File

@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

19
gradlew vendored
View File

@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,10 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -143,12 +140,16 @@ fi
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -193,6 +194,10 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in

1
gradlew.bat vendored
View File

@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

View File

@@ -11,6 +11,7 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import javax.annotation.Nullable;
@@ -28,12 +29,27 @@ public interface TurtleUpgradeModeller<T extends ITurtleUpgrade> {
* When the current turtle is {@literal null}, this function should be constant for a given upgrade and side.
*
* @param upgrade The upgrade that you're getting the model for.
* @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models!
* @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models, unless
* {@link #getModel(ITurtleUpgrade, CompoundTag, TurtleSide)} is overriden.
* @param side Which side of the turtle (left or right) the upgrade resides on.
* @return The model that you wish to be used to render your upgrade.
*/
TransformedModel getModel(T upgrade, @Nullable ITurtleAccess turtle, TurtleSide side);
/**
* Obtain the model to be used when rendering a turtle peripheral.
* <p>
* This is used when rendering the turtle's item model, and so no {@link ITurtleAccess} is available.
*
* @param upgrade The upgrade that you're getting the model for.
* @param data Upgrade data instance for current turtle side.
* @param side Which side of the turtle (left or right) the upgrade resides on.
* @return The model that you wish to be used to render your upgrade.
*/
default TransformedModel getModel(T upgrade, CompoundTag data, TurtleSide side) {
return getModel(upgrade, (ITurtleAccess) null, side);
}
/**
* A basic {@link TurtleUpgradeModeller} which renders using the upgrade's {@linkplain ITurtleUpgrade#getCraftingItem()
* crafting item}.

View File

@@ -5,9 +5,11 @@
package dan200.computercraft.api.pocket;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.upgrades.UpgradeBase;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.Map;
@@ -69,6 +71,8 @@ public interface IPocketAccess {
*
* @return The upgrade's NBT.
* @see #updateUpgradeNBTData()
* @see UpgradeBase#getUpgradeItem(CompoundTag)
* @see UpgradeBase#getUpgradeData(ItemStack)
*/
CompoundTag getUpgradeNBTData();
@@ -80,7 +84,10 @@ public interface IPocketAccess {
void updateUpgradeNBTData();
/**
* Remove the current peripheral and create a new one. You may wish to do this if the methods available change.
* Remove the current peripheral and create a new one.
* <p>
* You may wish to do this if the methods available change, for instance when the {@linkplain #getEntity() owning
* entity} changes.
*/
void invalidatePeripheral();
@@ -88,6 +95,8 @@ public interface IPocketAccess {
* Get a list of all upgrades for the pocket computer.
*
* @return A collection of all upgrade names.
* @deprecated This is a relic of a previous API, which no longer makes sense with newer versions of ComputerCraft.
*/
@Deprecated(forRemoval = true)
Map<ResourceLocation, IPeripheral> getUpgrades();
}

View File

@@ -8,10 +8,13 @@ import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.lua.ILuaCallback;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.api.upgrades.UpgradeData;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.Container;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.ApiStatus;
@@ -245,23 +248,51 @@ public interface ITurtleAccess {
void playAnimation(TurtleAnimation animation);
/**
* Returns the turtle on the specified side of the turtle, if there is one.
* Returns the upgrade on the specified side of the turtle, if there is one.
*
* @param side The side to get the upgrade from.
* @return The upgrade on the specified side of the turtle, if there is one.
* @see #setUpgrade(TurtleSide, ITurtleUpgrade)
* @see #getUpgradeWithData(TurtleSide)
* @see #setUpgradeWithData(TurtleSide, UpgradeData)
*/
@Nullable
ITurtleUpgrade getUpgrade(TurtleSide side);
/**
* Returns the upgrade on the specified side of the turtle, along with its {@linkplain #getUpgradeNBTData(TurtleSide)
* update data}.
*
* @param side The side to get the upgrade from.
* @return The upgrade on the specified side of the turtle, along with its upgrade data, if there is one.
* @see #getUpgradeWithData(TurtleSide)
* @see #setUpgradeWithData(TurtleSide, UpgradeData)
*/
default @Nullable UpgradeData<ITurtleUpgrade> getUpgradeWithData(TurtleSide side) {
var upgrade = getUpgrade(side);
return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData(side));
}
/**
* Set the upgrade for a given side, resetting peripherals and clearing upgrade specific data.
*
* @param side The side to set the upgrade on.
* @param upgrade The upgrade to set, may be {@code null} to clear.
* @see #getUpgrade(TurtleSide)
* @deprecated Use {@link #setUpgradeWithData(TurtleSide, UpgradeData)}
*/
void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade);
@Deprecated
default void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade) {
setUpgradeWithData(side, upgrade == null ? null : UpgradeData.ofDefault(upgrade));
}
/**
* Set the upgrade for a given side and its upgrade data.
*
* @param side The side to set the upgrade on.
* @param upgrade The upgrade to set, may be {@code null} to clear.
* @see #getUpgradeWithData(TurtleSide)
*/
void setUpgradeWithData(TurtleSide side, @Nullable UpgradeData<ITurtleUpgrade> upgrade);
/**
* Returns the peripheral created by the upgrade on the specified side of the turtle, if there is one.
@@ -281,6 +312,8 @@ public interface ITurtleAccess {
* @param side The side to get the upgrade data for.
* @return The upgrade-specific data.
* @see #updateUpgradeNBTData(TurtleSide)
* @see UpgradeBase#getUpgradeItem(CompoundTag)
* @see UpgradeBase#getUpgradeData(ItemStack)
*/
CompoundTag getUpgradeNBTData(TurtleSide side);

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.api.turtle;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.upgrades.UpgradeBase;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import javax.annotation.Nullable;
@@ -79,4 +80,17 @@ public interface ITurtleUpgrade extends UpgradeBase {
*/
default void update(ITurtleAccess turtle, TurtleSide side) {
}
/**
* Get upgrade data that should be persisted when the turtle was broken.
* <p>
* This method should be overridden when you don't need to store all upgrade data by default. For instance, if you
* store peripheral state in the upgrade data, which should be lost when the turtle is broken.
*
* @param upgradeData Data that currently stored for this upgrade
* @return Filtered version of this data.
*/
default CompoundTag getPersistedData(CompoundTag upgradeData) {
return upgradeData;
}
}

View File

@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.turtle;
import net.minecraft.util.StringRepresentable;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.ItemStack;
/**
* Indicates if an equipped turtle item will consume durability.
*
* @see TurtleUpgradeDataProvider.ToolBuilder#consumeDurability(TurtleToolDurability)
*/
public enum TurtleToolDurability implements StringRepresentable {
/**
* The equipped tool always consumes durability when using.
*/
ALWAYS("always"),
/**
* The equipped tool consumes durability if it is {@linkplain ItemStack#isEnchanted() enchanted} or has
* {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}.
*/
WHEN_ENCHANTED("when_enchanted"),
/**
* The equipped tool never consumes durability. Tools which have been damaged cannot be used as upgrades.
*/
NEVER("never");
private final String serialisedName;
/**
* The codec which may be used for serialising/deserialising {@link TurtleToolDurability}s.
*/
public static final StringRepresentable.EnumCodec<TurtleToolDurability> CODEC = StringRepresentable.fromEnum(TurtleToolDurability::values);
TurtleToolDurability(String serialisedName) {
this.serialisedName = serialisedName;
}
@Override
public String getSerializedName() {
return serialisedName;
}
}

View File

@@ -13,8 +13,10 @@ import net.minecraft.data.DataGenerator;
import net.minecraft.data.PackOutput;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import javax.annotation.Nullable;
@@ -61,6 +63,8 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
private @Nullable Item craftingItem;
private @Nullable Float damageMultiplier = null;
private @Nullable TagKey<Block> breakable;
private boolean allowEnchantments = false;
private TurtleToolDurability consumeDurability = TurtleToolDurability.NEVER;
ToolBuilder(ResourceLocation id, TurtleUpgradeSerialiser<?> serialiser, Item toolItem) {
this.id = id;
@@ -104,6 +108,28 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
return this;
}
/**
* Indicate that this upgrade allows items which have been {@linkplain ItemStack#isEnchanted() enchanted} or have
* {@linkplain ItemStack#getAttributeModifiers(EquipmentSlot) custom attribute modifiers}.
*
* @return The tool builder, for further use.
*/
public ToolBuilder allowEnchantments() {
allowEnchantments = true;
return this;
}
/**
* Set when the tool will consume durability.
*
* @param durability The durability predicate.
* @return The tool builder, for further use.
*/
public ToolBuilder consumeDurability(TurtleToolDurability durability) {
consumeDurability = durability;
return this;
}
/**
* Provide a list of breakable blocks. If not given, the tool can break all blocks. If given, only blocks
* in this tag, those in {@link ComputerCraftTags.Blocks#TURTLE_ALWAYS_BREAKABLE} and "insta-mine" ones can
@@ -132,6 +158,10 @@ public abstract class TurtleUpgradeDataProvider extends UpgradeDataProvider<ITur
}
if (damageMultiplier != null) s.addProperty("damageMultiplier", damageMultiplier);
if (breakable != null) s.addProperty("breakable", breakable.location().toString());
if (allowEnchantments) s.addProperty("allowEnchantments", true);
if (consumeDurability != TurtleToolDurability.NEVER) {
s.addProperty("consumeDurability", consumeDurability.getSerializedName());
}
}));
}
}

View File

@@ -4,10 +4,14 @@
package dan200.computercraft.api.upgrades;
import dan200.computercraft.api.pocket.IPocketAccess;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.impl.PlatformHelper;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
@@ -50,6 +54,42 @@ public interface UpgradeBase {
*/
ItemStack getCraftingItem();
/**
* Returns the item stack representing a currently equipped turtle upgrade.
* <p>
* While upgrades can store upgrade data ({@link ITurtleAccess#getUpgradeNBTData(TurtleSide)} and
* {@link IPocketAccess#getUpgradeNBTData()}}, by default this data is discarded when an upgrade is unequipped,
* and the original item stack is returned.
* <p>
* By overriding this method, you can create a new {@link ItemStack} which contains enough data to
* {@linkplain #getUpgradeData(ItemStack) re-create the upgrade data} if the item is re-equipped.
* <p>
* When overriding this, you should override {@link #getUpgradeData(ItemStack)} and {@link #isItemSuitable(ItemStack)}
* at the same time,
*
* @param upgradeData The current upgrade data. This should <strong>NOT</strong> be mutated.
* @return The item stack returned when unequipping.
*/
default ItemStack getUpgradeItem(CompoundTag upgradeData) {
return getCraftingItem();
}
/**
* Extract upgrade data from an {@link ItemStack}.
* <p>
* This upgrade data will be available with {@link ITurtleAccess#getUpgradeNBTData(TurtleSide)} or
* {@link IPocketAccess#getUpgradeNBTData()}.
* <p>
* This should be an inverse to {@link #getUpgradeItem(CompoundTag)}.
*
* @param stack The stack that was equipped by the turtle or pocket computer. This will have the same item as
* {@link #getCraftingItem()}.
* @return The upgrade data that should be set on the turtle or pocket computer.
*/
default CompoundTag getUpgradeData(ItemStack stack) {
return new CompoundTag();
}
/**
* Determine if an item is suitable for being used for this upgrade.
* <p>

View File

@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.upgrades;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Contract;
import javax.annotation.Nullable;
/**
* An upgrade (i.e. a {@link ITurtleUpgrade}) and its current upgrade data.
* <p>
* <strong>IMPORTANT:</strong> The {@link #data()} in an upgrade data is often a reference to the original upgrade data.
* Be careful to take a {@linkplain #copy() defensive copy} if you plan to use the data in this upgrade.
*
* @param upgrade The current upgrade.
* @param data The upgrade's data.
* @param <T> The type of upgrade, either {@link ITurtleUpgrade} or {@link IPocketUpgrade}.
*/
public record UpgradeData<T extends UpgradeBase>(T upgrade, CompoundTag data) {
/**
* A utility method to construct a new {@link UpgradeData} instance.
*
* @param upgrade An upgrade.
* @param data The upgrade's data.
* @param <T> The type of upgrade.
* @return The new {@link UpgradeData} instance.
*/
public static <T extends UpgradeBase> UpgradeData<T> of(T upgrade, CompoundTag data) {
return new UpgradeData<>(upgrade, data);
}
/**
* Create an {@link UpgradeData} containing the default {@linkplain #data() data} for an upgrade.
*
* @param upgrade The upgrade instance.
* @param <T> The type of upgrade.
* @return The default upgrade data.
*/
public static <T extends UpgradeBase> UpgradeData<T> ofDefault(T upgrade) {
return of(upgrade, upgrade.getUpgradeData(upgrade.getCraftingItem()));
}
/**
* Take a copy of a (possibly {@code null}) {@link UpgradeData} instance.
*
* @param upgrade The copied upgrade data.
* @param <T> The type of upgrade.
* @return The newly created upgrade data.
*/
@Contract("!null -> !null; null -> null")
public static <T extends UpgradeBase> @Nullable UpgradeData<T> copyOf(@Nullable UpgradeData<T> upgrade) {
return upgrade == null ? null : upgrade.copy();
}
/**
* Get the {@linkplain UpgradeBase#getUpgradeItem(CompoundTag) upgrade item} for this upgrade.
* <p>
* This returns a defensive copy of the item, to prevent accidental mutation of the upgrade data or original
* {@linkplain UpgradeBase#getCraftingItem() upgrade stack}.
*
* @return This upgrade's item.
*/
public ItemStack getUpgradeItem() {
return upgrade.getUpgradeItem(data).copy();
}
/**
* Take a copy of this {@link UpgradeData}. This returns a new instance with the same upgrade and a fresh copy of
* the upgrade data.
*
* @return A copy of the current instance.
*/
public UpgradeData<T> copy() {
return new UpgradeData<>(upgrade(), data().copy());
}
}

View File

@@ -24,6 +24,7 @@ import org.apache.logging.log4j.Logger;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -104,7 +105,7 @@ public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends Upgra
protected abstract void addUpgrades(Consumer<Upgrade<R>> addUpgrade);
@Override
public final CompletableFuture<?> run(CachedOutput cache) {
public CompletableFuture<?> run(CachedOutput cache) {
var base = output.getOutputFolder().resolve("data");
Set<ResourceLocation> seen = new HashSet<>();
@@ -127,7 +128,7 @@ public abstract class UpgradeDataProvider<T extends UpgradeBase, R extends Upgra
}
});
this.upgrades = upgrades;
this.upgrades = Collections.unmodifiableList(upgrades);
return Util.sequenceFailFast(futures);
}

View File

@@ -7,6 +7,7 @@ import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
plugins {
id("cc-tweaked.publishing")
id("cc-tweaked.vanilla")
id("cc-tweaked.gametest")
}

View File

@@ -26,9 +26,11 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
private static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_normal.png");
private static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation(ComputerCraftAPI.MOD_ID, "textures/gui/turtle_advanced.png");
private static final int TEX_WIDTH = 254;
private static final int TEX_WIDTH = 278;
private static final int TEX_HEIGHT = 217;
private static final int FULL_TEX_SIZE = 512;
public TurtleScreen(TurtleMenu container, Inventory player, Component title) {
super(container, player, title, BORDER);
@@ -45,16 +47,16 @@ public class TurtleScreen extends AbstractComputerScreen<TurtleMenu> {
protected void renderBg(PoseStack transform, float partialTicks, int mouseX, int mouseY) {
var advanced = family == ComputerFamily.ADVANCED;
RenderSystem.setShaderTexture(0, advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL);
blit(transform, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0, TEX_WIDTH, TEX_HEIGHT);
blit(transform, leftPos + AbstractComputerMenu.SIDEBAR_WIDTH, topPos, 0, 0, 0, TEX_WIDTH, TEX_HEIGHT, FULL_TEX_SIZE, FULL_TEX_SIZE);
// Render selected slot
var slot = getMenu().getSelectedSlot();
if (slot >= 0) {
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
var slotX = slot % 4;
var slotY = slot / 4;
blit(transform,
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18,
0, 217, 24, 24
leftPos + TURTLE_START_X - 2 + slotX * 18, topPos + PLAYER_START_Y - 2 + slotY * 18, 0,
0, 217, 24, 24, FULL_TEX_SIZE, FULL_TEX_SIZE
);
}

View File

@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.client.model.turtle;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.VertexFormat;
import com.mojang.blaze3d.vertex.VertexFormatElement;
import com.mojang.math.Transformation;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.core.Direction;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Applies a {@link Transformation} (or rather a {@link Matrix4f}) to a list of {@link BakedQuad}s.
* <p>
* This does a little bit of magic compared with other system (i.e. Forge's {@code QuadTransformers}), as it needs to
* handle flipping models upside down.
* <p>
* This is typically used with a {@link BakedModel} subclass - see the loader-specific projects.
*/
public final class ModelTransformer {
public static final int[] ORDER = new int[]{ 3, 2, 1, 0 };
public static final int STRIDE = DefaultVertexFormat.BLOCK.getIntegerSize();
private static final int POS_OFFSET = findOffset(DefaultVertexFormat.BLOCK, DefaultVertexFormat.ELEMENT_POSITION);
private final Matrix4f transformation;
private final boolean invert;
private @Nullable TransformedQuads cache;
public ModelTransformer(Transformation transformation) {
this.transformation = transformation.getMatrix();
invert = transformation.getMatrix().determinant() < 0;
}
public List<BakedQuad> transform(List<BakedQuad> quads) {
if (quads.isEmpty()) return List.of();
// We do some basic caching here to avoid recomputing every frame. Most turtle models don't have culled faces,
// so it's not worth being smarter here.
var cache = this.cache;
if (cache != null && quads.equals(cache.original())) return cache.transformed();
List<BakedQuad> transformed = new ArrayList<>(quads.size());
for (var quad : quads) transformed.add(transformQuad(quad));
this.cache = new TransformedQuads(quads, transformed);
return transformed;
}
private BakedQuad transformQuad(BakedQuad quad) {
var inputData = quad.getVertices();
var outputData = new int[inputData.length];
for (var i = 0; i < 4; i++) {
var inStart = STRIDE * i;
// Reverse the order of the quads if we're inverting
var outStart = STRIDE * (invert ? ORDER[i] : i);
System.arraycopy(inputData, inStart, outputData, outStart, STRIDE);
// Apply the matrix to our position
var inPosStart = inStart + POS_OFFSET;
var outPosStart = outStart + POS_OFFSET;
var x = Float.intBitsToFloat(inputData[inPosStart]);
var y = Float.intBitsToFloat(inputData[inPosStart + 1]);
var z = Float.intBitsToFloat(inputData[inPosStart + 2]);
// Transform the position
var pos = new Vector4f(x, y, z, 1);
transformation.transformProject(pos);
outputData[outPosStart] = Float.floatToRawIntBits(pos.x());
outputData[outPosStart + 1] = Float.floatToRawIntBits(pos.y());
outputData[outPosStart + 2] = Float.floatToRawIntBits(pos.z());
}
var direction = Direction.rotate(transformation, quad.getDirection());
return new BakedQuad(outputData, quad.getTintIndex(), direction, quad.getSprite(), quad.isShade());
}
private record TransformedQuads(List<BakedQuad> original, List<BakedQuad> transformed) {
}
private static int findOffset(VertexFormat format, VertexFormatElement element) {
var offset = 0;
for (var other : format.getElements()) {
if (other == element) return offset / Integer.BYTES;
offset += element.getByteSize();
}
throw new IllegalArgumentException("Cannot find " + element + " in " + format);
}
}

View File

@@ -4,11 +4,13 @@
package dan200.computercraft.client.model.turtle;
import com.google.common.cache.CacheBuilder;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Transformation;
import dan200.computercraft.api.client.TransformedModel;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.render.TurtleBlockEntityRenderer;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
@@ -21,15 +23,17 @@ import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* Combines several individual models together to form a turtle.
*
* @param <T> The type of the resulting "baked model".
*/
public final class TurtleModelParts {
public final class TurtleModelParts<T> {
private static final Transformation identity, flip;
static {
@@ -42,33 +46,67 @@ public final class TurtleModelParts {
flip = new Transformation(stack.last().pose());
}
public record Combination(
private record Combination(
boolean colour,
@Nullable ITurtleUpgrade leftUpgrade,
@Nullable ITurtleUpgrade rightUpgrade,
@Nullable UpgradeData<ITurtleUpgrade> leftUpgrade,
@Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
@Nullable ResourceLocation overlay,
boolean christmas,
boolean flip
) {
Combination copy() {
if (leftUpgrade == null && rightUpgrade == null) return this;
return new Combination(
colour, UpgradeData.copyOf(leftUpgrade), UpgradeData.copyOf(rightUpgrade),
overlay, christmas, flip
);
}
}
private final BakedModel familyModel;
private final BakedModel colourModel;
private final Function<TransformedModel, BakedModel> transformer;
private final Function<Combination, T> buildModel;
/**
* A cache of {@link TransformedModel} to the transformed {@link BakedModel}. This helps us pool the transformed
* instances, reducing memory usage and hopefully ensuring their caches are hit more often!
*/
private final Map<TransformedModel, BakedModel> transformCache = new HashMap<>();
private final Map<TransformedModel, BakedModel> transformCache = CacheBuilder.newBuilder()
.concurrencyLevel(1)
.expireAfterAccess(30, TimeUnit.SECONDS)
.<TransformedModel, BakedModel>build()
.asMap();
public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer) {
/**
* A cache of {@link Combination}s to the combined model.
*/
private final Map<Combination, T> modelCache = CacheBuilder.newBuilder()
.concurrencyLevel(1)
.expireAfterAccess(30, TimeUnit.SECONDS)
.<Combination, T>build()
.asMap();
public TurtleModelParts(BakedModel familyModel, BakedModel colourModel, ModelTransformer transformer, Function<List<BakedModel>, T> combineModel) {
this.familyModel = familyModel;
this.colourModel = colourModel;
this.transformer = x -> transformer.transform(x.getModel(), x.getMatrix());
buildModel = x -> combineModel.apply(buildModel(x));
}
public Combination getCombination(ItemStack stack) {
public T getModel(ItemStack stack) {
var combination = getCombination(stack);
var existing = modelCache.get(combination);
if (existing != null) return existing;
// Take a defensive copy of the upgrade data, and add it to the cache.
var newCombination = combination.copy();
var newModel = buildModel.apply(newCombination);
modelCache.put(newCombination, newModel);
return newModel;
}
private Combination getCombination(ItemStack stack) {
var christmas = Holiday.getCurrent() == Holiday.CHRISTMAS;
if (!(stack.getItem() instanceof TurtleItem turtle)) {
@@ -76,8 +114,8 @@ public final class TurtleModelParts {
}
var colour = turtle.getColour(stack);
var leftUpgrade = turtle.getUpgrade(stack, TurtleSide.LEFT);
var rightUpgrade = turtle.getUpgrade(stack, TurtleSide.RIGHT);
var leftUpgrade = turtle.getUpgradeWithData(stack, TurtleSide.LEFT);
var rightUpgrade = turtle.getUpgradeWithData(stack, TurtleSide.RIGHT);
var overlay = turtle.getOverlay(stack);
var label = turtle.getLabel(stack);
var flip = label != null && (label.equals("Dinnerbone") || label.equals("Grumm"));
@@ -85,7 +123,7 @@ public final class TurtleModelParts {
return new Combination(colour != -1, leftUpgrade, rightUpgrade, overlay, christmas, flip);
}
public List<BakedModel> buildModel(Combination combo) {
private List<BakedModel> buildModel(Combination combo) {
var mc = Minecraft.getInstance();
var modelManager = mc.getItemRenderer().getItemModelShaper().getModelManager();
@@ -97,19 +135,20 @@ public final class TurtleModelParts {
if (overlayModelLocation != null) {
parts.add(transform(ClientPlatformHelper.get().getModel(modelManager, overlayModelLocation), transformation));
}
if (combo.leftUpgrade() != null) {
var model = TurtleUpgradeModellers.getModel(combo.leftUpgrade(), null, TurtleSide.LEFT);
parts.add(transform(model.getModel(), transformation.compose(model.getMatrix())));
}
if (combo.rightUpgrade() != null) {
var model = TurtleUpgradeModellers.getModel(combo.rightUpgrade(), null, TurtleSide.RIGHT);
parts.add(transform(model.getModel(), transformation.compose(model.getMatrix())));
}
addUpgrade(parts, transformation, TurtleSide.LEFT, combo.leftUpgrade());
addUpgrade(parts, transformation, TurtleSide.RIGHT, combo.rightUpgrade());
return parts;
}
public BakedModel transform(BakedModel model, Transformation transformation) {
private void addUpgrade(List<BakedModel> parts, Transformation transformation, TurtleSide side, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
if (upgrade == null) return;
var model = TurtleUpgradeModellers.getModel(upgrade.upgrade(), upgrade.data(), side);
parts.add(transform(model.getModel(), transformation.compose(model.getMatrix())));
}
private BakedModel transform(BakedModel model, Transformation transformation) {
if (transformation.equals(Transformation.identity())) return model;
return transformCache.computeIfAbsent(new TransformedModel(model, transformation), transformer);
}

View File

@@ -10,6 +10,7 @@ import com.mojang.math.Axis;
import com.mojang.math.Transformation;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.model.turtle.ModelTransformer;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.computer.core.ComputerFamily;
@@ -30,6 +31,7 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import org.joml.Vector4f;
import javax.annotation.Nullable;
import java.util.List;
@@ -146,16 +148,30 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
renderModel(transform, renderer, lightmapCoord, overlayLight, ClientPlatformHelper.get().getModel(modelManager, modelLocation), tints);
}
/**
* Render a block model.
*
* @param transform The current matrix stack.
* @param renderer The buffer to write to.
* @param lightmapCoord The current lightmap coordinate.
* @param overlayLight The overlay light.
* @param model The model to render.
* @param tints Tints for the quads, as an array of RGB values.
* @see net.minecraft.client.renderer.block.ModelBlockRenderer#renderModel
*/
private void renderModel(PoseStack transform, VertexConsumer renderer, int lightmapCoord, int overlayLight, BakedModel model, @Nullable int[] tints) {
random.setSeed(0);
renderQuads(transform, renderer, lightmapCoord, overlayLight, model.getQuads(null, null, random), tints);
for (var facing : DirectionUtil.FACINGS) {
random.setSeed(42);
renderQuads(transform, renderer, lightmapCoord, overlayLight, model.getQuads(null, facing, random), tints);
}
random.setSeed(42);
renderQuads(transform, renderer, lightmapCoord, overlayLight, model.getQuads(null, null, random), tints);
}
private static void renderQuads(PoseStack transform, VertexConsumer buffer, int lightmapCoord, int overlayLight, List<BakedQuad> quads, @Nullable int[] tints) {
var matrix = transform.last();
var inverted = matrix.pose().determinant() < 0;
for (var bakedquad : quads) {
var tint = -1;
@@ -167,7 +183,50 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
var r = (float) (tint >> 16 & 255) / 255.0F;
var g = (float) (tint >> 8 & 255) / 255.0F;
var b = (float) (tint & 255) / 255.0F;
buffer.putBulkData(matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
if (inverted) {
putBulkQuadInvert(buffer, matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
} else {
buffer.putBulkData(matrix, bakedquad, r, g, b, lightmapCoord, overlayLight);
}
}
}
/**
* A version of {@link VertexConsumer#putBulkData(PoseStack.Pose, BakedQuad, float, float, float, int, int)} for
* when the matrix is inverted.
*
* @param buffer The buffer to draw to.
* @param pose The current matrix stack.
* @param quad The quad to draw.
* @param red The red tint of this quad.
* @param green The green tint of this quad.
* @param blue The blue tint of this quad.
* @param lightmapCoord The lightmap coordinate
* @param overlayLight The overlay light.
*/
private static void putBulkQuadInvert(VertexConsumer buffer, PoseStack.Pose pose, BakedQuad quad, float red, float green, float blue, int lightmapCoord, int overlayLight) {
var matrix = pose.pose();
// It's a little dubious to transform using this matrix rather than the normal matrix. This mirrors the logic in
// Direction.rotate (so not out of nowhere!), but is a little suspicious.
var dirNormal = quad.getDirection().getNormal();
var normal = matrix.transform(new Vector4f(dirNormal.getX(), dirNormal.getY(), dirNormal.getZ(), 0.0f)).normalize();
var vertices = quad.getVertices();
for (var vertex : ModelTransformer.ORDER) {
var i = vertex * ModelTransformer.STRIDE;
var x = Float.intBitsToFloat(vertices[i]);
var y = Float.intBitsToFloat(vertices[i + 1]);
var z = Float.intBitsToFloat(vertices[i + 2]);
var transformed = matrix.transform(new Vector4f(x, y, z, 1));
var u = Float.intBitsToFloat(vertices[i + 4]);
var v = Float.intBitsToFloat(vertices[i + 5]);
buffer.vertex(
transformed.x(), transformed.y(), transformed.z(),
red, green, blue, 1.0F, u, v, overlayLight, lightmapCoord,
normal.x(), normal.y(), normal.z()
);
}
}

View File

@@ -14,8 +14,8 @@ import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager;
import net.minecraft.client.Minecraft;
import net.minecraft.nbt.CompoundTag;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
@@ -52,12 +52,18 @@ public final class TurtleUpgradeModellers {
}
}
public static TransformedModel getModel(ITurtleUpgrade upgrade, @Nullable ITurtleAccess access, TurtleSide side) {
public static TransformedModel getModel(ITurtleUpgrade upgrade, ITurtleAccess access, TurtleSide side) {
@SuppressWarnings("unchecked")
var modeller = (TurtleUpgradeModeller<ITurtleUpgrade>) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller);
return modeller.getModel(upgrade, access, side);
}
public static TransformedModel getModel(ITurtleUpgrade upgrade, CompoundTag data, TurtleSide side) {
@SuppressWarnings("unchecked")
var modeller = (TurtleUpgradeModeller<ITurtleUpgrade>) modelCache.computeIfAbsent(upgrade, TurtleUpgradeModellers::getModeller);
return modeller.getModel(upgrade, data, side);
}
private static TurtleUpgradeModeller<?> getModeller(ITurtleUpgrade upgradeA) {
var wrapper = TurtleUpgrades.instance().getWrapper(upgradeA);
if (wrapper == null) return NULL_TURTLE_MODELLER;

View File

@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.data.client;
import dan200.computercraft.data.DataProviders;
import dan200.computercraft.shared.turtle.inventory.UpgradeSlot;
import net.minecraft.client.renderer.texture.atlas.SpriteSources;
import net.minecraft.client.renderer.texture.atlas.sources.SingleFile;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import java.util.List;
import java.util.Optional;
/**
* A version of {@link DataProviders} which relies on client-side classes.
* <p>
* This is called from {@link DataProviders#add(DataProviders.GeneratorSink)}.
*/
public final class ClientDataProviders {
private ClientDataProviders() {
}
public static void add(DataProviders.GeneratorSink generator) {
generator.addFromCodec("Block atlases", PackType.CLIENT_RESOURCES, "atlases", SpriteSources.FILE_CODEC, out -> {
out.accept(new ResourceLocation("blocks"), List.of(
new SingleFile(UpgradeSlot.LEFT_UPGRADE, Optional.empty()),
new SingleFile(UpgradeSlot.RIGHT_UPGRADE, Optional.empty())
));
});
}
}

View File

@@ -37,6 +37,14 @@ import static net.minecraft.data.models.model.ModelLocationUtils.getModelLocatio
import static net.minecraft.data.models.model.TextureMapping.getBlockTexture;
class BlockModelProvider {
private static final TextureSlot CURSOR = TextureSlot.create("cursor");
private static final ModelTemplate COMPUTER_ON = new ModelTemplate(
Optional.of(new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/computer_on")),
Optional.empty(),
TextureSlot.FRONT, TextureSlot.SIDE, TextureSlot.TOP, CURSOR
);
private static final ModelTemplate MONITOR_BASE = new ModelTemplate(
Optional.of(new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/monitor_base")),
Optional.empty(),
@@ -142,11 +150,18 @@ class BlockModelProvider {
private static void registerComputer(BlockModelGenerators generators, ComputerBlock<?> block) {
generators.blockStateOutput.accept(MultiVariantGenerator.multiVariant(block)
.with(createHorizontalFacingDispatch())
.with(createModelDispatch(ComputerBlock.STATE, state -> ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(
block, "_" + state.getSerializedName(),
TextureMapping.orientableCube(block).put(TextureSlot.FRONT, getBlockTexture(block, "_front" + state.getTexture())),
generators.modelOutput
)))
.with(createModelDispatch(ComputerBlock.STATE, state -> switch (state) {
case OFF -> ModelTemplates.CUBE_ORIENTABLE.createWithSuffix(
block, "_" + state.getSerializedName(),
TextureMapping.orientableCube(block),
generators.modelOutput
);
case ON, BLINKING -> COMPUTER_ON.createWithSuffix(
block, "_" + state.getSerializedName(),
TextureMapping.orientableCube(block).put(CURSOR, new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/computer" + state.getTexture())),
generators.modelOutput
);
}))
);
generators.delegateItemModel(block, getModelLocation(block, "_blinking"));
}

View File

@@ -4,16 +4,20 @@
package dan200.computercraft.data;
import com.mojang.serialization.Codec;
import net.minecraft.data.DataProvider;
import net.minecraft.data.PackOutput;
import net.minecraft.data.loot.LootTableProvider.SubProviderEntry;
import net.minecraft.data.models.BlockModelGenerators;
import net.minecraft.data.models.ItemModelGenerators;
import net.minecraft.data.tags.TagsProvider;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@@ -37,11 +41,22 @@ public final class DataProviders {
generator.models(BlockModelProvider::addBlockModels, ItemModelProvider::addItemModels);
generator.add(out -> new LanguageProvider(out, turtleUpgrades, pocketUpgrades));
// Unfortunately we rely on some client-side classes in this code. We just load in the client side data provider
// and invoke that.
try {
Class.forName("dan200.computercraft.data.client.ClientDataProviders")
.getMethod("add", GeneratorSink.class).invoke(null, generator);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
interface GeneratorSink {
public interface GeneratorSink {
<T extends DataProvider> T add(DataProvider.Factory<T> factory);
<T> void addFromCodec(String name, PackType type, String directory, Codec<T> codec, Consumer<BiConsumer<ResourceLocation, T>> output);
void lootTable(List<SubProviderEntry> tables);
TagsProvider<Block> blockTags(Consumer<TagProvider.TagConsumer<Block>> tags);

View File

@@ -8,6 +8,7 @@ import com.google.gson.JsonObject;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.pocket.PocketUpgradeDataProvider;
import dan200.computercraft.api.turtle.TurtleUpgradeDataProvider;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.common.IColouredItem;
@@ -110,7 +111,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT);
for (var upgrade : turtleUpgrades.getGeneratedUpgrades()) {
var result = turtleItem.create(-1, null, -1, null, upgrade, -1, null);
var result = turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), -1, null);
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, result.getItem())
.group(String.format("%s:turtle_%s", ComputerCraftAPI.MOD_ID, nameId))
@@ -146,7 +147,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
var nameId = pocket.getFamily().name().toLowerCase(Locale.ROOT);
for (var upgrade : pocketUpgrades.getGeneratedUpgrades()) {
var result = pocket.create(-1, null, -1, upgrade);
var result = pocket.create(-1, null, -1, UpgradeData.ofDefault(upgrade));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, result.getItem())
.group(String.format("%s:pocket_%s", ComputerCraftAPI.MOD_ID, nameId))

View File

@@ -19,8 +19,6 @@ import dan200.computercraft.api.pocket.PocketUpgradeSerialiser;
import dan200.computercraft.api.redstone.BundledRedstoneProvider;
import dan200.computercraft.api.turtle.TurtleRefuelHandler;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.core.apis.ApiFactories;
import dan200.computercraft.core.asm.GenericMethod;
import dan200.computercraft.core.filesystem.WritableFileMount;
import dan200.computercraft.impl.detail.DetailRegistryImpl;
import dan200.computercraft.impl.network.wired.WiredNodeImpl;
@@ -79,7 +77,7 @@ public abstract class AbstractComputerCraftAPI implements ComputerCraftAPIServic
@Override
public final void registerGenericSource(GenericSource source) {
GenericMethod.register(source);
GenericSources.register(source);
}
@Override

View File

@@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis;
package dan200.computercraft.impl;
import dan200.computercraft.api.lua.ILuaAPIFactory;
@@ -11,19 +11,24 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Objects;
/**
* The global factory for {@link ILuaAPIFactory}s.
*
* @see dan200.computercraft.core.ComputerContext.Builder#apiFactories(Collection)
* @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory)
*/
public final class ApiFactories {
private ApiFactories() {
}
private static final Collection<ILuaAPIFactory> factories = new LinkedHashSet<>();
private static final Collection<ILuaAPIFactory> factoriesView = Collections.unmodifiableCollection(factories);
public static synchronized void register(ILuaAPIFactory factory) {
static synchronized void register(ILuaAPIFactory factory) {
Objects.requireNonNull(factory, "provider cannot be null");
factories.add(factory);
}
public static Iterable<ILuaAPIFactory> getAll() {
return factoriesView;
public static Collection<ILuaAPIFactory> getAll() {
return Collections.unmodifiableCollection(factories);
}
}

View File

@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.impl;
import dan200.computercraft.api.lua.GenericSource;
import dan200.computercraft.core.asm.GenericMethod;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
/**
* The global registry for {@link GenericSource}s.
*
* @see dan200.computercraft.core.ComputerContext.Builder#genericMethods(Collection)
* @see dan200.computercraft.api.ComputerCraftAPI#registerGenericSource(GenericSource)
*/
public final class GenericSources {
private GenericSources() {
}
private static final Collection<GenericSource> sources = new LinkedHashSet<>();
static synchronized void register(GenericSource source) {
Objects.requireNonNull(source, "provider cannot be null");
sources.add(source);
}
public static Collection<GenericMethod> getAllMethods() {
return sources.stream().flatMap(GenericMethod::getMethods).toList();
}
}

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.impl;
import com.google.gson.*;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.api.upgrades.UpgradeSerialiser;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.core.Registry;
@@ -74,13 +75,13 @@ public class UpgradeManager<R extends UpgradeSerialiser<? extends T>, T extends
}
@Nullable
public T get(ItemStack stack) {
public UpgradeData<T> get(ItemStack stack) {
if (stack.isEmpty()) return null;
for (var wrapper : current.values()) {
var craftingStack = wrapper.upgrade().getCraftingItem();
if (!craftingStack.isEmpty() && craftingStack.getItem() == stack.getItem() && wrapper.upgrade().isItemSuitable(stack)) {
return wrapper.upgrade();
return UpgradeData.of(wrapper.upgrade, wrapper.upgrade.getUpgradeData(stack));
}
}

View File

@@ -11,6 +11,7 @@ import dan200.computercraft.api.detail.VanillaDetailRegistries;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.pocket.PocketUpgradeSerialiser;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades;
@@ -446,12 +447,12 @@ public final class ModRegistry {
private static void addTurtle(CreativeModeTab.Output out, TurtleItem turtle) {
out.accept(turtle.create(-1, null, -1, null, null, 0, null));
TurtleUpgrades.getVanillaUpgrades()
.map(x -> turtle.create(-1, null, -1, null, x, 0, null))
.map(x -> turtle.create(-1, null, -1, null, UpgradeData.ofDefault(x), 0, null))
.forEach(out::accept);
}
private static void addPocket(CreativeModeTab.Output out, PocketComputerItem pocket) {
out.accept(pocket.create(-1, null, -1, null));
PocketUpgrades.getVanillaUpgrades().map(x -> pocket.create(-1, null, -1, x)).forEach(out::accept);
PocketUpgrades.getVanillaUpgrades().map(x -> pocket.create(-1, null, -1, UpgradeData.ofDefault(x))).forEach(out::accept);
}
}

View File

@@ -6,9 +6,12 @@ package dan200.computercraft.shared.command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.command.arguments.ComputersArgumentType;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -169,7 +172,10 @@ public final class CommandComputerCraft {
.then(command("queue")
.requires(UserLevel.ANYONE)
.arg("computer", manyComputers())
.arg(
RequiredArgumentBuilder.<CommandSourceStack, ComputersArgumentType.ComputersSupplier>argument("computer", manyComputers())
.suggests((context, builder) -> Suggestions.empty())
)
.argManyValue("args", StringArgumentType.string(), Collections.emptyList())
.executes((ctx, args) -> {
var computers = getComputersArgument(ctx, "computer");

View File

@@ -49,6 +49,29 @@ public enum UserLevel implements Predicate<CommandSourceStack> {
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;
}
private static boolean isOwner(CommandSourceStack source) {
var server = source.getServer();
var sender = source.getEntity();

View File

@@ -18,7 +18,6 @@ import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.network.FriendlyByteBuf;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@@ -159,14 +158,14 @@ public final class ComputersArgumentType implements ArgumentType<ComputersArgume
}
@Override
public ComputersArgumentType.Template unpack(@NotNull ComputersArgumentType argumentType) {
public ComputersArgumentType.Template unpack(ComputersArgumentType argumentType) {
return new ComputersArgumentType.Template(this, argumentType.requireSome);
}
}
public record Template(Info info, boolean requireSome) implements ArgumentTypeInfo.Template<ComputersArgumentType> {
@Override
public ComputersArgumentType instantiate(@NotNull CommandBuildContext context) {
public ComputersArgumentType instantiate(CommandBuildContext context) {
return requireSome ? SOME : MANY;
}

View File

@@ -17,7 +17,6 @@ import net.minecraft.commands.synchronization.ArgumentTypeInfo;
import net.minecraft.commands.synchronization.ArgumentTypeInfos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
@@ -144,7 +143,7 @@ public final class RepeatArgumentType<T, U> implements ArgumentType<List<T>> {
) implements ArgumentTypeInfo.Template<RepeatArgumentType<?, ?>> {
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public RepeatArgumentType<?, ?> instantiate(@NotNull CommandBuildContext commandBuildContext) {
public RepeatArgumentType<?, ?> instantiate(CommandBuildContext commandBuildContext) {
var child = child().instantiate(commandBuildContext);
return flatten ? RepeatArgumentType.someFlat((ArgumentType) child, some()) : RepeatArgumentType.some(child, some());
}

View File

@@ -48,11 +48,15 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
return this;
}
public CommandBuilder<S> arg(String name, ArgumentType<?> type) {
args.add(RequiredArgumentBuilder.argument(name, type));
public CommandBuilder<S> arg(ArgumentBuilder<S, ?> arg) {
args.add(arg);
return this;
}
public CommandBuilder<S> arg(String name, ArgumentType<?> type) {
return arg(RequiredArgumentBuilder.argument(name, type));
}
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, List<T> empty) {
return argMany(name, type, () -> empty);
}
@@ -74,7 +78,7 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
return command -> {
// The node for no arguments
var tail = tail(ctx -> command.run(ctx, empty.get()));
var tail = setupTail(ctx -> command.run(ctx, empty.get()));
// The node for one or more arguments
ArgumentBuilder<S, ?> moreArg = RequiredArgumentBuilder
@@ -83,7 +87,7 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
// Chain all of them together!
tail.then(moreArg);
return link(tail);
return buildTail(tail);
};
}
@@ -94,20 +98,16 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
@Override
public CommandNode<S> executes(Command<S> command) {
if (args.isEmpty()) throw new IllegalStateException("Cannot have empty arg chain builder");
return link(tail(command));
return buildTail(setupTail(command));
}
private ArgumentBuilder<S, ?> tail(Command<S> command) {
var defaultTail = args.get(args.size() - 1);
defaultTail.executes(command);
if (requires != null) defaultTail.requires(requires);
return defaultTail;
private ArgumentBuilder<S, ?> setupTail(Command<S> command) {
return args.get(args.size() - 1).executes(command);
}
private CommandNode<S> link(ArgumentBuilder<S, ?> tail) {
private CommandNode<S> buildTail(ArgumentBuilder<S, ?> tail) {
for (var i = args.size() - 2; i >= 0; i--) tail = args.get(i).then(tail);
if (requires != null) tail.requires(requires);
return tail.build();
}
}

View File

@@ -10,6 +10,7 @@ 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;
@@ -18,6 +19,8 @@ import net.minecraft.network.chat.Component;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static dan200.computercraft.core.util.Nullability.assertNonNull;
import static dan200.computercraft.shared.command.text.ChatHelpers.coloured;
@@ -37,6 +40,29 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
return new HelpingArgumentBuilder(literal);
}
@Override
public LiteralArgumentBuilder<CommandSourceStack> requires(Predicate<CommandSourceStack> requirement) {
throw new IllegalStateException("Cannot use requires on a HelpingArgumentBuilder");
}
@Override
public Predicate<CommandSourceStack> getRequirement() {
// The requirement of this node is the union of all child's requirements.
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;
}
@Override
public LiteralArgumentBuilder<CommandSourceStack> executes(final Command<CommandSourceStack> command) {
throw new IllegalStateException("Cannot use executes on a HelpingArgumentBuilder");
@@ -80,9 +106,7 @@ public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<Command
helpCommand.node = node;
// Set up a /... help command
var helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal("help")
.requires(x -> getArguments().stream().anyMatch(y -> y.getRequirement().test(x)))
.executes(helpCommand);
var helpNode = LiteralArgumentBuilder.<CommandSourceStack>literal("help").executes(helpCommand);
// Add all normal command children to this and the help node
for (var child : getArguments()) {

View File

@@ -9,13 +9,16 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.network.PacketNetwork;
import dan200.computercraft.core.ComputerContext;
import dan200.computercraft.core.computer.ComputerThread;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.computer.mainthread.MainThread;
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
import dan200.computercraft.core.lua.CobaltLuaMachine;
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.impl.AbstractComputerCraftAPI;
import dan200.computercraft.impl.ApiFactories;
import dan200.computercraft.impl.GenericSources;
import dan200.computercraft.shared.CommonHooks;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import dan200.computercraft.shared.config.ConfigSpec;
@@ -67,11 +70,13 @@ public final class ServerContext {
this.server = server;
storageDir = server.getWorldPath(FOLDER);
mainThread = new MainThread(mainThreadConfig);
context = new ComputerContext(
new Environment(server),
new ComputerThread(ConfigSpec.computerThreads.get()),
mainThread, luaMachine
);
context = ComputerContext.builder(new Environment(server))
.computerThreads(ConfigSpec.computerThreads.get())
.mainThreadScheduler(mainThread)
.luaFactory(luaMachine)
.apiFactories(ApiFactories.getAll())
.genericMethods(GenericSources.getAllMethods())
.build();
idAssigner = new IDAssigner(storageDir.resolve("ids.json"));
}
@@ -133,6 +138,16 @@ public final class ServerContext {
return context;
}
/**
* Get the {@link MethodSupplier} used to find methods on peripherals.
*
* @return The {@link PeripheralMethod} method supplier.
* @see ComputerContext#peripheralMethods()
*/
public MethodSupplier<PeripheralMethod> peripheralMethods() {
return context.peripheralMethods();
}
/**
* Tick all components of this server context. This should <em>NOT</em> be called outside of {@link CommonHooks}.
*/

View File

@@ -7,7 +7,7 @@ package dan200.computercraft.shared.computer.upload;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.ByteBufferChannel;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.methods.ObjectSource;
import java.nio.ByteBuffer;
import java.util.Collections;

View File

@@ -4,21 +4,19 @@
package dan200.computercraft.shared.config;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.InMemoryCommentedFormat;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
import dan200.computercraft.core.apis.http.options.Action;
import dan200.computercraft.core.apis.http.options.AddressRule;
import dan200.computercraft.core.apis.http.options.InvalidRuleException;
import dan200.computercraft.core.apis.http.options.PartialOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.Locale;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.concurrent.ConcurrentHashMap;
import java.util.*;
import java.util.function.Consumer;
/**
* Parses, checks and generates {@link Config}s for {@link AddressRule}.
@@ -26,49 +24,65 @@ import java.util.concurrent.ConcurrentHashMap;
class AddressRuleConfig {
private static final Logger LOG = LoggerFactory.getLogger(AddressRuleConfig.class);
public static UnmodifiableConfig makeRule(String host, Action action) {
var config = InMemoryCommentedFormat.defaultInstance().createConfig(ConcurrentHashMap::new);
config.add("host", host);
config.add("action", action.name().toLowerCase(Locale.ROOT));
private static final AddressRule REJECT_ALL = AddressRule.parse("*", OptionalInt.empty(), Action.DENY.toPartial());
if (host.equals("*") && action == Action.ALLOW) {
config.setComment("max_download", """
The maximum size (in bytes) that a computer can download in a single request.
Note that responses may receive more data than allowed, but this data will not
be returned to the client.""");
config.set("max_download", AddressRule.MAX_DOWNLOAD);
public static List<UnmodifiableConfig> defaultRules() {
return List.of(
makeRule(config -> {
config.setComment("host", """
The magic "$private" host matches all private address ranges, such as localhost and 192.168.0.0/16.
This rule prevents computers accessing internal services, and is strongly recommended.""");
config.add("host", "$private");
config.setComment("max_upload", """
The maximum size (in bytes) that a computer can upload in a single request. This
includes headers and POST text.""");
config.set("max_upload", AddressRule.MAX_UPLOAD);
config.setComment("action", "Deny all requests to private IP addresses.");
config.add("action", Action.DENY.name().toLowerCase(Locale.ROOT));
}),
makeRule(config -> {
config.setComment("host", """
The wildcard "*" rule matches all remaining hosts.""");
config.add("host", "*");
config.setComment("max_websocket_message", "The maximum size (in bytes) that a computer can send or receive in one websocket packet.");
config.set("max_websocket_message", AddressRule.WEBSOCKET_MESSAGE);
config.setComment("action", "Allow all non-denied hosts.");
config.add("action", Action.ALLOW.name().toLowerCase(Locale.ROOT));
config.setComment("use_proxy", "Enable use of the HTTP/SOCKS proxy if it is configured.");
config.set("use_proxy", false);
}
config.setComment("max_download", """
The maximum size (in bytes) that a computer can download in a single request.
Note that responses may receive more data than allowed, but this data will not
be returned to the client.""");
config.set("max_download", AddressRule.MAX_DOWNLOAD);
config.setComment("max_upload", """
The maximum size (in bytes) that a computer can upload in a single request. This
includes headers and POST text.""");
config.set("max_upload", AddressRule.MAX_UPLOAD);
config.setComment("max_websocket_message", "The maximum size (in bytes) that a computer can send or receive in one websocket packet.");
config.set("max_websocket_message", AddressRule.WEBSOCKET_MESSAGE);
config.setComment("use_proxy", "Enable use of the HTTP/SOCKS proxy if it is configured.");
config.set("use_proxy", false);
})
);
}
private static UnmodifiableConfig makeRule(Consumer<CommentedConfig> setup) {
var config = InMemoryCommentedFormat.defaultInstance().createConfig(LinkedHashMap::new);
setup.accept(config);
return config;
}
public static boolean checkRule(UnmodifiableConfig builder) {
var hostObj = get(builder, "host", String.class).orElse(null);
var port = unboxOptInt(get(builder, "port", Number.class));
return hostObj != null && checkEnum(builder, "action", Action.class)
&& check(builder, "port", Number.class)
&& check(builder, "max_upload", Number.class)
&& check(builder, "max_download", Number.class)
&& check(builder, "websocket_message", Number.class)
&& check(builder, "use_proxy", Boolean.class)
&& AddressRule.parse(hostObj, port, PartialOptions.DEFAULT) != null;
public static AddressRule parseRule(UnmodifiableConfig builder) {
try {
return doParseRule(builder);
} catch (InvalidRuleException e) {
LOG.error("Malformed HTTP rule: {} HTTP will NOT work until this is fixed.", e.getMessage());
return REJECT_ALL;
}
}
@Nullable
public static AddressRule parseRule(UnmodifiableConfig builder) {
public static AddressRule doParseRule(UnmodifiableConfig builder) {
var hostObj = get(builder, "host", String.class).orElse(null);
if (hostObj == null) return null;
if (hostObj == null) throw new InvalidRuleException("No 'host' specified");
var action = getEnum(builder, "action", Action.class).orElse(null);
var port = unboxOptInt(get(builder, "port", Number.class));
@@ -88,38 +102,19 @@ class AddressRuleConfig {
return AddressRule.parse(hostObj, port, options);
}
private static <T> boolean check(UnmodifiableConfig config, String field, Class<T> klass) {
var value = config.get(field);
if (value == null || klass.isInstance(value)) return true;
LOG.warn("HTTP rule's {} is not a {}.", field, klass.getSimpleName());
return false;
}
private static <T extends Enum<T>> boolean checkEnum(UnmodifiableConfig config, String field, Class<T> klass) {
var value = config.get(field);
if (value == null) return true;
if (!(value instanceof String)) {
LOG.warn("HTTP rule's {} is not a string", field);
return false;
}
if (parseEnum(klass, (String) value) == null) {
LOG.warn("HTTP rule's {} is not a known option", field);
return false;
}
return true;
}
private static <T> Optional<T> get(UnmodifiableConfig config, String field, Class<T> klass) {
var value = config.get(field);
return klass.isInstance(value) ? Optional.of(klass.cast(value)) : Optional.empty();
if (value == null) return Optional.empty();
if (klass.isInstance(value)) return Optional.of(klass.cast(value));
throw new InvalidRuleException(String.format(
"Field '%s' should be a '%s' but is a %s.",
field, klass.getSimpleName(), value.getClass().getSimpleName()
));
}
private static <T extends Enum<T>> Optional<T> getEnum(UnmodifiableConfig config, String field, Class<T> klass) {
return get(config, field, String.class).map(x -> parseEnum(klass, x));
return get(config, field, String.class).map(x -> parseEnum(field, klass, x));
}
private static OptionalLong unboxOptLong(Optional<? extends Number> value) {
@@ -130,11 +125,14 @@ class AddressRuleConfig {
return value.map(Number::intValue).map(OptionalInt::of).orElse(OptionalInt.empty());
}
@Nullable
private static <T extends Enum<T>> T parseEnum(Class<T> klass, String x) {
private static <T extends Enum<T>> T parseEnum(String field, Class<T> klass, String x) {
for (var value : klass.getEnumConstants()) {
if (value.name().equalsIgnoreCase(x)) return value;
}
return null;
throw new InvalidRuleException(String.format(
"Field '%s' should be one of %s, but is '%s'.",
field, Arrays.stream(klass.getEnumConstants()).map(Enum::name).toList(), x
));
}
}

View File

@@ -9,7 +9,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.options.Action;
import dan200.computercraft.core.apis.http.options.ProxyType;
import dan200.computercraft.core.computer.mainthread.MainThreadConfig;
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer;
@@ -20,9 +19,7 @@ import org.apache.logging.log4j.core.filter.MarkerFilter;
import javax.annotation.Nullable;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class ConfigSpec {
@@ -182,9 +179,9 @@ public final class ConfigSpec {
httpEnabled = builder
.comment("""
Enable the "http" API on Computers. This also disables the "pastebin" and "wget"
programs, that many users rely on. It's recommended to leave this on and use the
"rules" config option to impose more fine-grained control.""")
Enable the "http" API on Computers. Disabling this also disables the "pastebin" and
"wget" programs, that many users rely on. It's recommended to leave this on and use
the "rules" config option to impose more fine-grained control.""")
.define("enabled", CoreConfig.httpEnabled);
httpWebsocketEnabled = builder
@@ -194,16 +191,23 @@ public final class ConfigSpec {
httpRules = builder
.comment("""
A list of rules which control behaviour of the "http" API for specific domains or
IPs. Each rule is an item with a 'host' to match against, and a series of
properties. Rules are evaluated in order, meaning earlier rules override later
ones.
The host may be a domain name ("pastebin.com"), wildcard ("*.pastebin.com") or
CIDR notation ("127.0.0.0/8").
If no rules, the domain is blocked.""")
.defineList("rules", Arrays.asList(
AddressRuleConfig.makeRule("$private", Action.DENY),
AddressRuleConfig.makeRule("*", Action.ALLOW)
), x -> x instanceof UnmodifiableConfig && AddressRuleConfig.checkRule((UnmodifiableConfig) x));
IPs. Each rule matches against a hostname and an optional port, and then sets several
properties for the request. Rules are evaluated in order, meaning earlier rules override
later ones.
Valid properties:
- "host" (required): The domain or IP address this rule matches. This may be a domain name
("pastebin.com"), wildcard ("*.pastebin.com") or CIDR notation ("127.0.0.0/8").
- "port" (optional): Only match requests for a specific port, such as 80 or 443.
- "action" (optional): Whether to allow or deny this request.
- "max_download" (optional): The maximum size (in bytes) that a computer can download in this
request.
- "max_upload" (optional): The maximum size (in bytes) that a computer can upload in a this request.
- "max_websocket_message" (optional): The maximum size (in bytes) that a computer can send or
receive in one websocket packet.
- "use_proxy" (optional): Enable use of the HTTP/SOCKS proxy if it is configured.""")
.defineList("rules", AddressRuleConfig.defaultRules(), x -> x instanceof UnmodifiableConfig);
httpMaxRequests = builder
.comment("""
@@ -395,8 +399,8 @@ public final class ConfigSpec {
// HTTP
CoreConfig.httpEnabled = httpEnabled.get();
CoreConfig.httpWebsocketEnabled = httpWebsocketEnabled.get();
CoreConfig.httpRules = httpRules.get().stream()
.map(AddressRuleConfig::parseRule).filter(Objects::nonNull).toList();
CoreConfig.httpRules = httpRules.get().stream().map(AddressRuleConfig::parseRule).toList();
CoreConfig.httpMaxRequests = httpMaxRequests.get();
CoreConfig.httpMaxWebsockets = httpMaxWebsockets.get();

View File

@@ -7,7 +7,6 @@ package dan200.computercraft.shared.container;
import net.minecraft.core.NonNullList;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
@@ -16,24 +15,6 @@ import net.minecraft.world.item.ItemStack;
public interface BasicContainer extends Container {
NonNullList<ItemStack> getContents();
@Override
default int getMaxStackSize() {
return 64;
}
@Override
default void startOpen(Player player) {
}
@Override
default void stopOpen(Player player) {
}
@Override
default boolean canPlaceItem(int slot, ItemStack stack) {
return true;
}
@Override
default int getContainerSize() {
return getContents().size();

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.shared.integration;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.ModRegistry;
@@ -56,14 +57,14 @@ public final class RecipeModHelpers {
for (var turtleSupplier : TURTLES) {
var turtle = turtleSupplier.get();
for (var upgrade : TurtleUpgrades.instance().getUpgrades()) {
upgradeItems.add(turtle.create(-1, null, -1, null, upgrade, 0, null));
upgradeItems.add(turtle.create(-1, null, -1, null, UpgradeData.ofDefault(upgrade), 0, null));
}
}
for (var pocketSupplier : POCKET_COMPUTERS) {
var pocket = pocketSupplier.get();
for (var upgrade : PocketUpgrades.instance().getUpgrades()) {
upgradeItems.add(pocket.create(-1, null, -1, upgrade));
upgradeItems.add(pocket.create(-1, null, -1, UpgradeData.ofDefault(upgrade)));
}
}

View File

@@ -9,6 +9,7 @@ import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
@@ -111,20 +112,22 @@ public class UpgradeRecipeGenerator<T> {
if (stack.getItem() instanceof TurtleItem item) {
// Suggest possible upgrades which can be applied to this turtle
var left = item.getUpgrade(stack, TurtleSide.LEFT);
var right = item.getUpgrade(stack, TurtleSide.RIGHT);
var left = item.getUpgradeWithData(stack, TurtleSide.LEFT);
var right = item.getUpgradeWithData(stack, TurtleSide.RIGHT);
if (left != null && right != null) return Collections.emptyList();
List<T> recipes = new ArrayList<>();
var ingredient = Ingredient.of(stack);
for (var upgrade : turtleUpgrades) {
if (upgrade.turtle == null) throw new NullPointerException();
// The turtle is facing towards us, so upgrades on the left are actually crafted on the right.
if (left == null) {
recipes.add(turtle(ingredient, upgrade.ingredient, turtleWith(stack, upgrade.turtle, right)));
recipes.add(turtle(ingredient, upgrade.ingredient, turtleWith(stack, UpgradeData.ofDefault(upgrade.turtle), right)));
}
if (right == null) {
recipes.add(turtle(upgrade.ingredient, ingredient, turtleWith(stack, left, upgrade.turtle)));
recipes.add(turtle(upgrade.ingredient, ingredient, turtleWith(stack, left, UpgradeData.ofDefault(upgrade.turtle))));
}
}
@@ -137,7 +140,8 @@ public class UpgradeRecipeGenerator<T> {
List<T> recipes = new ArrayList<>();
var ingredient = Ingredient.of(stack);
for (var upgrade : pocketUpgrades) {
recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, upgrade.pocket)));
if (upgrade.pocket == null) throw new NullPointerException();
recipes.add(pocket(upgrade.ingredient, ingredient, pocketWith(stack, UpgradeData.ofDefault(upgrade.pocket))));
}
return Collections.unmodifiableList(recipes);
@@ -180,21 +184,21 @@ public class UpgradeRecipeGenerator<T> {
if (stack.getItem() instanceof TurtleItem item) {
List<T> recipes = new ArrayList<>(0);
var left = item.getUpgrade(stack, TurtleSide.LEFT);
var right = item.getUpgrade(stack, TurtleSide.RIGHT);
var left = item.getUpgradeWithData(stack, TurtleSide.LEFT);
var right = item.getUpgradeWithData(stack, TurtleSide.RIGHT);
// The turtle is facing towards us, so upgrades on the left are actually crafted on the right.
if (left != null) {
recipes.add(turtle(
Ingredient.of(turtleWith(stack, null, right)),
Ingredient.of(left.getCraftingItem()),
Ingredient.of(left.getUpgradeItem()),
stack
));
}
if (right != null) {
recipes.add(turtle(
Ingredient.of(right.getCraftingItem()),
Ingredient.of(right.getUpgradeItem()),
Ingredient.of(turtleWith(stack, left, null)),
stack
));
@@ -204,9 +208,9 @@ public class UpgradeRecipeGenerator<T> {
} else if (stack.getItem() instanceof PocketComputerItem) {
List<T> recipes = new ArrayList<>(0);
var back = PocketComputerItem.getUpgrade(stack);
var back = PocketComputerItem.getUpgradeWithData(stack);
if (back != null) {
recipes.add(pocket(Ingredient.of(back.getCraftingItem()), Ingredient.of(pocketWith(stack, null)), stack));
recipes.add(pocket(Ingredient.of(back.getUpgradeItem()), Ingredient.of(pocketWith(stack, null)), stack));
}
return Collections.unmodifiableList(recipes);
@@ -215,7 +219,7 @@ public class UpgradeRecipeGenerator<T> {
}
}
private static ItemStack turtleWith(ItemStack stack, @Nullable ITurtleUpgrade left, @Nullable ITurtleUpgrade right) {
private static ItemStack turtleWith(ItemStack stack, @Nullable UpgradeData<ITurtleUpgrade> left, @Nullable UpgradeData<ITurtleUpgrade> right) {
var item = (TurtleItem) stack.getItem();
return item.create(
item.getComputerID(stack), item.getLabel(stack), item.getColour(stack),
@@ -223,7 +227,7 @@ public class UpgradeRecipeGenerator<T> {
);
}
private static ItemStack pocketWith(ItemStack stack, @Nullable IPocketUpgrade back) {
private static ItemStack pocketWith(ItemStack stack, @Nullable UpgradeData<IPocketUpgrade> back) {
var item = (PocketComputerItem) stack.getItem();
return item.create(
item.getComputerID(stack), item.getLabel(stack), item.getColour(stack), back
@@ -272,7 +276,7 @@ public class UpgradeRecipeGenerator<T> {
recipes.add(turtle(
ingredient, // Right upgrade, recipe on left
Ingredient.of(turtleItem.create(-1, null, -1, null, null, 0, null)),
turtleItem.create(-1, null, -1, null, turtle, 0, null)
turtleItem.create(-1, null, -1, null, UpgradeData.ofDefault(turtle), 0, null)
));
}
}
@@ -283,7 +287,7 @@ public class UpgradeRecipeGenerator<T> {
recipes.add(pocket(
ingredient,
Ingredient.of(pocketItem.create(-1, null, -1, null)),
pocketItem.create(-1, null, -1, pocket)
pocketItem.create(-1, null, -1, UpgradeData.ofDefault(pocket))
));
}
}

View File

@@ -12,19 +12,23 @@ import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.platform.RegistryWrappers;
import net.minecraft.core.Direction;
import net.minecraft.world.level.block.entity.BlockEntity;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Set;
class GenericPeripheral implements IDynamicPeripheral {
public final class GenericPeripheral implements IDynamicPeripheral {
private final BlockEntity tile;
private final Direction side;
private final String type;
private final Set<String> additionalTypes;
private final BlockEntity tile;
private final List<SaturatedMethod> methods;
GenericPeripheral(BlockEntity tile, @Nullable String name, Set<String> additionalTypes, List<SaturatedMethod> methods) {
GenericPeripheral(BlockEntity tile, Direction side, @Nullable String name, Set<String> additionalTypes, List<SaturatedMethod> methods) {
this.side = side;
var type = RegistryWrappers.BLOCK_ENTITY_TYPES.getKey(tile.getType());
this.tile = tile;
this.type = name != null ? name : type.toString();
@@ -32,6 +36,10 @@ class GenericPeripheral implements IDynamicPeripheral {
this.methods = methods;
}
public Direction side() {
return side;
}
@Override
public String[] getMethodNames() {
var names = new String[methods.size()];
@@ -54,7 +62,6 @@ class GenericPeripheral implements IDynamicPeripheral {
return additionalTypes;
}
@Nullable
@Override
public Object getTarget() {
return tile;

View File

@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.generic;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.PeripheralType;
import dan200.computercraft.core.methods.MethodSupplier;
import dan200.computercraft.core.methods.NamedMethod;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.shared.computer.core.ServerContext;
import net.minecraft.core.Direction;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.block.entity.BlockEntity;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* A builder for a {@link GenericPeripheral}.
* <p>
* This handles building a list of {@linkplain SaturatedMethod methods} and computing the appropriate
* {@link PeripheralType} from the {@linkplain NamedMethod#genericType() methods' peripheral types}.
* <p>
* See the platform-specific peripheral providers for the usage of this.
*/
final class GenericPeripheralBuilder {
private final MethodSupplier<PeripheralMethod> peripheralMethods;
private @Nullable String name;
private final Set<String> additionalTypes = new HashSet<>(0);
private final ArrayList<SaturatedMethod> methods = new ArrayList<>();
GenericPeripheralBuilder(MinecraftServer server) {
peripheralMethods = ServerContext.get(server).peripheralMethods();
}
@Nullable
IPeripheral toPeripheral(BlockEntity blockEntity, Direction side) {
if (methods.isEmpty()) return null;
methods.trimToSize();
return new GenericPeripheral(blockEntity, side, name, additionalTypes, methods);
}
boolean addMethods(Object target) {
return peripheralMethods.forEachSelfMethod(target, (name, method, info) -> {
methods.add(new SaturatedMethod(target, name, method));
// If we have a peripheral type, use it. Always pick the smallest one, so it's consistent (assuming mods
// don't change).
var type = info == null ? null : info.genericType();
if (type != null && type.getPrimaryType() != null) {
var primaryType = type.getPrimaryType();
if (this.name == null || this.name.compareTo(primaryType) > 0) this.name = primaryType;
}
if (type != null) additionalTypes.addAll(type.getAdditionalTypes());
});
}
}

View File

@@ -9,18 +9,20 @@ import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.MethodResult;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.core.asm.NamedMethod;
import dan200.computercraft.core.asm.PeripheralMethod;
import dan200.computercraft.core.methods.PeripheralMethod;
/**
* A {@link PeripheralMethod} along with the method's target.
*/
final class SaturatedMethod {
private final Object target;
private final String name;
private final PeripheralMethod method;
SaturatedMethod(Object target, NamedMethod<PeripheralMethod> method) {
SaturatedMethod(Object target, String name, PeripheralMethod method) {
this.target = target;
name = method.getName();
this.method = method.getMethod();
this.name = name;
this.method = method;
}
MethodResult apply(ILuaContext context, IComputerAccess computer, IArguments args) throws LuaException {

View File

@@ -16,10 +16,12 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.peripheral.NotAttachedException;
import dan200.computercraft.api.peripheral.WorkMonitor;
import dan200.computercraft.core.apis.PeripheralAPI;
import dan200.computercraft.core.asm.PeripheralMethod;
import dan200.computercraft.core.methods.PeripheralMethod;
import dan200.computercraft.core.util.LuaUtil;
import dan200.computercraft.shared.computer.core.ServerContext;
import dan200.computercraft.shared.peripheral.modem.ModemPeripheral;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -284,7 +286,8 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
private void attachPeripheralImpl(IComputerAccess computer, ConcurrentMap<String, RemotePeripheralWrapper> peripherals, String periphName, IPeripheral peripheral) {
if (!peripherals.containsKey(periphName) && !periphName.equals(getLocalPeripheral().getConnectedName())) {
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName);
var methods = ServerContext.get(((ServerLevel) getLevel()).getServer()).peripheralMethods().getSelfMethods(peripheral);
var wrapper = new RemotePeripheralWrapper(modem, peripheral, computer, periphName, methods);
peripherals.put(periphName, wrapper);
wrapper.attach();
}
@@ -314,7 +317,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
private volatile boolean attached;
private final Set<String> mounts = new HashSet<>();
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name) {
RemotePeripheralWrapper(WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name, Map<String, PeripheralMethod> methods) {
this.element = element;
this.peripheral = peripheral;
this.computer = computer;
@@ -322,7 +325,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
type = Objects.requireNonNull(peripheral.getType(), "Peripheral type cannot be null");
additionalTypes = peripheral.getAdditionalTypes();
methodMap = PeripheralAPI.getMethods(peripheral);
methodMap = methods;
}
public void attach() {

View File

@@ -190,7 +190,7 @@ public abstract class SpeakerPeripheral implements IPeripheral {
* The speaker supports [all of Minecraft's noteblock instruments](https://minecraft.fandom.com/wiki/Note_Block#Instruments).
* These are:
* <p>
* {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, @code "flute"},
* {@code "harp"}, {@code "basedrum"}, {@code "snare"}, {@code "hat"}, {@code "bass"}, {@code "flute"},
* {@code "bell"}, {@code "guitar"}, {@code "chime"}, {@code "xylophone"}, {@code "iron_xylophone"},
* {@code "cow_bell"}, {@code "didgeridoo"}, {@code "bit"}, {@code "banjo"} and {@code "pling"}.
*

View File

@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.platform;
import com.mojang.authlib.GameProfile;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
/**
* Shared constants for {@linkplain PlatformHelper#createFakePlayer(ServerLevel, GameProfile) fake player}
* implementations.
*
* @see net.minecraft.server.level.ServerPlayer
* @see net.minecraft.world.entity.player.Player
*/
final class FakePlayerConstants {
private FakePlayerConstants() {
}
/**
* The maximum distance this player can reach.
* <p>
* This is used in the override of {@link net.minecraft.world.entity.player.Player#mayUseItemAt(BlockPos, Direction, ItemStack)},
* to prevent the fake player reaching more than 2 blocks away.
*/
static final double MAX_REACH = 2;
}

View File

@@ -68,6 +68,13 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
return (PlatformHelper) dan200.computercraft.impl.PlatformHelper.get();
}
/**
* Check if we're running in a development environment.
*
* @return If we're running in a development environment.
*/
boolean isDevelopmentEnvironment();
/**
* Create a new config builder.
*

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.shared.pocket.apis;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.core.NonNullList;
@@ -14,6 +15,7 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.Objects;
/**
* Control the current pocket computer, adding or removing upgrades.
@@ -68,7 +70,7 @@ public class PocketAPI implements ILuaAPI {
if (newUpgrade == null) return new Object[]{ false, "Cannot find a valid upgrade" };
// Remove the current upgrade
if (previousUpgrade != null) storeItem(player, previousUpgrade.getCraftingItem().copy());
if (previousUpgrade != null) storeItem(player, previousUpgrade.getUpgradeItem());
// Set the new upgrade
computer.setUpgrade(newUpgrade);
@@ -93,7 +95,7 @@ public class PocketAPI implements ILuaAPI {
computer.setUpgrade(null);
storeItem(player, previousUpgrade.getCraftingItem().copy());
storeItem(player, previousUpgrade.getUpgradeItem());
return new Object[]{ true };
}
@@ -105,13 +107,13 @@ public class PocketAPI implements ILuaAPI {
}
}
private static @Nullable IPocketUpgrade findUpgrade(NonNullList<ItemStack> inv, int start, @Nullable IPocketUpgrade previous) {
private static @Nullable UpgradeData<IPocketUpgrade> findUpgrade(NonNullList<ItemStack> inv, int start, @Nullable UpgradeData<IPocketUpgrade> previous) {
for (var i = 0; i < inv.size(); i++) {
var invStack = inv.get((i + start) % inv.size());
if (!invStack.isEmpty()) {
var newUpgrade = PocketUpgrades.instance().get(invStack);
if (newUpgrade != null && newUpgrade != previous) {
if (newUpgrade != null && !Objects.equals(newUpgrade, previous)) {
// Consume an item from this stack and exit the loop
invStack = invStack.copy();
invStack.shrink(1);

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.shared.pocket.core;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.pocket.IPocketAccess;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily;
@@ -104,12 +105,13 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
}
@Override
@Deprecated(forRemoval = true)
public Map<ResourceLocation, IPeripheral> getUpgrades() {
return upgrade == null ? Collections.emptyMap() : Collections.singletonMap(upgrade.getUpgradeID(), getPeripheral(ComputerSide.BACK));
}
public @Nullable IPocketUpgrade getUpgrade() {
return upgrade;
public @Nullable UpgradeData<IPocketUpgrade> getUpgrade() {
return upgrade == null ? null : UpgradeData.of(upgrade, getUpgradeNBTData());
}
/**
@@ -119,13 +121,11 @@ public class PocketServerComputer extends ServerComputer implements IPocketAcces
*
* @param upgrade The new upgrade to set it to, may be {@code null}.
*/
public void setUpgrade(@Nullable IPocketUpgrade upgrade) {
if (this.upgrade == upgrade) return;
public void setUpgrade(@Nullable UpgradeData<IPocketUpgrade> upgrade) {
synchronized (this) {
PocketComputerItem.setUpgrade(stack, upgrade);
updateUpgradeNBTData();
this.upgrade = upgrade;
this.upgrade = upgrade == null ? null : upgrade.upgrade();
invalidatePeripheral();
}
}

View File

@@ -10,6 +10,7 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.shared.ModRegistry;
@@ -23,6 +24,7 @@ import dan200.computercraft.shared.pocket.apis.PocketAPI;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import dan200.computercraft.shared.pocket.inventory.PocketComputerMenuProvider;
import dan200.computercraft.shared.util.IDAssigner;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
@@ -58,7 +60,7 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
this.family = family;
}
public static ItemStack create(int id, @Nullable String label, int colour, ComputerFamily family, @Nullable IPocketUpgrade upgrade) {
public static ItemStack create(int id, @Nullable String label, int colour, ComputerFamily family, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
return switch (family) {
case NORMAL -> ModRegistry.Items.POCKET_COMPUTER_NORMAL.get().create(id, label, colour, upgrade);
case ADVANCED -> ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().create(id, label, colour, upgrade);
@@ -66,11 +68,14 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
};
}
public ItemStack create(int id, @Nullable String label, int colour, @Nullable IPocketUpgrade upgrade) {
public ItemStack create(int id, @Nullable String label, int colour, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
var result = new ItemStack(this);
if (id >= 0) result.getOrCreateTag().putInt(NBT_ID, id);
if (label != null) result.setHoverName(Component.literal(label));
if (upgrade != null) result.getOrCreateTag().putString(NBT_UPGRADE, upgrade.getUpgradeID().toString());
if (upgrade != null) {
result.getOrCreateTag().putString(NBT_UPGRADE, upgrade.upgrade().getUpgradeID().toString());
if (!upgrade.data().isEmpty()) result.getOrCreateTag().put(NBT_UPGRADE_INFO, upgrade.data().copy());
}
if (colour != -1) result.getOrCreateTag().putInt(NBT_COLOUR, colour);
return result;
}
@@ -207,7 +212,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
setInstanceID(stack, computer.register());
setSessionID(stack, registry.getSessionID());
computer.updateValues(entity, stack, getUpgrade(stack));
var upgrade = getUpgrade(stack);
computer.updateValues(entity, stack, upgrade);
computer.addAPI(new PocketAPI(computer));
// Only turn on when initially creating the computer, rather than each tick.
@@ -244,7 +251,7 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
public ItemStack withFamily(ItemStack stack, ComputerFamily family) {
return create(
getComputerID(stack), getLabel(stack), getColour(stack),
family, getUpgrade(stack)
family, getUpgradeWithData(stack)
);
}
@@ -294,20 +301,27 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
public static @Nullable IPocketUpgrade getUpgrade(ItemStack stack) {
var compound = stack.getTag();
return compound != null && compound.contains(NBT_UPGRADE)
? PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE)) : null;
if (compound == null || !compound.contains(NBT_UPGRADE)) return null;
return PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE));
}
public static void setUpgrade(ItemStack stack, @Nullable IPocketUpgrade upgrade) {
public static @Nullable UpgradeData<IPocketUpgrade> getUpgradeWithData(ItemStack stack) {
var compound = stack.getTag();
if (compound == null || !compound.contains(NBT_UPGRADE)) return null;
var upgrade = PocketUpgrades.instance().get(compound.getString(NBT_UPGRADE));
return upgrade == null ? null : UpgradeData.of(upgrade, NBTUtil.getCompoundOrEmpty(compound, NBT_UPGRADE_INFO));
}
public static void setUpgrade(ItemStack stack, @Nullable UpgradeData<IPocketUpgrade> upgrade) {
var compound = stack.getOrCreateTag();
if (upgrade == null) {
compound.remove(NBT_UPGRADE);
compound.remove(NBT_UPGRADE_INFO);
} else {
compound.putString(NBT_UPGRADE, upgrade.getUpgradeID().toString());
compound.putString(NBT_UPGRADE, upgrade.upgrade().getUpgradeID().toString());
compound.put(NBT_UPGRADE_INFO, upgrade.data().copy());
}
compound.remove(NBT_UPGRADE_INFO);
}
public static CompoundTag getUpgradeInfo(ItemStack stack) {

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.shared.pocket.recipes;
import dan200.computercraft.api.pocket.IPocketUpgrade;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.pocket.items.PocketComputerItem;
@@ -62,7 +63,7 @@ public final class PocketComputerUpgradeRecipe extends CustomRecipe {
if (PocketComputerItem.getUpgrade(computer) != null) return ItemStack.EMPTY;
// Check for upgrades around the item
IPocketUpgrade upgrade = null;
UpgradeData<IPocketUpgrade> upgrade = null;
for (var y = 0; y < inventory.getHeight(); y++) {
for (var x = 0; x < inventory.getWidth(); x++) {
var item = inventory.getItem(x + y * inventory.getWidth());

View File

@@ -555,7 +555,7 @@ public class TurtleAPI implements ILuaAPI {
* @cc.usage Refuel a turtle from the currently selected slot.
* <pre>{@code
* local level = turtle.getFuelLevel()
* if new_level == "unlimited" then error("Turtle does not need fuel", 0) end
* if level == "unlimited" then error("Turtle does not need fuel", 0) end
*
* local ok, err = turtle.refuel()
* if ok then

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.turtle.blocks;
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlock;
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlockEntity;
import dan200.computercraft.shared.computer.core.ComputerFamily;
@@ -128,7 +130,7 @@ public class TurtleBlock extends AbstractComputerBlock<TurtleBlockEntity> implem
if (stack.getItem() instanceof TurtleItem item) {
// Set Upgrades
for (var side : TurtleSide.values()) {
turtle.getAccess().setUpgrade(side, item.getUpgrade(stack, side));
turtle.getAccess().setUpgradeWithData(side, item.getUpgradeWithData(stack, side));
}
turtle.getAccess().setFuelLevel(item.getFuelLevel(stack));
@@ -161,11 +163,16 @@ public class TurtleBlock extends AbstractComputerBlock<TurtleBlockEntity> implem
var access = turtle.getAccess();
return TurtleItem.create(
turtle.getComputerID(), turtle.getLabel(), access.getColour(), turtle.getFamily(),
access.getUpgrade(TurtleSide.LEFT), access.getUpgrade(TurtleSide.RIGHT),
withPersistedData(access.getUpgradeWithData(TurtleSide.LEFT)),
withPersistedData(access.getUpgradeWithData(TurtleSide.RIGHT)),
access.getFuelLevel(), turtle.getOverlay()
);
}
private static @Nullable UpgradeData<ITurtleUpgrade> withPersistedData(@Nullable UpgradeData<ITurtleUpgrade> upgrade) {
return upgrade == null ? null : UpgradeData.of(upgrade.upgrade(), upgrade.upgrade().getPersistedData(upgrade.data()));
}
@Override
@Nullable
public <U extends BlockEntity> BlockEntityTicker<U> getTicker(Level level, BlockState state, BlockEntityType<U> type) {

View File

@@ -13,8 +13,8 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleAnimation;
import dan200.computercraft.api.turtle.TurtleCommand;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -34,7 +34,6 @@ import net.minecraft.tags.FluidTags;
import net.minecraft.world.Container;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.material.PushReaction;
@@ -141,17 +140,16 @@ public class TurtleBrain implements TurtleAccessInternal {
overlay = nbt.contains(NBT_OVERLAY) ? new ResourceLocation(nbt.getString(NBT_OVERLAY)) : null;
// Read upgrades
setUpgradeDirect(TurtleSide.LEFT, nbt.contains(NBT_LEFT_UPGRADE) ? TurtleUpgrades.instance().get(nbt.getString(NBT_LEFT_UPGRADE)) : null);
setUpgradeDirect(TurtleSide.RIGHT, nbt.contains(NBT_RIGHT_UPGRADE) ? TurtleUpgrades.instance().get(nbt.getString(NBT_RIGHT_UPGRADE)) : null);
setUpgradeDirect(TurtleSide.LEFT, readUpgrade(nbt, NBT_LEFT_UPGRADE, NBT_LEFT_UPGRADE_DATA));
setUpgradeDirect(TurtleSide.RIGHT, readUpgrade(nbt, NBT_RIGHT_UPGRADE, NBT_RIGHT_UPGRADE_DATA));
}
// NBT
upgradeNBTData.clear();
if (nbt.contains(NBT_LEFT_UPGRADE_DATA)) {
upgradeNBTData.put(TurtleSide.LEFT, nbt.getCompound(NBT_LEFT_UPGRADE_DATA).copy());
}
if (nbt.contains(NBT_RIGHT_UPGRADE_DATA)) {
upgradeNBTData.put(TurtleSide.RIGHT, nbt.getCompound(NBT_RIGHT_UPGRADE_DATA).copy());
}
private @Nullable UpgradeData<ITurtleUpgrade> readUpgrade(CompoundTag tag, String upgradeKey, String dataKey) {
if (!tag.contains(upgradeKey)) return null;
var upgrade = TurtleUpgrades.instance().get(tag.getString(upgradeKey));
if (upgrade == null) return null;
return UpgradeData.of(upgrade, tag.getCompound(dataKey));
}
private void writeCommon(CompoundTag nbt) {
@@ -465,23 +463,6 @@ public class TurtleBrain implements TurtleAccessInternal {
}
}
public @Nullable DyeColor getDyeColour() {
if (colourHex == -1) return null;
var colour = Colour.fromHex(colourHex);
return colour == null ? null : DyeColor.byId(15 - colour.ordinal());
}
public void setDyeColour(@Nullable DyeColor dyeColour) {
var newColour = -1;
if (dyeColour != null) {
newColour = Colour.values()[15 - dyeColour.getId()].getHex();
}
if (colourHex != newColour) {
colourHex = newColour;
BlockEntityHelpers.updateBlock(owner);
}
}
@Override
public void setColour(int colour) {
if (colour >= 0 && colour <= 0xFFFFFF) {
@@ -516,7 +497,7 @@ public class TurtleBrain implements TurtleAccessInternal {
}
@Override
public void setUpgrade(TurtleSide side, @Nullable ITurtleUpgrade upgrade) {
public void setUpgradeWithData(TurtleSide side, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
if (!setUpgradeDirect(side, upgrade) || owner.getLevel() == null) return;
// This is a separate function to avoid updating the block when reading the NBT. We don't need to do this as
@@ -529,19 +510,18 @@ public class TurtleBrain implements TurtleAccessInternal {
owner.updateInputsImmediately();
}
private boolean setUpgradeDirect(TurtleSide side, @Nullable ITurtleUpgrade upgrade) {
private boolean setUpgradeDirect(TurtleSide side, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
// Remove old upgrade
if (upgrades.containsKey(side)) {
if (upgrades.get(side) == upgrade) return false;
upgrades.remove(side);
} else {
if (upgrade == null) return false;
}
upgradeNBTData.remove(side);
var oldUpgrade = upgrades.remove(side);
if (oldUpgrade == null && upgrade == null) return false;
// Set new upgrade
if (upgrade != null) upgrades.put(side, upgrade);
if (upgrade == null) {
upgradeNBTData.remove(side);
} else {
upgrades.put(side, upgrade.upgrade());
upgradeNBTData.put(side, upgrade.data().copy());
}
// Notify clients and create peripherals
if (owner.getLevel() != null && !owner.getLevel().isClientSide) {
@@ -595,7 +575,7 @@ public class TurtleBrain implements TurtleAccessInternal {
public float getToolRenderAngle(TurtleSide side, float f) {
return (side == TurtleSide.LEFT && animation == TurtleAnimation.SWING_LEFT_TOOL) ||
(side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL)
(side == TurtleSide.RIGHT && animation == TurtleAnimation.SWING_RIGHT_TOOL)
? 45.0f * (float) Math.sin(getAnimationFraction(f) * Math.PI)
: 0.0f;
}

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.shared.turtle.core;
import dan200.computercraft.api.turtle.*;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.turtle.TurtleUtil;
@@ -18,10 +19,10 @@ public class TurtleEquipCommand implements TurtleCommand {
@Override
public TurtleCommandResult execute(ITurtleAccess turtle) {
// Determine the upgrade to replace
var oldUpgrade = turtle.getUpgrade(side);
var oldUpgrade = turtle.getUpgradeWithData(side);
// Determine the upgrade to equipLeft
ITurtleUpgrade newUpgrade;
UpgradeData<ITurtleUpgrade> newUpgrade;
var selectedStack = turtle.getInventory().getItem(turtle.getSelectedSlot());
if (!selectedStack.isEmpty()) {
newUpgrade = TurtleUpgrades.instance().get(selectedStack);
@@ -32,8 +33,8 @@ public class TurtleEquipCommand implements TurtleCommand {
// Do the swapping:
if (newUpgrade != null) turtle.getInventory().removeItem(turtle.getSelectedSlot(), 1);
if (oldUpgrade != null) TurtleUtil.storeItemOrDrop(turtle, oldUpgrade.getCraftingItem().copy());
turtle.setUpgrade(side, newUpgrade);
if (oldUpgrade != null) TurtleUtil.storeItemOrDrop(turtle, oldUpgrade.getUpgradeItem());
turtle.setUpgradeWithData(side, newUpgrade);
// Animate
if (newUpgrade != null || oldUpgrade != null) {

View File

@@ -74,19 +74,7 @@ public class TurtlePlaceCommand implements TurtleCommand {
}
}
public static boolean deployCopiedItem(
ItemStack stack, ITurtleAccess turtle, Direction direction, @Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) {
// Create a fake player, and orient it appropriately
var playerPosition = turtle.getPosition().relative(direction);
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, playerPosition, direction);
turtlePlayer.loadInventory(stack);
var result = deploy(stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage);
turtlePlayer.player().getInventory().clearContent();
return result;
}
private static boolean deploy(
public static boolean deploy(
ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction,
@Nullable Object[] extraArguments, @Nullable ErrorMessage outErrorMessage
) {

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.shared.turtle.inventory;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerComputer;
@@ -29,12 +30,13 @@ public final class TurtleMenu extends AbstractComputerMenu {
public static final int PLAYER_START_Y = 134;
public static final int TURTLE_START_X = SIDEBAR_WIDTH + 175;
public static final int PLAYER_START_X = SIDEBAR_WIDTH + BORDER;
public static final int UPGRADE_START_X = SIDEBAR_WIDTH + 254;
private final ContainerData data;
private TurtleMenu(
int id, Predicate<Player> canUse, ComputerFamily family, @Nullable ServerComputer computer, @Nullable ComputerContainerData menuData,
Inventory playerInventory, Container inventory, ContainerData data
Inventory playerInventory, Container inventory, Container turtleUpgrades, ContainerData data
) {
super(ModRegistry.Menus.TURTLE.get(), id, canUse, family, computer, menuData);
this.data = data;
@@ -58,19 +60,24 @@ public final class TurtleMenu extends AbstractComputerMenu {
for (var x = 0; x < 9; x++) {
addSlot(new Slot(playerInventory, x, PLAYER_START_X + x * 18, PLAYER_START_Y + 3 * 18 + 5));
}
// Turtle upgrades
addSlot(new UpgradeSlot(turtleUpgrades, TurtleSide.LEFT, 0, UPGRADE_START_X, PLAYER_START_Y + 1));
addSlot(new UpgradeSlot(turtleUpgrades, TurtleSide.RIGHT, 1, UPGRADE_START_X, PLAYER_START_Y + 1 + 18));
}
public static TurtleMenu ofBrain(int id, Inventory player, TurtleBrain turtle) {
return new TurtleMenu(
// Laziness in turtle.getOwner() is important here!
id, p -> turtle.getOwner().stillValid(p), turtle.getFamily(), turtle.getOwner().createServerComputer(), null,
player, turtle.getInventory(), (SingleContainerData) turtle::getSelectedSlot
player, turtle.getInventory(), new UpgradeContainer(turtle), (SingleContainerData) turtle::getSelectedSlot
);
}
public static TurtleMenu ofMenuData(int id, Inventory player, ComputerContainerData data) {
return new TurtleMenu(
id, x -> true, data.family(), null, data, player, new SimpleContainer(TurtleBlockEntity.INVENTORY_SIZE), new SimpleContainerData(1)
id, x -> true, data.family(), null, data,
player, new SimpleContainer(TurtleBlockEntity.INVENTORY_SIZE), new SimpleContainer(2), new SimpleContainerData(1)
);
}

View File

@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.turtle.inventory;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.TurtleUpgrades;
import net.minecraft.core.NonNullList;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
/**
* A fake {@link Container} which exposes the {@linkplain ITurtleAccess#getUpgrade(TurtleSide) upgrades} a turtle has.
*
* @see TurtleMenu
* @see UpgradeSlot
*/
class UpgradeContainer implements Container {
private static final int SIZE = 2;
private final ITurtleAccess turtle;
private final List<UpgradeData<ITurtleUpgrade>> lastUpgrade = Arrays.asList(null, null);
private final NonNullList<ItemStack> lastStack = NonNullList.withSize(2, ItemStack.EMPTY);
UpgradeContainer(ITurtleAccess turtle) {
this.turtle = turtle;
}
private TurtleSide getSide(int slot) {
return switch (slot) {
case 0 -> TurtleSide.LEFT;
case 1 -> TurtleSide.RIGHT;
default -> throw new IllegalArgumentException("Invalid slot " + slot);
};
}
@Override
public ItemStack getItem(int slot) {
var side = getSide(slot);
var upgrade = turtle.getUpgradeWithData(side);
if (upgrade == null) return ItemStack.EMPTY;
// We don't want to return getUpgradeItem directly here, as we'd end up recreating the stack each tick. To
// avoid that, we maintain a simple cache.
if (upgrade.equals(lastUpgrade.get(slot))) return lastStack.get(slot);
return setUpgradeStack(slot, upgrade);
}
private ItemStack setUpgradeStack(int slot, @Nullable UpgradeData<ITurtleUpgrade> upgrade) {
var stack = upgrade == null ? ItemStack.EMPTY : upgrade.getUpgradeItem();
lastUpgrade.set(slot, UpgradeData.copyOf(upgrade));
lastStack.set(slot, stack);
return stack;
}
@Override
public void setItem(int slot, ItemStack itemStack) {
var upgrade = TurtleUpgrades.instance().get(itemStack);
turtle.setUpgradeWithData(getSide(slot), upgrade);
setUpgradeStack(slot, upgrade);
}
@Override
public int getContainerSize() {
return SIZE;
}
@Override
public int getMaxStackSize() {
return 1;
}
@Override
public boolean isEmpty() {
for (var i = 0; i < SIZE; i++) {
if (!getItem(i).isEmpty()) return false;
}
return true;
}
@Override
public ItemStack removeItem(int slot, int count) {
return count <= 0 ? ItemStack.EMPTY : removeItemNoUpdate(slot);
}
@Override
public ItemStack removeItemNoUpdate(int slot) {
var current = getItem(slot);
setItem(slot, ItemStack.EMPTY);
return current;
}
@Override
public void setChanged() {
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public void clearContent() {
for (var i = 0; i < SIZE; i++) setItem(i, ItemStack.EMPTY);
}
}

View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.turtle.inventory;
import com.mojang.datafixers.util.Pair;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.impl.TurtleUpgrades;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
/**
* A slot in the turtle UI which holds the turtle's current upgrade.
*
* @see TurtleMenu
*/
public class UpgradeSlot extends Slot {
public static final ResourceLocation LEFT_UPGRADE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/turtle_upgrade_left");
public static final ResourceLocation RIGHT_UPGRADE = new ResourceLocation(ComputerCraftAPI.MOD_ID, "gui/turtle_upgrade_right");
private final TurtleSide side;
public UpgradeSlot(Container container, TurtleSide side, int slot, int xPos, int yPos) {
super(container, slot, xPos, yPos);
this.side = side;
}
@Override
public boolean mayPlace(ItemStack stack) {
return TurtleUpgrades.instance().get(stack) != null;
}
@Override
public int getMaxStackSize() {
return 1;
}
@Nullable
@Override
public Pair<ResourceLocation, ResourceLocation> getNoItemIcon() {
return Pair.of(InventoryMenu.BLOCK_ATLAS, side == TurtleSide.LEFT ? LEFT_UPGRADE : RIGHT_UPGRADE);
}
}

View File

@@ -8,12 +8,14 @@ import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.items.AbstractComputerItem;
import dan200.computercraft.shared.turtle.blocks.TurtleBlock;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.core.cauldron.CauldronInteraction;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
@@ -32,7 +34,7 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem {
public static ItemStack create(
int id, @Nullable String label, int colour, ComputerFamily family,
@Nullable ITurtleUpgrade leftUpgrade, @Nullable ITurtleUpgrade rightUpgrade,
@Nullable UpgradeData<ITurtleUpgrade> leftUpgrade, @Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
int fuelLevel, @Nullable ResourceLocation overlay
) {
return switch (family) {
@@ -46,7 +48,7 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem {
public ItemStack create(
int id, @Nullable String label, int colour,
@Nullable ITurtleUpgrade leftUpgrade, @Nullable ITurtleUpgrade rightUpgrade,
@Nullable UpgradeData<ITurtleUpgrade> leftUpgrade, @Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
int fuelLevel, @Nullable ResourceLocation overlay
) {
// Build the stack
@@ -58,11 +60,15 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem {
if (overlay != null) stack.getOrCreateTag().putString(NBT_OVERLAY, overlay.toString());
if (leftUpgrade != null) {
stack.getOrCreateTag().putString(NBT_LEFT_UPGRADE, leftUpgrade.getUpgradeID().toString());
var tag = stack.getOrCreateTag();
tag.putString(NBT_LEFT_UPGRADE, leftUpgrade.upgrade().getUpgradeID().toString());
if (!leftUpgrade.data().isEmpty()) tag.put(NBT_LEFT_UPGRADE_DATA, leftUpgrade.data().copy());
}
if (rightUpgrade != null) {
stack.getOrCreateTag().putString(NBT_RIGHT_UPGRADE, rightUpgrade.getUpgradeID().toString());
var tag = stack.getOrCreateTag();
tag.putString(NBT_RIGHT_UPGRADE, rightUpgrade.upgrade().getUpgradeID().toString());
if (!rightUpgrade.data().isEmpty()) tag.put(NBT_RIGHT_UPGRADE_DATA, rightUpgrade.data().copy());
}
return stack;
@@ -117,7 +123,7 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem {
return create(
getComputerID(stack), getLabel(stack),
getColour(stack), family,
getUpgrade(stack, TurtleSide.LEFT), getUpgrade(stack, TurtleSide.RIGHT),
getUpgradeWithData(stack, TurtleSide.LEFT), getUpgradeWithData(stack, TurtleSide.RIGHT),
getFuelLevel(stack), getOverlay(stack)
);
}
@@ -127,7 +133,20 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem {
if (tag == null) return null;
var key = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE;
return tag.contains(key) ? TurtleUpgrades.instance().get(tag.getString(key)) : null;
if (!tag.contains(key)) return null;
return TurtleUpgrades.instance().get(tag.getString(key));
}
public @Nullable UpgradeData<ITurtleUpgrade> getUpgradeWithData(ItemStack stack, TurtleSide side) {
var tag = stack.getTag();
if (tag == null) return null;
var key = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE;
if (!tag.contains(key)) return null;
var upgrade = TurtleUpgrades.instance().get(tag.getString(key));
if (upgrade == null) return null;
var dataKey = side == TurtleSide.LEFT ? NBT_LEFT_UPGRADE_DATA : NBT_RIGHT_UPGRADE_DATA;
return UpgradeData.of(upgrade, NBTUtil.getCompoundOrEmpty(tag, dataKey));
}
public @Nullable ResourceLocation getOverlay(ItemStack stack) {

View File

@@ -38,8 +38,8 @@ public class TurtleOverlayRecipe extends ShapelessRecipe {
turtle.getComputerID(stack),
turtle.getLabel(stack),
turtle.getColour(stack),
turtle.getUpgrade(stack, TurtleSide.LEFT),
turtle.getUpgrade(stack, TurtleSide.RIGHT),
turtle.getUpgradeWithData(stack, TurtleSide.LEFT),
turtle.getUpgradeWithData(stack, TurtleSide.RIGHT),
turtle.getFuelLevel(stack),
overlay
);

View File

@@ -6,6 +6,7 @@ package dan200.computercraft.shared.turtle.recipes;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.turtle.items.TurtleItem;
@@ -104,9 +105,10 @@ public final class TurtleUpgradeRecipe extends CustomRecipe {
// At this point we have a turtle + 1 or 2 items
// Get the turtle we already have
var itemTurtle = (TurtleItem) turtle.getItem();
var upgrades = new ITurtleUpgrade[]{
itemTurtle.getUpgrade(turtle, TurtleSide.LEFT),
itemTurtle.getUpgrade(turtle, TurtleSide.RIGHT),
@SuppressWarnings({ "unchecked", "rawtypes" })
UpgradeData<ITurtleUpgrade>[] upgrades = new UpgradeData[]{
itemTurtle.getUpgradeWithData(turtle, TurtleSide.LEFT),
itemTurtle.getUpgradeWithData(turtle, TurtleSide.RIGHT),
};
// Get the upgrades for the new items

View File

@@ -9,6 +9,7 @@ import dan200.computercraft.api.turtle.*;
import dan200.computercraft.shared.peripheral.modem.ModemState;
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
@@ -77,4 +78,9 @@ public class TurtleModem extends AbstractTurtleUpgrade {
}
}
}
@Override
public CompoundTag getPersistedData(CompoundTag upgradeData) {
return new CompoundTag();
}
}

View File

@@ -14,16 +14,22 @@ import dan200.computercraft.shared.util.DropConsumer;
import dan200.computercraft.shared.util.WorldUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.MobType;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.decoration.ArmorStand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
@@ -32,46 +38,127 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.EntityHitResult;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.function.Function;
import static net.minecraft.nbt.Tag.TAG_COMPOUND;
import static net.minecraft.nbt.Tag.TAG_LIST;
public class TurtleTool extends AbstractTurtleUpgrade {
protected static final TurtleCommandResult UNBREAKABLE = TurtleCommandResult.failure("Cannot break unbreakable block");
protected static final TurtleCommandResult INEFFECTIVE = TurtleCommandResult.failure("Cannot break block with this tool");
private static final TurtleCommandResult UNBREAKABLE = TurtleCommandResult.failure("Cannot break unbreakable block");
private static final TurtleCommandResult INEFFECTIVE = TurtleCommandResult.failure("Cannot break block with this tool");
private static final String TAG_ITEM_TAG = "Tag";
final ItemStack item;
final float damageMulitiplier;
@Nullable
final TagKey<Block> breakable;
final boolean allowEnchantments;
final TurtleToolDurability consumeDurability;
final @Nullable TagKey<Block> breakable;
public TurtleTool(ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier, @Nullable TagKey<Block> breakable) {
public TurtleTool(
ResourceLocation id, String adjective, Item craftItem, ItemStack toolItem, float damageMulitiplier,
boolean allowEnchantments, TurtleToolDurability consumeDurability, @Nullable TagKey<Block> breakable
) {
super(id, TurtleUpgradeType.TOOL, adjective, new ItemStack(craftItem));
item = toolItem;
this.damageMulitiplier = damageMulitiplier;
this.allowEnchantments = allowEnchantments;
this.consumeDurability = consumeDurability;
this.breakable = breakable;
}
@Override
public boolean isItemSuitable(ItemStack stack) {
var tag = stack.getTag();
if (tag == null || tag.isEmpty()) return true;
if (consumeDurability == TurtleToolDurability.NEVER && stack.isDamaged()) return false;
if (!allowEnchantments && isEnchanted(stack)) return false;
return true;
}
// Check we've not got anything vaguely interesting on the item. We allow other mods to add their
// own NBT, with the understanding such details will be lost to the mist of time.
if (stack.isDamaged() || stack.isEnchanted() || stack.hasCustomHoverName()) return false;
if (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty()) {
return false;
private static boolean isEnchanted(ItemStack stack) {
return !stack.isEmpty() && isEnchanted(stack.getTag());
}
private static boolean isEnchanted(@Nullable CompoundTag tag) {
if (tag == null || tag.isEmpty()) return false;
return (tag.contains(ItemStack.TAG_ENCH, TAG_LIST) && !tag.getList(ItemStack.TAG_ENCH, TAG_COMPOUND).isEmpty())
|| (tag.contains("AttributeModifiers", TAG_LIST) && !tag.getList("AttributeModifiers", TAG_COMPOUND).isEmpty());
}
@Override
public CompoundTag getUpgradeData(ItemStack stack) {
var upgradeData = super.getUpgradeData(stack);
// Store the item's current tag.
var itemTag = stack.getTag();
if (itemTag != null) upgradeData.put(TAG_ITEM_TAG, itemTag);
return upgradeData;
}
@Override
public ItemStack getUpgradeItem(CompoundTag upgradeData) {
// Copy upgrade data back to the item.
var item = super.getUpgradeItem(upgradeData).copy();
item.setTag(upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) ? upgradeData.getCompound(TAG_ITEM_TAG).copy() : null);
return item;
}
private ItemStack getToolStack(ITurtleAccess turtle, TurtleSide side) {
return getUpgradeItem(turtle.getUpgradeNBTData(side));
}
private void setToolStack(ITurtleAccess turtle, TurtleSide side, ItemStack stack) {
var upgradeData = turtle.getUpgradeNBTData(side);
var useDurability = switch (consumeDurability) {
case NEVER -> false;
case WHEN_ENCHANTED ->
upgradeData.contains(TAG_ITEM_TAG, TAG_COMPOUND) && isEnchanted(upgradeData.getCompound(TAG_ITEM_TAG));
case ALWAYS -> true;
};
if (!useDurability) return;
// If the tool has broken, remove the upgrade!
if (stack.isEmpty()) {
turtle.setUpgradeWithData(side, null);
return;
}
return true;
// If the tool has changed, no clue what's going on.
if (stack.getItem() != item.getItem()) return;
var itemTag = stack.getTag();
// Early return if the item hasn't changed to avoid redundant syncs with the client.
if (Objects.equals(itemTag, upgradeData.get(TAG_ITEM_TAG))) return;
if (itemTag == null) {
upgradeData.remove(TAG_ITEM_TAG);
} else {
upgradeData.put(TAG_ITEM_TAG, itemTag);
}
turtle.updateUpgradeNBTData(side);
}
private <T> T withEquippedItem(ITurtleAccess turtle, TurtleSide side, Direction direction, Function<TurtlePlayer, T> action) {
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtle.getPosition(), direction);
turtlePlayer.loadInventory(getToolStack(turtle, side));
var result = action.apply(turtlePlayer);
setToolStack(turtle, side, turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND));
turtlePlayer.player().getInventory().clearContent();
return result;
}
@Override
public TurtleCommandResult useTool(ITurtleAccess turtle, TurtleSide side, TurtleVerb verb, Direction direction) {
return switch (verb) {
case ATTACK -> attack(turtle, direction);
case DIG -> dig(turtle, direction);
case ATTACK -> attack(turtle, side, direction);
case DIG -> dig(turtle, side, direction);
};
}
@@ -86,16 +173,14 @@ public class TurtleTool extends AbstractTurtleUpgrade {
}
/**
* Attack an entity. This is a <em>very</em> cut down version of {@link Player#attack(Entity)}, which doesn't handle
* enchantments, knockback, etc... Unfortunately we can't call attack directly as damage calculations are rather
* different (and we don't want to play sounds/particles).
* Attack an entity.
*
* @param turtle The current turtle.
* @param side The side the tool is on.
* @param direction The direction we're attacking in.
* @return Whether an attack occurred.
* @see Player#attack(Entity)
*/
private TurtleCommandResult attack(ITurtleAccess turtle, Direction direction) {
private TurtleCommandResult attack(ITurtleAccess turtle, TurtleSide side, Direction direction) {
// Create a fake player, and orient it appropriately
var world = turtle.getLevel();
var position = turtle.getPosition();
@@ -107,10 +192,11 @@ public class TurtleTool extends AbstractTurtleUpgrade {
var turtlePos = player.position();
var rayDir = player.getViewVector(1.0f);
var hit = WorldUtil.clip(world, turtlePos, rayDir, 1.5, null);
var attacked = false;
if (hit instanceof EntityHitResult entityHit) {
// Load up the turtle's inventory
var stackCopy = item.copy();
turtlePlayer.loadInventory(stackCopy);
var stack = getToolStack(turtle, side);
turtlePlayer.loadInventory(stack);
var hitEntity = entityHit.getEntity();
@@ -118,66 +204,125 @@ public class TurtleTool extends AbstractTurtleUpgrade {
DropConsumer.set(hitEntity, TurtleUtil.dropConsumer(turtle));
// Attack the entity
var attacked = false;
var result = PlatformHelper.get().canAttackEntity(player, hitEntity);
if (result.consumesAction()) {
attacked = true;
} else if (result == InteractionResult.PASS && hitEntity.isAttackable() && !hitEntity.skipAttackInteraction(player)) {
var damage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier;
if (damage > 0.0f) {
var source = player.damageSources().playerAttack(player);
if (hitEntity instanceof ArmorStand) {
// Special case for armor stands: attack twice to guarantee destroy
hitEntity.hurt(source, damage);
if (hitEntity.isAlive()) hitEntity.hurt(source, damage);
attacked = true;
} else {
if (hitEntity.hurt(source, damage)) attacked = true;
}
}
attacked = attack(player, direction, hitEntity);
}
// Stop claiming drops
TurtleUtil.stopConsuming(turtle);
// Put everything we collected into the turtles inventory, then return
// Put everything we collected into the turtles inventory.
setToolStack(turtle, side, player.getItemInHand(InteractionHand.MAIN_HAND));
player.getInventory().clearContent();
if (attacked) return TurtleCommandResult.success();
}
return TurtleCommandResult.failure("Nothing to attack here");
return attacked ? TurtleCommandResult.success() : TurtleCommandResult.failure("Nothing to attack here");
}
private TurtleCommandResult dig(ITurtleAccess turtle, Direction direction) {
if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deployCopiedItem(item.copy(), turtle, direction, null, null)) {
return TurtleCommandResult.success();
/**
* Attack an entity. This is a copy of {@link Player#attack(Entity)}, with some unwanted features removed (sweeping
* edge). This is a little limited.
* <p>
* Ideally we'd use attack directly (if other mods mixin to that method, we won't support their features).
* Unfortunately,that doesn't give us any feedback to whether the attack occurred or not (and we don't want to play
* sounds/particles).
*
* @param player The fake player doing the attacking.
* @param direction The direction the turtle is attacking.
* @param entity The entity to attack.
* @return Whether we attacked or not.
* @see Player#attack(Entity)
*/
private boolean attack(ServerPlayer player, Direction direction, Entity entity) {
var baseDamage = (float) player.getAttributeValue(Attributes.ATTACK_DAMAGE) * damageMulitiplier;
var bonusDamage = EnchantmentHelper.getDamageBonus(
player.getItemInHand(InteractionHand.MAIN_HAND), entity instanceof LivingEntity target ? target.getMobType() : MobType.UNDEFINED
);
var damage = baseDamage + bonusDamage;
if (damage <= 0) return false;
var knockBack = EnchantmentHelper.getKnockbackBonus(player);
// We follow the logic in Player.attack of setting the entity on fire before attacking, so it's burning when it
// (possibly) dies.
var fireAspect = EnchantmentHelper.getFireAspect(player);
var onFire = false;
if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) {
onFire = true;
target.setSecondsOnFire(1);
}
var level = (ServerLevel) turtle.getLevel();
var turtlePosition = turtle.getPosition();
var source = player.damageSources().playerAttack(player);
if (!entity.hurt(source, damage)) {
// If we failed to damage the entity, undo us setting the entity on fire.
if (onFire) entity.clearFire();
return false;
}
var blockPosition = turtlePosition.relative(direction);
// Special case for armor stands: attack twice to guarantee destroy
if (entity.isAlive() && entity instanceof ArmorStand) entity.hurt(source, damage);
// Apply knockback
if (knockBack > 0) {
if (entity instanceof LivingEntity target) {
target.knockback(knockBack * 0.5, -direction.getStepX(), -direction.getStepZ());
} else {
entity.push(direction.getStepX() * knockBack * 0.5, 0.1, direction.getStepZ() * knockBack * 0.5);
}
}
// Apply remaining enchantments
if (entity instanceof LivingEntity target) EnchantmentHelper.doPostHurtEffects(target, player);
EnchantmentHelper.doPostDamageEffects(player, entity);
// Damage the original item stack.
if (entity instanceof LivingEntity target) {
player.getItemInHand(InteractionHand.MAIN_HAND).hurtEnemy(target, player);
}
// Apply fire aspect
if (entity instanceof LivingEntity target && fireAspect > 0 && !target.isOnFire()) {
target.setSecondsOnFire(4 * fireAspect);
}
return true;
}
private TurtleCommandResult dig(ITurtleAccess turtle, TurtleSide side, Direction direction) {
var level = (ServerLevel) turtle.getLevel();
var blockPosition = turtle.getPosition().relative(direction);
if (level.isEmptyBlock(blockPosition) || WorldUtil.isLiquidBlock(level, blockPosition)) {
return TurtleCommandResult.failure("Nothing to dig here");
}
var turtlePlayer = TurtlePlayer.getWithPosition(turtle, turtlePosition, direction);
turtlePlayer.loadInventory(item.copy());
return withEquippedItem(turtle, side, direction, turtlePlayer -> {
var stack = turtlePlayer.player().getItemInHand(InteractionHand.MAIN_HAND);
// Check if we can break the block
var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer);
if (!breakable.isSuccess()) return breakable;
// Right-click the block when using a shovel/hoe.
if (PlatformHelper.get().hasToolUsage(item) && TurtlePlaceCommand.deploy(stack, turtle, turtlePlayer, direction, null, null)) {
return TurtleCommandResult.success();
}
DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle));
var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition);
TurtleUtil.stopConsuming(turtle);
// Check if we can break the block
var breakable = checkBlockBreakable(level, blockPosition, turtlePlayer);
if (!breakable.isSuccess()) return breakable;
// Check spawn protection
return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block");
// And break it!
DropConsumer.set(level, blockPosition, TurtleUtil.dropConsumer(turtle));
var broken = !turtlePlayer.isBlockProtected(level, blockPosition) && turtlePlayer.player().gameMode.destroyBlock(blockPosition);
TurtleUtil.stopConsuming(turtle);
return broken ? TurtleCommandResult.success() : TurtleCommandResult.failure("Cannot break protected block");
});
}
private static boolean isTriviallyBreakable(BlockGetter reader, BlockPos pos, BlockState state) {
return state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE)
return
state.is(ComputerCraftTags.Blocks.TURTLE_ALWAYS_BREAKABLE)
// Allow breaking any "instabreak" block.
|| state.getDestroySpeed(reader, pos) == 0;
}

View File

@@ -5,6 +5,7 @@
package dan200.computercraft.shared.turtle.upgrades;
import com.google.gson.JsonObject;
import dan200.computercraft.api.turtle.TurtleToolDurability;
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
import dan200.computercraft.api.upgrades.UpgradeBase;
import dan200.computercraft.shared.platform.RegistryWrappers;
@@ -28,6 +29,8 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
var toolItem = GsonHelper.getAsItem(object, "item");
var craftingItem = GsonHelper.getAsItem(object, "craftingItem", toolItem);
var damageMultiplier = GsonHelper.getAsFloat(object, "damageMultiplier", 3.0f);
var allowEnchantments = GsonHelper.getAsBoolean(object, "allowEnchantments", false);
var consumeDurability = TurtleToolDurability.CODEC.byName(GsonHelper.getAsString(object, "consumeDurability", null), TurtleToolDurability.NEVER);
TagKey<Block> breakable = null;
if (object.has("breakable")) {
@@ -35,7 +38,7 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
breakable = TagKey.create(Registries.BLOCK, tag);
}
return new TurtleTool(id, adjective, craftingItem, new ItemStack(toolItem), damageMultiplier, breakable);
return new TurtleTool(id, adjective, craftingItem, new ItemStack(toolItem), damageMultiplier, allowEnchantments, consumeDurability, breakable);
}
@Override
@@ -46,9 +49,11 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
// damageMultiplier and breakable aren't used by the client, but we need to construct the upgrade exactly
// as otherwise syncing on an SP world will overwrite the (shared) upgrade registry with an invalid upgrade!
var damageMultiplier = buffer.readFloat();
var allowsEnchantments = buffer.readBoolean();
var consumesDurability = buffer.readEnum(TurtleToolDurability.class);
var breakable = buffer.readBoolean() ? TagKey.create(Registries.BLOCK, buffer.readResourceLocation()) : null;
return new TurtleTool(id, adjective, craftingItem, toolItem, damageMultiplier, breakable);
return new TurtleTool(id, adjective, craftingItem, toolItem, damageMultiplier, allowsEnchantments, consumesDurability, breakable);
}
@Override
@@ -57,6 +62,8 @@ public final class TurtleToolSerialiser implements TurtleUpgradeSerialiser<Turtl
RegistryWrappers.writeId(buffer, RegistryWrappers.ITEMS, upgrade.getCraftingItem().getItem());
buffer.writeItem(upgrade.item);
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());
}

View File

@@ -7,6 +7,7 @@ package dan200.computercraft.shared.util;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.BaseEncoding;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.nbt.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,6 +20,7 @@ import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -27,9 +29,42 @@ public final class NBTUtil {
@VisibleForTesting
static final BaseEncoding ENCODING = BaseEncoding.base16().lowerCase();
private static final CompoundTag EMPTY_TAG;
static {
// If in a development environment, create a magic immutable compound tag.
// We avoid doing this in prod, as I fear it might mess up the JIT inlining things.
if (PlatformHelper.get().isDevelopmentEnvironment()) {
try {
var ctor = CompoundTag.class.getDeclaredConstructor(Map.class);
ctor.setAccessible(true);
EMPTY_TAG = ctor.newInstance(Collections.emptyMap());
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
} else {
EMPTY_TAG = new CompoundTag();
}
}
private NBTUtil() {
}
/**
* Get a singleton empty {@link CompoundTag}. This tag should never be modified.
*
* @return The empty compound tag.
*/
public static CompoundTag emptyTag() {
if (EMPTY_TAG.size() != 0) LOG.error("The empty tag has been modified.");
return EMPTY_TAG;
}
public static CompoundTag getCompoundOrEmpty(CompoundTag tag, String key) {
var childTag = tag.get(key);
return childTag != null && childTag.getId() == Tag.TAG_COMPOUND ? (CompoundTag) childTag : emptyTag();
}
private static @Nullable Tag toNBTTag(@Nullable Object object) {
if (object == null) return null;
if (object instanceof Boolean) return ByteTag.valueOf((byte) ((boolean) (Boolean) object ? 1 : 0));

View File

@@ -217,5 +217,15 @@
"gui.computercraft.config.http.enabled.tooltip": "Active l'API \"http\" sur les ordinateurs. Cela désactive également les programmes \"pastebin\" et \"wget\",\nsur lesquels de nombreux utilisateurs comptent. Il est recommandé de laisser cette option activée et\nd'utiliser l'option de configuration \"rules\" pour imposer un contrôle plus précis.",
"gui.computercraft.config.peripheral.modem_range.tooltip": "La portée des modems sans fil à basse altitude par temps dégagé, en mètres.\nPlage : 0 ~ 100000",
"gui.computercraft.config.peripheral.monitor_bandwidth.tooltip": "La limite de la quantité de données du moniteur pouvant être envoyées *par tick*. Note:\n - La bande passante est mesurée avant la compression, donc les données envoyées\n au client sont plus petites.\n - Cela ignore le nombre de joueurs auxquels un paquet est envoyé. La mise à jour d'un\n moniteur pour un joueur consomme la même limite de bande passante que l'envoi à 20.\n - Un moniteur de taille normale envoie ~25ko de données. Ainsi, la valeur par défaut (1Mo) permet \n à environ 40 moniteurs d'être mis à jour en un seul tick.\nMettre à 0 pour désactiver.\nPlage: > 0",
"gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué."
"gui.computercraft.config.http.rules.tooltip": "Une liste de règles qui contrôlent le comportement de l'API \"http\" pour des domaines\nou des IP spécifiques. Chaque règle est un élément avec un 'hôte' à comparer et une série\nde propriétés. Les règles sont évaluées dans l'ordre, ce qui signifie que les règles antérieures\nremplacent les suivantes.\nL'hôte peut être un nom de domaine (\"pastebin.com\"), un astérisque (\"*.pastebin.com\") ou\nune notation CIDR (\"127.0.0.0/8\").\nS'il n'y a pas de règles, le domaine est bloqué.",
"gui.computercraft.config.http.proxy.host.tooltip": "Le nom d'hôte ou l'adresse IP du serveur proxy.",
"gui.computercraft.config.http.proxy.tooltip": "Tunnelise les requêtes HTTP et websocket via un serveur proxy. Affecte uniquement\nles règles HTTP avec \"use_proxy\" défini sur true (désactivé par défaut).\nSi l'authentification est requise pour le proxy, créez un fichier \"computercraft-proxy.pw\"\ndans le même dossier que \"computercraft-server.toml\", contenant le\nnom d'utilisateur et mot de passe séparés par deux-points, par ex. \"monutilisateur:monmotdepasse\". Pour\nProxy SOCKS4, seul le nom d'utilisateur est requis.",
"gui.computercraft.config.upload_max_size.tooltip": "La taille limite de téléversement de fichier, en octets. Doit être compris entre 1 Kio et 16 Mio.\nGardez à l'esprit que les téléversements sont traités en un seul clic - les fichiers volumineux ou\nde mauvaises performances réseau peuvent bloquer le thread du réseau. Et attention à l'espace disque !\nPlage : 1024 ~ 16777216",
"gui.computercraft.config.http.proxy": "Proxy",
"gui.computercraft.config.http.proxy.host": "Nom d'hôte",
"gui.computercraft.config.http.proxy.port": "Port",
"gui.computercraft.config.http.proxy.port.tooltip": "Le port du serveur proxy.\nPlage : 1 ~ 65536",
"gui.computercraft.config.http.proxy.type": "Type de proxy",
"gui.computercraft.config.http.proxy.type.tooltip": "Le type de proxy à utiliser.\nValeurs autorisées : HTTP, HTTPS, SOCKS4, SOCKS5",
"gui.computercraft.config.upload_max_size": "Taille limite de téléversement de fichiers (octets)"
}

View File

@@ -115,5 +115,85 @@
"upgrade.minecraft.diamond_pickaxe.adjective": "Добывающая",
"upgrade.minecraft.diamond_shovel.adjective": "Копающая",
"upgrade.minecraft.diamond_sword.adjective": "Боевая",
"gui.computercraft.pocket_computer_overlay": "Карманный компьютер открыт. Чтобы закрыть, нажми ESC."
"gui.computercraft.pocket_computer_overlay": "Карманный компьютер открыт. Чтобы закрыть, нажми ESC.",
"gui.computercraft.config.command_require_creative.tooltip": "Требовать творческий режим и права оператора для взаимодействия с\nкомандными компьютерами. Это поведение по умолчанию для Командных блоков ванильной игры.",
"gui.computercraft.config.default_computer_settings.tooltip": "Разделенный запятыми список системных настроек по умолчанию на новых компьютерах.\nНапример: \"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\"\nотключит всё автодополнение.",
"gui.computercraft.config.execution.computer_threads.tooltip": "Устанавливает количество потоков, на которых работают компьютеры. Большее число\nозначает, что больше компьютеров сможет работать одновременно, но может привести к лагу.\nОбратите внимание, что некоторые моды могут не работать с более чем одним потоком. Используйте с осторожностью.\nОграничение: > 1",
"gui.computercraft.config.execution.max_main_computer_time.tooltip": "Идеальный максимум времени, которое отведено компьютеру на выполнение задач, в миллисекундах.\nМы вполне возможно выйдем за этот лимит, так как невозможно предсказать сколько\nвремени будет затрачено на выполнение задач, это лишь верхний лимит среднего значения времени.\nОграничение: > 1",
"gui.computercraft.config.execution.max_main_global_time.tooltip": "Максимум времени, которое может быть потрачено на выполнение задач за один тик, в \nмиллисекундах. \nМы вполне возможно выйдем за этот лимит, так как невозможно предсказать сколько\nвремени будет затрачено на выполнение задач, это лишь верхний лимит среднего значения времени.\nОграничение: > 1",
"gui.computercraft.config.http.bandwidth.global_download": "Глобальный лимит на скачивание",
"gui.computercraft.config.http.bandwidth.global_download.tooltip": "Количество байтов, которое можно скачать за секунду. Все компьютеры делят эту пропускную способность. (байты в секунду)\nОграничение: > 1",
"gui.computercraft.config.http.bandwidth.global_upload.tooltip": "Количество байтов, которое можно загрузить за секунду. Все компьютеры делят эту пропускную способность. (байты в секунду)\nОграничение: > 1",
"tracking_field.computercraft.http_requests.name": "HTTP запросы",
"tracking_field.computercraft.turtle_ops.name": "Операции Черепашек",
"gui.computercraft.config.http.enabled.tooltip": "Включить API \"http\" на Компьютерах. Это также отключает программы \"pastebin\" и \"wget\", \nкоторые нужны многим пользователям. Рекомендуется оставить это включенным и использовать \nконфиг \"rules\" для более тонкой настройки.",
"gui.computercraft.config.http.max_websockets.tooltip": "Количество одновременно открытых веб-сокетов, которые может иметь компьютер. Установите на 0 для неограниченных веб-сокетов.\nОграничение: > 1",
"gui.computercraft.config.term_sizes": "Размер терминала",
"gui.computercraft.config.term_sizes.computer.height": "Высота терминала",
"gui.computercraft.config.term_sizes.monitor.height": "Максимальная высота монитора",
"gui.computercraft.config.term_sizes.monitor.width.tooltip": "Ограничение: 1 ~ 32",
"gui.computercraft.config.turtle.advanced_fuel_limit": "Лимит топлива Продвинутых Черепашек",
"gui.computercraft.config.turtle.need_fuel.tooltip": "Устанавливает, нуждаются ли Черепашки в топливе для передвижения.",
"gui.computercraft.terminal": "Компьютерный терминал",
"tracking_field.computercraft.computer_tasks.name": "Задачи",
"tracking_field.computercraft.server_tasks.name": "Серверные задачи",
"gui.computercraft.upload.no_response": "Перенос файлов",
"tracking_field.computercraft.avg": "%s (среднее)",
"gui.computercraft.config.command_require_creative": "Для использования командных компьютеров нужен творческий режим",
"gui.computercraft.config.computer_space_limit": "Лимит места на компьютерах (в байтах)",
"gui.computercraft.config.computer_space_limit.tooltip": "Лимит места на дисках компьютеров и черепашек, в байтах.",
"gui.computercraft.config.default_computer_settings": "Настройки Компьютера по умолчанию",
"gui.computercraft.config.disable_lua51_features": "Отключить функции Lua 5.1",
"gui.computercraft.config.disable_lua51_features.tooltip": "Поставьте, чтобы отключить функции из Lua 5.1, которые будут убраны в будущих\nобновлениях. Полезно для того, чтобы улучшить совместимость вперед ваших программ.",
"gui.computercraft.config.execution": "Выполнение",
"gui.computercraft.config.execution.computer_threads": "Потоки компьютера",
"gui.computercraft.config.execution.max_main_global_time": "Глобальный лимит времени на тик сервера",
"gui.computercraft.config.execution.tooltip": "Контролирует поведение выполнения задач компьютеров. Эта настройка преднезначается для \nтонкой настройки серверов, и в основном не должна быть изменена.",
"gui.computercraft.config.floppy_space_limit": "Лимит места на дискетах (байты)",
"gui.computercraft.config.floppy_space_limit.tooltip": "Лимит места для хранения информации на дискетах, в байтах.",
"gui.computercraft.config.http": "HTTP",
"gui.computercraft.config.http.bandwidth": "Пропускная способность",
"gui.computercraft.config.http.bandwidth.global_upload": "Глобальный лимит загрузки",
"gui.computercraft.config.http.bandwidth.tooltip": "Ограничивает пропускную способность, используемую компьютерами.",
"gui.computercraft.config.http.enabled": "Включить HTTP API",
"gui.computercraft.config.http.max_requests": "Максимум одновременных запросов",
"gui.computercraft.config.http.max_requests.tooltip": "Количество http-запросов, которые компьютер может сделать одновременно. Дополнительные запросы \nбудут поставлены в очередь, и отправлены когда существующие запросы будут выполнены. Установите на 0 для \nнеограниченных запросов.\nОграничение: > 0",
"gui.computercraft.config.http.max_websockets": "Максимум одновременных веб-сокетов",
"gui.computercraft.config.term_sizes.computer": "Компьютер",
"gui.computercraft.config.term_sizes.computer.height.tooltip": "Ограничение: 1 ~ 255",
"gui.computercraft.config.term_sizes.computer.tooltip": "Размер терминала на компьютерах.",
"gui.computercraft.config.term_sizes.computer.width": "Ширина терминала",
"gui.computercraft.config.term_sizes.computer.width.tooltip": "Ограничение: 1 ~ 255",
"gui.computercraft.config.term_sizes.monitor": "Монитор",
"gui.computercraft.config.term_sizes.monitor.height.tooltip": "Ограничение: 1 ~ 32",
"gui.computercraft.config.term_sizes.monitor.tooltip": "Максимальный размер мониторов (в блоках).",
"gui.computercraft.config.term_sizes.monitor.width": "Максимальная ширина мониторов",
"gui.computercraft.config.term_sizes.pocket_computer.height": "Высота терминала",
"gui.computercraft.config.term_sizes.pocket_computer.height.tooltip": "Ограничение: 1 ~ 255",
"gui.computercraft.config.term_sizes.pocket_computer.width": "Ширина терминала",
"gui.computercraft.config.term_sizes.pocket_computer.width.tooltip": "Ограничение: 1 ~ 255",
"gui.computercraft.config.turtle": "Черепашки",
"gui.computercraft.config.turtle.advanced_fuel_limit.tooltip": "Лимит топлива для Продвинутых Черепашек.\nОграничение: > 0",
"gui.computercraft.config.turtle.need_fuel": "Включить механику топлива",
"gui.computercraft.config.turtle.normal_fuel_limit": "Лимит топлива Черепашек",
"gui.computercraft.config.turtle.normal_fuel_limit.tooltip": "Лимит топлива для Черепашек.\nОграничение: > 0",
"gui.computercraft.config.turtle.tooltip": "Разные настройки, связанные с черепашками.",
"gui.computercraft.config.http.proxy.port": "Порт",
"gui.computercraft.config.http.proxy.port.tooltip": "Порт прокси-сервера.\nДиапазон: 1 ~ 65536",
"gui.computercraft.config.http.proxy.host": "Имя хоста",
"gui.computercraft.config.http.proxy": "Proxy",
"gui.computercraft.config.http.proxy.host.tooltip": "Имя хоста или IP-адрес прокси-сервера.",
"gui.computercraft.config.http.proxy.tooltip": "Туннелирует HTTP-запросы и запросы websocket через прокси-сервер. Влияет только на HTTP\nправила с параметром \"use_proxy\" в значении true (отключено по умолчанию).\nЕсли для прокси-сервера требуется аутентификация, создайте \"computercraft-proxy.pw\"\nфайл в том же каталоге, что и \"computercraft-server.toml\", содержащий имя\nпользователя и пароль, разделенные двоеточием, например \"myuser:mypassword\". Для\nпрокси-серверов SOCKS4 требуется только имя пользователя.",
"gui.computercraft.config.http.proxy.type": "Тип прокси-сервера",
"gui.computercraft.config.http.proxy.type.tooltip": "Тип используемого прокси-сервера.\nДопустимые значения: HTTP, HTTPS, SOCKS4, SOCKS5",
"gui.computercraft.upload.no_response.msg": "Ваш компьютер не использовал переданные вами файлы. Возможно, вам потребуется запустить программу %s и повторить попытку.",
"tracking_field.computercraft.max": "%s (максимальное)",
"tracking_field.computercraft.count": "%s (количество)",
"gui.computercraft.config.http.rules": "Разрешающие/запрещающие правила",
"gui.computercraft.config.http.websocket_enabled": "Включить веб-сокеты",
"gui.computercraft.config.http.websocket_enabled.tooltip": "Включить использование http веб-сокетов. Для этого необходимо, чтобы параметр «http_enable» был true.",
"gui.computercraft.config.log_computer_errors": "Регистрировать ошибки компьютера",
"gui.computercraft.config.log_computer_errors.tooltip": "Регистрировать исключения, вызванные периферийными устройствами и другими объектами Lua. Это облегчает\nдля авторам модов устранение проблем, но может привести к спаму в логах, если люди будут использовать\nглючные методы.",
"gui.computercraft.config.maximum_open_files": "Максимальное количество файлов, открытых на одном компьютере",
"gui.computercraft.config.http.rules.tooltip": "Список правил, которые контролируют поведение «http» API для определенных доменов или\nIP-адресов. Каждое правило представляет собой элемент с «узлом» для сопоставления и набором\nсвойств. Правила оцениваются по порядку, то есть более ранние правила перевешивают\nболее поздние.\nХост может быть доменным именем (\"pastebin.com\"), wildcard-сертификатом (\"*.pastebin.com\") или\nнотацией CIDR (\"127.0.0.0/8\").\nЕсли правил нет, домен блокируется."
}

View File

@@ -0,0 +1,39 @@
{
"parent": "minecraft:block/block",
"render_type": "cutout",
"textures": {
"particle": "#front"
},
"display": {
"firstperson_righthand": {
"rotation": [ 0, 135, 0 ],
"translation": [ 0, 0, 0 ],
"scale": [ 0.40, 0.40, 0.40 ]
}
},
"elements": [
{
"from": [ 0, 0, 0 ],
"to": [ 16, 16, 16 ],
"faces": {
"down": { "texture": "#top", "cullface": "down" },
"up": { "texture": "#top", "cullface": "up" },
"north": { "texture": "#front", "cullface": "north" },
"south": { "texture": "#side", "cullface": "south" },
"west": { "texture": "#side", "cullface": "west" },
"east": { "texture": "#side", "cullface": "east" }
}
},
{
"from": [ 0, 0, 0 ],
"to": [ 16, 16, 16 ],
"faces": {
"north": {
"texture": "#cursor",
"cullface": "north",
"forge_data": {"block_light": 15, "sky_light": 15}
}
}
}
]
}

View File

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

View File

@@ -1,6 +0,0 @@
{
"animation": {
"frametime": 8,
"frames": [ 0, 1 ]
}
}

View File

@@ -1,6 +0,0 @@
{
"animation": {
"frametime": 8,
"frames": [ 0, 1 ]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

View File

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

View File

@@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
SPDX-License-Identifier: MPL-2.0

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