1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-10-23 09:57:39 +00:00

Compare commits

...

29 Commits

Author SHA1 Message Date
Jonathan Coates
8be6b1b772 Bump CC:T to 1.109.3 2024-01-03 20:00:52 +00:00
Jonathan Coates
104d5e70de Update to Cobalt 0.8.2
- Fix error when using "goto" as the first statement in an if block.
 - Fix incorrect resizing of table's hash part when adding and removing
   keys.
2024-01-03 18:44:37 +00:00
Jonathan Coates
e3bda2f763 Add command computers to the operator blocks tab
Fixes #1666
2024-01-03 18:42:31 +00:00
Jonathan Coates
234f69e8e5 Add a MessageType for network messages
Everything old is new again!

CC's network message implementation has gone through several iterations:

 - Originally network messages were implemented with a single class,
   which held an packet id/type and and opaque blobs of data (as
   string/int/byte/NBT arrays), and a big switch statement to decode and
   process this data.

 - In 42d3901ee3, we split the messages
   into different classes all inheriting from NetworkMessage - this bit
   we've stuck with ever since.

   Each packet had a `getId(): int` method, which returned the
   discriminator for this packet.

 - However, getId() was only used when registering the packet, not when
   sending, and so in ce0685c31f we
   removed it, just passing in a constant integer at registration
   instead.

 - In 53abe5e56e, we made some relatively
   minor changes to make the code more multi-loader/split-source
   friendly. However, this meant when we finally came to add Fabric
   support (8152f19b6e), we had to
   re-implement a lot of Forge's network code.

In 1.20.4, Forge moves to a system much closer to Fabric's (and indeed,
Minecraft's own CustomPacketPayload), and so it makes sense to adapt to
that now. As such, we:

 - Add a new MessageType interface. This is implemented by the
   loader-specific modules, and holds whatever information is needed to
   register the packet (e.g. discriminator, reader function).

 - Each NetworkMessage now has a type(): MessageType<?> function. This
   is used by the Fabric networking code (and for NeoForge's on 1.20.4)
   instead of a class lookup.

 - NetworkMessages now creates/stores these MessageType<T>s (much like
   we'd do for registries), and provides getters for the
   clientbound/serverbound messages. Mod initialisers then call these
   getters to register packets.

 - For Forge, this is relatively unchanged. For Fabric, we now
   `FabricPacket`s.
2024-01-03 10:23:41 +00:00
Jonathan Coates
ed3a17f9b9 Fix trailing-comma errors on method calls
We were only matching `f(a, ` patterns, and not `x:f(a, `. We now just
match against any usages of call_args - hadn't quite realised we could
do that!
2023-12-28 17:07:39 +00:00
Jonathan Coates
0349c2b1f9 Allow local domains in the standalone emulator
- Clean up option parsing a bit, so it uses the Option, rather than its
   corresponding character code.
 - Add a new -L/--allow-local-domains flag to remove the $private rule
   from the HTTP rules.
2023-12-20 17:43:08 +00:00
Jonathan Coates
03f9e6bd6d Prevent sending too many websocket messages at once 2023-12-20 17:30:57 +00:00
Jonathan Coates
9d8c933a14 Remove several usages of ComputerFamily
While ComputerFamily is still useful, there's definitely some places
where it adds an extra layer of indirection. This commit attempts to
clean up some places where we no longer need it.

 - Remove ComputerFamily from AbstractComputerBlock. The only place this
   was needed is in TurtleBlock, and that can be replaced with normal
   Minecraft explosion resistence!

 - Pass in the fuel limit to the turtle block entity, rather than
   deriving it from current family.

 - The turtle BERs now derive their model from the turtle's item, rather
   than the turtle's family.

 - When creating upgrade/overlay recipes, use the item's name, rather
   than {pocket,turtle}_family. This means we can drop getFamily() from
   IComputerItem (it is still needed on to handle the UI).

 - We replace IComputerItem.withFamily with a method to change to a
   different item of the same type. ComputerUpgradeRecipe no longer
   takes a family, and instead just uses the result's item.

 - Computer blocks now use the normal Block.asItem() to find their
   corresponding item, rather than looking it up via family.

The above means we can remove all the family-based XyzItem.create(...)
methods, which have always felt a little ugly.

We still need ComputerFamily for a couple of things:
 - Permission checks for command computers.
 - Checks for mouse/colour support in ServerComputer.
 - UI textures.
2023-12-20 14:17:38 +00:00
Jonathan Coates
78bb3da58c Improve our version tooling
- Add a check to ensure declared dependencies in the :core project, and
   those inherited from Minecraft are the same.
 - Compute the next Cobalt version, rather than specifying it manually.
 - Add the gradle versions plugin (and version catalog update), and
   update some versions.
2023-12-19 18:12:21 +00:00
Jonathan Coates
39a5e40c92 Bump CC:T to 1.109.2
Take two!
2023-12-16 22:46:30 +00:00
Jonathan Coates
763ba51919 Update Cobalt to 0.8.1 2023-12-16 22:39:48 +00:00
Jonathan Coates
cf6ec8c28f Add a slightly cleaner system for excluding deps
Previously we prevented our published full jar depending on any of the
other projects by excluding the whole cc.tweaked jar. However, as Cobalt
also now lives in that group, this meant we were missing the Cobalt
dependency.

Rather than specifying a wildcard, we now exclude the dependencies when
adding them to the project.
2023-12-16 22:35:15 +00:00
Jonathan Coates
95d3b646b2 Bump CC:T to 1.109.1 2023-12-16 19:09:39 +00:00
Jonathan Coates
488f66eead Fix mouse_drag not firing for right/middle buttons
This is a bit of an odd combination of a few bugs:
 - When the terminal component is blurred, we fire a mouse_up event for
   the last-held button. However, we had an off-by-1 error here, so this
   only triggered for the right/middle buttons.

 - This was obsucuring the second bug, which is when we clicked within
   the terminal, this caused the terminal to be blurred (thus releasing
   the mouse) and then focused again.

   We fix this by only setting the focus if there's actually a change.

Fixes #1655
2023-12-10 12:01:34 +00:00
Jonathan Coates
1f7d245876 Specify charset when printing error messages 2023-12-08 09:52:17 +00:00
Jonathan Coates
af12b3a0ea Fix goto/:: tokens erroring in error reporting 2023-12-07 19:47:39 +00:00
Jonathan Coates
eb3e8ba677 Fix deadlock when adding/removing observers
When adding/removing observers, we locked on the observer, then
acquired the global lock. When a metric is observed, then we acquire the
global lock and then the observer lock.

If these happen at the same time, we can easily end up with a deadlock.
We simply avoid holding the observer lock for the entire add/remove
process (instead only locking when actually needed).

Closes #1639
2023-12-01 12:33:03 +00:00
Jonathan Coates
2043939531 Add compostors to the list of usable blocks
Fixes #1638
2023-11-22 18:24:59 +00:00
Jonathan Coates
84a799d27a Add abstract classes for our generic peripherals
This commit adds abstract classes to describe the interface for our
mod-loader-specific generic peripherals (inventories, fluid storage,
item storage).

This offers several advantages:
 - Javadoc to illuaminate conversion no longer needs the Forge project
   (just core and common).

 - Ensures we have a consistent interface between Forge and Fabric.

Note, this does /not/ implement fluid or energy storage for Fabric. We
probably could do fluid without issue, but not something worth doing
right now.
2023-11-22 18:20:15 +00:00
Jonathan Coates
fe826f5c9c Allow generic sources to have instance methods
Rather than assuming static methods are generic, and instance methods
are direct, the Generator now has separate entrypoints for handling
instance and generic methods.

As a result of this change, we've also relaxed some of the validation
code. As a result, we now allow calling private/protected methods
which are annotated with @LuaFunction.
2023-11-22 10:06:11 +00:00
Jonathan Coates
f8b7422294 Fix several issues with the web emulator
- Bump TeaVM version to fix issues with locales and our "export"
   generation.
 - Fix TComputerThread not requeuing correctly.
2023-11-15 13:12:31 +00:00
Jonathan Coates
b343c01216 Bump CC:T to 1.109.0 2023-11-15 09:39:52 +00:00
Jonathan Coates
76968f2f28 Track allocations while executing computers
This adds a new "java_allocation" metric, which tracks the number of
bytes allocated while executing the computer (as measured by Java). This
is not an 100% reliable number, but hopefully gives some insight into
what computers are doing.
2023-11-09 18:36:35 +00:00
Jonathan Coates
1d365f5a0b Add option to allow repetition in JSON serialiser
Closes #1588
2023-11-08 21:29:43 +00:00
Jonathan Coates
7b240cbf7e Merge pull request #1615 from cc-tweaked/feature/much-breakage-very-wow
Remove text mode, update Cobalt
2023-11-08 20:05:49 +00:00
Jonathan Coates
d272a327c7 Update CraftOS version to 1.9 2023-11-08 19:40:14 +00:00
Jonathan Coates
0c0556a5bc Always use raw bytes in file handles
Historically CC has supported two modes when working with file handles
(and HTTP requests):

 - Text mode, which reads/write using UTF-8.
 - Binary mode, which reads/writes the raw bytes.

However, this can be confusing at times. CC/Lua doesn't actually support
unicode, so any characters beyond the 0.255 range were replaced with
'?'. This meant that most of the time you were better off just using
binary mode.

This commit unifies text and binary mode - we now /always/ read the raw
bytes of the file, rather than converting to/from UTF-8. Binary mode now
only specifies whether handle.read() returns a number (and .write(123)
writes a byte rather than coercing to a string).

 - Refactor the entire handle hierarchy. We now have an AbstractMount
   base class, which has the concrete implementation of all methods. The
   public-facing classes then re-export these methods by annotating
   them with @LuaFunction.

   These implementations are based on the
   Binary{Readable,Writable}Handle classes. The Encoded{..}Handle
   versions are now entirely removed.

 - As we no longer need to use BufferedReader/BufferedWriter, we can
   remove quite a lot of logic in Filesystem to handle wrapping
   closeable objects.

 - Add a new WritableMount.openFile method, which generalises
   openForWrite/openForAppend to accept OpenOptions. This allows us to
   support update mode (r+, w+) in fs.open.

 - fs.open now uses the new handle types, and supports update (r+, w+)
   mode.

 - http.request now uses the new readable handle type. We no longer
   encode the request body to UTF-8, nor decode the response from UTF-8.

 - Websockets now return text frame's contents directly, rather than
   converting it from UTF-8. Sending text frames now attempts to treat
   the passed string as UTF-8, rather than treating it as latin1.
2023-11-08 19:40:14 +00:00
Jonathan Coates
87345c6b2e Add pasting support to the standalone emulator
- Move paste normalisation code to StringUtil, so it can be shared by
   emulators.
 - Add paste support to the emulator.
2023-11-08 19:40:14 +00:00
Jonathan Coates
784e623776 Update Cobalt to 0.8.0
- Update Cobalt to 0.8.0, switching our Lua version to 5.2(ish).

 - Remove our `load` wrapper, as we no longer need to inject _ENV into
   the enviroment table.

 - Update the parser to handle labels and goto. This doesn't check that
   gotos are well formed, but at least means the parser doesn't fall
   over on them.

 - Update our docs to reflect the changes to Cobalt.
2023-11-08 18:42:17 +00:00
198 changed files with 3306 additions and 2364 deletions

View File

@@ -42,7 +42,6 @@ repositories {
url "https://squiddev.cc/maven/"
content {
includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
}
}
}

View File

@@ -13,6 +13,8 @@ plugins {
publishing
alias(libs.plugins.taskTree)
alias(libs.plugins.githubRelease)
alias(libs.plugins.gradleVersions)
alias(libs.plugins.versionCatalogUpdate)
id("org.jetbrains.gradle.plugin.idea-ext")
id("cc-tweaked")
}
@@ -102,3 +104,9 @@ idea.project.settings.compiler.javac {
}
.toMap()
}
versionCatalogUpdate {
sortByKey.set(false)
pin { versions.addAll("fastutil", "guava", "netty", "slf4j") }
keep { keepUnusedLibraries.set(true) }
}

View File

@@ -5,6 +5,8 @@
plugins {
`java-gradle-plugin`
`kotlin-dsl`
alias(libs.plugins.gradleVersions)
alias(libs.plugins.versionCatalogUpdate)
}
// Duplicated in settings.gradle.kts
@@ -75,3 +77,9 @@ gradlePlugin {
}
}
}
versionCatalogUpdate {
sortByKey.set(false)
keep { keepUnusedLibraries.set(true) }
catalogFile.set(file("../gradle/libs.versions.toml"))
}

View File

@@ -57,7 +57,6 @@ repositories {
filter {
includeGroup("cc.tweaked")
includeModule("org.squiddev", "Cobalt")
// Things we mirror
includeGroup("commoble.morered")
includeGroup("dev.architectury")
@@ -77,6 +76,12 @@ dependencies {
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
checkstyle(libs.findLibrary("checkstyle").get())
constraints {
checkstyle("org.codehaus.plexus:plexus-container-default:2.1.1") {
because("2.1.0 depends on deprecated Google collections module")
}
}
errorprone(libs.findLibrary("errorProne-core").get())
errorprone(libs.findLibrary("nullAway").get())
}
@@ -202,6 +207,7 @@ spotless {
val ktlintConfig = mapOf(
"ktlint_standard_no-wildcard-imports" to "disabled",
"ktlint_standard_class-naming" to "disabled",
"ktlint_standard_function-naming" to "disabled",
"ij_kotlin_allow_trailing_comma" to "true",
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
)

View File

@@ -10,9 +10,11 @@ import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.attributes.TestSuiteType
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.reporting.ReportingExtension
@@ -73,11 +75,17 @@ abstract class CCTweakedExtension(
*/
val sourceDirectories: SetProperty<SourceSetReference> = project.objects.setProperty(SourceSetReference::class.java)
/**
* Dependencies excluded from published artifacts.
*/
private val excludedDeps: ListProperty<Dependency> = project.objects.listProperty(Dependency::class.java)
/** All source sets referenced by this project. */
val sourceSets = sourceDirectories.map { x -> x.map { it.sourceSet } }
init {
sourceDirectories.finalizeValueOnRead()
excludedDeps.finalizeValueOnRead()
project.afterEvaluate { sourceDirectories.disallowChanges() }
}
@@ -246,6 +254,20 @@ abstract class CCTweakedExtension(
).resolve().single()
}
/**
* Exclude a dependency from being publisehd in Maven.
*/
fun exclude(dep: Dependency) {
excludedDeps.add(dep)
}
/**
* Configure a [MavenDependencySpec].
*/
fun configureExcludes(spec: MavenDependencySpec) {
for (dep in excludedDeps.get()) spec.exclude(dep)
}
companion object {
private val COMMIT_COUNTS = Pattern.compile("""^\s*[0-9]+\s+(.*)$""")
private val IGNORED_USERS = setOf(

View File

@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package cc.tweaked.gradle
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.artifacts.component.ModuleComponentSelector
import org.gradle.api.artifacts.component.ProjectComponentIdentifier
import org.gradle.api.artifacts.result.DependencyResult
import org.gradle.api.artifacts.result.ResolvedDependencyResult
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.language.base.plugins.LifecycleBasePlugin
abstract class DependencyCheck : DefaultTask() {
@get:Input
abstract val configuration: ListProperty<Configuration>
init {
description = "Check :core's dependencies are consistent with Minecraft's."
group = LifecycleBasePlugin.VERIFICATION_GROUP
}
@TaskAction
fun run() {
var ok = true
for (configuration in configuration.get()) {
configuration.incoming.resolutionResult.allDependencies {
if (!check(this@allDependencies)) ok = false
}
}
if (!ok) {
throw GradleException("Mismatched versions in Minecraft dependencies. gradle/libs.versions.toml may need updating.")
}
}
private fun check(dependency: DependencyResult): Boolean {
if (dependency !is ResolvedDependencyResult) {
logger.warn("Found unexpected dependency result {}", dependency)
return false
}
// Skip dependencies on non-modules.
val requested = dependency.requested
if (requested !is ModuleComponentSelector) return true
// If this dependency is specified within some project (so is non-transitive), or is pulled in via Minecraft,
// then check for consistency.
// It would be nice to be smarter about transitive dependencies, but avoiding false positives is hard.
val from = dependency.from.id
if (
from is ProjectComponentIdentifier ||
from is ModuleComponentIdentifier && (from.group == "net.minecraft" || from.group == "io.netty")
) {
// If the version is different between the requested and selected version, report an error.
val selected = dependency.selected.moduleVersion!!.version
if (requested.version != selected) {
logger.error("Requested dependency {} (via {}) but got version {}", requested, from, selected)
return false
}
return true
}
return true
}
}

View File

@@ -6,7 +6,6 @@ package cc.tweaked.gradle
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.file.FileSystemLocationProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec
@@ -129,3 +128,30 @@ fun <T> Property<T>.setProvider(provider: Provider<out T>) = set(provider)
/** Short-cut method to get the absolute path of a [FileSystemLocation] provider. */
fun Provider<out FileSystemLocation>.getAbsolutePath(): String = get().asFile.absolutePath
/**
* Get the version immediately after the provided version.
*
* For example, given "1.2.3", this will return "1.2.4".
*/
fun getNextVersion(version: String): String {
// Split a version like x.y.z-SNAPSHOT into x.y.z and -SNAPSHOT
val dashIndex = version.indexOf('-')
val mainVersion = if (dashIndex < 0) version else version.substring(0, dashIndex)
// Find the last component in x.y.z and increment it.
val lastIndex = mainVersion.lastIndexOf('.')
if (lastIndex < 0) throw IllegalArgumentException("Cannot parse version format \"$version\"")
val lastVersion = try {
version.substring(lastIndex + 1).toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("Cannot parse version format \"$version\"", e)
}
// Then append all components together.
val out = StringBuilder()
out.append(version, 0, lastIndex + 1)
out.append(lastVersion + 1)
if (dashIndex >= 0) out.append(version, dashIndex, version.length)
return out.toString()
}

View File

@@ -6,6 +6,8 @@ package cc.tweaked.gradle
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.plugins.BasePluginExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.specs.Spec
@@ -26,8 +28,13 @@ class MavenDependencySpec {
fun exclude(dep: Dependency) {
exclude {
// We have to cheat a little for project dependencies, as the project name doesn't match the artifact group.
val name = when (dep) {
is ProjectDependency -> dep.dependencyProject.extensions.getByType(BasePluginExtension::class.java).archivesName.get()
else -> dep.name
}
(dep.group.isNullOrEmpty() || dep.group == it.groupId) &&
(dep.name.isNullOrEmpty() || dep.name == it.artifactId) &&
(name.isNullOrEmpty() || name == it.artifactId) &&
(dep.version.isNullOrEmpty() || dep.version == it.version)
}
}

View File

@@ -109,6 +109,15 @@ class MinecraftConfigurations private constructor(private val project: Project)
project.extensions.configure(CCTweakedExtension::class.java) {
sourceDirectories.add(SourceSetReference.internal(client))
}
// Register a task to check there are no conflicts with the core project.
val checkDependencyConsistency =
project.tasks.register("checkDependencyConsistency", DependencyCheck::class.java) {
// We need to check both the main and client classpath *configurations*, as the actual configuration
configuration.add(configurations.named(main.runtimeClasspathConfigurationName))
configuration.add(configurations.named(client.runtimeClasspathConfigurationName))
}
project.tasks.named("check") { dependsOn(checkDependencyConsistency) }
}
private fun setupOutgoing(sourceSet: SourceSet, suffix: String = "") {

View File

@@ -12,7 +12,7 @@ SPDX-License-Identifier: MPL-2.0
The [`file_transfer`] event is queued when a user drags-and-drops a file on an open computer.
This event contains a single argument of type [`TransferredFiles`], which can be used to [get the files to be
transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.BinaryReadHandle`] with an
transferred][`TransferredFiles.getFiles`]. Each file returned is a [binary file handle][`fs.ReadHandle`] with an
additional [getName][`TransferredFile.getName`] method.
## Return values

View File

@@ -134,7 +134,7 @@ accepts blocks of DFPWM data and converts it to a list of 8-bit amplitudes, whic
As mentioned above, [`speaker.playAudio`] accepts at most 128×1024 samples in one go. DFPMW uses a single bit for each
sample, which means we want to process our audio in chunks of 16×1024 bytes (16KiB). In order to do this, we use
[`io.lines`], which provides a nice way to loop over chunks of a file. You can of course just use [`fs.open`] and
[`fs.BinaryReadHandle.read`] if you prefer.
[`fs.ReadHandle.read`] if you prefer.
## Processing audio
As mentioned near the beginning of this guide, PCM audio is pretty easy to work with as it's just a list of amplitudes.

View File

@@ -21,6 +21,20 @@ of the mod should run fine on later versions.
However, some changes to the underlying game, or CC: Tweaked's own internals may break some programs. This page serves
as documentation for breaking changes and "gotchas" one should look out for between versions.
## CC: Tweaked 1.109.0 to 1.109.2 {#cct-1.109}
- Update to Lua 5.2:
- Support for Lua 5.0's pseudo-argument `arg` has been removed. You should always use `...` for varargs.
- Environments are no longer baked into the runtime, and instead use the `_ENV` local or upvalue. `getfenv`/`setfenv`
now only work on Lua functions with an `_ENV` upvalue. `getfenv` will return the global environment when called
with other functions, and `setfenv` will have no effect.
- `load`/`loadstring` defaults to using the global environment (`_G`) rather than the current coroutine's
environment.
- Support for dumping functions (`string.dump`) and loading binary chunks has been removed.
- `math.random` now uses Lua 5.4's random number generator.
- File handles, HTTP requests and websockets now always use the original bytes rather than encoding/decoding to UTF-8.
## Minecraft 1.13 {#mc-1.13}
- The "key code" for [`key`] and [`key_up`] events has changed, due to Minecraft updating to LWJGL 3. Make sure you're
using the constants provided by the [`keys`] API, rather than hard-coding numerical values.

View File

@@ -9,17 +9,19 @@ SPDX-License-Identifier: MPL-2.0
-->
# Lua 5.2/5.3 features in CC: Tweaked
CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However, Cobalt and CC:T implement additional features from Lua 5.2 and 5.3 (as well as some deprecated 5.0 features) that are not available in base 5.1. This page lists all of the compatibility for these newer versions.
CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.2. However, Cobalt and CC:T implement additional
features from Lua 5.2 and 5.3 (as well as some deprecated 5.0 and 5.1 features). This page lists all of the
compatibility for these newer versions.
## Lua 5.2
| Feature | Supported? | Notes |
|---------------------------------------------------------------|------------|-------------------------------------------------------------------|
| `goto`/labels | | |
| `_ENV` | 🔶 | The `_ENV` global points to `getfenv()`, but it cannot be set. |
| `goto`/labels | | |
| `_ENV` | | |
| `\z` escape | ✔ | |
| `\xNN` escape | ✔ | |
| Hex literal fractional/exponent parts | ✔ | |
| Empty statements | | |
| Empty statements | | |
| `__len` metamethod | ✔ | |
| `__ipairs` metamethod | ❌ | Deprecated in Lua 5.3. `ipairs` uses `__len`/`__index` instead. |
| `__pairs` metamethod | ✔ | |
@@ -27,12 +29,12 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
| `collectgarbage` isrunning, generational, incremental options | ❌ | `collectgarbage` does not exist in CC:T. |
| New `load` syntax | ✔ | |
| `loadfile` mode parameter | ✔ | Supports both 5.1 and 5.2+ syntax. |
| Removed `loadstring` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `getfenv`, `setfenv` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `loadstring` | | |
| Removed `getfenv`, `setfenv` | 🔶 | Only supports closures with an `_ENV` upvalue. |
| `rawlen` function | ✔ | |
| Negative index to `select` | ✔ | |
| Removed `unpack` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Arguments to `xpcall` | ✔ | |
| Removed `unpack` | | |
| Arguments to `xpcall` | ✔ | |
| Second return value from `coroutine.running` | ✔ | |
| Removed `module` | ✔ | |
| `package.loaders` -> `package.searchers` | ❌ | |
@@ -40,14 +42,14 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
| `package.config` | ✔ | |
| `package.searchpath` | ✔ | |
| Removed `package.seeall` | ✔ | |
| `string.dump` on functions with upvalues (blanks them out) | | |
| `string.rep` separator | ✔ | |
| `string.dump` on functions with upvalues (blanks them out) | | `string.dump` is not supported |
| `string.rep` separator | ✔ | |
| `%g` match group | ❌ | |
| Removal of `%z` match group | ❌ | |
| Removed `table.maxn` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `table.maxn` | | |
| `table.pack`/`table.unpack` | ✔ | |
| `math.log` base argument | ✔ | |
| Removed `math.log10` | 🔶 | Only if `disable_lua51_features` is enabled in the configuration. |
| Removed `math.log10` | | |
| `*L` mode to `file:read` | ✔ | |
| `os.execute` exit type + return value | ❌ | `os.execute` does not exist in CC:T. |
| `os.exit` close argument | ❌ | `os.exit` does not exist in CC:T. |
@@ -61,7 +63,7 @@ CC: Tweaked is based off of the Cobalt Lua runtime, which uses Lua 5.1. However,
| Tail call hooks | ❌ | |
| `=` prefix for chunks | ✔ | |
| Yield across C boundary | ✔ | |
| Removal of ambiguity error | | |
| Removal of ambiguity error | | |
| Identifiers may no longer use locale-dependent letters | ✔ | |
| Ephemeron tables | ❌ | |
| Identical functions may be reused | ❌ | Removed in Lua 5.4 |

View File

@@ -95,10 +95,10 @@ function pullEventRaw(filter) end
-- nearest multiple of 0.05.
function sleep(time) end
--- Get the current CraftOS version (for example, `CraftOS 1.8`).
--- Get the current CraftOS version (for example, `CraftOS 1.9`).
--
-- This is defined by `bios.lua`. For the current version of CC:Tweaked, this
-- should return `CraftOS 1.8`.
-- should return `CraftOS 1.9`.
--
-- @treturn string The current CraftOS version.
-- @usage os.version()

View File

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

View File

@@ -10,28 +10,29 @@
fabric-api = "0.86.1+1.20.1"
fabric-loader = "0.14.21"
forge = "47.1.0"
forgeSpi = "6.0.0"
forgeSpi = "7.0.1"
mixin = "0.8.5"
parchment = "2023.08.20"
parchmentMc = "1.20.1"
# Normal dependencies
asm = "9.5"
autoService = "1.1.1"
checkerFramework = "3.32.0"
cobalt = "0.7.3"
cobalt-next = "0.7.4" # Not a real version, used to constrain the version we accept.
commonsCli = "1.3.1"
# Core dependencies (these versions are tied to the version Minecraft uses)
fastutil = "8.5.9"
guava = "31.1-jre"
jetbrainsAnnotations = "24.0.1"
netty = "4.1.82.Final"
slf4j = "2.0.1"
# Core dependencies (independent of Minecraft)
asm = "9.6"
autoService = "1.1.1"
checkerFramework = "3.42.0"
cobalt = "0.8.2"
commonsCli = "1.6.0"
jetbrainsAnnotations = "24.1.0"
jsr305 = "3.0.2"
jzlib = "1.1.3"
kotlin = "1.8.10"
kotlin-coroutines = "1.6.4"
netty = "4.1.82.Final"
kotlin = "1.9.21"
kotlin-coroutines = "1.7.3"
nightConfig = "3.6.7"
slf4j = "2.0.1"
# Minecraft mods
emi = "1.0.8+1.20.1"
@@ -47,29 +48,31 @@ sodium = "mc1.20-0.4.10"
# Testing
hamcrest = "2.2"
jqwik = "1.7.4"
junit = "5.10.0"
jqwik = "1.8.2"
junit = "5.10.1"
# Build tools
cctJavadoc = "1.8.0"
checkstyle = "10.12.3"
cctJavadoc = "1.8.2"
checkstyle = "10.12.6"
curseForgeGradle = "1.0.14"
errorProne-core = "2.21.1"
errorProne-core = "2.23.0"
errorProne-plugin = "3.1.0"
fabric-loom = "1.3.7"
fabric-loom = "1.3.9"
forgeGradle = "6.0.8"
githubRelease = "2.4.1"
githubRelease = "2.5.2"
gradleVersions = "0.50.0"
ideaExt = "1.1.7"
illuaminate = "0.1.0-44-g9ee0055"
librarian = "1.+"
lwjgl = "3.3.1"
lwjgl = "3.3.3"
minotaur = "2.+"
mixinGradle = "0.7.+"
mixinGradle = "0.7.38"
nullAway = "0.9.9"
spotless = "6.21.0"
spotless = "6.23.3"
taskTree = "2.1.1"
teavm = "0.9.0-SQUID.1"
teavm = "0.10.0-SQUID.2"
vanillaGradle = "0.2.1-SNAPSHOT"
versionCatalogUpdate = "0.8.1"
vineflower = "1.11.0"
[libraries]
@@ -78,7 +81,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" }
autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
checkerFramework = { module = "org.checkerframework:checker-qual", version.ref = "checkerFramework" }
cobalt = { module = "org.squiddev:Cobalt", version.ref = "cobalt" }
cobalt = { module = "cc.tweaked:cobalt", version.ref = "cobalt" }
commonsCli = { module = "commons-cli:commons-cli", version.ref = "commonsCli" }
fastutil = { module = "it.unimi.dsi:fastutil", version.ref = "fastutil" }
forgeSpi = { module = "net.minecraftforge:forgespi", version.ref = "forgeSpi" }
@@ -162,10 +165,12 @@ vineflower = { module = "io.github.juuxel:loom-vineflower", version.ref = "vinef
[plugins]
forgeGradle = { id = "net.minecraftforge.gradle", version.ref = "forgeGradle" }
githubRelease = { id = "com.github.breadmoirai.github-release", version.ref = "githubRelease" }
gradleVersions = { id = "com.github.ben-manes.versions", version.ref = "gradleVersions" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
librarian = { id = "org.parchmentmc.librarian.forgegradle", version.ref = "librarian" }
mixinGradle = { id = "org.spongepowered.mixin", version.ref = "mixinGradle" }
taskTree = { id = "com.dorongold.task-tree", version.ref = "taskTree" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "versionCatalogUpdate" }
[bundles]
annotations = ["jsr305", "checkerFramework", "jetbrainsAnnotations"]
@@ -184,5 +189,5 @@ test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
testRuntime = ["junit-jupiter-engine", "jqwik-engine"]
# Build tools
teavm-api = [ "teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api" ]
teavm-tooling = [ "teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl" ]
teavm-api = ["teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-metaprogramming-api"]
teavm-tooling = ["teavm-tooling", "teavm-metaprogramming-impl", "teavm-jso-impl"]

View File

@@ -6,7 +6,7 @@
(sources
/doc/
/projects/forge/build/docs/luaJavadoc/
/projects/common/build/docs/luaJavadoc/
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
/projects/core/src/main/resources/data/computercraft/lua/rom/
/projects/core/src/test/resources/test-rom
@@ -36,7 +36,7 @@
(library-path
/doc/stub/
/projects/forge/build/docs/luaJavadoc/
/projects/common/build/docs/luaJavadoc/
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/command/
@@ -88,7 +88,7 @@
(/doc/stub/
/projects/core/src/main/resources/data/computercraft/lua/bios.lua
/projects/core/src/main/resources/data/computercraft/lua/rom/apis/
/projects/forge/build/docs/luaJavadoc/)
/projects/common/build/docs/luaJavadoc/)
(linters -var:unused-global)
(lint (allow-toplevel-global true)))

View File

@@ -2,13 +2,12 @@
//
// SPDX-License-Identifier: MPL-2.0
import cc.tweaked.gradle.annotationProcessorEverywhere
import cc.tweaked.gradle.clientClasses
import cc.tweaked.gradle.commonClasses
import cc.tweaked.gradle.*
plugins {
id("cc-tweaked.vanilla")
id("cc-tweaked.gametest")
id("cc-tweaked.illuaminate")
id("cc-tweaked.publishing")
}
@@ -19,6 +18,10 @@ minecraft {
)
}
configurations {
register("cctJavadoc")
}
dependencies {
// Pull in our other projects. See comments in MinecraftConfigurations on this nastiness.
implementation(project(":core"))
@@ -41,4 +44,53 @@ dependencies {
testModImplementation(libs.bundles.kotlin)
testFixturesImplementation(testFixtures(project(":core")))
"cctJavadoc"(libs.cctJavadoc)
}
illuaminate {
version.set(libs.versions.illuaminate)
}
val luaJavadoc by tasks.registering(Javadoc::class) {
description = "Generates documentation for Java-side Lua functions."
group = JavaBasePlugin.DOCUMENTATION_GROUP
val sourceSets = listOf(sourceSets.main.get(), project(":core").sourceSets.main.get())
for (sourceSet in sourceSets) {
source(sourceSet.java)
classpath += sourceSet.compileClasspath
}
destinationDir = layout.buildDirectory.dir("docs/luaJavadoc").get().asFile
val options = options as StandardJavadocDocletOptions
options.docletpath = configurations["cctJavadoc"].files.toList()
options.doclet = "cc.tweaked.javadoc.LuaDoclet"
options.addStringOption("project-root", rootProject.file(".").absolutePath)
options.noTimestamp(false)
javadocTool.set(
javaToolchains.javadocToolFor {
languageVersion.set(cc.tweaked.gradle.CCTweakedPlugin.JAVA_VERSION)
},
)
}
val lintLua by tasks.registering(IlluaminateExec::class) {
group = JavaBasePlugin.VERIFICATION_GROUP
description = "Lint Lua (and Lua docs) with illuaminate"
// Config files
inputs.file(rootProject.file("illuaminate.sexp")).withPropertyName("illuaminate.sexp")
// Sources
inputs.files(rootProject.fileTree("doc")).withPropertyName("docs")
inputs.files(project(":core").fileTree("src/main/resources/data/computercraft/lua")).withPropertyName("lua rom")
inputs.files(luaJavadoc)
args = listOf("lint")
workingDir = rootProject.projectDir
doFirst { if (System.getenv("GITHUB_ACTIONS") != null) println("::add-matcher::.github/matchers/illuaminate.json") }
doLast { if (System.getenv("GITHUB_ACTIONS") != null) println("::remove-matcher owner=illuaminate::") }
}

View File

@@ -19,6 +19,7 @@ import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
@@ -144,6 +145,11 @@ public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> ext
|| super.mouseDragged(x, y, button, deltaX, deltaY);
}
@Override
public void setFocused(@Nullable GuiEventListener listener) {
// Don't clear and re-focus if we're already focused.
if (listener != getFocused()) super.setFocused(listener);
}
@Override
protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {

View File

@@ -8,8 +8,8 @@ import com.mojang.blaze3d.vertex.Tesselator;
import dan200.computercraft.client.render.RenderTypes;
import dan200.computercraft.client.render.text.FixedWidthFontRenderer;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.util.StringUtil;
import dan200.computercraft.shared.computer.core.InputHandler;
import net.minecraft.SharedConstants;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.AbstractWidget;
@@ -112,26 +112,8 @@ public class TerminalWidget extends AbstractWidget {
}
private void paste() {
var clipboard = Minecraft.getInstance().keyboardHandler.getClipboard();
// Clip to the first occurrence of \r or \n
var newLineIndex1 = clipboard.indexOf('\r');
var newLineIndex2 = clipboard.indexOf('\n');
if (newLineIndex1 >= 0 && newLineIndex2 >= 0) {
clipboard = clipboard.substring(0, Math.min(newLineIndex1, newLineIndex2));
} else if (newLineIndex1 >= 0) {
clipboard = clipboard.substring(0, newLineIndex1);
} else if (newLineIndex2 >= 0) {
clipboard = clipboard.substring(0, newLineIndex2);
}
// Filter the string
clipboard = SharedConstants.filterText(clipboard);
if (!clipboard.isEmpty()) {
// Clip to 512 characters and queue the event
if (clipboard.length() > 512) clipboard = clipboard.substring(0, 512);
computer.queueEvent("paste", new Object[]{ clipboard });
}
var clipboard = StringUtil.normaliseClipboardString(Minecraft.getInstance().keyboardHandler.getClipboard());
if (!clipboard.isEmpty()) computer.queueEvent("paste", new Object[]{ clipboard });
}
@Override
@@ -264,7 +246,7 @@ public class TerminalWidget extends AbstractWidget {
keysDown.clear();
// When blurring, we should make the last mouse button go up
if (lastMouseButton > 0) {
if (lastMouseButton >= 0) {
computer.mouseUp(lastMouseButton + 1, lastMouseX + 1, lastMouseY + 1);
lastMouseButton = -1;
}

View File

@@ -11,7 +11,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.client.platform.ClientPlatformHelper;
import dan200.computercraft.client.turtle.TurtleUpgradeModellers;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.turtle.blocks.TurtleBlockEntity;
import dan200.computercraft.shared.util.Holiday;
import net.minecraft.client.Minecraft;
@@ -21,7 +20,6 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
@@ -29,8 +27,6 @@ import net.minecraft.world.phys.HitResult;
import javax.annotation.Nullable;
public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBlockEntity> {
private static final ModelResourceLocation NORMAL_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_normal", "inventory");
private static final ModelResourceLocation ADVANCED_TURTLE_MODEL = new ModelResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced", "inventory");
private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_colour");
private static final ResourceLocation ELF_OVERLAY_MODEL = new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/turtle_elf_overlay");
@@ -42,13 +38,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
font = context.getFont();
}
public static ResourceLocation getTurtleModel(ComputerFamily family, boolean coloured) {
return switch (family) {
default -> coloured ? COLOUR_TURTLE_MODEL : NORMAL_TURTLE_MODEL;
case ADVANCED -> coloured ? COLOUR_TURTLE_MODEL : ADVANCED_TURTLE_MODEL;
};
}
public static @Nullable ResourceLocation getTurtleOverlayModel(@Nullable ResourceLocation overlay, boolean christmas) {
if (overlay != null) return overlay;
if (christmas) return ELF_OVERLAY_MODEL;
@@ -78,7 +67,6 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
var matrix = transform.last().pose();
var opacity = (int) (mc.options.getBackgroundOpacity(0.25f) * 255) << 24;
var width = -font.width(label) / 2.0f;
// TODO: Check this looks okay
font.drawInBatch(label, width, (float) 0, 0x20ffffff, false, matrix, buffers, Font.DisplayMode.SEE_THROUGH, opacity, lightmapCoord);
font.drawInBatch(label, width, (float) 0, 0xffffffff, false, matrix, buffers, Font.DisplayMode.NORMAL, 0, lightmapCoord);
@@ -96,10 +84,18 @@ public class TurtleBlockEntityRenderer implements BlockEntityRenderer<TurtleBloc
// Render the turtle
var colour = turtle.getColour();
var family = turtle.getFamily();
var overlay = turtle.getOverlay();
renderModel(transform, buffers, lightmapCoord, overlayLight, getTurtleModel(family, colour != -1), colour == -1 ? null : new int[]{ colour });
if (colour == -1) {
// Render the turtle using its item model.
var modelManager = Minecraft.getInstance().getItemRenderer().getItemModelShaper();
var model = modelManager.getItemModel(turtle.getBlockState().getBlock().asItem());
if (model == null) model = modelManager.getModelManager().getMissingModel();
renderModel(transform, buffers, lightmapCoord, overlayLight, model, null);
} else {
// Otherwise render it using the colour item.
renderModel(transform, buffers, lightmapCoord, overlayLight, COLOUR_TURTLE_MODEL, new int[]{ colour });
}
// Render the overlay
var overlayModel = getTurtleOverlayModel(overlay, Holiday.getCurrent() == Holiday.CHRISTMAS);

View File

@@ -174,6 +174,7 @@ public final class LanguageProvider implements DataProvider {
// Metrics
add(Metrics.COMPUTER_TASKS, "Tasks");
add(Metrics.SERVER_TASKS, "Server tasks");
add(Metrics.JAVA_ALLOCATION, "Java Allocations");
add(Metrics.PERIPHERAL_OPS, "Peripheral calls");
add(Metrics.FS_OPS, "Filesystem operations");
add(Metrics.HTTP_REQUESTS, "HTTP requests");

View File

@@ -13,7 +13,6 @@ import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.util.Colour;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.platform.RecipeIngredients;
import dan200.computercraft.shared.platform.RegistryWrappers;
@@ -38,7 +37,6 @@ import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.Blocks;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.function.Consumer;
@@ -106,14 +104,13 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
private void turtleUpgrades(Consumer<FinishedRecipe> add) {
for (var turtleItem : turtleItems()) {
var base = turtleItem.create(-1, null, -1, null, null, 0, null);
var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT);
var name = RegistryWrappers.ITEMS.getKey(turtleItem);
for (var upgrade : turtleUpgrades.getGeneratedUpgrades()) {
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))
.group(name.toString())
.pattern("#T")
.define('T', base.getItem())
.define('#', upgrade.getCraftingItem().getItem())
@@ -121,9 +118,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("turtle_%s/%s/%s",
nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
))
name.withSuffix(String.format("/%s/%s", upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()))
);
}
}
@@ -141,15 +136,13 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
private void pocketUpgrades(Consumer<FinishedRecipe> add) {
for (var pocket : pocketComputerItems()) {
var base = pocket.create(-1, null, -1, null);
if (base.isEmpty()) continue;
var nameId = pocket.getFamily().name().toLowerCase(Locale.ROOT);
var name = RegistryWrappers.ITEMS.getKey(pocket).withPath(x -> x.replace("pocket_computer_", "pocket_"));
for (var upgrade : pocketUpgrades.getGeneratedUpgrades()) {
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))
.group(name.toString())
.pattern("#")
.pattern("P")
.define('P', base.getItem())
@@ -158,9 +151,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
inventoryChange(base.getItem(), upgrade.getCraftingItem().getItem()))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.IMPOSTOR_SHAPED.get(), add).withResultTag(result.getTag()),
new ResourceLocation(ComputerCraftAPI.MOD_ID, String.format("pocket_%s/%s/%s",
nameId, upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()
))
name.withSuffix(String.format("/%s/%s", upgrade.getUpgradeID().getNamespace(), upgrade.getUpgradeID().getPath()))
);
}
}
@@ -190,12 +181,10 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
private void turtleOverlay(Consumer<FinishedRecipe> add, String overlay, Consumer<ShapelessRecipeBuilder> build) {
for (var turtleItem : turtleItems()) {
var base = turtleItem.create(-1, null, -1, null, null, 0, null);
var nameId = turtleItem.getFamily().name().toLowerCase(Locale.ROOT);
var group = "%s:turtle_%s_overlay".formatted(ComputerCraftAPI.MOD_ID, nameId);
var name = RegistryWrappers.ITEMS.getKey(turtleItem);
var builder = ShapelessRecipeBuilder.shapeless(RecipeCategory.REDSTONE, base.getItem())
.group(group)
.group(name.withSuffix("_overlay").toString())
.unlockedBy("has_turtle", inventoryChange(base.getItem()));
build.accept(builder);
builder
@@ -204,7 +193,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
RecipeWrapper
.wrap(ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get(), add)
.withExtraData(x -> x.addProperty("overlay", new ResourceLocation(ComputerCraftAPI.MOD_ID, "block/" + overlay).toString())),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_%s_overlays/%s".formatted(nameId, overlay))
name.withSuffix("_overlays/" + overlay)
);
}
}
@@ -253,7 +242,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer_advanced_upgrade")
);
@@ -277,7 +266,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.COMPUTER_NORMAL.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.NORMAL)));
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.TURTLE_ADVANCED.get())
@@ -288,7 +277,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.COMPUTER_ADVANCED.get())
.define('I', ingredients.woodenChest())
.unlockedBy("has_computer", inventoryChange(ModRegistry.Items.COMPUTER_NORMAL.get()))
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)));
.save(RecipeWrapper.wrap(ModRegistry.RecipeSerializers.TURTLE.get(), add));
ShapedRecipeBuilder
.shaped(RecipeCategory.REDSTONE, ModRegistry.Blocks.TURTLE_ADVANCED.get())
@@ -300,7 +289,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('B', ingredients.goldBlock())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.TURTLE_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "turtle_advanced_upgrade")
);
@@ -367,7 +356,7 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
.define('C', ModRegistry.Items.POCKET_COMPUTER_NORMAL.get())
.unlockedBy("has_components", inventoryChange(itemPredicate(ModRegistry.Items.POCKET_COMPUTER_NORMAL.get()), itemPredicate(ingredients.goldIngot())))
.save(
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add).withExtraData(family(ComputerFamily.ADVANCED)),
RecipeWrapper.wrap(ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get(), add),
new ResourceLocation(ComputerCraftAPI.MOD_ID, "pocket_computer_advanced_upgrade")
);
@@ -519,10 +508,6 @@ class RecipeProvider extends net.minecraft.data.recipes.RecipeProvider {
return tag;
}
private static Consumer<JsonObject> family(ComputerFamily family) {
return json -> json.addProperty("family", family.getSerializedName());
}
private static void addSpecial(Consumer<FinishedRecipe> add, SimpleCraftingRecipeSerializer<?> special) {
SpecialRecipeBuilder.special(special).save(add, RegistryWrappers.RECIPE_SERIALIZERS.getKey(special).toString());
}

View File

@@ -58,7 +58,10 @@ class TagProvider {
tags.tag(ComputerCraftTags.Blocks.TURTLE_SWORD_BREAKABLE).addTag(BlockTags.WOOL).add(Blocks.COBWEB);
tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE).addTag(BlockTags.CAULDRONS).addTag(BlockTags.BEEHIVES);
tags.tag(ComputerCraftTags.Blocks.TURTLE_CAN_USE)
.addTag(BlockTags.BEEHIVES)
.addTag(BlockTags.CAULDRONS)
.add(Blocks.COMPOSTER);
// Make all blocks aside from command computer mineable.
tags.tag(BlockTags.MINEABLE_WITH_PICKAXE).add(
@@ -76,6 +79,8 @@ class TagProvider {
ModRegistry.Blocks.WIRED_MODEM_FULL.get(),
ModRegistry.Blocks.CABLE.get()
);
tags.tag(BlockTags.WITHER_IMMUNE).add(ModRegistry.Blocks.COMPUTER_COMMAND.get());
}
public static void itemTags(ItemTagConsumer tags) {

View File

@@ -14,12 +14,15 @@ import dan200.computercraft.shared.computer.metrics.ComputerMBean;
import dan200.computercraft.shared.peripheral.monitor.MonitorWatcher;
import dan200.computercraft.shared.util.DropConsumer;
import dan200.computercraft.shared.util.TickScheduler;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.storage.loot.BuiltInLootTables;
@@ -111,4 +114,17 @@ public final class CommonHooks {
public static boolean onLivingDrop(Entity entity, ItemStack stack) {
return DropConsumer.onLivingDrop(entity, stack);
}
/**
* Add items to an existing creative tab.
*
* @param key The {@link ResourceKey} for this creative tab.
* @param context Additional parameters used for building the contents.
* @param out The creative tab output to append items to.
*/
public static void onBuildCreativeTab(ResourceKey<CreativeModeTab> key, CreativeModeTab.ItemDisplayParameters context, CreativeModeTab.Output out) {
if (key == CreativeModeTabs.OP_BLOCKS && context.hasPermissions()) {
out.accept(ModRegistry.Items.COMPUTER_COMMAND.get());
}
}
}

View File

@@ -34,6 +34,7 @@ import dan200.computercraft.shared.computer.inventory.ViewComputerMenu;
import dan200.computercraft.shared.computer.items.CommandComputerItem;
import dan200.computercraft.shared.computer.items.ComputerItem;
import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.data.BlockNamedEntityLootCondition;
import dan200.computercraft.shared.data.ConstantLootConditionSerializer;
import dan200.computercraft.shared.data.HasComputerIdLootCondition;
@@ -139,19 +140,17 @@ public final class ModRegistry {
}
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_NORMAL = REGISTRY.register("computer_normal",
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.STONE), ComputerFamily.NORMAL, BlockEntities.COMPUTER_NORMAL));
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.STONE), BlockEntities.COMPUTER_NORMAL));
public static final RegistryEntry<ComputerBlock<ComputerBlockEntity>> COMPUTER_ADVANCED = REGISTRY.register("computer_advanced",
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.COMPUTER_ADVANCED));
() -> new ComputerBlock<>(computerProperties().mapColor(MapColor.GOLD), BlockEntities.COMPUTER_ADVANCED));
public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command", () -> new CommandComputerBlock<>(
computerProperties().strength(-1, 6000000.0F),
ComputerFamily.COMMAND, BlockEntities.COMPUTER_COMMAND
));
public static final RegistryEntry<ComputerBlock<CommandComputerBlockEntity>> COMPUTER_COMMAND = REGISTRY.register("computer_command",
() -> new CommandComputerBlock<>(computerProperties().strength(-1, 6000000.0F), BlockEntities.COMPUTER_COMMAND));
public static final RegistryEntry<TurtleBlock> TURTLE_NORMAL = REGISTRY.register("turtle_normal",
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.STONE), ComputerFamily.NORMAL, BlockEntities.TURTLE_NORMAL));
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.STONE), BlockEntities.TURTLE_NORMAL));
public static final RegistryEntry<TurtleBlock> TURTLE_ADVANCED = REGISTRY.register("turtle_advanced",
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.GOLD), ComputerFamily.ADVANCED, BlockEntities.TURTLE_ADVANCED));
() -> new TurtleBlock(turtleProperties().mapColor(MapColor.GOLD).explosionResistance(TurtleBlock.IMMUNE_EXPLOSION_RESISTANCE), BlockEntities.TURTLE_ADVANCED));
public static final RegistryEntry<SpeakerBlock> SPEAKER = REGISTRY.register("speaker", () -> new SpeakerBlock(properties().mapColor(MapColor.STONE)));
public static final RegistryEntry<DiskDriveBlock> DISK_DRIVE = REGISTRY.register("disk_drive", () -> new DiskDriveBlock(properties().mapColor(MapColor.STONE)));
@@ -192,9 +191,9 @@ public final class ModRegistry {
ofBlock(Blocks.COMPUTER_COMMAND, (p, s) -> new CommandComputerBlockEntity(BlockEntities.COMPUTER_COMMAND.get(), p, s));
public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_NORMAL =
ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, ComputerFamily.NORMAL));
ofBlock(Blocks.TURTLE_NORMAL, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_NORMAL.get(), p, s, () -> Config.turtleFuelLimit, ComputerFamily.NORMAL));
public static final RegistryEntry<BlockEntityType<TurtleBlockEntity>> TURTLE_ADVANCED =
ofBlock(Blocks.TURTLE_ADVANCED, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_ADVANCED.get(), p, s, ComputerFamily.ADVANCED));
ofBlock(Blocks.TURTLE_ADVANCED, (p, s) -> new TurtleBlockEntity(BlockEntities.TURTLE_ADVANCED.get(), p, s, () -> Config.advancedTurtleFuelLimit, ComputerFamily.ADVANCED));
public static final RegistryEntry<BlockEntityType<SpeakerBlockEntity>> SPEAKER =
ofBlock(Blocks.SPEAKER, (p, s) -> new SpeakerBlockEntity(BlockEntities.SPEAKER.get(), p, s));
@@ -311,7 +310,10 @@ public final class ModRegistry {
() -> new MenuType<>(PrinterMenu::new, FeatureFlags.VANILLA_SET));
public static final RegistryEntry<MenuType<HeldItemMenu>> PRINTOUT = REGISTRY.register("printout",
() -> ContainerData.toType(HeldItemContainerData::new, HeldItemMenu::createPrintout));
() -> ContainerData.toType(
HeldItemContainerData::new,
(id, inventory, data) -> new HeldItemMenu(Menus.PRINTOUT.get(), id, inventory.player, data.getHand())
));
public static final RegistryEntry<MenuType<ViewComputerMenu>> VIEW_COMPUTER = REGISTRY.register("view_computer",
() -> ContainerData.toType(ComputerContainerData::new, ViewComputerMenu::new));
@@ -371,11 +373,11 @@ public final class ModRegistry {
public static final RegistryEntry<SimpleCraftingRecipeSerializer<ClearColourRecipe>> DYEABLE_ITEM_CLEAR = simple("clear_colour", ClearColourRecipe::new);
public static final RegistryEntry<RecipeSerializer<TurtleRecipe>> TURTLE = REGISTRY.register("turtle", () -> TurtleRecipe.validatingSerialiser(TurtleRecipe::of));
public static final RegistryEntry<SimpleCraftingRecipeSerializer<TurtleUpgradeRecipe>> TURTLE_UPGRADE = simple("turtle_upgrade", TurtleUpgradeRecipe::new);
public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serializer::new);
public static final RegistryEntry<RecipeSerializer<TurtleOverlayRecipe>> TURTLE_OVERLAY = REGISTRY.register("turtle_overlay", TurtleOverlayRecipe.Serialiser::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PocketComputerUpgradeRecipe>> POCKET_COMPUTER_UPGRADE = simple("pocket_computer_upgrade", PocketComputerUpgradeRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<PrintoutRecipe>> PRINTOUT = simple("printout", PrintoutRecipe::new);
public static final RegistryEntry<SimpleCraftingRecipeSerializer<DiskRecipe>> DISK = simple("disk", DiskRecipe::new);
public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", ComputerUpgradeRecipe.Serializer::new);
public static final RegistryEntry<RecipeSerializer<ComputerUpgradeRecipe>> COMPUTER_UPGRADE = REGISTRY.register("computer_upgrade", () -> CustomShapedRecipe.validatingSerialiser(ComputerUpgradeRecipe::of));
}
public static class Permissions {
@@ -464,8 +466,8 @@ public final class ModRegistry {
* Register any objects which must be done on the main thread.
*/
public static void registerMainThread() {
CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_NORMAL.get(), TurtleItem.CAULDRON_INTERACTION);
CauldronInteraction.WATER.put(ModRegistry.Items.TURTLE_ADVANCED.get(), TurtleItem.CAULDRON_INTERACTION);
CauldronInteraction.WATER.put(Items.TURTLE_NORMAL.get(), TurtleItem.CAULDRON_INTERACTION);
CauldronInteraction.WATER.put(Items.TURTLE_ADVANCED.get(), TurtleItem.CAULDRON_INTERACTION);
}
private static void addTurtle(CreativeModeTab.Output out, TurtleItem turtle) {

View File

@@ -4,8 +4,6 @@
package dan200.computercraft.shared.common;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.network.container.HeldItemContainerData;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.MenuProvider;
@@ -28,10 +26,6 @@ public class HeldItemMenu extends AbstractContainerMenu {
stack = player.getItemInHand(hand).copy();
}
public static HeldItemMenu createPrintout(int id, Inventory inventory, HeldItemContainerData data) {
return new HeldItemMenu(ModRegistry.Menus.PRINTOUT.get(), id, inventory.player, data.getHand());
}
public ItemStack getStack() {
return stack;
}

View File

@@ -7,7 +7,6 @@ package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.annotations.ForgeOverride;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.common.IBundledRedstoneBlock;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.items.IComputerItem;
import dan200.computercraft.shared.platform.RegistryEntry;
import dan200.computercraft.shared.util.BlockEntityHelpers;
@@ -42,13 +41,11 @@ import javax.annotation.Nullable;
public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntity> extends HorizontalDirectionalBlock implements IBundledRedstoneBlock, EntityBlock {
private static final ResourceLocation DROP = new ResourceLocation(ComputerCraftAPI.MOD_ID, "computer");
private final ComputerFamily family;
protected final RegistryEntry<BlockEntityType<T>> type;
private final BlockEntityTicker<T> serverTicker = (level, pos, state, computer) -> computer.serverTick();
protected AbstractComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
protected AbstractComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
super(settings);
this.family = family;
this.type = type;
}
@@ -82,10 +79,6 @@ public abstract class AbstractComputerBlock<T extends AbstractComputerBlockEntit
protected abstract ItemStack getItem(AbstractComputerBlockEntity tile);
public ComputerFamily getFamily() {
return family;
}
@Override
@Deprecated
public int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction incomingSide) {

View File

@@ -4,7 +4,6 @@
package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.platform.RegistryEntry;
import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.entity.BlockEntityType;
@@ -17,7 +16,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
* @see dan200.computercraft.shared.computer.items.CommandComputerItem
*/
public class CommandComputerBlock<T extends CommandComputerBlockEntity> extends ComputerBlock<T> implements GameMasterBlock {
public CommandComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
super(settings, family, type);
public CommandComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
super(settings, type);
}
}

View File

@@ -4,9 +4,8 @@
package dan200.computercraft.shared.computer.blocks;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.items.ComputerItemFactory;
import dan200.computercraft.shared.computer.items.ComputerItem;
import dan200.computercraft.shared.platform.RegistryEntry;
import net.minecraft.core.Direction;
import net.minecraft.world.item.ItemStack;
@@ -25,8 +24,8 @@ public class ComputerBlock<T extends ComputerBlockEntity> extends AbstractComput
public static final EnumProperty<ComputerState> STATE = EnumProperty.create("state", ComputerState.class);
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public ComputerBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<T>> type) {
super(settings, family, type);
public ComputerBlock(Properties settings, RegistryEntry<BlockEntityType<T>> type) {
super(settings, type);
registerDefaultState(defaultBlockState()
.setValue(FACING, Direction.NORTH)
.setValue(STATE, ComputerState.OFF)
@@ -46,6 +45,9 @@ public class ComputerBlock<T extends ComputerBlockEntity> extends AbstractComput
@Override
protected ItemStack getItem(AbstractComputerBlockEntity tile) {
return tile instanceof ComputerBlockEntity ? ComputerItemFactory.create((ComputerBlockEntity) tile) : ItemStack.EMPTY;
if (!(tile instanceof ComputerBlockEntity computer)) return ItemStack.EMPTY;
if (!(asItem() instanceof ComputerItem item)) return ItemStack.EMPTY;
return item.create(computer.getComputerID(), computer.getLabel());
}
}

View File

@@ -32,11 +32,9 @@ public class ComputerBlockEntity extends AbstractComputerBlockEntity {
@Override
protected ServerComputer createComputer(int id) {
var family = getFamily();
return new ServerComputer(
(ServerLevel) getLevel(), getBlockPos(), id, label,
family, Config.computerTermWidth,
Config.computerTermHeight
getFamily(), Config.computerTermWidth, Config.computerTermHeight
);
}

View File

@@ -4,33 +4,8 @@
package dan200.computercraft.shared.computer.core;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.StringRepresentable;
public enum ComputerFamily implements StringRepresentable {
NORMAL("normal"),
ADVANCED("advanced"),
COMMAND("command");
private final String name;
ComputerFamily(String name) {
this.name = name;
}
public static ComputerFamily getFamily(JsonObject json, String name) {
var familyName = GsonHelper.getAsString(json, name);
for (var family : values()) {
if (family.getSerializedName().equalsIgnoreCase(familyName)) return family;
}
throw new JsonSyntaxException("Unknown computer family '" + familyName + "' for field " + name);
}
@Override
public String getSerializedName() {
return name;
}
public enum ComputerFamily {
NORMAL,
ADVANCED,
COMMAND,
}

View File

@@ -21,7 +21,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static dan200.computercraft.core.filesystem.MountHelpers.NO_SUCH_FILE;
import static dan200.computercraft.api.filesystem.MountConstants.NO_SUCH_FILE;
/**
* A mount backed by Minecraft's {@link ResourceManager}.

View File

@@ -8,7 +8,6 @@ import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.filesystem.Mount;
import dan200.computercraft.api.media.IMedia;
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlock;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.config.Config;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
@@ -22,11 +21,8 @@ import javax.annotation.Nullable;
import java.util.List;
public abstract class AbstractComputerItem extends BlockItem implements IComputerItem, IMedia {
private final ComputerFamily family;
public AbstractComputerItem(AbstractComputerBlock<?> block, Properties settings) {
super(block, settings);
family = block.getFamily();
}
@Override
@@ -45,13 +41,6 @@ public abstract class AbstractComputerItem extends BlockItem implements ICompute
return IComputerItem.super.getLabel(stack);
}
@Override
public final ComputerFamily getFamily() {
return family;
}
// IMedia implementation
@Override
public boolean setLabel(ItemStack stack, @Nullable String label) {
if (label != null) {

View File

@@ -5,8 +5,8 @@
package dan200.computercraft.shared.computer.items;
import dan200.computercraft.shared.computer.blocks.ComputerBlock;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
@@ -24,9 +24,9 @@ public class ComputerItem extends AbstractComputerItem {
}
@Override
public ItemStack withFamily(ItemStack stack, ComputerFamily family) {
var result = ComputerItemFactory.create(getComputerID(stack), null, family);
if (stack.hasCustomHoverName()) result.setHoverName(stack.getHoverName());
return result;
public ItemStack changeItem(ItemStack stack, Item newItem) {
return newItem instanceof ComputerItem computer
? computer.create(getComputerID(stack), getLabel(stack))
: ItemStack.EMPTY;
}
}

View File

@@ -1,29 +0,0 @@
// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.computer.items;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.ComputerBlockEntity;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
public final class ComputerItemFactory {
private ComputerItemFactory() {
}
public static ItemStack create(ComputerBlockEntity tile) {
return create(tile.getComputerID(), tile.getLabel(), tile.getFamily());
}
public static ItemStack create(int id, @Nullable String label, ComputerFamily family) {
return switch (family) {
case NORMAL -> ModRegistry.Items.COMPUTER_NORMAL.get().create(id, label);
case ADVANCED -> ModRegistry.Items.COMPUTER_ADVANCED.get().create(id, label);
case COMMAND -> ModRegistry.Items.COMPUTER_COMMAND.get().create(id, label);
};
}
}

View File

@@ -4,7 +4,7 @@
package dan200.computercraft.shared.computer.items;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import javax.annotation.Nullable;
@@ -21,7 +21,15 @@ public interface IComputerItem {
return stack.hasCustomHoverName() ? stack.getHoverName().getString() : null;
}
ComputerFamily getFamily();
ItemStack withFamily(ItemStack stack, ComputerFamily family);
/**
* Create a new stack, changing the underlying item.
* <p>
* This should copy the computer's data to a different item of the same type (for instance, converting a normal
* computer to an advanced one).
*
* @param stack The current computer stack.
* @param newItem The new item.
* @return The new stack, possibly {@linkplain ItemStack#EMPTY empty} if {@code newItem} is of the same type.
*/
ItemStack changeItem(ItemStack stack, Item newItem);
}

View File

@@ -46,12 +46,14 @@ public final class GlobalMetrics {
* Add a new global metrics observer. This will receive metrics data for all computers.
*
* @param tracker The observer to add.
* @return Whether the observer was added. {@code false} if the observer was already registered.
*/
public void addObserver(ComputerMetricsObserver tracker) {
public boolean addObserver(ComputerMetricsObserver tracker) {
synchronized (lock) {
if (trackers.contains(tracker)) return;
if (trackers.contains(tracker)) return false;
trackers.add(tracker);
enabled = true;
return true;
}
}
@@ -59,11 +61,13 @@ public final class GlobalMetrics {
* Remove a previously-registered global metrics observer.
*
* @param tracker The observer to add.
* @return Whether the observer was removed. {@code false} if the observer was not registered.
*/
public void removeObserver(ComputerMetricsObserver tracker) {
public boolean removeObserver(ComputerMetricsObserver tracker) {
synchronized (lock) {
trackers.remove(tracker);
var changed = trackers.remove(tracker);
enabled = !trackers.isEmpty();
return changed;
}
}

View File

@@ -10,6 +10,7 @@ import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.metrics.ComputerMetricsObserver;
import dan200.computercraft.shared.computer.metrics.GlobalMetrics;
import javax.annotation.concurrent.GuardedBy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -21,29 +22,31 @@ import java.util.Map;
*/
public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
private final GlobalMetrics owner;
private boolean tracking = false;
@GuardedBy("this")
private final List<ComputerMetrics> timings = new ArrayList<>();
@GuardedBy("this")
private final Map<ServerComputer, ComputerMetrics> timingLookup = new MapMaker().weakKeys().makeMap();
public BasicComputerMetricsObserver(GlobalMetrics owner) {
this.owner = owner;
}
public synchronized void start() {
if (!tracking) owner.addObserver(this);
tracking = true;
public void start() {
if (!owner.addObserver(this)) return;
timings.clear();
timingLookup.clear();
synchronized (this) {
timings.clear();
timingLookup.clear();
}
}
public synchronized boolean stop() {
if (!tracking) return false;
owner.removeObserver(this);
tracking = false;
timingLookup.clear();
public boolean stop() {
if (!owner.removeObserver(this)) return false;
synchronized (this) {
timingLookup.clear();
}
return true;
}
@@ -57,6 +60,7 @@ public class BasicComputerMetricsObserver implements ComputerMetricsObserver {
return new ArrayList<>(timings);
}
@GuardedBy("this")
private ComputerMetrics getMetrics(ServerComputer computer) {
var existing = timingLookup.get(computer);
if (existing != null) return existing;

View File

@@ -4,57 +4,44 @@
package dan200.computercraft.shared.computer.recipe;
import com.google.gson.JsonObject;
import com.mojang.serialization.DataResult;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.items.IComputerItem;
import dan200.computercraft.shared.recipe.ShapedRecipeSpec;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.RecipeSerializer;
/**
* A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting it from one {@linkplain ComputerFamily
* family} to another.
* A recipe which "upgrades" a {@linkplain IComputerItem computer}, converting to it a new item (for instance a normal
* turtle to an advanced one).
*
* @see IComputerItem#changeItem(ItemStack, Item)
*/
public final class ComputerUpgradeRecipe extends ComputerConvertRecipe {
private final ComputerFamily family;
private final Item result;
private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe, ComputerFamily family) {
private ComputerUpgradeRecipe(ResourceLocation identifier, ShapedRecipeSpec recipe) {
super(identifier, recipe);
this.family = family;
this.result = recipe.result().getItem();
}
public static DataResult<ComputerUpgradeRecipe> of(ResourceLocation id, ShapedRecipeSpec recipe) {
if (!(recipe.result().getItem() instanceof IComputerItem)) {
return DataResult.error(() -> recipe.result().getItem() + " is not a computer item");
}
return DataResult.success(new ComputerUpgradeRecipe(id, recipe));
}
@Override
protected ItemStack convert(IComputerItem item, ItemStack stack) {
return item.withFamily(stack, family);
return item.changeItem(stack, result);
}
@Override
public RecipeSerializer<ComputerUpgradeRecipe> getSerializer() {
return ModRegistry.RecipeSerializers.COMPUTER_UPGRADE.get();
}
public static class Serializer implements RecipeSerializer<ComputerUpgradeRecipe> {
@Override
public ComputerUpgradeRecipe fromJson(ResourceLocation identifier, JsonObject json) {
var recipe = ShapedRecipeSpec.fromJson(json);
var family = ComputerFamily.getFamily(json, "family");
return new ComputerUpgradeRecipe(identifier, recipe, family);
}
@Override
public ComputerUpgradeRecipe fromNetwork(ResourceLocation identifier, FriendlyByteBuf buf) {
var recipe = ShapedRecipeSpec.fromNetwork(buf);
var family = buf.readEnum(ComputerFamily.class);
return new ComputerUpgradeRecipe(identifier, recipe, family);
}
@Override
public void toNetwork(FriendlyByteBuf buf, ComputerUpgradeRecipe recipe) {
recipe.toSpec().toNetwork(buf);
buf.writeEnum(recipe.family);
}
}
}

View File

@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.network;
/**
* A type of message to send over the network.
* <p>
* Much like recipe or argument serialisers, each type of {@link NetworkMessage} should have a unique type associated
* with it. This holds platform-specific information about how the packet should be sent over the network.
*
* @param <T> The type of message to send
* @see NetworkMessages
* @see NetworkMessage#type()
*/
public interface MessageType<T extends NetworkMessage<?>> {
}

View File

@@ -17,6 +17,13 @@ import net.minecraft.network.FriendlyByteBuf;
* @see ServerNetworkContext
*/
public interface NetworkMessage<T> {
/**
* Get the type of this message.
*
* @return The type of this message.
*/
MessageType<?> type();
/**
* Write this packet to a buffer.
* <p>
@@ -24,7 +31,7 @@ public interface NetworkMessage<T> {
*
* @param buf The buffer to write data to.
*/
void toBytes(FriendlyByteBuf buf);
void write(FriendlyByteBuf buf);
/**
* Handle this {@link NetworkMessage}.

View File

@@ -4,48 +4,84 @@
package dan200.computercraft.shared.network;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.shared.network.client.*;
import dan200.computercraft.shared.network.server.*;
import dan200.computercraft.shared.platform.PlatformHelper;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import java.util.function.Function;
import java.util.*;
/**
* Registry for all packets provided by CC: Tweaked.
* List of all {@link MessageType}s provided by CC: Tweaked.
*
* @see PlatformHelper The platform helper is used to send packets.
*/
public final class NetworkMessages {
private static final IntSet seenIds = new IntOpenHashSet();
private static final Set<String> seenChannel = new HashSet<>();
private static final List<MessageType<? extends NetworkMessage<ServerNetworkContext>>> serverMessages = new ArrayList<>();
private static final List<MessageType<? extends NetworkMessage<ClientNetworkContext>>> clientMessages = new ArrayList<>();
public static final MessageType<ComputerActionServerMessage> COMPUTER_ACTION = registerServerbound(0, "computer_action", ComputerActionServerMessage.class, ComputerActionServerMessage::new);
public static final MessageType<QueueEventServerMessage> QUEUE_EVENT = registerServerbound(1, "queue_event", QueueEventServerMessage.class, QueueEventServerMessage::new);
public static final MessageType<KeyEventServerMessage> KEY_EVENT = registerServerbound(2, "key_event", KeyEventServerMessage.class, KeyEventServerMessage::new);
public static final MessageType<MouseEventServerMessage> MOUSE_EVENT = registerServerbound(3, "mouse_event", MouseEventServerMessage.class, MouseEventServerMessage::new);
public static final MessageType<UploadFileMessage> UPLOAD_FILE = registerServerbound(4, "upload_file", UploadFileMessage.class, UploadFileMessage::new);
public static final MessageType<ChatTableClientMessage> CHAT_TABLE = registerClientbound(10, "chat_table", ChatTableClientMessage.class, ChatTableClientMessage::new);
public static final MessageType<PocketComputerDataMessage> POCKET_COMPUTER_DATA = registerClientbound(11, "pocket_computer_data", PocketComputerDataMessage.class, PocketComputerDataMessage::new);
public static final MessageType<PocketComputerDeletedClientMessage> POCKET_COMPUTER_DELETED = registerClientbound(12, "pocket_computer_deleted", PocketComputerDeletedClientMessage.class, PocketComputerDeletedClientMessage::new);
public static final MessageType<ComputerTerminalClientMessage> COMPUTER_TERMINAL = registerClientbound(13, "computer_terminal", ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new);
public static final MessageType<PlayRecordClientMessage> PLAY_RECORD = registerClientbound(14, "play_record", PlayRecordClientMessage.class, PlayRecordClientMessage::new);
public static final MessageType<MonitorClientMessage> MONITOR_CLIENT = registerClientbound(15, "monitor_client", MonitorClientMessage.class, MonitorClientMessage::new);
public static final MessageType<SpeakerAudioClientMessage> SPEAKER_AUDIO = registerClientbound(16, "speaker_audio", SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new);
public static final MessageType<SpeakerMoveClientMessage> SPEAKER_MOVE = registerClientbound(17, "speaker_move", SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new);
public static final MessageType<SpeakerPlayClientMessage> SPEAKER_PLAY = registerClientbound(18, "speaker_play", SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new);
public static final MessageType<SpeakerStopClientMessage> SPEAKER_STOP = registerClientbound(19, "speaker_stop", SpeakerStopClientMessage.class, SpeakerStopClientMessage::new);
public static final MessageType<UploadResultMessage> UPLOAD_RESULT = registerClientbound(20, "upload_result", UploadResultMessage.class, UploadResultMessage::new);
public static final MessageType<UpgradesLoadedMessage> UPGRADES_LOADED = registerClientbound(21, "upgrades_loaded", UpgradesLoadedMessage.class, UpgradesLoadedMessage::new);
private NetworkMessages() {
}
public interface PacketRegistry {
<T extends NetworkMessage<ClientNetworkContext>> void registerClientbound(int id, Class<T> type, Function<FriendlyByteBuf, T> decoder);
<T extends NetworkMessage<ServerNetworkContext>> void registerServerbound(int id, Class<T> type, Function<FriendlyByteBuf, T> decoder);
private static <C, T extends NetworkMessage<C>> MessageType<T> register(
List<MessageType<? extends NetworkMessage<C>>> messages,
int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader
) {
if (!seenIds.add(id)) throw new IllegalArgumentException("Duplicate id " + id);
if (!seenChannel.add(channel)) throw new IllegalArgumentException("Duplicate channel " + channel);
var type = PlatformHelper.get().createMessageType(id, new ResourceLocation(ComputerCraftAPI.MOD_ID, channel), klass, reader);
messages.add(type);
return type;
}
public static void register(PacketRegistry registry) {
// Server messages
registry.registerServerbound(0, ComputerActionServerMessage.class, ComputerActionServerMessage::new);
registry.registerServerbound(1, QueueEventServerMessage.class, QueueEventServerMessage::new);
registry.registerServerbound(2, KeyEventServerMessage.class, KeyEventServerMessage::new);
registry.registerServerbound(3, MouseEventServerMessage.class, MouseEventServerMessage::new);
registry.registerServerbound(4, UploadFileMessage.class, UploadFileMessage::new);
private static <T extends NetworkMessage<ServerNetworkContext>> MessageType<T> registerServerbound(int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
return register(serverMessages, id, channel, klass, reader);
}
// Client messages
registry.registerClientbound(10, ChatTableClientMessage.class, ChatTableClientMessage::new);
registry.registerClientbound(11, PocketComputerDataMessage.class, PocketComputerDataMessage::new);
registry.registerClientbound(12, PocketComputerDeletedClientMessage.class, PocketComputerDeletedClientMessage::new);
registry.registerClientbound(13, ComputerTerminalClientMessage.class, ComputerTerminalClientMessage::new);
registry.registerClientbound(14, PlayRecordClientMessage.class, PlayRecordClientMessage::new);
registry.registerClientbound(15, MonitorClientMessage.class, MonitorClientMessage::new);
registry.registerClientbound(16, SpeakerAudioClientMessage.class, SpeakerAudioClientMessage::new);
registry.registerClientbound(17, SpeakerMoveClientMessage.class, SpeakerMoveClientMessage::new);
registry.registerClientbound(18, SpeakerPlayClientMessage.class, SpeakerPlayClientMessage::new);
registry.registerClientbound(19, SpeakerStopClientMessage.class, SpeakerStopClientMessage::new);
registry.registerClientbound(20, UploadResultMessage.class, UploadResultMessage::new);
registry.registerClientbound(21, UpgradesLoadedMessage.class, UpgradesLoadedMessage::new);
private static <T extends NetworkMessage<ClientNetworkContext>> MessageType<T> registerClientbound(int id, String channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
return register(clientMessages, id, channel, klass, reader);
}
/**
* Get all serverbound message types.
*
* @return An unmodifiable sequence of all serverbound message types.
*/
public static Collection<MessageType<? extends NetworkMessage<ServerNetworkContext>>> getServerbound() {
return Collections.unmodifiableCollection(serverMessages);
}
/**
* Get all clientbound message types.
*
* @return An unmodifiable sequence of all clientbound message types.
*/
public static Collection<MessageType<? extends NetworkMessage<ClientNetworkContext>>> getClientbound() {
return Collections.unmodifiableCollection(clientMessages);
}
}

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.command.text.TableBuilder;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
@@ -43,7 +45,7 @@ public class ChatTableClientMessage implements NetworkMessage<ClientNetworkConte
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUtf(table.getId(), MAX_LEN);
buf.writeVarInt(table.getColumns());
buf.writeBoolean(table.getHeaders() != null);
@@ -63,4 +65,9 @@ public class ChatTableClientMessage implements NetworkMessage<ClientNetworkConte
public void handle(ClientNetworkContext context) {
context.handleChatTable(table);
}
@Override
public MessageType<ChatTableClientMessage> type() {
return NetworkMessages.CHAT_TABLE;
}
}

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -25,7 +27,7 @@ public class ComputerTerminalClientMessage implements NetworkMessage<ClientNetwo
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(containerId);
terminal.write(buf);
}
@@ -34,4 +36,9 @@ public class ComputerTerminalClientMessage implements NetworkMessage<ClientNetwo
public void handle(ClientNetworkContext context) {
context.handleComputerTerminal(containerId, terminal);
}
@Override
public MessageType<ComputerTerminalClientMessage> type() {
return NetworkMessages.COMPUTER_TERMINAL;
}
}

View File

@@ -5,7 +5,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
@@ -25,7 +27,7 @@ public class MonitorClientMessage implements NetworkMessage<ClientNetworkContext
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeBlockPos(pos);
state.write(buf);
}
@@ -34,4 +36,9 @@ public class MonitorClientMessage implements NetworkMessage<ClientNetworkContext
public void handle(ClientNetworkContext context) {
context.handleMonitorData(pos, state);
}
@Override
public MessageType<MonitorClientMessage> type() {
return NetworkMessages.MONITOR_CLIENT;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.diskdrive.DiskDriveBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
@@ -43,7 +45,7 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeBlockPos(pos);
buf.writeNullable(soundEvent, (b, e) -> e.writeToNetwork(b));
buf.writeNullable(name, FriendlyByteBuf::writeUtf);
@@ -53,4 +55,9 @@ public class PlayRecordClientMessage implements NetworkMessage<ClientNetworkCont
public void handle(ClientNetworkContext context) {
context.handlePlayRecord(pos, soundEvent, name);
}
@Override
public MessageType<PlayRecordClientMessage> type() {
return NetworkMessages.PLAY_RECORD;
}
}

View File

@@ -7,7 +7,9 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.terminal.NetworkedTerminal;
import dan200.computercraft.shared.computer.terminal.TerminalState;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.pocket.core.PocketServerComputer;
import net.minecraft.network.FriendlyByteBuf;
@@ -35,7 +37,7 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId);
buf.writeEnum(state);
buf.writeVarInt(lightState);
@@ -46,4 +48,9 @@ public class PocketComputerDataMessage implements NetworkMessage<ClientNetworkCo
public void handle(ClientNetworkContext context) {
context.handlePocketComputerData(instanceId, state, lightState, terminal);
}
@Override
public MessageType<PocketComputerDataMessage> type() {
return NetworkMessages.POCKET_COMPUTER_DATA;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
@@ -20,7 +22,7 @@ public class PocketComputerDeletedClientMessage implements NetworkMessage<Client
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(instanceId);
}
@@ -28,4 +30,9 @@ public class PocketComputerDeletedClientMessage implements NetworkMessage<Client
public void handle(ClientNetworkContext context) {
context.handlePocketComputerDeleted(instanceId);
}
@Override
public MessageType<PocketComputerDeletedClientMessage> type() {
return NetworkMessages.POCKET_COMPUTER_DELETED;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
@@ -47,7 +49,7 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
buf.writeFloat(volume);
@@ -58,4 +60,9 @@ public class SpeakerAudioClientMessage implements NetworkMessage<ClientNetworkCo
public void handle(ClientNetworkContext context) {
context.handleSpeakerAudio(source, pos, volume);
}
@Override
public MessageType<SpeakerAudioClientMessage> type() {
return NetworkMessages.SPEAKER_AUDIO;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
@@ -33,7 +35,7 @@ public class SpeakerMoveClientMessage implements NetworkMessage<ClientNetworkCon
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
}
@@ -42,4 +44,9 @@ public class SpeakerMoveClientMessage implements NetworkMessage<ClientNetworkCon
public void handle(ClientNetworkContext context) {
context.handleSpeakerMove(source, pos);
}
@Override
public MessageType<SpeakerMoveClientMessage> type() {
return NetworkMessages.SPEAKER_MOVE;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import dan200.computercraft.shared.peripheral.speaker.SpeakerPosition;
import net.minecraft.network.FriendlyByteBuf;
@@ -43,7 +45,7 @@ public class SpeakerPlayClientMessage implements NetworkMessage<ClientNetworkCon
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
pos.write(buf);
buf.writeResourceLocation(sound);
@@ -55,4 +57,9 @@ public class SpeakerPlayClientMessage implements NetworkMessage<ClientNetworkCon
public void handle(ClientNetworkContext context) {
context.handleSpeakerPlay(source, pos, sound, volume, pitch);
}
@Override
public MessageType<SpeakerPlayClientMessage> type() {
return NetworkMessages.SPEAKER_PLAY;
}
}

View File

@@ -4,7 +4,9 @@
package dan200.computercraft.shared.network.client;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.peripheral.speaker.SpeakerBlockEntity;
import net.minecraft.network.FriendlyByteBuf;
@@ -29,7 +31,7 @@ public class SpeakerStopClientMessage implements NetworkMessage<ClientNetworkCon
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeUUID(source);
}
@@ -37,4 +39,9 @@ public class SpeakerStopClientMessage implements NetworkMessage<ClientNetworkCon
public void handle(ClientNetworkContext context) {
context.handleSpeakerStop(source);
}
@Override
public MessageType<SpeakerStopClientMessage> type() {
return NetworkMessages.SPEAKER_STOP;
}
}

View File

@@ -13,7 +13,9 @@ import dan200.computercraft.api.upgrades.UpgradeSerialiser;
import dan200.computercraft.impl.PocketUpgrades;
import dan200.computercraft.impl.TurtleUpgrades;
import dan200.computercraft.impl.UpgradeManager;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.platform.PlatformHelper;
import net.minecraft.core.Registry;
import net.minecraft.network.FriendlyByteBuf;
@@ -27,7 +29,7 @@ import java.util.Objects;
/**
* Syncs turtle and pocket upgrades to the client.
*/
public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContext> {
public final class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContext> {
private final Map<String, UpgradeManager.UpgradeWrapper<TurtleUpgradeSerialiser<?>, ITurtleUpgrade>> turtleUpgrades;
private final Map<String, UpgradeManager.UpgradeWrapper<PocketUpgradeSerialiser<?>, IPocketUpgrade>> pocketUpgrades;
@@ -65,7 +67,7 @@ public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContex
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
toBytes(buf, TurtleUpgradeSerialiser.registryId(), turtleUpgrades);
toBytes(buf, PocketUpgradeSerialiser.registryId(), pocketUpgrades);
}
@@ -95,4 +97,9 @@ public class UpgradesLoadedMessage implements NetworkMessage<ClientNetworkContex
TurtleUpgrades.instance().loadFromNetwork(turtleUpgrades);
PocketUpgrades.instance().loadFromNetwork(pocketUpgrades);
}
@Override
public MessageType<UpgradesLoadedMessage> type() {
return NetworkMessages.UPGRADES_LOADED;
}
}

View File

@@ -6,7 +6,9 @@ package dan200.computercraft.shared.network.client;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -43,7 +45,7 @@ public class UploadResultMessage implements NetworkMessage<ClientNetworkContext>
}
@Override
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(containerId);
buf.writeEnum(result);
if (result == UploadResult.ERROR) buf.writeComponent(Nullability.assertNonNull(errorMessage));
@@ -53,4 +55,9 @@ public class UploadResultMessage implements NetworkMessage<ClientNetworkContext>
public void handle(ClientNetworkContext context) {
context.handleUploadResult(containerId, result, errorMessage);
}
@Override
public MessageType<UploadResultMessage> type() {
return NetworkMessages.UPLOAD_RESULT;
}
}

View File

@@ -5,6 +5,8 @@
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -23,8 +25,8 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeEnum(action);
}
@@ -37,6 +39,11 @@ public class ComputerActionServerMessage extends ComputerServerMessage {
}
}
@Override
public MessageType<ComputerActionServerMessage> type() {
return NetworkMessages.COMPUTER_ACTION;
}
public enum Action {
TURN_ON,
SHUTDOWN,

View File

@@ -28,7 +28,7 @@ public abstract class ComputerServerMessage implements NetworkMessage<ServerNetw
@Override
@OverridingMethodsMustInvokeSuper
public void toBytes(FriendlyByteBuf buf) {
public void write(FriendlyByteBuf buf) {
buf.writeVarInt(containerId);
}

View File

@@ -5,6 +5,8 @@
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -30,8 +32,8 @@ public class KeyEventServerMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeByte(type);
buf.writeVarInt(key);
}
@@ -45,4 +47,9 @@ public class KeyEventServerMessage extends ComputerServerMessage {
input.keyDown(key, type == TYPE_REPEAT);
}
}
@Override
public MessageType<KeyEventServerMessage> type() {
return NetworkMessages.KEY_EVENT;
}
}

View File

@@ -5,6 +5,8 @@
package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -37,8 +39,8 @@ public class MouseEventServerMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeByte(type);
buf.writeVarInt(arg);
buf.writeVarInt(x);
@@ -55,4 +57,9 @@ public class MouseEventServerMessage extends ComputerServerMessage {
case TYPE_SCROLL -> input.mouseScroll(arg, x, y);
}
}
@Override
public MessageType<MouseEventServerMessage> type() {
return NetworkMessages.MOUSE_EVENT;
}
}

View File

@@ -7,6 +7,8 @@ package dan200.computercraft.shared.network.server;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.menu.ServerInputHandler;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import dan200.computercraft.shared.util.NBTUtil;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -37,8 +39,8 @@ public class QueueEventServerMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeUtf(event);
buf.writeNbt(args == null ? null : NBTUtil.encodeObjects(args));
}
@@ -47,4 +49,9 @@ public class QueueEventServerMessage extends ComputerServerMessage {
protected void handle(ServerNetworkContext context, ComputerMenu container) {
container.getInput().queueEvent(event, args);
}
@Override
public MessageType<QueueEventServerMessage> type() {
return NetworkMessages.QUEUE_EVENT;
}
}

View File

@@ -9,6 +9,8 @@ import dan200.computercraft.shared.computer.menu.ComputerMenu;
import dan200.computercraft.shared.computer.upload.FileSlice;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessages;
import io.netty.handler.codec.DecoderException;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -91,8 +93,8 @@ public class UploadFileMessage extends ComputerServerMessage {
}
@Override
public void toBytes(FriendlyByteBuf buf) {
super.toBytes(buf);
public void write(FriendlyByteBuf buf) {
super.write(buf);
buf.writeUUID(uuid);
buf.writeByte(flag);
@@ -166,4 +168,9 @@ public class UploadFileMessage extends ComputerServerMessage {
input.continueUpload(uuid, slices);
if ((flag & FLAG_LAST) != 0) input.finishUpload(player, uuid);
}
@Override
public MessageType<UploadFileMessage> type() {
return NetworkMessages.UPLOAD_FILE;
}
}

View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.generic.methods;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import dan200.computercraft.api.peripheral.PeripheralType;
/**
* Methods for interacting with blocks which store energy.
* <p>
* This works with energy storage blocks, as well as generators and machines which consume energy.
* <p>
* > [!NOTE]
* > Due to limitations with Forge's energy API, it is not possible to measure throughput (i.e. FE used/generated per
* > tick).
*
* @param <T> The type for energy storage.
* @cc.module energy_storage
* @cc.since 1.94.0
*/
public abstract class AbstractEnergyMethods<T> implements GenericPeripheral {
@Override
public final PeripheralType getType() {
return PeripheralType.ofAdditional("energy_storage");
}
@Override
public final String id() {
return ComputerCraftAPI.MOD_ID + ":energy";
}
/**
* Get the energy of this block.
*
* @param energy The current energy storage.
* @return The energy stored in this block, in FE.
*/
@LuaFunction(mainThread = true)
public abstract int getEnergy(T energy);
/**
* Get the maximum amount of energy this block can store.
*
* @param energy The current energy storage.
* @return The energy capacity of this block.
*/
@LuaFunction(mainThread = true)
public abstract int getEnergyCapacity(T energy);
}

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.shared.peripheral.generic.methods;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.PeripheralType;
import java.util.Map;
import java.util.Optional;
/**
* Methods for interacting with tanks and other fluid storage blocks.
*
* @param <T> The type for fluid inventories.
* @cc.module fluid_storage
* @cc.since 1.94.0
*/
public abstract class AbstractFluidMethods<T> implements GenericPeripheral {
@Override
public final PeripheralType getType() {
return PeripheralType.ofAdditional("fluid_storage");
}
@Override
public final String id() {
return ComputerCraftAPI.MOD_ID + ":fluid";
}
/**
* Get all "tanks" in this fluid storage.
* <p>
* Each tank either contains some amount of fluid or is empty. Tanks with fluids inside will return some basic
* information about the fluid, including its name and amount.
* <p>
* The returned table is sparse, and so empty tanks will be `nil` - it is recommended to loop over using [`pairs`]
* rather than [`ipairs`].
*
* @param fluids The current fluid handler.
* @return All tanks.
* @cc.treturn { (table|nil)... } All tanks in this fluid storage.
*/
@LuaFunction(mainThread = true)
public abstract Map<Integer, Map<String, ?>> tanks(T fluids);
/**
* Move a fluid from one fluid container to another connected one.
* <p>
* This allows you to pull fluid in the current fluid container to another container <em>on the same wired
* network</em>. Both containers must attached to wired modems which are connected via a cable.
*
* @param from Container to move fluid from.
* @param computer The current computer.
* @param toName The name of the peripheral/container to push to. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param limit The maximum amount of fluid to move.
* @param fluidName The fluid to move. If not given, an arbitrary fluid will be chosen.
* @return The amount of moved fluid.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an fluid container.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
*/
@LuaFunction(mainThread = true)
public abstract int pushFluid(
T from, IComputerAccess computer, String toName, Optional<Integer> limit, Optional<String> fluidName
) throws LuaException;
/**
* Move a fluid from a connected fluid container into this oneone.
* <p>
* This allows you to pull fluid in the current fluid container from another container <em>on the same wired
* network</em>. Both containers must attached to wired modems which are connected via a cable.
*
* @param to Container to move fluid to.
* @param computer The current computer.
* @param fromName The name of the peripheral/container to push to. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param limit The maximum amount of fluid to move.
* @param fluidName The fluid to move. If not given, an arbitrary fluid will be chosen.
* @return The amount of moved fluid.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an fluid container.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
*/
@LuaFunction(mainThread = true)
public abstract int pullFluid(
T to, IComputerAccess computer, String fromName, Optional<Integer> limit, Optional<String> fluidName
) throws LuaException;
}

View File

@@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
//
// SPDX-License-Identifier: LicenseRef-CCPL
package dan200.computercraft.shared.peripheral.generic.methods;
import dan200.computercraft.api.ComputerCraftAPI;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.peripheral.GenericPeripheral;
import dan200.computercraft.api.peripheral.IComputerAccess;
import dan200.computercraft.api.peripheral.PeripheralType;
import javax.annotation.Nullable;
import java.util.Map;
import java.util.Optional;
/**
* Methods for interacting with inventories.
*
* @param <T> The type for inventories.
* @cc.module inventory
* @cc.since 1.94.0
*/
public abstract class AbstractInventoryMethods<T> implements GenericPeripheral {
@Override
public final PeripheralType getType() {
return PeripheralType.ofAdditional("inventory");
}
@Override
public final String id() {
return ComputerCraftAPI.MOD_ID + ":inventory";
}
/**
* Get the size of this inventory.
*
* @param inventory The current inventory.
* @return The number of slots in this inventory.
*/
@LuaFunction(mainThread = true)
public abstract int size(T inventory);
/**
* List all items in this inventory. This returns a table, with an entry for each slot.
* <p>
* Each item in the inventory is represented by a table containing some basic information, much like
* {@link dan200.computercraft.shared.turtle.apis.TurtleAPI#getItemDetail(ILuaContext, Optional, Optional)}
* includes. More information can be fetched with {@link #getItemDetail}. The table contains the item `name`, the
* `count` and an a (potentially nil) hash of the item's `nbt.` This NBT data doesn't contain anything useful, but
* allows you to distinguish identical items.
* <p>
* The returned table is sparse, and so empty slots will be `nil` - it is recommended to loop over using [`pairs`]
* rather than [`ipairs`].
*
* @param inventory The current inventory.
* @return All items in this inventory.
* @cc.treturn { (table|nil)... } All items in this inventory.
* @cc.usage Find an adjacent chest and print all items in it.
*
* <pre>{@code
* local chest = peripheral.find("minecraft:chest")
* for slot, item in pairs(chest.list()) do
* print(("%d x %s in slot %d"):format(item.count, item.name, slot))
* end
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract Map<Integer, Map<String, ?>> list(T inventory);
/**
* Get detailed information about an item.
* <p>
* The returned information contains the same information as each item in
* {@link #list}, as well as additional details like the display name
* (`displayName`), and item and item durability (`damage`, `maxDamage`, `durability`).
* <p>
* Some items include more information (such as enchantments) - it is
* recommended to print it out using [`textutils.serialize`] or in the Lua
* REPL, to explore what is available.
* <p>
* > [Deprecated fields][!INFO]
* > Older versions of CC: Tweaked exposed an {@code itemGroups} field, listing the
* > creative tabs an item was available under. This information is no longer available on
* > more recent versions of the game, and so this field will always be empty. Do not use this
* > field in new code!
*
* @param inventory The current inventory.
* @param slot The slot to get information about.
* @return Information about the item in this slot, or {@code nil} if not present.
* @throws LuaException If the slot is out of range.
* @cc.treturn table Information about the item in this slot, or {@code nil} if not present.
* @cc.usage Print some information about the first in a chest.
*
* <pre>{@code
* local chest = peripheral.find("minecraft:chest")
* local item = chest.getItemDetail(1)
* if not item then print("No item") return end
*
* print(("%s (%s)"):format(item.displayName, item.name))
* print(("Count: %d/%d"):format(item.count, item.maxCount))
*
* if item.damage then
* print(("Damage: %d/%d"):format(item.damage, item.maxDamage))
* end
* }</pre>
*/
@Nullable
@LuaFunction(mainThread = true)
public abstract Map<String, ?> getItemDetail(T inventory, int slot) throws LuaException;
/**
* Get the maximum number of items which can be stored in this slot.
* <p>
* Typically this will be limited to 64 items. However, some inventories (such as barrels or caches) can store
* hundreds or thousands of items in one slot.
*
* @param inventory Inventory to probe.
* @param slot The slot
* @return The maximum number of items in this slot.
* @throws LuaException If the slot is out of range.
* @cc.usage Count the maximum number of items an adjacent chest can hold.
* <pre>{@code
* local chest = peripheral.find("minecraft:chest")
* local total = 0
* for i = 1, chest.size() do
* total = total + chest.getItemLimit(i)
* end
* print(total)
* }</pre>
* @cc.since 1.96.0
*/
@LuaFunction(mainThread = true)
public abstract long getItemLimit(T inventory, int slot) throws LuaException;
/**
* Push items from one inventory to another connected one.
* <p>
* This allows you to push an item in an inventory to another inventory <em>on the same wired network</em>. Both
* inventories must attached to wired modems which are connected via a cable.
*
* @param from Inventory to move items from.
* @param computer The current computer.
* @param toName The name of the peripheral/inventory to push to. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param fromSlot The slot in the current inventory to move items to.
* @param limit The maximum number of items to move. Defaults to the current stack limit.
* @param toSlot The slot in the target inventory to move to. If not given, the item will be inserted into any slot.
* @return The number of transferred items.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory.
* @throws LuaException If either source or destination slot is out of range.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
* @cc.usage Wrap two chests, and push an item from one to another.
* <pre>{@code
* local chest_a = peripheral.wrap("minecraft:chest_0")
* local chest_b = peripheral.wrap("minecraft:chest_1")
*
* chest_a.pushItems(peripheral.getName(chest_b), 1)
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract int pushItems(
T from, IComputerAccess computer, String toName, int fromSlot, Optional<Integer> limit, Optional<Integer> toSlot
) throws LuaException;
/**
* Pull items from a connected inventory into this one.
* <p>
* This allows you to transfer items between inventories <em>on the same wired network</em>. Both this and the source
* inventory must attached to wired modems which are connected via a cable.
*
* @param to Inventory to move items to.
* @param computer The current computer.
* @param fromName The name of the peripheral/inventory to pull from. This is the string given to [`peripheral.wrap`],
* and displayed by the wired modem.
* @param fromSlot The slot in the source inventory to move items from.
* @param limit The maximum number of items to move. Defaults to the current stack limit.
* @param toSlot The slot in current inventory to move to. If not given, the item will be inserted into any slot.
* @return The number of transferred items.
* @throws LuaException If the peripheral to transfer to doesn't exist or isn't an inventory.
* @throws LuaException If either source or destination slot is out of range.
* @cc.see peripheral.getName Allows you to get the name of a [wrapped][`peripheral.wrap`] peripheral.
* @cc.usage Wrap two chests, and push an item from one to another.
* <pre>{@code
* local chest_a = peripheral.wrap("minecraft:chest_0")
* local chest_b = peripheral.wrap("minecraft:chest_1")
*
* chest_a.pullItems(peripheral.getName(chest_b), 1)
* }</pre>
*/
@LuaFunction(mainThread = true)
public abstract int pullItems(
T to, IComputerAccess computer, String fromName, int fromSlot, Optional<Integer> limit, Optional<Integer> toSlot
) throws LuaException;
}

View File

@@ -10,6 +10,7 @@ import com.mojang.brigadier.arguments.ArgumentType;
import dan200.computercraft.api.network.wired.WiredElement;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.shared.config.ConfigFile;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.container.ContainerData;
@@ -164,6 +165,18 @@ public interface PlatformHelper extends dan200.computercraft.impl.PlatformHelper
*/
void openMenu(Player player, MenuProvider owner, ContainerData menu);
/**
* Create a new {@link MessageType}.
*
* @param id The descriminator for this message type.
* @param channel The channel name for this message type.
* @param klass The type of this message.
* @param reader The function which reads the packet from a buffer. Should be the inverse to {@link NetworkMessage#write(FriendlyByteBuf)}.
* @param <T> The type of this message.
* @return The new {@link MessageType} instance.
*/
<T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader);
/**
* Send a message to a specific player.
*

View File

@@ -12,7 +12,6 @@ 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;
import dan200.computercraft.shared.common.IColouredItem;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ServerContext;
@@ -60,14 +59,6 @@ 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 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);
default -> ItemStack.EMPTY;
};
}
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);
@@ -243,17 +234,16 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I
return IComputerItem.super.getLabel(stack);
}
@Override
public ComputerFamily getFamily() {
return family;
}
@Override
public ItemStack withFamily(ItemStack stack, ComputerFamily family) {
return create(
public ItemStack changeItem(ItemStack stack, Item newItem) {
return newItem instanceof PocketComputerItem pocket ? pocket.create(
getComputerID(stack), getLabel(stack), getColour(stack),
family, getUpgradeWithData(stack)
);
getUpgradeWithData(stack)
) : ItemStack.EMPTY;
}
// IMedia

View File

@@ -81,7 +81,6 @@ public final class PocketComputerUpgradeRecipe extends CustomRecipe {
if (upgrade == null) return ItemStack.EMPTY;
// Construct the new stack
var family = itemComputer.getFamily();
var computerID = itemComputer.getComputerID(computer);
var label = itemComputer.getLabel(computer);
var colour = itemComputer.getColour(computer);

View File

@@ -11,6 +11,7 @@ import dan200.computercraft.api.turtle.TurtleCommandResult;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.shared.peripheral.generic.methods.AbstractInventoryMethods;
import dan200.computercraft.shared.turtle.core.*;
import java.util.Optional;
@@ -749,7 +750,7 @@ public class TurtleAPI implements ILuaAPI {
* -- count = 13,
* -- }
* }</pre>
* @see dan200.computercraft.shared.peripheral.generic.methods.InventoryMethods#getItemDetail Describes the information returned by a detailed query.
* @see AbstractInventoryMethods#getItemDetail Describes the information returned by a detailed query.
*/
@LuaFunction
public final MethodResult getItemDetail(ILuaContext context, Optional<Integer> slot, Optional<Boolean> detailed) throws LuaException {

View File

@@ -10,7 +10,6 @@ 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;
import dan200.computercraft.shared.platform.RegistryEntry;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import dan200.computercraft.shared.turtle.items.TurtleItem;
@@ -52,6 +51,16 @@ import static dan200.computercraft.shared.util.WaterloggableHelpers.getFluidStat
public class TurtleBlock extends AbstractComputerBlock<TurtleBlockEntity> implements SimpleWaterloggedBlock {
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
/**
* The explosion resistance to use when a turtle is "immune" to explosions.
* <p>
* This is used as the default explosion resistance for advanced turtles, and the resistance for entity-based
* explosions (e.g. creepers).
*
* @see #getExplosionResistance(BlockState, BlockGetter, BlockPos, Explosion)
*/
public static final float IMMUNE_EXPLOSION_RESISTANCE = 2000f;
private static final VoxelShape DEFAULT_SHAPE = Shapes.box(
0.125, 0.125, 0.125,
0.875, 0.875, 0.875
@@ -59,8 +68,8 @@ public class TurtleBlock extends AbstractComputerBlock<TurtleBlockEntity> implem
private final BlockEntityTicker<TurtleBlockEntity> clientTicker = (level, pos, state, computer) -> computer.clientTick();
public TurtleBlock(Properties settings, ComputerFamily family, RegistryEntry<BlockEntityType<TurtleBlockEntity>> type) {
super(settings, family, type);
public TurtleBlock(Properties settings, RegistryEntry<BlockEntityType<TurtleBlockEntity>> type) {
super(settings, type);
registerDefaultState(getStateDefinition().any()
.setValue(FACING, Direction.NORTH)
.setValue(WATERLOGGED, false)
@@ -149,20 +158,21 @@ public class TurtleBlock extends AbstractComputerBlock<TurtleBlockEntity> implem
@ForgeOverride
public float getExplosionResistance(BlockState state, BlockGetter world, BlockPos pos, Explosion explosion) {
var exploder = explosion.getDirectSourceEntity();
if (getFamily() == ComputerFamily.ADVANCED || exploder instanceof LivingEntity || exploder instanceof AbstractHurtingProjectile) {
return 2000;
if (exploder instanceof LivingEntity || exploder instanceof AbstractHurtingProjectile) {
return IMMUNE_EXPLOSION_RESISTANCE;
}
return explosionResistance;
return getExplosionResistance();
}
@Override
protected ItemStack getItem(AbstractComputerBlockEntity tile) {
if (!(tile instanceof TurtleBlockEntity turtle)) return ItemStack.EMPTY;
if (!(asItem() instanceof TurtleItem item)) return ItemStack.EMPTY;
var access = turtle.getAccess();
return TurtleItem.create(
turtle.getComputerID(), turtle.getLabel(), access.getColour(), turtle.getFamily(),
return item.create(
turtle.getComputerID(), turtle.getLabel(), access.getColour(),
withPersistedData(access.getUpgradeWithData(TurtleSide.LEFT)),
withPersistedData(access.getUpgradeWithData(TurtleSide.RIGHT)),
access.getFuelLevel(), turtle.getOverlay()

View File

@@ -38,6 +38,7 @@ import net.minecraft.world.phys.Vec3;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.function.IntSupplier;
public class TurtleBlockEntity extends AbstractComputerBlockEntity implements BasicContainer {
public static final int INVENTORY_SIZE = 16;
@@ -53,13 +54,17 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
private final NonNullList<ItemStack> inventory = NonNullList.withSize(INVENTORY_SIZE, ItemStack.EMPTY);
private final NonNullList<ItemStack> inventorySnapshot = NonNullList.withSize(INVENTORY_SIZE, ItemStack.EMPTY);
private boolean inventoryChanged = false;
private final IntSupplier fuelLimit;
private TurtleBrain brain = new TurtleBrain(this);
private MoveState moveState = MoveState.NOT_MOVED;
private @Nullable IPeripheral peripheral;
private @Nullable Runnable onMoved;
public TurtleBlockEntity(BlockEntityType<? extends TurtleBlockEntity> type, BlockPos pos, BlockState state, ComputerFamily family) {
public TurtleBlockEntity(BlockEntityType<? extends TurtleBlockEntity> type, BlockPos pos, BlockState state, IntSupplier fuelLimit, ComputerFamily family) {
super(type, pos, state, family);
this.fuelLimit = fuelLimit;
}
boolean hasMoved() {
@@ -172,8 +177,6 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
return hasPeripheralUpgradeOnSide(localSide);
}
// IDirectionalTile
@Override
public Direction getDirection() {
return getBlockState().getValue(TurtleBlock.FACING);
@@ -272,6 +275,10 @@ public class TurtleBlockEntity extends AbstractComputerBlockEntity implements Ba
// Privates
public int getFuelLimit() {
return fuelLimit.getAsInt();
}
private boolean hasPeripheralUpgradeOnSide(ComputerSide side) {
ITurtleUpgrade upgrade;
switch (side) {

View File

@@ -395,11 +395,7 @@ public class TurtleBrain implements TurtleAccessInternal {
@Override
public int getFuelLimit() {
if (owner.getFamily() == ComputerFamily.ADVANCED) {
return Config.advancedTurtleFuelLimit;
} else {
return Config.turtleFuelLimit;
}
return owner.getFuelLimit();
}
@Override

View File

@@ -10,9 +10,7 @@ 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;
@@ -20,6 +18,7 @@ import net.minecraft.core.cauldron.CauldronInteraction;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.LayeredCauldronBlock;
@@ -32,20 +31,6 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem {
super(block, settings);
}
public static ItemStack create(
int id, @Nullable String label, int colour, ComputerFamily family,
@Nullable UpgradeData<ITurtleUpgrade> leftUpgrade, @Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
int fuelLevel, @Nullable ResourceLocation overlay
) {
return switch (family) {
case NORMAL ->
ModRegistry.Items.TURTLE_NORMAL.get().create(id, label, colour, leftUpgrade, rightUpgrade, fuelLevel, overlay);
case ADVANCED ->
ModRegistry.Items.TURTLE_ADVANCED.get().create(id, label, colour, leftUpgrade, rightUpgrade, fuelLevel, overlay);
default -> ItemStack.EMPTY;
};
}
public ItemStack create(
int id, @Nullable String label, int colour,
@Nullable UpgradeData<ITurtleUpgrade> leftUpgrade, @Nullable UpgradeData<ITurtleUpgrade> rightUpgrade,
@@ -119,13 +104,13 @@ public class TurtleItem extends AbstractComputerItem implements IColouredItem {
}
@Override
public ItemStack withFamily(ItemStack stack, ComputerFamily family) {
return create(
public ItemStack changeItem(ItemStack stack, Item newItem) {
return newItem instanceof TurtleItem turtle ? turtle.create(
getComputerID(stack), getLabel(stack),
getColour(stack), family,
getColour(stack),
getUpgradeWithData(stack, TurtleSide.LEFT), getUpgradeWithData(stack, TurtleSide.RIGHT),
getFuelLevel(stack), getOverlay(stack)
);
) : ItemStack.EMPTY;
}
public @Nullable ITurtleUpgrade getUpgrade(ItemStack stack, TurtleSide side) {

View File

@@ -58,7 +58,7 @@ public class TurtleOverlayRecipe extends CustomShapelessRecipe {
return ModRegistry.RecipeSerializers.TURTLE_OVERLAY.get();
}
public static class Serializer implements RecipeSerializer<TurtleOverlayRecipe> {
public static class Serialiser implements RecipeSerializer<TurtleOverlayRecipe> {
@Override
public TurtleOverlayRecipe fromJson(ResourceLocation id, JsonObject json) {
var recipe = ShapelessRecipeSpec.fromJson(json);

View File

@@ -10,6 +10,7 @@ accessWidener v1 named
accessible method net/minecraft/client/renderer/item/ItemProperties register (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/item/ClampedItemPropertyFunction;)V
accessible method net/minecraft/client/renderer/blockentity/BlockEntityRenderers register (Lnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/client/renderer/blockentity/BlockEntityRendererProvider;)V
accessible class net/minecraft/world/item/CreativeModeTab$Output
accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey;
# Containers
accessible class net/minecraft/world/inventory/MenuType$MenuSupplier

View File

@@ -13,6 +13,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.impl.AbstractComputerCraftAPI;
import dan200.computercraft.impl.ComputerCraftAPIService;
import dan200.computercraft.shared.config.ConfigFile;
import dan200.computercraft.shared.network.MessageType;
import dan200.computercraft.shared.network.NetworkMessage;
import dan200.computercraft.shared.network.client.ClientNetworkContext;
import dan200.computercraft.shared.network.container.ContainerData;
@@ -168,6 +169,13 @@ public class TestPlatformHelper extends AbstractComputerCraftAPI implements Plat
throw new UnsupportedOperationException("Cannot open menu inside tests");
}
@Override
public <T extends NetworkMessage<?>> MessageType<T> createMessageType(int id, ResourceLocation channel, Class<T> klass, FriendlyByteBuf.Reader<T> reader) {
record TypeImpl<T extends NetworkMessage<?>>(Function<FriendlyByteBuf, T> reader) implements MessageType<T> {
}
return new TypeImpl<>(reader);
}
@Override
public ComponentAccess<IPeripheral> createPeripheralAccess(Consumer<Direction> invalidate) {
throw new UnsupportedOperationException("Cannot interact with the world inside tests");

View File

@@ -29,7 +29,7 @@ class PlayRecordClientMessageTest {
@Property
public void testRoundTrip(@ForAll("message") PlayRecordClientMessage message) {
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
message.toBytes(buffer);
message.write(buffer);
var converted = new PlayRecordClientMessage(buffer);
assertEquals(buffer.readableBytes(), 0, "Whole packet was read");

View File

@@ -63,7 +63,7 @@ public class UploadFileMessageTest {
private static List<UploadFileMessage> roundtripPackets(List<UploadFileMessage> packets) {
return packets.stream().map(packet -> {
var buffer = new FriendlyByteBuf(Unpooled.directBuffer());
packet.toBytes(buffer);
packet.write(buffer);
// We include things like file size in the packet, but not in the count, so grant a slightly larger threshold.
assertThat("Packet is too large", buffer.writerIndex(), lessThanOrEqualTo(MAX_PACKET_SIZE + 128));
if ((packet.flag & FLAG_LAST) == 0) {

View File

@@ -7,7 +7,8 @@ package dan200.computercraft.api.filesystem;
import javax.annotation.Nullable;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import static dan200.computercraft.api.filesystem.MountConstants.EPOCH;
/**
* A simple version of {@link BasicFileAttributes}, which provides what information a {@link Mount} already exposes.
@@ -20,8 +21,6 @@ import java.time.Instant;
public record FileAttributes(
boolean isDirectory, long size, FileTime creationTime, FileTime lastModifiedTime
) implements BasicFileAttributes {
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
/**
* Create a new {@link FileAttributes} instance with the {@linkplain #creationTime() creation time} and
* {@linkplain #lastModifiedTime() last modified time} set to the Unix epoch.

View File

@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.api.filesystem;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.List;
import java.util.Set;
/**
* Useful constants functions for working with mounts.
*
* @see Mount
* @see WritableMount
*/
public final class MountConstants {
/**
* A {@link FileTime} set to the Unix EPOCH, intended for {@link BasicFileAttributes}'s file times.
*/
public static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
/**
* The minimum size of a file for file {@linkplain WritableMount#getCapacity() capacity calculations}.
*/
public static final long MINIMUM_FILE_SIZE = 500;
/**
* The error message used when the file does not exist.
*/
public static final String NO_SUCH_FILE = "No such file";
/**
* The error message used when trying to use a file as a directory (for instance when
* {@linkplain Mount#list(String, List) listing its contents}).
*/
public static final String NOT_A_DIRECTORY = "Not a directory";
/**
* The error message used when trying to use a directory as a file (for instance when
* {@linkplain Mount#openForRead(String) opening for reading}).
*/
public static final String NOT_A_FILE = "Not a file";
/**
* The error message used when attempting to modify a read-only file or mount.
*/
public static final String ACCESS_DENIED = "Access denied";
/**
* The error message used when trying to overwrite a file (for instance when
* {@linkplain WritableMount#rename(String, String) renaming files} or {@linkplain WritableMount#makeDirectory(String)
* creating directories}).
*/
public static final String FILE_EXISTS = "File exists";
/**
* The error message used when trying to {@linkplain WritableMount#openForWrite(String) opening a directory to read}.
*/
public static final String CANNOT_WRITE_TO_DIRECTORY = "Cannot write to directory";
/**
* The error message used when the mount runs out of space.
*/
public static final String OUT_OF_SPACE = "Out of space";
/**
* The error message to throw when an unsupported set of options were passed to
* {@link WritableMount#openFile(String, Set)}.
*/
public static final String UNSUPPORTED_MODE = "Unsupported mode";
public static final Set<OpenOption> READ_OPTIONS = Set.of(StandardOpenOption.READ);
public static final Set<OpenOption> WRITE_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
public static final Set<OpenOption> APPEND_OPTIONS = Set.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
private MountConstants() {
}
}

View File

@@ -7,8 +7,13 @@ package dan200.computercraft.api.filesystem;
import dan200.computercraft.api.peripheral.IComputerAccess;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.util.Set;
/**
* Represents a part of a virtual filesystem that can be mounted onto a computer using {@link IComputerAccess#mount(String, Mount)}
@@ -51,22 +56,51 @@ public interface WritableMount extends Mount {
void rename(String source, String dest) throws IOException;
/**
* Opens a file with a given path, and returns an {@link OutputStream} for writing to it.
* Opens a file with a given path, and returns an {@link SeekableByteChannel} for writing to it.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to.
* @return A channel for writing to.
* @throws IOException If the file could not be opened for writing.
* @deprecated Replaced with more the generic {@link #openFile(String, Set)}.
*/
@Deprecated(forRemoval = true)
SeekableByteChannel openForWrite(String path) throws IOException;
/**
* Opens a file with a given path, and returns an {@link OutputStream} for appending to it.
* Opens a file with a given path, and returns an {@link SeekableByteChannel} for appending to it.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @return A stream for writing to.
* @return A channel for writing to.
* @throws IOException If the file could not be opened for writing.
* @deprecated Replaced with more the generic {@link #openFile(String, Set)}.
*/
@Deprecated(forRemoval = true)
SeekableByteChannel openForAppend(String path) throws IOException;
/**
* Opens a file with a given path, and returns an {@link SeekableByteChannel}.
* <p>
* This allows opening a file in a variety of options, much like {@link FileChannel#open(Path, Set, FileAttribute[])}.
* <p>
* At minimum, the option sets {@link MountConstants#READ_OPTIONS}, {@link MountConstants#WRITE_OPTIONS} and
* {@link MountConstants#APPEND_OPTIONS} should be supported. It is recommended any valid combination of
* {@link StandardOpenOption#READ}, {@link StandardOpenOption#WRITE}, {@link StandardOpenOption#CREATE},
* {@link StandardOpenOption#TRUNCATE_EXISTING} and {@link StandardOpenOption#APPEND} are supported.
* <p>
* Unsupported modes (or combinations of modes) should throw an exception with the message
* {@link MountConstants#UNSUPPORTED_MODE "Unsupported mode"}.
*
* @param path A file path in normalised format, relative to the mount location. ie: "programs/myprogram".
* @param options For options used for opening a file.
* @return A channel for writing to.
* @throws IOException If the file could not be opened for writing.
*/
SeekableByteChannel openForAppend(String path) throws IOException;
default SeekableByteChannel openFile(String path, Set<OpenOption> options) throws IOException {
if (options.equals(MountConstants.READ_OPTIONS)) return openForRead(path);
if (options.equals(MountConstants.WRITE_OPTIONS)) return openForWrite(path);
if (options.equals(MountConstants.APPEND_OPTIONS)) return openForAppend(path);
throw new IOException(MountConstants.UNSUPPORTED_MODE);
}
/**
* Get the amount of free space on the mount, in bytes. You should decrease this value as the user writes to the

View File

@@ -11,11 +11,10 @@ import dan200.computercraft.api.peripheral.IPeripheral;
* A generic source of {@link LuaFunction} functions.
* <p>
* Unlike normal objects ({@link IDynamicLuaObject} or {@link IPeripheral}), methods do not target this object but
* instead are defined as {@code static} and accept their target as the first parameter. This allows you to inject
* methods onto objects you do not own, as well as declaring methods for a specific "trait" (for instance, a Forge
* capability or Fabric block lookup interface).
* accept their target as the first parameter. This allows you to inject methods onto objects you do not own, as well as
* declaring methods for a specific "trait" (for instance, a Forge capability or Fabric block lookup interface).
* <p>
* Currently the "generic peripheral" system is incompatible with normal peripherals. Peripherals explicitly provided
* Currently, the "generic peripheral" system is incompatible with normal peripherals. Peripherals explicitly provided
* by capabilities/the block lookup API take priority. Block entities which use this system are given a peripheral name
* determined by their id, rather than any peripheral provider, though additional types may be provided by overriding
* {@link GenericPeripheral#getType()}.
@@ -25,7 +24,7 @@ import dan200.computercraft.api.peripheral.IPeripheral;
* <pre>{@code
* public class InventoryMethods implements GenericSource {
* \@LuaFunction( mainThread = true )
* public static int size(IItemHandler inventory) {
* public int size(IItemHandler inventory) {
* return inventory.getSlots();
* }
*

View File

@@ -195,6 +195,19 @@ public interface IArguments {
return LuaValues.encode(getString(index));
}
/**
* Get the argument, converting it to the raw-byte representation of its string by following Lua conventions.
* <p>
* This is equivalent to {@link #getStringCoerced(int)}, but then
*
* @param index The argument number.
* @return The argument's value. This is a <em>read only</em> buffer.
* @throws LuaException If the argument cannot be converted to Java.
*/
default ByteBuffer getBytesCoerced(int index) throws LuaException {
return LuaValues.encode(getStringCoerced(index));
}
/**
* Get a string argument as an enum value.
*

View File

@@ -4,22 +4,22 @@
package dan200.computercraft.core.apis;
import dan200.computercraft.api.filesystem.MountConstants;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.apis.handles.ReadWriteHandle;
import dan200.computercraft.core.apis.handles.WriteHandle;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.metrics.Metrics;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.*;
/**
* Interact with the computer's files and filesystem, allowing you to manipulate files, directories and paths. This
@@ -55,6 +55,9 @@ import java.util.function.Function;
* @cc.module fs
*/
public class FSAPI implements ILuaAPI {
private static final Set<OpenOption> READ_EXTENDED = Set.of(StandardOpenOption.READ, StandardOpenOption.WRITE);
private static final Set<OpenOption> WRITE_EXTENDED = union(Set.of(StandardOpenOption.READ), MountConstants.WRITE_OPTIONS);
private final IAPIEnvironment environment;
private @Nullable FileSystem fileSystem = null;
@@ -301,8 +304,6 @@ public class FSAPI implements ILuaAPI {
}
}
// FIXME: Add individual handle type documentation
/**
* Opens a file for reading or writing at a path.
* <p>
@@ -311,10 +312,13 @@ public class FSAPI implements ILuaAPI {
* <li><strong>"r"</strong>: Read mode</li>
* <li><strong>"w"</strong>: Write mode</li>
* <li><strong>"a"</strong>: Append mode</li>
* <li><strong>"r+"</strong>: Update mode (allows reading and writing), all data is preserved</li>
* <li><strong>"w+"</strong>: Update mode, all data is erased.</li>
* </ul>
* <p>
* The mode may also have a "b" at the end, which opens the file in "binary
* mode". This allows you to read binary files, as well as seek within a file.
* mode". This changes {@link ReadHandle#read(Optional)} and {@link WriteHandle#write(IArguments)}
* to read/write single bytes as numbers rather than strings.
*
* @param path The path to the file to open.
* @param mode The mode to open the file with.
@@ -354,42 +358,38 @@ public class FSAPI implements ILuaAPI {
* file.write("Just testing some code")
* file.close() -- Remember to call close, otherwise changes may not be written!
* }</pre>
* @cc.changed 1.109.0 Add support for update modes ({@code r+} and {@code w+}).
* @cc.changed 1.109.0 Opening a file in non-binary mode now uses the raw bytes of the file rather than encoding to
* UTF-8.
*/
@LuaFunction
public final Object[] open(String path, String mode) throws LuaException {
if (mode.isEmpty()) throw new LuaException(MountConstants.UNSUPPORTED_MODE);
var binary = mode.indexOf('b') >= 0;
try (var ignored = environment.time(Metrics.FS_OPS)) {
switch (mode) {
case "r" -> {
// Open the file for reading, then create a wrapper around the reader
var reader = getFileSystem().openForRead(path, EncodedReadableHandle::openUtf8);
return new Object[]{ new EncodedReadableHandle(reader.get(), reader) };
case "r", "rb" -> {
var reader = getFileSystem().openForRead(path);
return new Object[]{ new ReadHandle(reader.get(), reader, binary) };
}
case "w" -> {
// Open the file for writing, then create a wrapper around the writer
var writer = getFileSystem().openForWrite(path, false, EncodedWritableHandle::openUtf8);
return new Object[]{ new EncodedWritableHandle(writer.get(), writer) };
case "w", "wb" -> {
var writer = getFileSystem().openForWrite(path, MountConstants.WRITE_OPTIONS);
return new Object[]{ WriteHandle.of(writer.get(), writer, binary, true) };
}
case "a" -> {
// Open the file for appending, then create a wrapper around the writer
var writer = getFileSystem().openForWrite(path, true, EncodedWritableHandle::openUtf8);
return new Object[]{ new EncodedWritableHandle(writer.get(), writer) };
case "a", "ab" -> {
var writer = getFileSystem().openForWrite(path, MountConstants.APPEND_OPTIONS);
return new Object[]{ WriteHandle.of(writer.get(), writer, binary, false) };
}
case "rb" -> {
// Open the file for binary reading, then create a wrapper around the reader
var reader = getFileSystem().openForRead(path, Function.identity());
return new Object[]{ BinaryReadableHandle.of(reader.get(), reader) };
case "r+", "r+b" -> {
var reader = getFileSystem().openForWrite(path, READ_EXTENDED);
return new Object[]{ new ReadWriteHandle(reader.get(), reader, binary) };
}
case "wb" -> {
// Open the file for binary writing, then create a wrapper around the writer
var writer = getFileSystem().openForWrite(path, false, Function.identity());
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer, true) };
case "w+", "w+b" -> {
var writer = getFileSystem().openForWrite(path, WRITE_EXTENDED);
return new Object[]{ new ReadWriteHandle(writer.get(), writer, binary) };
}
case "ab" -> {
// Open the file for binary appending, then create a wrapper around the reader
var writer = getFileSystem().openForWrite(path, true, Function.identity());
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer, false) };
}
default -> throw new LuaException("Unsupported mode");
default -> throw new LuaException(MountConstants.UNSUPPORTED_MODE);
}
} catch (FileSystemException e) {
return new Object[]{ null, e.getMessage() };
@@ -498,4 +498,11 @@ public class FSAPI implements ILuaAPI {
throw new LuaException(e.getMessage());
}
}
private static Set<OpenOption> union(Set<OpenOption> a, Set<OpenOption> b) {
Set<OpenOption> union = new HashSet<>();
union.addAll(a);
union.addAll(b);
return Set.copyOf(union);
}
}

View File

@@ -4,10 +4,7 @@
package dan200.computercraft.core.apis;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.CoreConfig;
import dan200.computercraft.core.apis.http.*;
import dan200.computercraft.core.apis.http.request.HttpRequest;
@@ -18,6 +15,7 @@ import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@@ -73,7 +71,8 @@ public class HTTPAPI implements ILuaAPI {
@LuaFunction
public final Object[] request(IArguments args) throws LuaException {
String address, postString, requestMethod;
String address, requestMethod;
ByteBuffer postBody;
Map<?, ?> headerTable;
boolean binary, redirect;
Optional<Double> timeoutArg;
@@ -81,7 +80,8 @@ public class HTTPAPI implements ILuaAPI {
if (args.get(0) instanceof Map) {
var options = args.getTable(0);
address = getStringField(options, "url");
postString = optStringField(options, "body", null);
var postString = optStringField(options, "body", null);
postBody = postString == null ? null : LuaValues.encode(postString);
headerTable = optTableField(options, "headers", Map.of());
binary = optBooleanField(options, "binary", false);
requestMethod = optStringField(options, "method", null);
@@ -90,7 +90,7 @@ public class HTTPAPI implements ILuaAPI {
} else {
// Get URL and post information
address = args.getString(0);
postString = args.optString(1, null);
postBody = args.optBytes(1).orElse(null);
headerTable = args.optTable(2, Map.of());
binary = args.optBoolean(3, false);
requestMethod = null;
@@ -103,7 +103,7 @@ public class HTTPAPI implements ILuaAPI {
HttpMethod httpMethod;
if (requestMethod == null) {
httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST;
httpMethod = postBody == null ? HttpMethod.GET : HttpMethod.POST;
} else {
httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT));
if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) {
@@ -113,7 +113,7 @@ public class HTTPAPI implements ILuaAPI {
try {
var uri = HttpRequest.checkUri(address);
var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect, timeout);
var request = new HttpRequest(requests, apiEnvironment, address, postBody, headers, binary, redirect, timeout);
// Make the request
if (!request.queue(r -> r.request(uri, httpMethod))) {

View File

@@ -1,45 +1,100 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import dan200.computercraft.core.util.IoUtil;
import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"}
* mode.
*
* @cc.module fs.BinaryReadHandle
* The base class for all file handle types.
*/
public class BinaryReadableHandle extends HandleGeneric {
public abstract class AbstractHandle {
private static final int BUFFER_SIZE = 8192;
private final SeekableByteChannel channel;
private @Nullable TrackingCloseable closeable;
protected final boolean binary;
private final ByteBuffer single = ByteBuffer.allocate(1);
BinaryReadableHandle(SeekableByteChannel channel, TrackingCloseable closeable) {
super(closeable);
protected AbstractHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
this.channel = channel;
this.closeable = closeable;
this.binary = binary;
}
public static BinaryReadableHandle of(SeekableByteChannel channel, TrackingCloseable closeable) {
return new BinaryReadableHandle(channel, closeable);
protected void checkOpen() throws LuaException {
var closeable = this.closeable;
if (closeable == null || !closeable.isOpen()) throw new LuaException("attempt to use a closed file");
}
public static BinaryReadableHandle of(SeekableByteChannel channel) {
return of(channel, new TrackingCloseable.Impl(channel));
/**
* Close this file, freeing any resources it uses.
* <p>
* Once a file is closed it may no longer be read or written to.
*
* @throws LuaException If the file has already been closed.
*/
@LuaFunction
public final void close() throws LuaException {
checkOpen();
IoUtil.closeQuietly(closeable);
closeable = null;
}
/**
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* given by {@code offset}, relative to a start position determined by {@code whence}:
* <p>
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
* - {@code "cur"}: Relative to the current position. This is the default.
* - {@code "end"}: Relative to the end of the file.
* <p>
* In case of success, {@code seek} returns the new file position from the beginning of the file.
*
* @param whence Where the offset is relative to.
* @param offset The offset to seek to.
* @return The new position.
* @throws LuaException If the file has been closed.
* @cc.treturn [1] number The new position.
* @cc.treturn [2] nil If seeking failed.
* @cc.treturn string The reason seeking failed.
* @cc.since 1.80pr1.9
*/
@Nullable
public Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
checkOpen();
long actualOffset = offset.orElse(0L);
try {
switch (whence.orElse("cur")) {
case "set" -> channel.position(actualOffset);
case "cur" -> channel.position(channel.position() + actualOffset);
case "end" -> channel.position(channel.size() + actualOffset);
default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
}
return new Object[]{ channel.position() };
} catch (IllegalArgumentException e) {
return new Object[]{ null, "Position is negative" };
} catch (IOException e) {
return null;
}
}
/**
@@ -51,17 +106,21 @@ public class BinaryReadableHandle extends HandleGeneric {
* @throws LuaException When trying to read a negative number of bytes.
* @throws LuaException If the file has been closed.
* @cc.treturn [1] nil If we are at the end of the file.
* @cc.treturn [2] number The value of the byte read. This is returned when the {@code count} is absent.
* @cc.treturn [2] number The value of the byte read. This is returned if the file is opened in binary mode and
* {@code count} is absent
* @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
* @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number.
*/
@Nullable
@LuaFunction
public final Object[] read(Optional<Integer> countArg) throws LuaException {
public Object[] read(Optional<Integer> countArg) throws LuaException {
checkOpen();
try {
if (countArg.isPresent()) {
int count = countArg.get();
if (binary && countArg.isEmpty()) {
single.clear();
var b = channel.read(single);
return b == -1 ? null : new Object[]{ single.get(0) & 0xFF };
} else {
int count = countArg.orElse(1);
if (count < 0) throw new LuaException("Cannot read a negative number of bytes");
if (count == 0) return channel.position() >= channel.size() ? null : new Object[]{ "" };
@@ -109,10 +168,6 @@ public class BinaryReadableHandle extends HandleGeneric {
assert pos == totalRead;
return new Object[]{ bytes };
}
} else {
single.clear();
var b = channel.read(single);
return b == -1 ? null : new Object[]{ single.get(0) & 0xFF };
}
} catch (IOException e) {
return null;
@@ -128,8 +183,7 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.since 1.80pr1
*/
@Nullable
@LuaFunction
public final Object[] readAll() throws LuaException {
public Object[] readAll() throws LuaException {
checkOpen();
try {
var expected = 32;
@@ -137,16 +191,14 @@ public class BinaryReadableHandle extends HandleGeneric {
var stream = new ByteArrayOutputStream(expected);
var buf = ByteBuffer.allocate(8192);
var readAnything = false;
while (true) {
buf.clear();
var r = channel.read(buf);
if (r == -1) break;
readAnything = true;
stream.write(buf.array(), 0, r);
}
return readAnything ? new Object[]{ stream.toByteArray() } : null;
return new Object[]{ stream.toByteArray() };
} catch (IOException e) {
return null;
}
@@ -163,8 +215,7 @@ public class BinaryReadableHandle extends HandleGeneric {
* @cc.changed 1.81.0 `\r` is now stripped.
*/
@Nullable
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
public Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
checkOpen();
boolean withTrailing = withTrailingArg.orElse(false);
try {
@@ -206,28 +257,64 @@ public class BinaryReadableHandle extends HandleGeneric {
}
/**
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* given by {@code offset}, relative to a start position determined by {@code whence}:
* <p>
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
* - {@code "cur"}: Relative to the current position. This is the default.
* - {@code "end"}: Relative to the end of the file.
* <p>
* In case of success, {@code seek} returns the new file position from the beginning of the file.
* Write a string or byte to the file.
*
* @param whence Where the offset is relative to.
* @param offset The offset to seek to.
* @return The new position.
* @param arguments The value to write.
* @throws LuaException If the file has been closed.
* @cc.treturn [1] number The new position.
* @cc.treturn [2] nil If seeking failed.
* @cc.treturn string The reason seeking failed.
* @cc.since 1.80pr1.9
* @cc.tparam [1] string contents The string to write.
* @cc.tparam [2] number charcode The byte to write, if the file was opened in binary mode.
* @cc.changed 1.80pr1 Now accepts a string to write multiple bytes.
*/
@Nullable
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
public void write(IArguments arguments) throws LuaException {
checkOpen();
return handleSeek(channel, whence, offset);
try {
var arg = arguments.get(0);
if (binary && arg instanceof Number) {
var number = ((Number) arg).intValue();
writeSingle((byte) number);
} else {
channel.write(arguments.getBytesCoerced(0));
}
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
/**
* Write a string of characters to the file, following them with a new line character.
*
* @param text The text to write to the file.
* @throws LuaException If the file has been closed.
*/
public void writeLine(Coerced<ByteBuffer> text) throws LuaException {
checkOpen();
try {
channel.write(text.value());
writeSingle((byte) '\n');
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
private void writeSingle(byte value) throws IOException {
single.clear();
single.put(value);
single.flip();
channel.write(single);
}
/**
* Save the current file without closing it.
*
* @throws LuaException If the file has been closed.
*/
public void flush() throws LuaException {
checkOpen();
try {
// Technically this is not needed
if (channel instanceof FileChannel channel) channel.force(false);
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
}

View File

@@ -1,117 +0,0 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.api.lua.LuaValues;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
/**
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"}
* modes.
*
* @cc.module fs.BinaryWriteHandle
*/
public class BinaryWritableHandle extends HandleGeneric {
final SeekableByteChannel channel;
private final ByteBuffer single = ByteBuffer.allocate(1);
protected BinaryWritableHandle(SeekableByteChannel channel, TrackingCloseable closeable) {
super(closeable);
this.channel = channel;
}
public static BinaryWritableHandle of(SeekableByteChannel channel, TrackingCloseable closeable, boolean canSeek) {
return canSeek ? new Seekable(channel, closeable) : new BinaryWritableHandle(channel, closeable);
}
/**
* Write a string or byte to the file.
*
* @param arguments The value to write.
* @throws LuaException If the file has been closed.
* @cc.tparam [1] number charcode The byte to write.
* @cc.tparam [2] string contents The string to write.
* @cc.changed 1.80pr1 Now accepts a string to write multiple bytes.
*/
@LuaFunction
public final void write(IArguments arguments) throws LuaException {
checkOpen();
try {
var arg = arguments.get(0);
if (arg instanceof Number) {
var number = ((Number) arg).intValue();
single.clear();
single.put((byte) number);
single.flip();
channel.write(single);
} else if (arg instanceof String) {
channel.write(arguments.getBytes(0));
} else {
throw LuaValues.badArgumentOf(arguments, 0, "string or number");
}
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
/**
* Save the current file without closing it.
*
* @throws LuaException If the file has been closed.
*/
@LuaFunction
public final void flush() throws LuaException {
checkOpen();
try {
// Technically this is not needed
if (channel instanceof FileChannel channel) channel.force(false);
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
public static class Seekable extends BinaryWritableHandle {
public Seekable(SeekableByteChannel channel, TrackingCloseable closeable) {
super(channel, closeable);
}
/**
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
* given by {@code offset}, relative to a start position determined by {@code whence}:
* <p>
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
* - {@code "cur"}: Relative to the current position. This is the default.
* - {@code "end"}: Relative to the end of the file.
* <p>
* In case of success, {@code seek} returns the new file position from the beginning of the file.
*
* @param whence Where the offset is relative to.
* @param offset The offset to seek to.
* @return The new position.
* @throws LuaException If the file has been closed.
* @cc.treturn [1] number The new position.
* @cc.treturn [2] nil If seeking failed.
* @cc.treturn string The reason seeking failed.
* @cc.since 1.80pr1.9
*/
@Nullable
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
checkOpen();
return handleSeek(channel, whence, offset);
}
}
}

View File

@@ -1,163 +0,0 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
/**
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"}
* mode.
*
* @cc.module fs.ReadHandle
*/
public class EncodedReadableHandle extends HandleGeneric {
private static final int BUFFER_SIZE = 8192;
private final BufferedReader reader;
public EncodedReadableHandle(BufferedReader reader, TrackingCloseable closable) {
super(closable);
this.reader = reader;
}
public EncodedReadableHandle(BufferedReader reader) {
this(reader, new TrackingCloseable.Impl(reader));
}
/**
* Read a line from the file.
*
* @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}.
* @return The read string.
* @throws LuaException If the file has been closed.
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
* @cc.changed 1.81.0 Added option to return trailing newline.
*/
@Nullable
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
checkOpen();
boolean withTrailing = withTrailingArg.orElse(false);
try {
var line = reader.readLine();
if (line != null) {
// While this is technically inaccurate, it's better than nothing
if (withTrailing) line += "\n";
return new Object[]{ line };
} else {
return null;
}
} catch (IOException e) {
return null;
}
}
/**
* Read the remainder of the file.
*
* @return The file, or {@code null} if at the end of it.
* @throws LuaException If the file has been closed.
* @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
*/
@Nullable
@LuaFunction
public final Object[] readAll() throws LuaException {
checkOpen();
try {
var result = new StringBuilder();
var line = reader.readLine();
while (line != null) {
result.append(line);
line = reader.readLine();
if (line != null) {
result.append("\n");
}
}
return new Object[]{ result.toString() };
} catch (IOException e) {
return null;
}
}
/**
* Read a number of characters from this file.
*
* @param countA The number of characters to read, defaulting to 1.
* @return The read characters.
* @throws LuaException When trying to read a negative number of characters.
* @throws LuaException If the file has been closed.
* @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
* @cc.since 1.80pr1.4
*/
@Nullable
@LuaFunction
public final Object[] read(Optional<Integer> countA) throws LuaException {
checkOpen();
try {
int count = countA.orElse(1);
if (count < 0) {
// Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so
// it seems best to remain somewhat consistent.
throw new LuaException("Cannot read a negative number of characters");
} else if (count <= BUFFER_SIZE) {
// If we've got a small count, then allocate that and read it.
var chars = new char[count];
var read = reader.read(chars);
return read < 0 ? null : new Object[]{ new String(chars, 0, read) };
} else {
// If we've got a large count, read in bunches of 8192.
var buffer = new char[BUFFER_SIZE];
// Read the initial set of characters, failing if none are read.
var read = reader.read(buffer, 0, Math.min(buffer.length, count));
if (read < 0) return null;
var out = new StringBuilder(read);
var totalRead = read;
out.append(buffer, 0, read);
// Otherwise read until we either reach the limit or we no longer consume
// the full buffer.
while (read >= BUFFER_SIZE && totalRead < count) {
read = reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead));
if (read < 0) break;
totalRead += read;
out.append(buffer, 0, read);
}
return new Object[]{ out.toString() };
}
} catch (IOException e) {
return null;
}
}
public static BufferedReader openUtf8(ReadableByteChannel channel) {
return open(channel, StandardCharsets.UTF_8);
}
public static BufferedReader open(ReadableByteChannel channel, Charset charset) {
// Create a charset decoder with the same properties as StreamDecoder does for
// InputStreams: namely, replace everything instead of erroring.
var decoder = charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
return new BufferedReader(Channels.newReader(channel, decoder, -1));
}
}

View File

@@ -1,95 +0,0 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
/**
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes.
*
* @cc.module fs.WriteHandle
*/
public class EncodedWritableHandle extends HandleGeneric {
private final BufferedWriter writer;
public EncodedWritableHandle(BufferedWriter writer, TrackingCloseable closable) {
super(closable);
this.writer = writer;
}
/**
* Write a string of characters to the file.
*
* @param textA The text to write to the file.
* @throws LuaException If the file has been closed.
*/
@LuaFunction
public final void write(Coerced<String> textA) throws LuaException {
checkOpen();
var text = textA.value();
try {
writer.write(text, 0, text.length());
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
/**
* Write a string of characters to the file, following them with a new line character.
*
* @param textA The text to write to the file.
* @throws LuaException If the file has been closed.
*/
@LuaFunction
public final void writeLine(Coerced<String> textA) throws LuaException {
checkOpen();
var text = textA.value();
try {
writer.write(text, 0, text.length());
writer.newLine();
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
/**
* Save the current file without closing it.
*
* @throws LuaException If the file has been closed.
*/
@LuaFunction
public final void flush() throws LuaException {
checkOpen();
try {
writer.flush();
} catch (IOException e) {
throw new LuaException(e.getMessage());
}
}
public static BufferedWriter openUtf8(WritableByteChannel channel) {
return open(channel, StandardCharsets.UTF_8);
}
public static BufferedWriter open(WritableByteChannel channel, Charset charset) {
// Create a charset encoder with the same properties as StreamEncoder does for
// OutputStreams: namely, replace everything instead of erroring.
var encoder = charset.newEncoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
return new BufferedWriter(Channels.newWriter(channel, encoder, -1));
}
}

View File

@@ -1,76 +0,0 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import dan200.computercraft.core.util.IoUtil;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
public abstract class HandleGeneric {
private @Nullable TrackingCloseable closeable;
protected HandleGeneric(TrackingCloseable closeable) {
this.closeable = closeable;
}
protected void checkOpen() throws LuaException {
var closeable = this.closeable;
if (closeable == null || !closeable.isOpen()) throw new LuaException("attempt to use a closed file");
}
protected final void close() {
IoUtil.closeQuietly(closeable);
closeable = null;
}
/**
* Close this file, freeing any resources it uses.
* <p>
* Once a file is closed it may no longer be read or written to.
*
* @throws LuaException If the file has already been closed.
*/
@LuaFunction("close")
public final void doClose() throws LuaException {
checkOpen();
close();
}
/**
* Shared implementation for various file handle types.
*
* @param channel The channel to seek in
* @param whence The seeking mode.
* @param offset The offset to seek to.
* @return The new position of the file, or null if some error occurred.
* @throws LuaException If the arguments were invalid
* @see <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
*/
@Nullable
protected static Object[] handleSeek(SeekableByteChannel channel, Optional<String> whence, Optional<Long> offset) throws LuaException {
long actualOffset = offset.orElse(0L);
try {
switch (whence.orElse("cur")) {
case "set" -> channel.position(actualOffset);
case "cur" -> channel.position(channel.position() + actualOffset);
case "end" -> channel.position(channel.size() + actualOffset);
default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
}
return new Object[]{ channel.position() };
} catch (IllegalArgumentException e) {
return new Object[]{ null, "Position is negative" };
} catch (IOException e) {
return null;
}
}
}

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
/**
* A file handle opened for reading with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)}.
*
* @cc.module fs.ReadHandle
*/
public class ReadHandle extends AbstractHandle {
public ReadHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
public ReadHandle(SeekableByteChannel channel, boolean binary) {
this(channel, new TrackingCloseable.Impl(channel), binary);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] read(Optional<Integer> countArg) throws LuaException {
return super.read(countArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readAll() throws LuaException {
return super.readAll();
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
return super.readLine(withTrailingArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
}

View File

@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2018 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
/**
* A file handle opened for reading and writing with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)}.
*
* @cc.module fs.ReadWriteHandle
*/
public class ReadWriteHandle extends AbstractHandle {
public ReadWriteHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] read(Optional<Integer> countArg) throws LuaException {
return super.read(countArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readAll() throws LuaException {
return super.readAll();
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
return super.readLine(withTrailingArg);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void write(IArguments arguments) throws LuaException {
super.write(arguments);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void writeLine(Coerced<ByteBuffer> text) throws LuaException {
super.writeLine(text);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void flush() throws LuaException {
super.flush();
}
}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2017 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.apis.handles;
import dan200.computercraft.api.lua.Coerced;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.filesystem.TrackingCloseable;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Optional;
/**
* A file handle opened for writing by {@link dan200.computercraft.core.apis.FSAPI#open}.
*
* @cc.module fs.WriteHandle
*/
public class WriteHandle extends AbstractHandle {
protected WriteHandle(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
public static WriteHandle of(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary, boolean canSeek) {
return canSeek ? new Seekable(channel, closeable, binary) : new WriteHandle(channel, closeable, binary);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void write(IArguments arguments) throws LuaException {
super.write(arguments);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void writeLine(Coerced<ByteBuffer> text) throws LuaException {
super.writeLine(text);
}
/**
* {@inheritDoc}
*/
@Override
@LuaFunction
public final void flush() throws LuaException {
super.flush();
}
public static class Seekable extends WriteHandle {
Seekable(SeekableByteChannel channel, TrackingCloseable closeable, boolean binary) {
super(channel, closeable, binary);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
@LuaFunction
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
return super.seek(whence, offset);
}
}
}

View File

@@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -57,21 +57,21 @@ public class HttpRequest extends Resource<HttpRequest> {
final AtomicInteger redirects;
public HttpRequest(
ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable String postText,
ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, @Nullable ByteBuffer postBody,
HttpHeaders headers, boolean binary, boolean followRedirects, int timeout
) {
super(limiter);
this.environment = environment;
this.address = address;
postBuffer = postText != null
? Unpooled.wrappedBuffer(postText.getBytes(StandardCharsets.UTF_8))
postBuffer = postBody != null
? Unpooled.wrappedBuffer(postBody)
: Unpooled.buffer(0);
this.headers = headers;
this.binary = binary;
redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0);
this.timeout = timeout;
if (postText != null) {
if (postBody != null) {
if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) {
headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8");
}

View File

@@ -6,8 +6,7 @@ package dan200.computercraft.core.apis.http.request;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.apis.http.HTTPRequestException;
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.options.Options;
@@ -188,9 +187,7 @@ public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpOb
// Prepare to queue an event
var contents = new ArrayByteChannel(bytes);
var reader = request.isBinary()
? BinaryReadableHandle.of(contents)
: new EncodedReadableHandle(EncodedReadableHandle.open(contents, responseCharset));
var reader = new ReadHandle(contents, request.isBinary());
var stream = new HttpResponseHandle(reader, status.code(), status.reasonPhrase(), headers);
if (status.code() >= 200 && status.code() < 400) {

View File

@@ -7,18 +7,16 @@ package dan200.computercraft.core.apis.http.request;
import dan200.computercraft.api.lua.IArguments;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.HTTPAPI;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
import dan200.computercraft.core.apis.handles.HandleGeneric;
import dan200.computercraft.core.apis.handles.AbstractHandle;
import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.methods.ObjectSource;
import java.util.List;
import java.util.Map;
/**
* A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or
* {@link BinaryReadableHandle binary file} if the request used binary mode), though provides several request specific
* methods.
* A http response. This provides the same methods as a {@link ReadHandle file}, though provides several request
* specific methods.
*
* @cc.module http.Response
* @see HTTPAPI#request(IArguments) On how to make a http request.
@@ -29,7 +27,7 @@ public class HttpResponseHandle implements ObjectSource {
private final String responseStatus;
private final Map<String, String> responseHeaders;
public HttpResponseHandle(HandleGeneric reader, int responseCode, String responseStatus, Map<String, String> responseHeaders) {
public HttpResponseHandle(AbstractHandle reader, int responseCode, String responseStatus, Map<String, String> responseHeaders) {
this.reader = reader;
this.responseCode = responseCode;
this.responseStatus = responseStatus;

View File

@@ -5,14 +5,13 @@
package dan200.computercraft.core.apis.http.websocket;
import com.google.common.base.Strings;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.Logging;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.apis.http.HTTPRequestException;
import dan200.computercraft.core.apis.http.NetworkUtils;
import dan200.computercraft.core.apis.http.Resource;
import dan200.computercraft.core.apis.http.ResourceGroup;
import dan200.computercraft.core.apis.http.*;
import dan200.computercraft.core.apis.http.options.Options;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.util.AtomicHelpers;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
@@ -24,10 +23,8 @@ import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.concurrent.GenericFutureListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +32,7 @@ import javax.annotation.Nullable;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Provides functionality to verify and connect to a remote websocket.
@@ -57,6 +55,9 @@ public class Websocket extends Resource<Websocket> implements WebsocketClient {
private final HttpHeaders headers;
private final int timeout;
private final AtomicInteger inFlight = new AtomicInteger(0);
private final GenericFutureListener<? extends io.netty.util.concurrent.Future<? super Void>> onSend = f -> inFlight.decrementAndGet();
public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers, int timeout) {
super(limiter);
this.environment = environment;
@@ -170,18 +171,27 @@ public class Websocket extends Resource<Websocket> implements WebsocketClient {
}
@Override
public void sendText(String message) {
environment.observe(Metrics.WEBSOCKET_OUTGOING, message.length());
var channel = channel();
if (channel != null) channel.writeAndFlush(new TextWebSocketFrame(message));
public void sendText(String message) throws LuaException {
sendMessage(new TextWebSocketFrame(message), message.length());
}
@Override
public void sendBinary(ByteBuffer message) {
environment.observe(Metrics.WEBSOCKET_OUTGOING, message.remaining());
public void sendBinary(ByteBuffer message) throws LuaException {
long size = message.remaining();
sendMessage(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message)), size);
}
private void sendMessage(WebSocketFrame frame, long size) throws LuaException {
var channel = channel();
if (channel != null) channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(message)));
if (channel == null) return;
// Grow the number of in-flight requests, aborting if we've hit the limit. This is then decremented when the
// promise finishes.
if (!AtomicHelpers.incrementToLimit(inFlight, ResourceQueue.DEFAULT_LIMIT)) {
throw new LuaException("Too many ongoing websocket messages");
}
environment.observe(Metrics.WEBSOCKET_OUTGOING, size);
channel.writeAndFlush(frame).addListener(onSend);
}
}

View File

@@ -4,6 +4,7 @@
package dan200.computercraft.core.apis.http.websocket;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.apis.http.HTTPRequestException;
import java.io.Closeable;
@@ -39,15 +40,17 @@ public interface WebsocketClient extends Closeable {
* Send a text websocket frame.
*
* @param message The message to send.
* @throws LuaException If the message could not be sent.
*/
void sendText(String message);
void sendText(String message) throws LuaException;
/**
* Send a binary websocket frame.
*
* @param message The message to send.
* @throws LuaException If the message could not be sent.
*/
void sendBinary(ByteBuffer message);
void sendBinary(ByteBuffer message) throws LuaException;
/**
* Parse an address, ensuring it is a valid websocket URI.

View File

@@ -8,6 +8,11 @@ import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.apis.IAPIEnvironment;
import dan200.computercraft.core.apis.http.options.Options;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
@@ -24,6 +29,8 @@ import static dan200.computercraft.core.apis.http.websocket.WebsocketClient.MESS
* @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
*/
public class WebsocketHandle {
private static final CharsetDecoder DECODER = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPLACE);
private final IAPIEnvironment environment;
private final String address;
private final WebsocketClient websocket;
@@ -68,18 +75,23 @@ public class WebsocketHandle {
* @cc.changed 1.81.0 Added argument for binary mode.
*/
@LuaFunction
public final void send(Coerced<String> message, Optional<Boolean> binary) throws LuaException {
public final void send(Coerced<ByteBuffer> message, Optional<Boolean> binary) throws LuaException {
checkOpen();
var text = message.value();
if (options.websocketMessage() != 0 && text.length() > options.websocketMessage()) {
if (options.websocketMessage() != 0 && text.remaining() > options.websocketMessage()) {
throw new LuaException("Message is too large");
}
if (binary.orElse(false)) {
websocket.sendBinary(LuaValues.encode(text));
websocket.sendBinary(text);
} else {
websocket.sendText(text);
try {
websocket.sendText(DECODER.decode(text).toString());
} catch (CharacterCodingException e) {
// This shouldn't happen, but worth mentioning.
throw new LuaException("Message is not valid UTF8");
}
}
}

View File

@@ -51,15 +51,15 @@ class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
var frame = (WebSocketFrame) msg;
if (frame instanceof TextWebSocketFrame textFrame) {
var data = textFrame.text();
var data = NetworkUtils.toBytes(textFrame.content());
websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length());
websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length);
websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, false);
} else if (frame instanceof BinaryWebSocketFrame) {
var converted = NetworkUtils.toBytes(frame.content());
var data = NetworkUtils.toBytes(frame.content());
websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, converted.length);
websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, true);
websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length);
websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, true);
} else if (frame instanceof CloseWebSocketFrame closeFrame) {
websocket.close(closeFrame.statusCode(), closeFrame.reasonText());
}

View File

@@ -5,7 +5,7 @@
package dan200.computercraft.core.apis.transfer;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
import dan200.computercraft.core.apis.handles.ReadHandle;
import dan200.computercraft.core.methods.ObjectSource;
import java.nio.channels.SeekableByteChannel;
@@ -15,19 +15,19 @@ import java.util.Optional;
/**
* A binary file handle that has been transferred to this computer.
* <p>
* This inherits all methods of {@link BinaryReadableHandle binary file handles}, meaning you can use the standard
* {@link BinaryReadableHandle#read(Optional) read functions} to access the contents of the file.
* This inherits all methods of {@link ReadHandle binary file handles}, meaning you can use the standard
* {@link ReadHandle#read(Optional) read functions} to access the contents of the file.
*
* @cc.module [kind=event] file_transfer.TransferredFile
* @see BinaryReadableHandle
* @see ReadHandle
*/
public class TransferredFile implements ObjectSource {
private final String name;
private final BinaryReadableHandle handle;
private final ReadHandle handle;
public TransferredFile(String name, SeekableByteChannel contents) {
this.name = name;
handle = BinaryReadableHandle.of(contents);
handle = new ReadHandle(contents, true);
}
/**

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